구조체와 열거형의 복잡한 패턴 매칭과 가변 바인딩

구조체(Struct)와 열거형(Enum)의 여러가지 패턴 매칭(Match) 방법과 가변 바인딩(mutable binding)에 대해 알아보겠습니다. 구조체 분해후 바인딩, if let, _, .., | 등의 사용법과 참조와 Shadowing에 대해서도 알아봅니다.

1. 구조체(Struct)에서 복잡한 패턴 매칭과 가변 바인딩

가. 구조체 필드 일부만 매칭하기:

구조체 패턴에서 모든 필드를 반드시 매칭하지 않아도 되며, ..을 써서 나머지 필드는 무시할 수 있습니다.

struct Point {
x: i32,
y: i32,
z: i32,
}

fn main() {
let p = Point { x: 1, y: 2, z: 3 };

match p {
Point { x, .. } => println!("x 값에만 관심 있음: {}", x),
}
}

나. 가변 바인딩 (mutable binding)

기본적으로 Rust의 match에서 바인딩된 변수는 불변입니다. 가변으로 만들려면 mut 키워드를 변수 바인딩 전에 붙입니다.

let mut p = Point { x: 1, y: 2, z: 3 };

match &mut p { // 구조체를 가변 참조로 매칭
Point { x, y, .. } => {
*x += 10; // 가변 참조로 접근 가능
*y += 20;
println!("변경된 x: {}, y: {}", x, y);
}
}
  • 위 예에서 &mut p로 가변 참조를 패턴 매칭했고, 패턴 안의 x와 y는 가변 참조(&mut i32)가 되어 값을 변경할 수 있습니다.

다. 구조체 분해 후 변수 재바인딩

let p = Point { x: 10, y: 20, z: 30 };
let Point { x: a, y: b, z: c } = p;
println!("a: {}, b: {}, c: {}", a, b, c);

a, b, c가 각각 p의필드 값에 바인딩됩니다.

2. 열거형(Enum)에서 복잡한 패턴 매칭과 가변 바인딩

가. 열거형 variant에 포함된 여러 필드 매칭

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

fn main() {
let mut msg = Message::Move { x: 10, y: 20 };

match &mut msg {
Message::Move { x, y } => {
*x += 5; // 가변 참조로 내부 값 변경 가능
*y += 5;
println!("이동 좌표 변경: x={}, y={}", x, y);
}
_ => {}
}
}
  • match &mut msg를 통해 열거형 값을 가변 참조로 매칭하면, 내부 필드도 가변 참조로 바인딩되어 값 변경이 가능합니다.

나. 특정 variant만 언패킹하기

if let Message::Write(text) = msg {
println!("메시지: {}", text);
}
  • if let 문법을 활용해 관심 있는 variant 한 가지만 매칭해 변수 바인딩 후 처리할 수 있습니다.

다. 여러 variant를 한 패턴에 묶기

match msg {
Message::Quit | Message::Write(_) => println!("Quit 또는 Write variant"),
_ => println!("다른 variant"),
}
  • 여러 variant를 하나로 합쳐 처리할 수도 있습니다.

3. 패턴 바인딩 시 참조와 가변성

  • 기본적으로 match 패턴 내 변수 바인딩은 값 소유권을 가져오거나 복사하지만, 참조 및 가변 참조로도 바인딩할 수 있습니다.
let p = Point { x: 10, y: 20, z: 0 };

match &p { // 불변 참조 패턴 매칭
Point { x, y, .. } => println!("x={}, y={}", x, y),
}

let mut p2 = Point { x: 0, y: 0, z: 0 };

match &mut p2 { // 가변 참조 패턴 매칭
Point { x, y, .. } => {
*x += 1;
*y += 1;
println!("수정된 값 x={}, y={}", x, y);
}
}
  • 참조에 따라 변수 바인딩 시 가변성 여부가 달라지고, 이를 통해 구조체 또는 열거형 내부 값을 안전하게 변경할 수 있습니다.

4. 변수 이름 재사용(shadowing)과 패턴 매칭

패턴 매칭 시, 이전에 바인딩된 변수 이름과 동일한 이름을 써도 새로운 바인딩이 생성됩니다.

let x = 5;
let p = Point { x: 10, y: 20, z: 30 };

match p {
Point { x, y, .. } => {
// 여기 x는 패턴 내의 새 바인딩, 밖의 x와 다른 변수
println!("패턴 매칭 내의 x: {}, 외부 x: {}", x, 5);
}
}

5. 요약 정리

기능예시 (일부만)설명
구조체 부분 패턴 매칭Point { x, .. }특정 필드만 분해, 나머지는 무시
가변 참조 매칭match &mut p와 Point { x, y, .. }구조체 필드를 가변 참조로 바인딩해 값 수정 가능
열거형 가변 매칭match &mut msg와 Message::Move { x, y }enum 내부 필드를 가변 참조로 바인딩해 값 수정 가능
if let 구문if let Message::Write(text) = msg특정 variant만 골라 간단히 바인딩 후 처리
패턴 내 변수 이름 재사용Point { x, y } 내 x는새로운 바인딩기존 변수와 이름이 동일해도 새 변수로 처리

이상으로 구조체와 열거형에서 변수 바인딩 시 사용하는 다양한 패턴 매칭 기법과 가변 바인딩 방법에 대해 설명드렸습니다.

자바 클래스, 인터페이스와 러스트의 구조체, 메소드, 트레이트 방식 비교

자바(Java)는 모든 메소드를 클래스 내부에 정의합니다. 그러나, 러스트(Rust)는 데이터 구조(Struct)와 메소드(Impl block)가 물리적으로 분리되어 있습니다. 구조체 안에는 데이터만 정의하고, 별도의 impl 블록에서 그 구조체에 대한 메소드를 구현합니다.

Ⅰ. 데이터 구조, 메소드 구현 방식 비교

1. 자바: 클래스 내부에 메소드

자바(Java)는 모든 메소드를 클래스 내부에 정의합니다. 이는 객체 지향 프로그래밍(OOP)의 특징으로, 데이터(필드)와 행동(메소드)이 하나의 단위(클래스)에 밀접하게 결합(cohesion)되어 있습니다.

2. 러스트: 구조체와 메소드의 분리

러스트(Rust)에서는 데이터 구조(Struct)와 메소드(Impl block)가 물리적으로 분리되어 있습니다. 구조체 안에는 데이터만 정의하고, 별도의 impl 블록에서 그 구조체에 대한 메소드를 구현합니다.

3. 장단점

구분자바(메소드=클래스 내부)러스트(메소드=구조체 외부)
응집성데이터와 행동이 하나에 묶임데이터와 행동이 분리되어 명확
확장성상속 등 OOP 구조 확장 용이Trait, 여러 impl로 유연하게 확장
가독성클래스 안에서 한 번에 파악 가능데이터 정의와 메소드 구현 분리
유연성하나의 클래스가 하나의 메소드 집합같은 구조체에 여러 impl, trait 구현 가능
재사용성상속, 인터페이스로 구현Trait 등으로 다양한 방식의 재사용
관리의 용이성대형 클래스에서 복잡해질 수 있음관련 없는 메소드를 구조체와 별도 구현 가능
코드 조직화OOP 방식(클래스-중심)데이터 중심(struct), 행동 분리(impl, trait)
  • 자바는 객체 중심의 응집력, 개발 및 이해 용이성이 강점이나, 큰 클래스가 복잡해지거나 상속 구조의 한계 등이 있습니다.
  • 러스트는 데이터와 행동의 분리로 역할이 명확하며, trait 기반의 확장성과 안전성이 강점이지만, 초보자에겐 코드 연관성 파악이 다소 불편할 수 있습니다.

Ⅱ. 예제를 통한 비교

1. 자바(Java)

자바에서는 데이터(필드)와 메소드가 한 클래스 내부에 정의되어 객체 지향 프로그래밍 패러다임에 맞춰 응집되어 있습니다.

public class Point {
private int x;
private int y;

// 생성자
public Point(int x, int y) {
this.x = x;
this.y = y;
}

// 메소드: 두 점 사이 거리 계산
public double distance(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}

// getter 메소드
public int getX() { return x; }
public int getY() { return y; }
}

// 사용
public class Main {
public static void main(String[] args) {
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.distance(p2)); // 출력: 5.0
}
}
  • 징: Point 클래스 내부에 데이터(x, y)와 동작(거리 계산 메소드)을 모두 포함.
  • 장점: 객체 지향적 응집성(cohesion) 강화, 한 곳에서 모든 관련 기능 파악 가능.
  • 단점: 클래스가 커질수록 복잡성 증가, 상속 구조 제한 등.

2. 러스트(Rust)

러스트는 데이터 구조체(struct)와 메소드 구현부(impl 블록)를 분리해서 작성합니다. 이로써 역할 분리가 명확해지고, 여러 impl 블록과 trait을 통해 유연하게 확장할 수 있습니다.

// 데이터 정의: 구조체는 필드만 가짐
struct Point {
x: i32,
y: i32,
}

// 메소드 구현: impl 블록에서 정의
impl Point {
// 연관 함수(정적 메서드와 유사)
fn new(x: i32, y: i32) -> Point {
Point { x, y }
}

// 메소드: 두 점 사이 거리 계산
fn distance(&self, other: &Point) -> f64 {
let dx = (self.x - other.x) as f64;
let dy = (self.y - other.y) as f64;
(dx.powi(2) + dy.powi(2)).sqrt()
}
}

fn main() {
let p1 = Point::new(0, 0);
let p2 = Point::new(3, 4);
println!("{}", p1.distance(&p2)); // 출력: 5.0
}
  • 특징: struct는 데이터만 선언, 메소드는 별도의 impl 블록에서 구현.
  • 장점: 데이터와 행동이 분리되어 역할 명확, 여러 impl이나 trait로 기능 확장 유리, 컴파일타임 안전성 증대.
  • 단점: 관련 데이터와 메소드가 코드상 분리되어 있어 한눈에 파악하기 어려울 수 있음, 전통적인 OOP 방식과 차이 있음.

3. 비교 표

항목자바(Java)러스트(Rust)
구조클래스 내부에 데이터와 메소드가 함께 있음구조체(데이터)와 impl 블록(메소드)로 분리
작성 방식한 클래스 파일 내에서 모든 정의struct로 데이터 정의, 별도 impl로 메소드 구현
확장성상속과 인터페이스 기반 확장 (단일 상속)여러 impl 블록과 trait 조합으로 유연하고 다중 확장 가능
가독성관련 데이터와 메소드가 한 곳에 있어 파악 용이데이터와 메소드가 분리되어 코드가 흩어질 수 있음
안전성런타임 검사 및 가비지 컬렉션컴파일 타임 소유권 및 빌림 검사로 메모리 안전성 강화
메모리참조 타입 중심, 힙 할당 및 가비지 컬렉션 필요값 타입 중심, 명확한 메모리 제어 및 성능 최적화 가능
객체 지향전통적인 OOP 완전 지원클래스는 없으나 trait로 인터페이스 역할 및 객체지향 유사 기능 제공

러스트의 구조체+impl 방식은 자바와는 다르게 데이터와 메소드가 분리되어 있지만, impl 블록 내에서 메소드를 묶어 객체 지향적 프로그래밍의 많은 특징을 흉내 낼 수 있습니다. trait를 활용하면 인터페이스 역할도 하며, 상속 대신 다중 trait 구현으로 유연하게 기능을 확장할 수 있습니다.

Ⅲ . 자바의 상속과 인터페이스 구조와 러스트의 고급 Trait(트레이트) 및 패턴 비교

자바의 상속과 인터페이스 구조와 러스트의 고급 Trait(트레이트) 및 패턴을 심도 있게 비교 설명하면 다음과 같습니다.

1. 자바의 상속과 인터페이스 구조

가. 상속 (Inheritance)

  • 클래스 간에 “is-a” 관계를 표현하는 가장 기본적인 메커니즘입니다.
  • 한 클래스가 다른 클래스(부모 클래스)를 상속받아 멤버 변수와 메소드를 재사용하거나 오버라이드할 수 있습니다.
  • 단일 상속만 지원하여 다중 상속의 복잡성을 회피합니다.
  • 상속 구조가 깊어지면 유지보수가 어려워지고, 부모 클래스 변경 시 자식 클래스에 의도치 않은 영향이 발생할 수 있음.

나. 인터페이스 (Interface)

  • 여러 클래스가 구현해야 하는 메소드의 명세(계약)를 정의합니다.
  • 자바 8부터는 디폴트 메소드 구현도 지원하여 일부 기능적 확장 가능.
  • 인터페이스의 다중 구현이 가능해 상속 단점을 보완하고 다형성 제공.
  • 인터페이스는 구현 메소드가 없거나 기본 구현만 제공하므로, 설계 시 유연성을 줌.
interface Flyer {
void fly();
}
class Bird implements Flyer {
public void fly() {
System.out.println("Bird is flying");
}
}
class Airplane implements Flyer {
public void fly() {
System.out.println("Airplane is flying");
}
}

2. 러스트의 고급 Trait 및 패턴

가. Trait(트레이트) 개념

  • 러스트의 트레이트는 특정 기능을 구현하도록 강제하는 인터페이스 역할을 합니다.
  • 타입에 따라 여러 트레이트를 다중으로 구현할 수 있어 매우 유연합니다.
  • 트레이트 내에 메소드 기본 구현(default method)을 제공해 부분 구현도 가능.
  • Trait는 다중 상속을 대체하며, 조합(composition) 및 다형성을 지원합니다.

나. 고급 트레이트 특징

(1) 동일 메소드명을 가진 다중 트레이트 구현 (충돌 해결)

  • 예를 들어, 같은 이름 fly() 메소드를 가진 서로 다른 트레이트 Pilot, Wizard를 Human 타입에 모두 구현 가능.
  • 호출 시 Pilot::fly(&human), Wizard::fly(&human)처럼 트레이트 이름을 명시해 충돌 해결.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;

impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}

impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}

impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}

fn main() {
let person = Human;
Pilot::fly(&person); // This is your captain speaking.
Wizard::fly(&person); // Up!
person.fly(); // *waving arms furiously*
}

(2) 슈퍼 트레이트 (Super-traits)

  • 한 트레이트가 다른 트레이트의 구현을 전제로 하는 경우 사용.
  • 예: OutlinePrint 트레이트가 Display 트레이트를 반드시 구현한 경우에만 구현 가능하도록 제한.
use std::fmt::Display;

trait OutlinePrint: Display {
fn outline_print(&self) {
// Display 기능 이용해 출력하는 구현
println!("*{}*", self);
}
}

(3) 제네릭과 트레이트 바운드 (Trait Bounds)

  • 함수, 구조체, 열거형 등에서 트레이트를 타입 매개변수 조건으로 지정하여 유연한 재사용성 제공.
fn print_info<T: Display + Clone>(item: T) {
println!("{}", item);
}

(4) Newtype 패턴

  • 외부 타입에 대해 새로운 타입을 만든 뒤 트레이트를 구현해 동작을 확장하거나 수정하는 패턴.
  • 원래 타입의 구현과 격리돼 안전성과 캡슐화 유지에 도움.

다. 자바 상속-인터페이스와 러스트 Trait 패턴 비교

특징자바 (상속, 인터페이스)러스트 (고급 Trait 패턴)
다중 상속 지원 여부클래스는 단일 상속, 인터페이스 다중 구현 가능다중 트레이트 구현 완전 지원
메소드 충돌 처리인터페이스 디폴트 메소드 충돌은 명시적 오버라이딩 필요트레이트별로 명시적 호출(TraitName::method)로 충돌 처리
구현 분리 여부상속 기반으로 구현 내용 일부 공유, 부모-자식 직접 연관데이터와 행동 완전 분리, 여러 impl 블록과 트레이트로 유연한 구현
확장성 및 유연성인터페이스와 추상 클래스 활용 가능, 런타임 다형성 제공트레이트 조합 및 바운드로 더 강력하고 유연한 제네릭 기반 다형성 가능
안전성런타임에 예외 발생 가능, 컴파일 타임 타입 검사 제한컴파일 타임 엄격한 타입 검사 및 소유권 규칙으로 높은 안전성 보장
메모리 구조클래스는 힙에 할당, 가비지 컬렉션 필요구조체는 값 타입(STACK 기반), 트레이트 객체는 런타임에 크기를 모르는 타입 표현 지원

라. 러스트 Trait를 활용한 고급 설계 패턴 예시

(1) Trait 객체 (Trait Objects)

  • 런타임에 다양한 타입을 동적으로 처리하고자 할 때 사용.
  • Box<dyn Trait> 형태로 포인터에 담아 추상화 제공.
trait Animal {
fn speak(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}

impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}

fn make_animal_speak(animal: Box<dyn Animal>) {
animal.speak();
}

fn main() {
let dog = Box::new(Dog);
let cat = Box::new(Cat);

make_animal_speak(dog);
make_animal_speak(cat);
}

(2) 제네릭과 Trait Bound를 이용한 추상화

fn print_animal_speak<T: Animal>(animal: T) {
animal.speak();
}

(3) Newtype 패턴

struct Wrapper(Vec<String>);

impl std::fmt::Display for Wrapper {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}

5. 러스트 Trait 객체와 동적 디스패치(Dynamic Dispatch)

가. 기본 개념

  • 러스트의 Trait 객체는 런타임에 다양한 타입들을 추상화하고 동적으로 호출할 수 있게 하는 기능입니다.
  • 구체적인 타입을 명시하지 않고 특정 트레이트를 구현한 여러 객체를 하나의 타입으로 사용할 때 유용합니다.
  • dyn Trait 키워드로 표현하며, 이때 메소드 호출은 컴파일 타임이 아닌 런타임에 결정(동적 디스패치)됩니다.

나. 장점

  • 다양한 타입에 대해 동일 인터페이스 역할을 수행할 수 있게 하며, 실행 시점에 적절한 메소드가 호출됩니다.
  • 유연한 설계와 다형성 제공.

다. 단점

  • 동적 디스패치로 인해 약간의 성능 오버헤드 발생.
  • 컴파일 타임에 타입 크기를 알 수 없으므로 보통 Box<dyn Trait>, &dyn Trait 형태로 사용.

예제 코드

trait Animal {
fn speak(&self);
}

struct Dog;

impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}

struct Cat;

impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}

fn animal_speak(animal: &dyn Animal) {
animal.speak();
}

fn main() {
let dog = Dog;
let cat = Cat;

animal_speak(&dog); // Woof!
animal_speak(&cat); // Meow!
}
  • animal_speak 함수는 타입이 &dyn Animal로, 호출하는 객체가 Dog이든 Cat이든 런타임에 올바른 speak 메서드를 호출합니다.

요약

  • 자바의 상속과 인터페이스는 클래스 중심의 단일 상속과 다중 인터페이스 구현으로 객체 지향 프로그래밍 패러다임을 완성하며, 런타임 다형성을 제공합니다.
  • 러스트의 트레이트는 다중 상속을 대체하고, 유연한 기능 확장과 컴파일 타임 안전성을 제공하며, 타입별로 여러 트레이트를 조합해 강력한 추상화와 다형성을 구현합니다.
  • 러스트에서 트레이트는 충돌 해결, 슈퍼 트레이트, 제네릭 바운드, 트레이트 객체 등 다양한 고급 패턴으로 복잡한 설계를 수행할 수 있습니다.
  • 두 언어 모두 장단점이 있으므로, 팀의 경험과 프로젝트 요구사항에 따라 적합한 방식을 선택하는 것이 중요합니다.

데이터 타입에 따른 변수 바인딩(Variable Binding)

Rust에서 variable binding(변수 바인딩)은 값을 변수 이름에 연결하는 과정을 의미하며, Rust 프로그래밍에서 변수 선언과 관련된 기본 개념입니다. Rust는 정적 타입 언어이고 기본적으로 변수가 불변(immutable)으로 선언되기에, 가변성을 가지려면 일부러 명시해야 합니다. 이와 함께 Rust의 바인딩은 shadowing(변수 가리기)이나 스코프와 밀접하게 연관되어 있어, 변수 활용 시 중요한 개념입니다.

Ⅰ. 기본 자료형(Primitive Type)

1. 변수 바인딩 기본

Rust에서 변수 바인딩은 let 키워드로 선언합니다. 이때 변수는 기본적으로 불변입니다. 따라서 선언 후 값을 변경하려 하면 컴파일 에러가 발생합니다.

fn main() {
let x = 5;
println!("x의 값: {}", x);
// x = 6; // 오류: 불변 변수에 재할당 불가
}

이 코드는 x를 5로 바인딩했지만, 이후 x = 6과 같이 변경하려 하면 컴파일 에러가 발생합니다. Rust 컴파일러는 이런 불변 변수 변경 시도를 막아 안정성을 보장합니다.

2. 가변 변수 (Mutable Binding)

변수를 변경 가능하게 하려면 mut 키워드를 사용해 가변 변수로 선언해야 합니다.

fn main() {
let mut x = 5;
println!("초기 x: {}", x);
x = 6;
println!("변경된 x: {}", x);
}

이렇게 하면 x에 대한 값 변경이 가능해집니다. 컴파일러가 에러 발생 시 mut를 추가하라고 친절히 안내하는 점도 Rust의 특징입니다.

3. 타입 추론 및 명시적 타입 선언

Rust 컴파일러는 대부분의 경우 변수 선언 시 타입을 자동 추론합니다.

let x = 42;      // i32로 추론
let y = true; // bool로 추론
let z: u32 = 10; // 명시적 타입 선언

필요에 따라 타입을 명시할 수도 있지만, 대부분 타입 추론에 맡깁니다.

4. 변수 가리기 (Shadowing)

Rust의 독특한 특징인 변수 가리기는 같은 이름의 변수를 같은 스코프 내에서 다시 let으로 선언하여 새 변수를 만드는 것입니다. 이때 이전 변수는 새 변수에 의해 가려집니다.

fn main() {
let x = 5;
let x = x + 1; // 이전 x를 가리고 새 x 선언
{
let x = x * 2;
println!("내부 스코프의 x: {}", x); // 12
}
println!("외부 스코프의 x: {}", x); // 6
}

위 예제에서 첫 번째 x는 5, 두 번째는 6, 내부 스코프의 x는 12입니다. 이렇게 하면 가변 변수 없이도 값 변경 효과를 낼 수 있고 타입도 변경할 수 있습니다.

5. 스코프와 변수 유효 범위

변수는 선언된 블록 {} 내에서만 유효하며, 블록을 벗어나면 사라집니다.

fn main() {
let x = 1;
{
let y = 2;
println!("내부 스코프 y: {}", y); // 2
}
// println!("외부 스코프 y: {}", y); // 오류: y는 유효하지 않음
}

또한 내부 스코프에서 같은 이름으로 변수를 다시 선언하면 외부 변수를 가립니다.

6. 실습 예제 종합

fn main() {
// 불변 변수
let a = 10;
println!("a: {}", a);
// a = 20; // 컴파일 에러!

// 가변 변수
let mut b = 10;
println!("b 초기값: {}", b);
b = 20;
println!("b 변경 후: {}", b);

// 변수 가리기 (shadowing)
let c = 5;
let c = c + 10; // 이전 c 가리기, 이제 c = 15
println!("c: {}", c);

{
let c = c * 2; // 내부 스코프 가리기, c = 30
println!("내부 블록의 c: {}", c);
}

println!("외부 블록의 c: {}", c);

// 타입 변경이 가능한 shadowing
let d = "문자열";
println!("d는 문자열: {}", d);
let d = d.len(); // 같은 이름 변수에 정수 대입 (shadowing)
println!("d는 문자열 길이: {}", d);
}

출력 결과:

a: 10
b 초기값: 10
b 변경 후: 20
c: 15
내부 블록의 c: 30
외부 블록의 c: 15
d는 문자열: 문자열
d는 문자열 길이: 6

Ⅱ. 벡터(Vector)와 튜플(Tuple) 타입 변수 바인딩

1. 벡터(Vector) 타입 변수 바인딩

벡터는 같은 타입 요소들을 동적 크기로 저장하는 컬렉션입니다. Rust에서 벡터는 Vec<T> 타입으로 표현됩니다.

  • 벡터 바인딩 기본 예:
fn main() {
// 빈 vector 생성 (타입 명시 필요)
let mut v: Vec<i32> = Vec::new();
// 값 추가 가능하려면 mut 필요
v.push(1);
v.push(2);
println!("{:?}", v); // 출력: [1, 2]

// 초기값과 함께 벡터 생성 (타입 추론 가능)
let v2 = vec![10, 20, 30];
println!("{:?}", v2); // 출력: [10, 20, 30]
}
  • 벡터의 요소 접근:
let first = v2[0];       // 인덱스를 통한 접근 (주의: 범위 초과 시 패닉)
let maybe_first = v2.get(0); // Option 타입 반환 (안전 접근)
  • 가변 벡터 요소 수정도 mut으로 가능:
let mut v3 = vec![1, 2, 3];
v3[1] = 5; // 두 번째 요소를 5로 변경

벡터는 가변성을 가지고 가리키는 데이터가 동적으로 바뀔 수 있으므로 보통 mut 바인딩과 함께 선언합니다.

2. 3차원 벡터

1) 가변 크기: Vec<Vec<Vec<T>>>

// 2 x 3 x 4 크기의 0으로 채워진 3차원 벡터
let x = 2;
let y = 3;
let z = 4;
let mut v: Vec<Vec<Vec<i32>>> = vec![vec![vec![0; z]; y]; x];

// 읽기
let a = v[1][2][3];

// 쓰기
v[0][1][2] = 42;

헬퍼 함수를 하나 두면 더 편합니다:

fn new_3d<T: Clone>(x: usize, y: usize, z: usize, value: T) -> Vec<Vec<Vec<T>>> {
vec![vec![vec![value.clone(); z]; y]; x]
}

let mut v = new_3d(2, 3, 4, 0i32);

2) 고정 크기: 중첩 배열(스택 또는 Box로 힙에)

크기가 컴파일 타임에 고정되어 있다면 배열을 사용할 수 있습니다.

const X: usize = 2;
const Y: usize = 3;
const Z: usize = 4;

let mut v: [[[i32; Z]; Y]; X] = [[[0; Z]; Y]; X];

v[1][2][3] = 7;

크기가 커서 스택에 올리기 부담스럽다면 Box로 감싸 힙에 둘 수도있습니다.

let mut v: Box<[[[i32; Z]; Y]; X]> = Box::new([[[0; Z]; Y]; X]);

3) 평탄화(flat)한 1차원 Vec<T> + 인덱스 계산

성능/메모리 지역성을 위해 1차원 벡터에 직접 담고 인덱스를 수식으로 계산하는 패턴도 자주 씁니다.

struct Vec3D<T> {
data: Vec<T>,
x: usize,
y: usize,
z: usize,
}

impl<T: Clone> Vec3D<T> {
fn new(x: usize, y: usize, z: usize, value: T) -> Self {
Self {
data: vec![value; x * y * z],
x, y, z,
}
}

#[inline]
fn idx(&self, i: usize, j: usize, k: usize) -> usize {
// (i, j, k) -> linear index
i * self.y * self.z + j * self.z + k
}

fn get(&self, i: usize, j: usize, k: usize) -> &T {
&self.data[self.idx(i, j, k)]
}

fn get_mut(&mut self, i: usize, j: usize, k: usize) -> &mut T {
let idx = self.idx(i, j, k);
&mut self.data[idx]
}
}

let mut v = Vec3D::new(2, 3, 4, 0i32);
*v.get_mut(1, 2, 3) = 5;

2. 튜플(Tuple) 타입 변수 바인딩

튜플은 여러 개의 서로 다른 타입 값을 하나로 묶는 복합 타입입니다. 고정 길이이며 각각 요소의 타입은 다를 수 있습니다.

  • 튜플 선언과 바인딩:
let tup: (i32, f64, char) = (500, 6.4, 'z');
  • 튜플의 요소에 접근하려면 패턴 매칭 또는 점(.) 표기법 사용:
let (x, y, z) = tup;  // 구조 분해 (destructuring)
println!("x: {}, y: {}, z: {}", x, y, z);

let five_hundred = tup.0;
let six_point_four = tup.1;
let z_char = tup.2;

튜플은 기본적으로 불변이며, 가변 선언 시 요소 변경 가능:

let mut tup2 = (1, 2);
tup2.0 = 5; // 첫 번째 요소 변경 가능

4. 스코프와 바인딩의 변수 가리기

벡터, 튜플 등도 기본 자료형과 마찬가지로 스코프 내에서 변수 가리기(shadowing)가 가능합니다:

let v = vec![1];
let v = vec![2, 3]; // 이전 v 가려짐
println!("{:?}", v); // [2, 3]

Ⅲ. 배열

1. 1차원 배열 정의 방법

let arr = [1, 2, 3];  // 길이 3의 1차원 배열

2. 2차원 배열

let arr = [[1, 2, 3], [4, 5, 6]];  // 2x3 배열

3. 3차원 배열

let arr = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
];

Ⅳ. 해시맵

Rust에서 해시맵(HashMap)의 변수 바인딩도 기본적으로 let 키워드를 사용해 이름을 해시맵 값에 바인딩하는 방식으로 이루어집니다. 즉, 해시맵도 다른 변수처럼 데이터 구조 전체를 하나의 변수 이름에 연결하는 행위입니다.

  • 해시맵을 선언할 때는 use std::collections::HashMap;를 임포트하고,
  • 보통 HashMap::new()를 호출하여 빈 해시맵을 만들고,
  • let mut를 붙여 가변 바인딩을 해야 해시맵에 키-값 쌍을 추가하거나 수정할 수 있습니다.

예를 들어:

use std::collections::HashMap;

fn main() {
// 가변 해시맵 바인딩
let mut scores = HashMap::new();

// 값 삽입
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

// 값 접근
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

println!("Blue 팀 점수: {}", score);

// 해시맵의 모든 키-값 쌍 출력
for (key, value) in &scores {
println!("{key}: {value}");
}
}
  • 여기서 scores 변수에 해시맵 데이터 구조가 바인딩되었고,
  • mut 키워드 때문에 값 삽입과 변경이 가능합니다.
  • .insert() 메서드로 키-값 쌍을 추가하거나 갱신하며,
  • .get() 메서드로 해당 키에 연관된 값을 읽을 수 있습니다.

요약하면, 해시맵도 배열, 벡터, 튜플, 구조체와 같이 Rust에서 변수 바인딩 개념이 동일하게 적용됩니다. let으로 이름을 데이터에 바인딩하고, 변경하려면 mut가 필수입니다. 해시맵은 키-값 쌍 컬렉션이라는 점에서 특수하지만, 바인딩이라는 개념 면에서는 일반 변수와 차이가 없습니다.

추가적으로 해시맵 내부에 구조체 같은 복합 데이터 타입을 저장해도, 변수 바인딩과 관련된 기본 원칙은 같습니다.

Ⅴ. 사용자 정의 타입

Rust에서 사용자 정의 타입(예: 구조체와 열거형)에 대한 변수 바인딩은 기본 타입이나 벡터, 튜플 등과 동일하게 let 키워드를 사용하여 값을 변수 이름에 연결하는 것을 의미합니다. 하지만 구조체와 열거형은 내부 필드나 variant에 데이터를 포함할 수 있어서, 변수 바인딩과 활용이 더 다양한 형태로 나타납니다.

아래에서 구조체와 열거형을 사용자 정의 타입의 변수 바인딩 예제로 구체적으로 설명합니다.

1. 구조체(Struct) 변수 바인딩 예제

// 구조체 정의
struct Point {
x: i32,
y: i32,
}

fn main() {
// 구조체 인스턴스를 생성하고 변수 p에 바인딩
let p = Point { x: 10, y: 20 };

// 구조체 필드에 접근
println!("x: {}, y: {}", p.x, p.y);

// 가변 바인딩 시 필드 값 변경 가능
let mut p_mut = Point { x: 5, y: 5 };
p_mut.x = 15;
println!("변경된 x: {}", p_mut.x);

// 구조체를 분해하여 필드 값을 각각 변수에 바인딩
let Point { x: a, y: b } = p;
println!("분해된 x: {}, y: {}", a, b);
}
  • let p = Point { … };에서 p는 Point 타입 값에 변수 바인딩입니다.
  • 패턴 매칭처럼 let Point { x: a, y: b } = p; 구문으로 구조체 필드를 변수 a, b에 바인딩할 수도 있습니다.
  • mut 키워드로 가변 바인딩을 선언하면 구조체 필드를 수정할 수 있습니다.

2. 열거형(Enum) 변수 바인딩 예제

// 열거형 정의
enum Message {
Quit, // 데이터 없는 variant
Move { x: i32, y: i32 }, // 필드가 있는 variant (struct-like)
Write(String), // 튜플 형태 variant
ChangeColor(i32, i32, i32), // 여러 필드를 가진 튜플 variant
}

fn main() {
// enum 값에 변수 바인딩
let msg = Message::Move { x: 10, y: 20 };

// match로 variant별 데이터 바인딩과 처리
match msg {
Message::Quit => println!("Quit variant"),
Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
Message::Write(text) => println!("Write message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to {}, {}, {}", r, g, b),
}
}
  • let msg = Message::Move { x: 10, y: 20 };에서 msg가 Message enum의 Move variant 값에 바인딩되었습니다.
  • match 문에서는 각 variant의 내부 데이터를 별도의 변수(x, y, text, r, g, b)에 패턴 매칭을 사용한 변수 바인딩으로 추출합니다.
  • 열거형 값 하나가 여러 variant 중 하나로만 존재할 수 있기에, match를 통해안전하게 처리합니다.

요약

구분변수 바인딩 방식설명
기본 타입let x = 5;변수 이름이 단순 값과 바인딩됨
구조체let p = Point { … };변수 이름이 구조체 인스턴스에 바인딩
let Point { x: a, y: b } = p;구조체 필드를 변수에 분해하여 바인딩
열거형let msg = Message::Write(“Hi”.to_string());변수에 enum variant 값 바인딩
match msg { Message::Write(text) => { … } }match 내 패턴 매칭으로 variant 내 데이터에 변수 바인딩

이처럼 사용자 정의 타입에서도 변수 바인딩은 let을 사용해 값을 변수 이름에 연결하는 동일한 개념이며, 구조체는 필드별 분해 바인딩이 가능하고, 열거형은 variant별 패턴 매칭을 통해 내부 데이터를 변수에 바인딩하는 방식으로 동작합니다.

다른 언어와 비교한 Rust의 동시성(concurrency) 장,단점

Rust의 동시성(concurrency)은 안전성과 성능을 모두 고려한 설계로, 데이터 경쟁(data race)을 컴파일 타임에 방지하고, 성능 저하 없이 병렬 처리를 가능하게 합니다. C++, Java, Python, Go 등 타 언어와 비교해 장단점을 알아보겠습니다.

1. Rust의 동시성 개념

Rust는 동시성을 다음 세 가지 방식으로 지원합니다:

  1. 스레드 기반 동시성 (std::thread)
    • OS 스레드를 생성하여 병렬 작업 수행.
    • thread:spawn을 통해 새로운 스레드를 실행.
  2. 메시지 기반 통신 (std::sync::mpsc)
    • 채널을 통해 스레드 간 데이터 교환.
    • 데이터 소유권을 안전하게 이동.
  3. 비동기 프로그래밍 (async/await, tokio, async-std)
    • 효율적인 I/O 처리.
    • 싱글 스레드에서 수천 개의 작업을 동시에 처리할 수 있음.
    • Future를이용한 논블로킹 방식.

2. Rust 동시성의 핵심 특징

특징설명
데이터 레이스 방지컴파일 타임에 mut, &mut, Send, Sync 등을 통해 공유 자원에 대한 안전성 확보
제로 코스트 추상화고급 추상화를 사용해도 런타임 오버헤드 없음
fearless concurrency안전하게 동시성을 구현할 수 있어 “두려움 없는 동시성”이라고도 불림

3. 타 언어와의 비교

가. Rust vs C++

항목RustC++
안전성컴파일 타임 데이터 레이스 방지런타임에서 버그 발견 가능
메모리 모델소유권 시스템수동 메모리 관리
쓰레드 API안전하고 모던한 추상화복잡하고 안전하지 않은 경우 많음

🔹 Rust는 안전하고 버그 없는 병렬 처리를 제공
🔸 C++은 성능은 뛰어나지만 관리 책임이 개발자에게 있음 (예: 뮤텍스 실수 → 데이터 손상)

나. Rust vs Java

항목RustJava
런타임없음 (네이티브 실행)JVM 기반
동기화Mutex, RwLock, channel 등 명시적synchronized, volatile, ExecutorService 등
성능시스템 수준 고성능GC와 JVM 오버헤드 존재

🔹 Rust는 GC 없는 고성능 동시성
🔸 Java는 GC로 메모리 관리가 쉽지만 지연 가능성 존재

다. Rust vs Python

항목RustPython
성능매우 빠름느림 (인터프리터 기반)
GIL (Global Interpreter Lock)없음있음 (멀티 코어 병렬 처리 불가)
비동기 처리고성능 async/awaitasyncio로 가능하나 성능은 낮음

🔹 Rust는 진짜 병렬 처리 가능
🔸 Python은 GIL 때문에 CPU 병렬처리에 약함 (I/O 병렬만 현실적)

라. 요약

구분Rust의 장점Rust의 단점
성능네이티브 수준의 성능안전성을 위한 빌드 시간 증가
안전성데이터 레이스를 컴파일 타임에 방지초기 진입 장벽 (개념이 복잡함)
표현력async/await, channel, Mutex 등 현대적 추상화도구/라이브러리 생태계가 다른 언어보다 적은 편
병렬성GIL 없음, 진짜 병렬 처리 가능쓰레드 디버깅이 어려울 수 있음

4. Rust 동시성이 특히 유리한 분야

  • 고성능 웹 서버 (예: Actix, Axum)
  • 실시간 시스템 (예: 게임, IoT)
  • 병렬 데이터 처리 (예: 이미지/영상 처리)
  • 시스템 프로그래밍 (드라이버, 임베디드)

5. 1부터 100만까지 숫자의 합을 4개 스레드로 나눠 병렬 계산 비교

가. Rust 버전

use std::thread;

fn main() {
let data: Vec<u64> = (1..=1_000_000).collect();
let chunk_size = data.len() / 4;

let mut handles = vec![];

for i in 0..4 {
let chunk = data[i * chunk_size..(i + 1) * chunk_size].to_vec();
let handle = thread::spawn(move || chunk.iter().sum::<u64>());
handles.push(handle);
}

let total: u64 = handles.into_iter().map(|h| h.join().unwrap()).sum();
println!("합계: {}", total);
}

설명:

use std::thread;
  • Rust의 표준 라이브러리에서 thread 모듈을 가져옵니다. 병렬 처리를 위해 사용됩니다.
let data: Vec<u64> = (1..=1_000_000).collect();
  • (1..=1_000_000)는 표현식이며 RangeInclusive 타입으로, 1부터 1_000_000까지 포함하는 이터레이터입니다.
  • .collect()는 이터레이터(iterator)를 모아서 컬렉션(예: Vec, HashMap, String)으로 변환하는 메서드입니다.
  • 명시적으로 Vec 타입을 선언했기 때문에, collect()는 모든 숫자를 벡터로 수집하게 됩니다.
let chunk_size = data.len() / 4;
  • 데이터를 4개의 스레드로 나눌 것이기 때문에, 각 스레드가 처리할 데이터의 크기를 계산합니다.
  • chunk_size = 250_000
let mut handles = vec![];
  • 스레드 핸들(JoinHandle)들을 저장할 벡터.
  • 각 스레드는 나중에 .join()으로 결과를 수집할 수 있습니다.
for i in 0..4 {
let chunk = data[i * chunk_size..(i + 1) * chunk_size].to_vec();
let handle = thread::spawn(move || chunk.iter().sum::<u64>());
handles.push(handle);
}
  • 루프를 4번 돌며 벡터를 4등분합니다.
let chunk = data[i * chunk_size..(i + 1) * chunk_size].to_vec();
  • data를 4등분하여 각 스레드에 넘길 복사본을 만든 후 chunk에 할당합니다.
  • i가 0~3까지 반복되므로:
    • i = 0 일 때는 data[0..250000]
    • i = 1 일 때는 data[250000..500000]
    • i = 2 일 때는 data[500000..750000]
    • i = 3 일 때는 data[750000..1000000]
  • 이렇게 전체 데이터를 4개의 슬라이스(slice)로 나눕니다. 하지만 슬라이스는 참조(&)이며, 여러 스레드가 같은 데이터를 공유할 때, 데이터 경합(data race)을 막기 위해 컴파일러가 참조의 안전성을 보장해야 하므로, .to_vec()을 사용하여 슬라이스의 복사본을 만들어 소유권을 가지는 새 벡터로 만듭니다. 이제 이 벡터는 독립적 소유권을 가지므로, move를 통해 클로저에 안전하게 넘길 수 있습니다
let handle = thread::spawn(move || chunk.iter().sum::<u64>());
  • thread::spawn(…) → 새로운 스레드(thread)를 만들어서 주어진 작업을 실행합니다.
  • move || … → 클로저(익명 함수)에서 외부 변수인 chunk의 소유권을 이동시켜 사용합니다.
  • chunk.iter().sum::() → chunk의 모든 원소를 합산하여 u64값을 반환합니다.
  • 반환된 handle은 JoinHandle 타입이고, 이걸 handles 벡터에 저장해 나중에 결과를 수집합니다.
handles.push(handle);
  • thread::spawn(…)의 결과인 JoinHandle을 handles 벡터에 저장합니다.
  • 이 handles는 모든 스레드 작업이 끝난 뒤 결과를 수집하는 데 사용됩니다.
let total: u64 = handles.into_iter().map(|h| h.join().unwrap()).sum();
  • 스레드에서 계산한 4개의 부분합을 모아서 전체 합을 계산한다.
  • handles.into_iter() .into_iter()는 handles 벡터의 소유권을 consuming iterator로 가져옵니다(copy가 아닌 move). 즉, 이후 handles는 더 이상 사용할 수 없습니다.
  • .map(|h| h.join().unwrap())의각 h는 JoinHandle 이고, h.join()은 이 스레드가 끝날 때까지 기다리고, .unwrap()으로 에러 무시하고 강제 추출합니다. .map(…) 부분은 4개의 스레드를 기다리며 각각의 계산된 합을 모아 [u64; 4] 형태로 만듭니다
  • .sum()은 [u64; 4]을 전부 더해서 최종 합계를 구해서, total에 할당합니다.
println!("합계: {}", total);
  • “합계: 500000500000″을 출력합니다.
    500000500000은 1000000 * 1000001 / 2 = 500000500000입니다.

나. Python (threading 사용, CPU 병렬 처리 실패 예)

import threading

data = list(range(1, 1_000_001))
results = [0] * 4

def worker(idx, chunk):
results[idx] = sum(chunk)

threads = []
chunk_size = len(data) // 4

for i in range(4):
t = threading.Thread(target=worker, args=(i, data[i*chunk_size:(i+1)*chunk_size]))
threads.append(t)
t.start()

for t in threads:
t.join()

print("합계:", sum(results))

🔸 설명:

  • Python은 GIL(Global Interpreter Lock) 때문에 진짜 병렬 아님
  • threading은 CPU 병렬 처리 불가 → 오히려 느림
  • multiprocessing을 쓰면 병렬 가능하지만 복잡도 증가

다. Java 버전

import java.util.concurrent.*;

public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(4);
int[] data = new int[1_000_000];
for (int i = 0; i < data.length; i++) data[i] = i + 1;

Future<Long>[] results = new Future[4];
int chunkSize = data.length / 4;

for (int i = 0; i < 4; i++) {
final int start = i * chunkSize;
final int end = (i + 1) * chunkSize;
results[i] = executor.submit(() -> {
long sum = 0;
for (int j = start; j < end; j++) sum += data[j];
return sum;
});
}

long total = 0;
for (Future<Long> result : results) {
total += result.get();
}

executor.shutdown();
System.out.println("합계: " + total);
}
}

🔸 설명:

  • ExecutorService를 통해 병렬 처리
  • 비교적 안전하지만, Future.get()이 blocking이며 예외 처리 필요
  • GC가 있어 런타임 예측 어려움

라. C++ 버전 (C++11 이상)

#include <iostream>
#include <vector>
#include <thread>
#include <numeric>

int main() {
std::vector<uint64_t> data(1'000'000);
std::iota(data.begin(), data.end(), 1);

uint64_t partial_sums[4] = {};
std::vector<std::thread> threads;

size_t chunk_size = data.size() / 4;

for (int i = 0; i < 4; ++i) {
threads.emplace_back([i, chunk_size, &data, &partial_sums]() {
partial_sums[i] = std::accumulate(
data.begin() + i * chunk_size,
data.begin() + (i + 1) * chunk_size,
0ULL
);
});
}

for (auto& t : threads) t.join();

uint64_t total = 0;
for (auto s : partial_sums) total += s;

std::cout << "합계: " << total << std::endl;
}

🔸 설명:

  • 고성능이지만, &data, &partial_sums는 데이터 경쟁 가능성 존재
  • 공유 자원 동기화에 실패하면 잘못된 결과 나올 수 있음
  • 동기화 도구 사용 시 성능 저하 우려

마. 언어별 동시성 비교

언어병렬 처리 성능안전성코드 복잡도주의사항
Rust매우 뛰어남컴파일 타임 안전 보장다소 복잡소유권, 라이프타임 이해 필요
Python낮음 (GIL)안전하나 느림간단multiprocessing 사용 시 복잡
Java중간런타임 에러 가능보통예외 처리, GC
C++고성능데이터 레이스 가능복잡직접 동기화 필요

6. Rust vs Go 동시성 비교 요약

항목RustGo
동시성 모델명시적 스레드 + 채널 + async/await경량 고루틴(goroutine) + 채널(channel)
메모리 관리수동 + 소유권 시스템 (GC 없음)GC 있음 (자동 메모리 관리)
안전성컴파일 타임에 데이터 경쟁 차단런타임에 데이터 레이스 가능 (race detector 필요)
런타임없음 (zero-cost abstraction)있음 (스케줄러 + GC)
학습 곡선가파름 (소유권/라이프타임 개념 필요)비교적 완만
성능매우 뛰어남 (GC 없음)빠르지만 GC 오버헤드 존재

7. 10개의 작업을 동시 실행 비교

가. Rust:

use std::thread;

fn main() {
let mut handles = vec![];

for i in 0..10 {
let handle = thread::spawn(move || {
println!("Rust 스레드 {} 실행", i);
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}
}

✅ 특징:

  • std::thread::spawn으로 OS 스레드 생성
  • 스레드 수 제한 없음 (하지만 무거움)
  • 안전하게 소유권 이동 (move) → 데이터 경쟁 없음

나. Go: 고루틴 10개 실행

package main

import (
"fmt"
"time"
)

func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Printf("Go 고루틴 %d 실행\n", i)
}(i)
}

time.Sleep(time.Second) // 고루틴이 끝날 시간 대기
}

✅ 특징:

  • go 키워드 하나로 병렬 실행
  • 고루틴은 스레드보다 훨씬 가볍고 수천 개 생성 가능
  • 단, 공유 자원 접근 시 데이터 레이스 가능 (예: i 변수 캡처 문제 발생 가능)

다. 동시성 핵심 차이

항목RustGo
실행 단위OS 스레드 또는 async task고루틴 (경량 스레드)
병렬 처리 수단스레드, 채널, async/await고루틴, 채널
데이터 보호컴파일 타임 소유권 체크뮤텍스, 채널, race detector
동시성 철학“Fearless Concurrency” (두려움 없는 동시성)“Do not communicate by sharing memory…”
GC없음 (직접 메모리 관리)있음 (자동 정리되지만 성능 오버헤드 발생 가능)

라. 성능과 안전성 비교

항목RustGo
성능GC가 없어서 시스템 자원 최대 활용GC와 스케줄러의 오버헤드 존재
안전성데이터 경쟁을 컴파일 타임에 방지기본적으로 가능함 (race detector로 검사해야 함)
스케일링수천 개의 작업 처리 시 async 필요수천 개 고루틴도 가볍게 처리 가능
디버깅 난이도복잡 (라이프타임, borrow checker 등)비교적 단순

마. 상황별 언어 선택

상황Rust 추천Go 추천
고성능 시스템 (e.g. 게임, 실시간 처리, WebAssembly)X
빠른 개발, 유지보수 쉬운 서버 (e.g. 웹 API, 클라우드 백엔드)가능하지만 무겁고 복잡매우 적합
메모리 제어 필요 (e.g. 임베디드, 드라이버)X
초고성능 네트워크 서버 (e.g. Actix, Tokio 기반)GC로 한계 가능
간단한 병렬 작업, CLI 툴한계

바. 결론

항목RustGo
성능최상급좋음 (하지만 GC 존재)
안전성컴파일 타임 보장런타임 race 가능
개발 속도어렵고 장벽 높음빠르고 쉬움
확장성 (스케일)async 사용 시 매우 뛰어남고루틴 덕분에 뛰어남
유지보수성복잡간단하고 명확

사. 요약

  • Rust: 동시성을 정밀하게 제어해야 하거나, 성능과 안전이 최우선인 경우 유리
  • Go: 빠르게 개발하고, 다수의 작업을 단순하게 병렬 처리할 때 탁월

Rust의 attribute 종류 및 의미

Rust에서의 attribute(속성)는 컴파일러에게 특정 코드에 대한 추가적인 정보를 제공하여, 코드의 컴파일 방식 또는 동작 방식을 제어하거나, 경고/에러 메시지를 제어하는 데 사용됩니다. derive, allow, warn, cfg, test 등 다양한 종류가 있습니다.

1. attribute의 적용 범위별 종류

Rust의 attribute는 크게 다음과 같은 종류로 나눌 수 있습니다:

종류형태 예시설명
Item attribute#[derive(Debug)]함수, 구조체 등 개별 항목에 적용
Crate attribute#![allow(dead_code)]크레이트 전체에 적용, 보통 파일 상단에 위치
Inner attribute#![cfg(test)]모듈 또는 크레이트 내부에 선언, 내부 항목에 영향을 줌
Outer attribute#[test]특정 항목(함수 등)에만 적용

2. 주요 attribute

가. 컴파일러 관련 attribute

Attribute설명예시
#[allow(…)]특정 경고를 무시#[allow(dead_code)]
#[warn(…)]경고를 표시 (기본값)#[warn(unused_variables)]
#[deny(…)]해당 사항이 있으면 컴파일 에러#[deny(missing_docs)]
#[forbid(…)]deny보다 강하게 재정의 불가#[forbid(unsafe_code)]

나. 파생(derive) 관련 attribute

Rust의 많은 기능은 trait을 자동으로 구현해주는 #[derive(…)]를 통해 사용합니다.

#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}

Debug, Clone, PartialEq 트레이트를 자동 구현합니다.

다. 조건부 컴파일 관련 attribute

Attribute설명예시
#[cfg(…)]조건에 따라 컴파일 여부 결정#[cfg(target_os = “windows”)]
#[cfg_attr(…)]조건이 참일 때에만 다른 attribute 적용#[cfg_attr(test, derive(Debug))]

[cfg_attr(test, derive(Debug))]는 테스트를 실행할 때만 Debug 트레이트를 자동 구현합니다.

라. 테스트/벤치마크 관련 attribute

Attribute설명예시
#[test]단위 테스트 함수임을 표시#[test]
fn test_add() { … }
#[bench]벤치마크 함수 표시 (불안정)#[bench]
fn bench_sort(…)
#[ignore]테스트 실행 시 무시됨#[test] #[ignore]
fn test_heavy() {}

마. 매크로 관련 attribute

Attribute설명예시
#[macro_use]외부 크레이트의 매크로를 현재 스코프로 불러옴 (Rust 2015 방식)#[macro_use]
extern crate log;
#[proc_macro]프로시저 매크로 정의프로시저 매크로 crate에서 사용됨

바. 기타 유용한 attribute

Attribute설명예시
#[inline], #[inline(always)]함수 인라인화 힌트#[inline(always)]
fn fast_fn() {}
#[repr(C)]구조체 메모리 레이아웃 C와 호환#[repr(C)]
struct MyStruct { … }
#[non_exhaustive]미래 확장을 위해 열거형 등의 exhaustive 검사 방지#[non_exhaustive]
enum MyEnum { … }
#[must_use]반환값을 사용하지 않으면 경고#[must_use]
fn compute() -> i32 { … }

※ 함수를 호출할 때 일반적으로는 함수 본문은 어딘가에 있고 호출하면 그 함수로 점프(jump)해서 실행한 뒤 다시 돌아옵니다. 하지만 인라인화는 이런 점프 없이 함수의 코드를 호출한 곳에 ‘복붙’하듯 직접 삽입하는 방식입니다.

사. #![…]과 #[…] 차이

구분형태설명
Inner attribute#![…]모듈, 크레이트 전체에 적용 (파일 상단에 위치)
Outer attribute#[…]특정 항목(함수, struct 등)에 적용

3. attribute 예제

가. #[derive(…)] — 트레이트 자동 구현

(1) 설명: 구조체(struct)나 열거형(enum)에 대해, 특정 트레이트의 기본 구현을 자동 생성합니다.

(2) 자주 사용되는 트레이트:

  • Debug : {:?} 포맷으로 출력 가능
  • Clone, Copy : 값 복사 가능
  • PartialEq, Eq : 값 비교 가능
  • PartialOrd, Ord : 정렬 가능
  • Hash : 해시 사용 가능

(3) 예제:

#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}

fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Clone 사용
println!("{:?}", p2); // Debug 사용
println!("p1 == p2? {}", p1 == p2); // PartialEq 사용
}

나. #[allow(…)], #[warn(…)], #[deny(…)], #[forbid(…)]

(1) 설명: 컴파일 경고/에러를 제어합니다. => 위 2.의 가. 컴파일러 관련 attribute 설명 참고

(2) 주요 옵션:

  • dead_code : 사용되지 않는 코드
  • unused_variables : 사용되지 않는 변수
  • non_snake_case : snake_case 규칙 위반

(3) 예제:

#[allow(dead_code)]
fn unused() {
println!("사용되지 않지만 경고 없음");
}

#[deny(unused_variables)]
fn foo() {
let x = 5; // 컴파일 에러 발생: 변수 사용 안 함
}

다. #[test], #[ignore] — 테스트 함수 지정

(1) 설명: 테스트 프레임워크를 위한 attribute입니다.

(2) 예제:

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn heavy_test() {
// 실행하려면 `cargo test -- --ignored`
}
}

라. #[cfg(…)] — 조건부 컴파일

(1) 설명: 플랫폼, 기능 등에 따라 코드 포함 여부 결정

(2) 예제:

#[cfg(target_os = "windows")]
fn run_on_windows() {
println!("Windows 전용 코드");
}

#[cfg(not(target_os = "windows"))]
fn run_on_unix() {
println!("Unix 계열 전용 코드");
}

(3) Cargo.toml에서 feature와 연계:

[features]
my_feature = []
#[cfg(feature = "my_feature")]
fn my_func() {
println!("my_feature가 활성화됨");
}

“my_feature”라는 기능(feature)이 활성화됐을 때만 my_func() 함수의 정의 자체를 컴파일에 포함시키고, 아니면 아예 없는 코드처럼 무시해버립니다.

마. #[inline] — 인라인 최적화 힌트

(1) 설명: 컴파일러에게 해당 함수를 인라인하도록 유도

(2) 예제:

#[inline(always)]
fn fast_add(a: i32, b: i32) -> i32 {
a + b
}

fn main() {
let x = add(3, 4);
}
  • 일반 호출: main에서 add 함수로 점프하고, 결과를 받아와서 x에 저장
  • 인라인: 컴파일할 때 add(3, 4)를 그냥 3 + 4로 바꿔서 main 함수 안에 넣어버림

바. #[repr(…)] — 메모리 레이아웃 제어

(1) 설명: 구조체/열거형의 메모리 정렬 방법을 지정(repr은 representation(표현)의 약어)

(2) 종류:

  • C: C 언어와 동일한 레이아웃
  • packed: 패딩 없이 압축
  • transparent: 단일 필드 감싸기

(3) 예제:

#[repr(C)]
struct MyStruct {
a: u8,
b: u32,
}

사. #[non_exhaustive] — 미래 확장을 위한 열거형

(1) 설명: 나중에 항목이 더 생길 수 있으니, “지금 있는 것만 가지고 match를 완벽하게 쓰지 마세요“라는 의미입니다.

(2) 예제:

#[non_exhaustive]
pub enum Error {
Io,
Parse,
}

이 코드는 다음과 같은 의미를 가집니다.

Error enum은 지금은 Io와 Parse 두 가지 variant만 있지만, 앞으로 새로운 variant가 추가될 수 있으므로 다른 크레이트에서는 이 enum을 match할 때 지금 있는 것만으로 열거하지 못하게 제한하는 것입니다.

그래서 #[non_exhaustive]를 붙이면, 사용자는 반드시 _를 이용한 default arm을 추가해야만 컴파일이 가능하며, 이것은 다른 크레이트가 match를 지금 있는 것만으로 exhaustively(완전하게) 작성하면, 나중에 enum에 새로운 variant를 추가했을 때 컴파일이 깨질 수 있기 때문입니다.

아. #[must_use] — 반환값 사용하지 않으면 경고

(1) 설명: 실수로 무시하면 안 되는 반환값을 경고로 알려줌

(2) 예제:

#[must_use]
fn important_result() -> Result<(), String> {
Err("Error".into())
}

fn main() {
important_result(); // 경고 발생!
}

위 코드는 important_result 함수의 반환 값을 사용하지 않아 warning(경고)이 발생하므로 아래와 같은 식으로 수정해야 합니다.

fn main() {
    let result = important_result(); // OK
    if let Err(e) = result {
        println!("에러 발생: {}", e);
    }
}

자. #[macro_use], #[macro_export] — 매크로 관련

(1) 설명: 외부 crate의 매크로를 가져오거나 내보낼 때 사용

(2) 예제:

// 외부 매크로 사용
#[macro_use]
extern crate log;

// 매크로 정의 및 export
#[macro_export]
macro_rules! hello {
() => {
println!("Hello, macro!");
};
}
  • #[macro_use] : 외부 크레이트 log에서 정의된 매크로(log!, info!, warn! 등)들을 이 파일 안에서 직접 사용할 수 있도록 가져오라는 의미로서, 예전 Rust 스타일 (Rust 2018 이전)의 방식이며, 최신 Rust (2018 edition 이후)에서는 use log::info; 식으로 가져오는 게 일반적입니다.
  • #[macro_export] : 이 매크로를 다른 모듈/크레이트에서 사용할 수 있게 공개(export) 하겠다는 의미입니다. 따라서, 이 매크로를 crate_name::hello!() 식으로 다른 crate에서도 쓸 수 있습니다.

Rust의 특별한 용어 정리

Rust에는 소유권, 참조, 라이프타임 등 고유한 용어들이 있는데 이외에도 생소하거나 중요한 용어인 variant, field, pattern, match arm, block, scope, associated type, attribute에 대해서 살펴 보겠습니다.

1. variant

  • 정의: enum에서 각각의 경우(상태/종류)를 의미.
  • 예시:
enum Color {
Red,
Blue,
Green,
}
let c = Color::Red; // Red는 Color 타입의 variant

여기서 Red, Blue, Green이 각각 variant이다.

2. field

  • 정의: struct(또는 enum의 variant)에 속하는 개별 데이터 항목.
  • 예시:
struct Point {
x: i32, // field
y: i32, // field
}
let p = Point { x: 1, y: 2 };

x와 y가 각각 field이고, i32는 각각의 field의 타입(type of the field 또는 필드형/필드 타입)입니다.

enum Message {
Quit, // 필드 없음
Move { x: i32, y: i32 }, // 필드 x, y가 있는 구조체 스타일
Write(String), // 이름 없는 튜플 스타일의 필드 String
ChangeColor(i32, i32, i32), // 이름 없는 튜플 스타일의 필드 i32
}

Message enum의 variant가 갖는 값 또는 구조가 곧 해당 variant의 “필드”입니다. 구조체 스타일인 경우는 필드 개념이 동일하며, 튜플 형식인 경우는 구조체와 달리 필드가 없지만 값이 필드가 됩니다.

enum은 이 필드(데이터) 덕분에, variant별로 타입에 따라 다양한 정보를 유연하게 표현할 수 있습니다.

3. pattern

  • 정의: 값을 구조적으로 분해 처리하기 위한 형태. match, let, 함수 매개 변수 등에서 사용합니다.
  • 예시:
// struct Point 정의
struct Point {
x: i32,
y: i32,
}

fn main() {
// 튜플 패턴 예제
let (a, b) = (1, 2);
println!("a = {}, b = {}", a, b);

// 구조체 및 구조체 패턴 매칭 예제
let p = Point { x: 10, y: 20 };
match p {
Point { x, y } => println!("({}, {})", x, y),
}
}

패턴을 이용해 복잡한 데이터를 쉽게 분해할 수 있다.

  • let (a, b) = (1, 2); => 오른쪽 (1, 2)는 타입이 (i32, i32)인 튜플이며, 이 값을 튜플로 받아서, 첫 번째 요소는 변수 a에, 두 번째 요소는 변수 b에 바인딩해줘”라는 뜻입니다.
  • match p {
    Point { x, y } => println!(“({}, {})”, x, y),
    } 에서
    Point { x, y }는 매칭 대상(p)이 해당 구조와 일치하는지 검사하고, 일치한다면 그 필드값을 변수로 분해해주는 패턴 역할을 합니다. 따라서 match에서 구조체 내부 값을 분해하고 싶으면 항상 이런 형태의 패턴을 사용하게 됩니다.

4. match arm

  • 정의: match 구문의 각분기(패턴 + 처리 블록).
  • 예시:
let n = 3;
match n {
1 => println!("One"), // arm 1
2 | 3 | 5 => println!("Prime"), // arm 2
_ => println!("Other"), // arm 3
}

각각이 match arm이며, 패턴과 실행할 코드 블록으로 구성되어 있다.

match arm에서의 패턴(pattern)은, 매칭 대상이 되는 값이 어떤 구조나 값을 가지고 있는지 비교하고, 해당 구조와 일치하면 그 arm의 코드를 실행하도록 하는 역할을 합니다.
즉, match 구문의 각 arm(갈래, 분기)은 패턴 => 실행코드 형태로 이루어지며, 패턴은 값의 형태를 설명하거나 내부 값을 분해하는 구조입니다

4. block

  • 정의: 중괄호 {}로 둘러싸인 코드 구역. 블록은 표현식이며 값과 타입을 가진다.
  • 예시:
let result = {
let x = 2;
x * x // 마지막 표현식이 결과값이 됨
};
// result = 4

블록 내에서 선언된 변수는 해당 블록에서만 유효하다.

5. scope

  • 정의: 변수나 아이템이 유효한 코드의 범위.
  • 예시:
fn main() {
let x = 10; // x는 main 함수 블록(scope)에서만 유효
}

scope이 끝나면 변수는 더 이상 쓸 수 없다.

가. block과 scope 비교

(1) block

  • 코드에서 중괄호 {}로 둘러싸인 부분 자체를 block이라고 합니다.
  • 예시:rust{ let a = 1; println!("{a}"); }
  • 이 부분 전체가 block입니다.

(2) scope

  • scope는 어떤 변수(또는 아이템)를 ‘볼 수 있고 사용할 수 있는 코드의 범위’입니다.
  • scope는 보통 block에 의해 결정되지만, 완전히 같지는 않습니다.
    • 모든 block은 새로운 scope를 열지만,
    • scope의 개념은 block 외에도 함수, 모듈, crate 등 더 넓거나 좁게 적용될 수 있습니다.

(3) 차이점 및 예제

  • block: { ... }로 감싸진 모든 코드 덩어리를 의미.
  • scope: 그 안에서 선언된 변수나 아이템이 유효한 코드의 범위.
  • 모든 block이 scope를 정의하지만, scope는 더 넓은 개념입니다.

예시:

fn main() {                      // main 함수 block, 여기가 scope 시작
let outer = 10; // 'outer'의 scope는 main 함수 전체

{ // 새로운 block 시작, 이 안이 block scope
let inner = 20; // 'inner'의 scope는 이 중괄호 안
println!("{}, {}", outer, inner);
} // inner는 여기서 scope 종료

println!("{}", outer); // ok
println!("{}", inner); // 에러, inner는 scope out!
}

여기서 inner는 작은 block(scope)에만 존재.
outer는 main block(scope) 전체에 존재.

6. associated type

  • 정의: 트레잇에서 타입 매개변수 대신 트레잇에 연결된 타입을 선언.
  • 예시:
trait Iterator {
type Item; // associated type

fn next(&mut self) -> Option<Self::Item>;
}

struct Counter;
impl Iterator for Counter {
type Item = i32;
fn next(&mut self) -> Option<i32> { /* 구현 */ None }
}

Iterator 트레잇에서 Item이 associated type이다.

가. 타입 매개변수와 associated type(연관 타입)의 정의

(1) 타입 매개변수:

  • generic에서 사용하는 타입 변수입니다.
  • 예: trait MyTrait<T> { … }에서 T가 타입 매개변수입니다.

(2) associated type(연관 타입)

  • 트레잇에서 선언해서, 트레잇을 구현할 때 구체적으로 지정하는 타입입니다.
  • trait Iterator { type Item; … }에서 Item이 이에 해당.

두 가지의 차이를 간단히 예시로 정리하면:

방법선언 방식사용 예시주의점
타입 매개변수trait MyTrait<T> { … }struct Foo;
impl MyTrait<u32> for Foo { … }
사용 때마다 타입 지정 필요
associated typetrait MyTrait { type Output; … }struct Bar;
impl MyTrait for Bar { type Output = u32; }
트레잇 구현이 타입 결정

나. 예시

(1) 타입 매개변수(Generic parameter)를 사용하는 트레잇

// 타입 매개변수를 사용하는 트레잇
trait MyTrait<T> {
fn foo(&self, x: T);
}

// 해당 트레잇을 구현하는 타입 예시
struct MyType;

impl MyTrait<i32> for MyType {
fn foo(&self, x: i32) {
println!("MyTrait<i32>::foo called with x = {}", x);
}
}

fn main() {
let t = MyType;
t.foo(100);
}

실행 결과:

MyTrait<i32>::foo called with x = 100

(2) Associated Type(연관 타입)을 사용하는 트레잇 예시

// 연관 타입을 사용하는 트레잇
trait AnotherTrait {
type Output;
fn bar(&self) -> Self::Output;
}

// 해당 트레잇을 구현하는 타입 예시
struct MyType;

impl AnotherTrait for MyType {
type Output = i32;
fn bar(&self) -> i32 {
42
}
}

fn main() {
let t = MyType;
let v: i32 = t.bar();
println!("AnotherTrait::bar returned {}", v);
}

실행 결과:

AnotherTrait::bar returned 42

7. attribute

  • 정의: 컴파일러에게 부가 정보를 주는 메타데이터. #[…] 또는 #![…] 형태.
  • 예시:
#[derive(Debug)]
struct MyStruct;

#[cfg(target_os = "linux")]
fn linux_only() {}

#[allow(dead_code)]
fn unused_function() {}

주로 코드 자동 생성(derive), 조건부 컴파일(cfg), 경고 제어 등에 사용된다.

attribute에 대해서는 다음 편에서 더 자세히 다룰 예정입니다.

웹 페이지를 작업표시줄에 고정하기

웹 페이지를 작업표시줄에 추가하는 방법을 알아보겠습니다.

그동안에는 주소 표시줄(Address Bar, URL Bar)의 별표 아이콘을 눌러 URL을 북마크에 추가하는 것만 알고 있었는데,

URL을 작업 표시줄에 고정하는 기능이 있어서 공유하고자 합니다.

크롬과 엣지로 나눠서 알아보고, 작업 표시줄에서 제거하는 방법도 살펴보겠습니다.

버전에 따라 다를 수 있으므로 크롬 버전은 138.0.7204.101이고, 엣지 버전은 138.0.3351.95입니다.

1. URL을 작업 표시줄에 고정

가. 크롬

원하는 URL, 여기서는 perplexity.ai에 접속한 후
오른쪽 위 세로 3점을 누르고, ‘전송, 저장, 공유’를 누른 다음, ‘페이지를 앱으로 설치’ 메뉴를 누릅니다.

그러면 아래와 같은 창이 열리는데, 이름을 수정할 수 있는데, 굳이 수정할 필요없으므로 설치 버튼을 누릅니다.

그러면 아래와 같이 기존 크롬 창과는 별도의 창에 perplexity.ai가 열리고,

작업 표시줄에 perplexity.ai 아이콘이 배치되는데,

마우스 오른쪽 버튼을 누른 후 ‘작업 표시줄에 고정’을 누르면 앱이 고정됩니다.

나. 엣지

엣지는 보다 직관적입니다.
오른쪽 위 가로 세 점을 누른 후 기타 도구 > ‘작업 표시줄에 고정’ 메뉴를 누르면

아래와 같이 ‘작업 표시줄에 고정할 것인지’를 묻는데, 예를 선택하면

아래와 같이 바로 작업 표시줄에 고정됩니다.

2. 작업 표시줄에서 제거하기

가. 크롬

크롬에서 고정한 앱을 제거하기 위해 마우스 오른쪽 버튼을 누른 후 ‘작업 표시줄에서 제거’를 눌러도

세로 3점을 누르고, ‘전송, 저장, 공유’를 누르면, ‘Perplexity에서 열기’만 보이고, ‘페이지를 앱으로 설치’ 메뉴는 보이지 않습니다. 앱이 지워지지 않았다는 것입니다.

따라서, 앱을 지우려면 주소 표시줄에 chrome://apps라고 입력하고 엔터키를 누르면 아래와 같이 앱 목록이 보이는데, 여기서 Perplexity 앱을 마우스 오른쪽 버튼을 누른 후

제거 메뉴를 눌러야 합니다.

그리고, perplexity.ai 사이트로 이동한 후 세로 3점을 누르고, ‘전송, 저장, 공유’를 누르면, ‘페이지를 앱으로 설치’ 메뉴가 보입니다.

나. 엣지

엣지로 작업 표시줄에 고정한 것은 크롬처럼 앱에서 찾아서 지울 필요없이 ‘작업 표시줄에서 제거’만 하면 바로 지워집니다.

가로 3점을 누른 후 확인해 보면 ‘작업 표시줄에 고정’이 활성화되어 있습니다.

Rust에서 사용하는 기호와 연산자

Rust의 기호(=>, ::, ., -> 등)와 연산자(산술, 비교, 사칙연산자, 논리 연산자, 패턴 매치 연산자 등) 는 종류도 많고 다른 언어와 다른 것도 있어 많이 헷갈리므로 이것에 대해 전체적으로 알아보겠습니다.

Ⅰ. 기호

1. =>

match 문에서 패턴 매핑 결과를 지정할 때 사용합니다.

let num = 2;
match num {
1 => println!("one"),
2 => println!("two"),
_ => println!("other"),
}

위에서 2 => println!(“two”)는 num이 2일 때 실행됩니다.

2. ::

경로(네임스페이스) 구분자. 모듈, 구조체, 열거형, 연관함수, 상수 등에 접근할 때 사용합니다.

let color = Color::Red;
let s = String::from("hello");

Color 열거형의 Red variant, String 타입의 from 연관 함수에 접근합니다.

3. .

필드 접근하거나 메서드를 호출할 때 사용합니다.

let s = String::from("hi");
let len = s.len(); // 메서드 호출
let point = (3, 4);
let x = point.0; // 튜플의 첫 번째 요소

s.len()은 s 객체의 len 메서드를, point.0은 튜플의 첫번째 값을 뜻합니다.

4. ->

함수 또는 클로저의 반환 타입을 지정할 때 사용합니다.

fn add_one(x: i32) -> i32 {
x + 1
}

이 함수는 매개변수 x를 받아 i32 타입으로 결과를 반환합니다.

5. :

변수의 타입을 명시하거나 패턴 매칭에서 사용됩니다.

let score: i32 = 100;

변수 score의 타입이 i32임을 명시합니다.

아래는 패턴 매칭에서 :이 사용된 예입니다.
Point { x, y: 0 }에서 y: 0은 y 필드가 정확히 0일 때 매칭됨을 의미합니다.

struct Point { x: i32, y: i32 }

fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => println!("On neither axis: ({x}, {y})"),
}
}

6. ;

구문(문장)의 끝을 표시합니다.

let x = 5;
println!("{}", x);

7. ,

목록(튜플, 인수 등)을 구분할 때 사용합니다.

rustlet point = (3, 4);
fn add(x: i32, y: i32) -> i32 { x + y }

8. ..

범위를 표현할 때 사용합니다.

for i in 1..5 {
println!("{}", i); // 1, 2, 3, 4 출력 (5는 포함X)
}

..는 시작값 이상, 끝값 미만의 범위를 의미하며, 끝값을 포함할 때는 ..=으로 사용합니다.

9. &

참조(Reference)를 의미합니다.

let x = 3;
let y = &x;

y는 x의 참조를 가집니다 (메모리 주소 공유).

10. *

역참조(Dereference)를 의미합니다.

let x = 5;
let y = &x;
println!("{}", *y); // y가 참조하는 실제 값(x) 출력

*y는 y가 가리키는 값을 가져옵니다.

11. @

의미: 패턴 매칭에서 값 바인딩

let v = Some(10);
if let id @ Some(x) = v {
println!("id: {:?}", id);
}

id @ Some(x)는 Some(10) 전체를 id에 바인딩합니다.

Ⅱ. 연산자

1. 산술 연산자

연산자설명예시결과 설명
+덧셈let a = 10 + 5;a는 15
뺄셈let b = 10 – 5;b는 5
*곱셈let c = 10 * 5;c는 50
/나눗셈let d = 10 / 2d는 5
%나머지let e = 10 % 3e는 1

2. 비교(관계) 연산자

연산자설명예시결과
==같다a == btrue 또는 false
!=같지 않다a != btrue 또는 false
>크다a > btrue 또는 false
<작다a < btrue 또는 false
>=크거나 같다a >= btrue 또는 false
<=작거나 같다a <= btrue 또는 false

3. 논리 연산자

연산자설명예시결과
&&논리 AND(a > 1 ) && (b < 5)둘 다 true면 true
<code>||</code>논리 OR`(a == 1)
!논리 NOT!is_validtrue->false, false->true

4. 비트 연산자

연산자설명예시결과
&비트 ANDa & b각 비트 AND
<code>|</code>비트 OR`ab`
^비트 XORa ^ b각 비트 XOR
!비트 NOT!a각 비트 반전
<<왼쪽 시프트a << 2비트를 왼쪽 이동
>>오른쪽 시프트a >> 1비트를 오른쪽 이동

5. 복합 할당 연산자

연산자설명예시
+=더해서 할당a +=1;
-=빼서 할당b -=2;
*=곱해서 할당c *= 3;
/=나눠서 할당d /= 2;
%=나머지를 할당e %= 4;

※ Rust는 ++와 –(증가/감소 연산자)를 지원하지 않습니다.

6. 기타 연산자

가. as: 타입 변환

let x: f32 = 10 as f32;

나. 단항 부정 연산자

-a : a의 음수
!a : a의 반전

Ⅲ. 패턴 매칭 관련 연산자 및 문법

match는 여러 패턴에 따라 코드를 분기할 수 있는 핵심 문법으로, C 계열의 switch보다 다양하고 강력한 매칭을 제공합니다.

let value = Some(7);
match value {
Some(x) if x > 5 => println!("greater than five: {}", x),
Some(x) => println!("got: {}", x),
None => println!("no value"),
}

1. | (or 패턴 연산자)

여러 패턴을 한 번에 처리할 수 있습니다.

let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("other"),
}
  • 이 예시에서 1 | 2는 x가 1 또는 2일 때 모두 해당 arm을 실행합니다.

2. _ (와일드카드/전부수용 패턴)

모든 값을 의미하며, 사용하지 않을 값을 무시할 때 씁니다.

match some_value {
1 => println!("one"),
_ => println!("other"),
}

3. @ (패턴 바인딩 연산자)

패턴과 동시에 값을 바인딩할 때 사용합니다.

let v = Some(42);
match v {
id @ Some(n) if n > 40 => println!("big! {:?}", id),
_ => println!("other"),
}
  • id @ Some(n)은 Some(42) 전체를 id에 바인딩하면서 n 값도 패턴 매칭합니다.

4. if let

특정 패턴만 처리하고 나머지는 무시하고 싶을 때 간결하게 쓸 수 있는 문법입니다.

if let Some(x) = option {
println!("have value: {}", x);
}

5. while let

while let은Rust에서 반복문과 패턴 매칭을 결합해, 어떤 값이 특정 패턴에 계속 일치하는 동안만 루프를 실행하는 구문입니다. 보통 Option, Result, Iterator 등에서 값을 꺼내거나 처리할 때 매우 자주 사용됩니다.

가. Stack처럼 값 꺼내기

let mut stack = vec![1, 2, 3];

while let Some(top) = stack.pop() {
println!("스택에서 꺼냄: {}", top);
}
  • 벡터에서 값을 꺼내는 동작이지만, 동시에 벡터를 스택(LIFO)처럼 쓰는 대표적인 패턴이라 “스택처럼 값 꺼내기”라고 표현한 것입니다.
  • stack.pop()이 벡터의 마지막 요소를 꺼내서(Some) 반환합니다. 벡터가 비어 있다면 None을 반환합니다. Vec은 동적 배열이지만, pop은 이를 스택처럼 사용하게 해 줍니다.
  • 값이 없을 때 None이 반환되고 루프가 끝납니다.

나. Option을 이용한 카운팅

let mut optional = Some(0);

while let Some(i) = optional {
if i > 9 {
println!("9보다 커서 종료!");
optional = None;
} else {
println!("현재 값: {}", i);
optional = Some(i + 1);
}
}
  • optional이 Some(i)에 매칭되는 동안 루프 실행.

다. Result 타입, Iterator 등에도 활용

let mut results = vec![Ok(1), Err("Error"), Ok(2)];

while let Some(res) = results.pop() {
match res {
Ok(v) => println!("ok: {}", v),
Err(e) => println!("err: {}", e),
}
}
  • Result값을 반복적으로 처리하다가 벡터가 비면 자동 종료.

라. while let, if let, match 비교

구문실용 상황특징와 차이점
while let값이 반복적으로 패턴에 매칭될 때패턴 매칭이 실패하면 자동으로 루프 종료
if let단일 조건만 한 번 검사할 때한 번만 검사, 반복X
match모든 경우의 수를 분기 처리할 때모든 possibility처리, 반복X

Funtion, Method and Associated Functions

Function은 특정 구조체나 인스턴스와 관계없이 독립적으로 정의되는데, Method는 구조체(또는 enum) 인스턴스의 상태를 읽거나 변경하는 동작을 구현하며, Assosiated Functions는 특정 타입과 연결되어 있지만 해당 타입의 인스턴스와는 독립적인 함수를 의미합니다.

1. 함수와 메소드의 차이점

가. 함수 (Function)

  • 특정 구조체나 인스턴스와 관계없이 독립적으로 정의됩니다.
  • fn 키워드를 사용해 선언하며, 어디서든 호출할 수 있습니다.
  • 호출 시 add(3, 5)처럼 함수 이름만 사용합니다.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

나. 메소드 (Method)

  • 구조체(struct), 열거형(enum) 등 특정 타입의 impl 블록 안에서 정의됩니다.
  • 첫 번째 파라미터로 반드시 self, &self, &mut self 중 하나를 받습니다.
  • 인스턴스를 통해서만 호출할 수 있으며, rect.area()처럼 점(.) 연산자를 사용합니다.
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

이처럼 Rust에서는 함수는 독립성, 메소드는 인스턴스와의 종속성이 가장 큰 차이입니다.

2. 함수와 메소드의 차이점

가. 함수(Function)

(1) 장점

  • 독립적이어서 다양한 곳에서 재사용하기 쉽고, 유닛 테스트가 간편합니다.
  • 간단한 연산이나 유틸리티 작업에 적합합니다.
  • 이름만으로 호출하므로 사용이 직관적입니다.

(2)

  • 구조체 등 데이터 타입과 직접적으로 연관된 행동을 묶어 표현하기 어렵습니다.
  • 많은 함수가 생기면 네임스페이스 충돌이나 코드의 구조적 복잡도가 증가할 수 있습니다.
  • 데이터 캡슐화와 추상화에서 약점이 있습니다.

나. 메소드(Method)

(1)장점

  • 데이터와 행위의 결합(캡슐화): 구조체의 로직과 동작을 impl 블록에 모아, 객체지향적 코드를 짤 수 있습니다.
  • 가독성: 인스턴스.메소드(…) 형태로 표현해 자연스럽고 읽기 쉬운 코드를 만듭니다.
  • 확장성: 타입별로만 동작을 추가하거나 오버라이드하는 데 용이합니다.

(2) 단점

  • 인스턴스가 있어야만 호출 가능합니다. 즉, 독립적으로 동작할 수 없는 경우가 많습니다.
  • 객체의 상태 변경이 필요하다면 &mut self 등으로 가변 참조를 써야 하므로, 소유권·빌림 규칙에 주의를 기울여야 합니다.
  • 단순 기능(예: 수학 연산 등)에는 불필요하게 복잡합니다.
구분장점단점
함수독립성, 재사용성, 간결함, 테스트 용이성데이터 캡슐화 불가, 네임스페이스 충돌 위험
메소드데이터와 로직 결합, 캡슐화, 가독성, 타입별 동작 확장 용이인스턴스 필요, 소유권·참조 규칙 신경써야, 불필요한 복잡성 가능

Rust에서는 함수는 독립적이고 범용적인 동작에, 메소드는 인스턴스와 연관된 행동에 사용하는 것이 일반적입니다.

2. 메소드와 연관 함수의 차이점

가. 메소드

  • 메서드는 반드시 첫 번째 인자로 self, &self 또는 &mut self를 받습니다.
  • 구조체 인스턴스를 통해 점(.) 연산자로 호출합니다.
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };  

    // 메소드 호출
    println!("사각형의 면적: {}", rect.area());
}

나. 연관 함수

  • 연관 함수는 self를 인자로 받지 않습니다.
  • 타입 이름을 통해 이중 콜론(::) 연산자로 호출합니다.
  • 주로 생성자 역할(예: new)로 사용되고, 구조체 등의 유틸리티 함수로도 사용됩니다.

(예제 1 – 생성자)

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}

fn main() {
    // 연관 함수 사용
    let rect = Rectangle::new(30, 50);
    println!("사각형의 면적: {}", rect.area());    
}

fn new의 반환 타입이 Rectange 구조체입니다.

(예제 2 – 유틸리티 함수)

struct Circle {
    radius: f64,
}

impl Circle {
    // 연관 함수 (유틸리티 함수)
    fn area(radius: f64) -> f64 {
        std::f64::consts::PI * radius * radius
    }
}

fn main() {
    // 연관 함수 호출 (유틸리티 함수)
    let circle_area = Circle::area(5.0);
    println!("Circle area: {}", circle_area);
}

메소드와 달리 Circle::area(5.0) 처럼 ::을 붙여 호출합니다.

위 코드를 실행하면
Circle area: 78.53981633974483라고 처리 결과가 제대로 표시되지만

“구조체 Circle안의 radius 필드가 사용되지 않았다”는 경고가 표시됩니다.


다시 말해 연관 함수의 인수 radius는 이름은 같지만 구조체의 radius가 아닙니다. 또한 Circle이란 인스턴스를 생성하지 않고도 원의 면적을 구했습니다.

3. 메소드와 연관 함수의 장단점

가. 메서드

(1) 사용 목적

  • 구조체(혹은 enum) 인스턴스의 **상태(필드)**를 읽거나 변경하는 동작을 구현합니다.
  • 객체의 행동을 정의하고, 객체의 데이터와 직접 상호작용합니다.

(2) 장점

  • 캡슐화: 인스턴스의 데이터를 안전하게 다루며, 객체의 상태를 직접 관리할 수 있습니다.
  • 가독성:rect.area()처럼 객체의 행동을 자연스럽게 표현할 수 있습니다.
  • 유지보수성: 객체의 동작이 구조체 내부에 모여 있어 코드 관리가 용이합니다.

나. 연관 함수

(1) 사용 목적

  • 구조체의 생성자 역할(예: new)이나, 인스턴스와 무관한 동작(예: 유틸리티 함수)을 구현합니다.
  • 인스턴스 없이 타입 자체에 대해 동작하는 기능을 제공합니다.

(2) 장점

  • 유연성: 인스턴스가 없어도 타입 이름으로 직접 호출할 수 있습니다(Rectangle::new() 등).
  • 명확성: 생성자나 특정 타입 관련 기능을 명확하게 분리할 수 있습니다.
  • 재사용성: 인스턴스와 무관한 기능을 여러 곳에서 활용할 수 있습니다.

다. 요약

  • 메서드는 객체의 상태와 밀접하게 연관된 동작을 구현하며, 객체지향적 설계와 데이터 캡슐화에 강점을 가집니다.
  • 연관 함수는 객체 생성이나 타입 자체와 관련된 기능을 제공하며, 인스턴스가 필요 없는 동작을 분리해 코드의 명확성과 재사용성을 높입니다.

Rust의 Slice와 컬렉션 참조

Rust의 slice(슬라이스)는 배열, 벡터, 문자열 등 메모리상에 연속적으로 저장된 데이터의 일부 구간을 참조하는 타입이고, 컬렉션 참조는 컬렉션(배열 포함) 전체에 대한 참조입니다. 그러나, 모두 소유권을 갖지 않고 데이터를 참조하는 것은 같지만, 그 목적과 내부 구조, 사용 방식에 중요한 차이가 있습니다.

1. 슬라이스(Slice)

가.기본 개념

  • 슬라이스는 배열, 벡터, 문자열 등 메모리상에 연속적으로 저장된 데이터의 일부 구간을 참조하는 타입으로, &[T] 또는 &str(문자열 슬라이스)와 같이 참조 형태로 표현된다.
  • 슬라이스는 원본 데이터의 소유권을 이동시키지 않으며, 원본 데이터가 유효할 때만 사용할 수 있다.
  • 슬라이스는 연속된 메모리 블록의 시작 주소와 길이 정보를 함께 저장하는 fat pointer(두 개의 값: 포인터 + 길이)이다.
  • 슬라이스는 항상 연속적인 데이터만 참조할 수 있습니다.

나.문법과 사용법

  • 슬라이스는 [start..end] 범위 문법을 사용합니다.
    • start는 포함, end는 포함하지 않습니다(즉, 반 열린 구간).
      let a = [1, 2, 3, 4, 5];
      let slice = &a[1..4]; // [2, 3, 4]
  • start나 end를 생략하면 처음이나 끝을 의미합니다.
    &a[..3] → 처음부터 3번째 전까지
    &a[2..] → 2번째부터 끝까지(끝 포함)
    &a[..] → 전체

다. 문자열 슬라이스

  • 문자열 슬라이스는 String이나 문자열 리터럴의 일부를 참조하며 타입은 &str 입니다.
  • 문자열 일부 참조
    let s = String::from(“hello world”);
    let hello = &s[0..5]; // “hello”
    let world = &s[6..11]; // “world”
  • 문자열 리터럴 
    let s = “hello, world”;     // &’static str
    let part: &str = &s[0..5]; // “hello” 부분만 참조
    “hello” 자체도 &str 타입의 슬라이스이므로, 문자열 리터럴의 일부도 참조할 수 있습니다.

라. 특징과 주의점

  • 슬라이스는 참조이므로, 원본 데이터가 유효해야 한다.
    원본 데이터가 변경(예: String의 clear 등)되면 기존 슬라이스는 무효화되어 컴파일 에러가 발생합다.
  • 슬라이스는 소유권과 라이프타임 개념을 이해하는 데 중요한 역할을 합니다. -> 아래 ‘3. 슬라이스와 라이프타임’에서 설명
  • 슬라이스는 함수의 파라미터로 자주 사용됩니다.
fn main() {
    let s = "hello, world"; 
    println!("First two elements: {:?}", first_two(s));
}

fn first_two(slice: &str) -> &str {
    if slice.len() < 2 {
        panic!("Slice must have at least two elements");
    }
    &slice[0..2]
}

위 코드에서 s는 문자열 리터럴인 문자열 슬라이스이므로 함수로 넘길 때는 &를 안붙여도 되지만(붙여도 됨), 함수에서 받을 때는 타입을 &str으로 지정하고, 반환도 &slice[0..2]이므로&str 타입으로 지정해야 합니다.

실행 결과는 First two elements: “he”입니다.

2. 컬렉션 참조

가. 개념

  • 컬렉션 참조는 컬렉션 전체에 대한 참조입니다. 예를 들어, &Vec, &String, &[T; N] 등입니다. 여기서 &[T; N]은 배열 전체에 대한 참조입니다. 엄격하게 말하면 배열은 컬렉션은 아니지만 “배열 전체를 참조한다”는 측면에서 컬렉션 참조에 포함해서 설명합니다.
  • 이 참조는 해당 컬렉션 타입의 모든 메서드와 속성을 사용할 수 있게 해줍니다.
  • 컬렉션 참조는 그 자체가 소유한 데이터 전체를 가리키며, 슬라이스처럼 부분만을 가리키지는 않습니다.
  • 예시:
    let v = vec![1, 2, 3];
    let r = &v; // Vec 전체에 대한 참조

벡터 컬렉션 참조의 간단한 예시는 다음과 같습니다.

fn main() {
let v = vec![100, 32, 57]; // Vec<i32> 생성
// 벡터에 대한 불변 참조를 사용하여 각 요소 출력
for i in &v {
println!("{}", i);
}
}
  • 여기서 &v는 Vec 타입의 벡터에 대한 불변 참조(&Vec)입니다.
  • for i in &v 구문은 벡터의 각 요소에 대한 참조(&i32)를 순회하며 안전하게 접근합니다.
  • 벡터 자체를 소유하지않고 참조만 하므로, 함수 인자나 반복문에서 자주 활용됩니다.

나. 예제

fn print_array(arr: &[i32; 3]) {
println!("{:?}", arr);
}

fn main() {
let data = [10, 20, 30];
print_array(&data); // &[i32; 3] 타입의 참조 전달
}
  • 위 예제에서 print_array 함수는 크기가 3인 i32 배열에 대한 참조(&[i32; 3])를 인자로 받습니다.
  • &data는 [i32; 3] 타입 배열 전체에 대한 참조입니다.

다. 슬라이스와 주요 차이점 비교

구분슬라이스
(&[T], &str)
컬렉션 참조 (&Vec<T>, &String)
대상컬렉션의 일부(연속 구간)컬렉션 전체
내부 구조포인터 + 길이 (fat pointer)포인터 (컬렉션 구조체 전체)
메서드 사용슬라이스 관련 메서드만 사용 가능컬렉션의 모든 메서드 사용 가능(벡터의 len(), push() 등
용도부분 참조, 함수 인자 등전체 참조, 컬렉션의 메서드 활용
동적 크기길이가 런타임에 결정됨컬렉션 타입에 따라 다름
예시&arr[1..3], &vec[..], &s[2..]&vec, &arr, &String

&[T; N]은 배열 전체를 참조하고, &[T]는 슬라이스로, 배열의 일부 구간(혹은 전체)을 참조할 수 있습니다.
예를 들어, &arr[1..3]은 &[T] 타입(슬라이스)이지만, &arr은 &[T; N] 타입(컬렉션 참조)입니다. 이처럼 &[T; N]은 정확히 N개 원소를 가진 배열 전체에 대한 참조입니다.

또한 &str과 &String은 문자열 슬라이스와 문자열 참조로 다르지만, &String이 &str으로 자동 변환이 가능하기 때문에 함수 인자로는 &str이 더 범용적이고 권장됩니다.

3. 슬라이스와 라이프타임

가. 개념

Rust에서 라이프타임(lifetime)은 참조가 유효한 기간을 명확하게 지정하는 개념으로, 참조의 유효성에 직접적인 영향을 미칩니다. 라이프타임이 중요한 이유와 그 영향은 다음과같습니다.

  • 댕글링 참조 방지
    라이프타임의 가장 큰 목적은 댕글링 참조(dangling reference)를 방지하는 것입니다. 댕글링 참조란, 이미 스코프를 벗어나 소멸된 데이터를 참조하는 상황을 의미합니다. Rust는 각 참조자의 라이프타임을 추적하여, 참조가 원본 데이터보다 오래 살아남을 수 없도록 컴파일 타임에 강제합니다.
  • 안전한 메모리 접근 보장
    라이프타임을 통해 참조자가 항상 유효한 메모리만 가리키도록 보장합니다. 예를 들어, 아래와 같은 코드는 컴파일되지 않습니다.

    let r; // r의 라이프타임 시작
    {
    let x = 5; // x의 라이프타임
    r = &x; // r이 x를 참조
    } // x의 라이프타임 종료, r은 더 이상 유효하지 않음
    println!(“{}”, r); // 컴파일 에러!
    Rust는 위 코드에서 r이 x보다 오래 살아남으려 하므로 컴파일 에러를 발생시킵니다.
  • 함수와 구조체에서의 명확한 라이프타임 관계
    여러 참조가 얽히는 함수나 구조체에서는, 각각의 참조가 얼마나 오래 유효해야 하는지 명확히 지정해야 합니다. 라이프타임 파라미터를 명시함으로써, 함수가 반환하는 참조가 입력 참조자 중 어느 것과 연관되어 있는지 컴파일러가 알 수 있습니다.
  • 암묵적 추론과 명시적 지정
    대부분의 경우 Rust는 라이프타임을 자동으로 추론하지만, 여러 참조가 얽히거나 복잡한 관계가 있을 때는 명시적으로 라이프타임을 지정해야 합니다. 이를 통해 참조 유효성에 대한 컴파일러의 보장이 더욱 강력해집니다.

나. 예제 1

Rust에서 슬라이스의 라이프타임이 중요한 이유를 보여주는 대표적인 예제는, 두 개의 문자열 슬라이스 중 더 긴 쪽을 반환하는 함수입니다. 이 예제를 통해 슬라이스의 라이프타임을 명확히 지정하지 않으면 컴파일 에러가 발생하고, 올바르게 지정하면 안전하게 참조를 반환할 수 있음을 알 수 있습니다.

fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

// 라이프타임 명시가 반드시 필요!
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
  • 이 함수 시그니처에서 ‘a라는 라이프타임 파라미터를 명시했습니다.
  • 이 뜻은, x와 y 모두 최소한 ‘a만큼 살아 있어야 하며, 반환되는 슬라이스도 ‘a만큼 살아 있어야 한다는 의미입니다.
  • 실제로 함수가 호출될 때, Rust는 x와 y의 라이프타임 중 더 짧은 쪽에 맞춰 반환값의 라이프타임을 제한합니다. 따라서, ‘a와 ‘b를 사용해서
    fn longest<‘a, ‘b>(x: &’a str, y: &’b str) -> &’a str
    라고 하면 x가 반환될 수도 있고 y가 반환될 수도 있기 때문에 에러가 발생합니다.
  • 또한 라이프타임을 명시하지 않아도, Rust는 반환되는 참조가 입력 참조자 중 어느 쪽과 연관되어 있는지 알 수 없어 컴파일 오류가 발생합니다. 따라서, 라이프타임을 ‘a로 통일한 것입니다.

이처럼, 슬라이스의 라이프타임을 명확히 지정하지 않으면 댕글링 참조가 생길 수 있고, Rust는 이를 컴파일 타임에 방지합니다.
따라서 라이프타임 명시는 슬라이스가 안전하게 사용될 수 있는 핵심적인 장치입니다.

위 코드에서 as_str()은 String 타입을 &str 타입으로 변환하는 메소드입니다.

다. 예제2

fn main() {
let result;
{
let string1 = String::from("abc");
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
// string1, string2는 여기서 소멸
}
println!("The longest string is {}", result); // 컴파일 에러!
}
  • 위 코드는 result가 내부 스코프의 데이터인 string1 또는 string2의 슬라이스를 참조하게 되므로, 컴파일 에러를 발생시켜 댕글링 참조를 차단합니다.