Page

관련 포스팅

2023년 9월 12일 화요일

Week01 - Rust Code01 분석 (Arrays, Numbers, Tuple, Vectors, 그리고 trait)


Week01에 배운 기초적인 내용을 기반으로 코드 분석에 들어가보자.

우선 rust는 새롭게 프로젝트를 시작할 때마다 cargo로 만들어줘야 한다. 그래야 src 폴더 및 toml 파일이 생기게 된다.

TOML이란?


TOML은 "Tom's Obvious, Minimal Language"의 약자다. 이것은 설정 파일을 위한 데이터 직렬화 언어로, 쉽게 읽고 쓸 수 있도록 설계되었다. TOML은 주로 Rust의 패키지 관리 도구인 Cargo에서 Cargo.toml 파일 형식으로 사용된다.

TOML 파일은 다양한 데이터 타입과 복잡한 데이터 구조를 표현할 수 있으며, 주석도 쉽게 추가할 수 있다. 기본적으로 키-값 쌍을 사용하고, 중괄호나 대괄호 등은 사용하지 않는다.

예를 들어, 간단한 TOML 파일은 다음과 같이 생겼다:

# 이것은 주석이다

[package]
name = "my_project"
version = "0.1.0"

[dependencies]
serde = "1.0"



여기서 [package]과 [dependencies]는 섹션을 나타내며, 그 아래의 name = "my_project"나 version = "0.1.0" 등은 키-값 쌍이다. 이렇게 해서 설정 정보나 패키지 의존성 등을 간단하게 표현할 수 있다.


main.rs

// "arrays"라는 이름의 모듈을 현재 파일에서 사용한다고 선언한다.
mod arrays;
// "numbers"라는 이름의 모듈을 사용한다고 선언한다.
mod numbers;
// "tuple_structs"라는 모듈을 사용한다고 선언한다.
mod tuple_structs;
// "tuples"라는 모듈을 사용한다고 선언한다.
mod tuples;
// "vectors"라는 모듈을 사용한다고 선언한다.
mod vectors;

// main 함수를 정의한다. 프로그램 실행 시 가장 먼저 호출되는 함수이다.
fn main() {
    // 콘솔에 "Hello, world!"를 출력한다.
    println!("Hello, world!");
    // "numbers" 모듈의 `f` 함수를 2를 인자로 호출하고 그 결과를 출력한다.
    println!("The square of 2 is {}", numbers::f(2));
    // "numbers" 모듈의 `run` 함수를 호출한다.
    numbers::run();
    // "tuples" 모듈의 `run` 함수를 호출한다.
    tuples::run();
    // "tuple_structs" 모듈의 `run` 함수를 호출한다.
    tuple_structs::run();
    // "arrays" 모듈의 `run` 함수를 호출한다.
    arrays::run();
    // "vectors" 모듈의 `run` 함수를 호출한다.
    vectors::run();
}

// `hello`라는 새로운 함수를 정의한다.
fn hello() {
    // 이 함수에서도 "Hello, world!"를 출력한다.
    println!("Hello, world!");
}

// `triple`이라는 함수를 정의하고, 가변 참조의 벡터를 인자로 받는다.
fn triple(v: &mut Vec<i32>) {
    // 벡터 `v`의 각 요소에 대해 반복한다.
    for x in v {
        // 벡터의 각 요소를 3으로 곱한다.
        *x *= 3;
    }
}



사실 이 main.rs는 entry point 이상의 의미는 그닥 없다. 그저 모듈별 함수를 호출해주는 역할이 대부분이기 때문이다.

여기서 triple 함수를 정의하는 부분이 다소 생소해서 이 부분만 살펴보자.

  • fn triple(v: &mut Vec<i32>) { ... } 부분은 triple이라는 함수를 정의하는 코드다.
  • fn: 함수를 정의할 때 사용하는 키워드이다.
  • triple: 함수의 이름이다.
  • v: &mut Vec<i32>: 함수의 매개변수를 정의한다. 여기서 v는 매개변수의 이름이고, &mut Vec<i32>는 매개변수의 타입을 나타낸다.
    • Vec<i32>: i32 타입의 요소를 가지는 벡터이다.
    • &mut: 뮤터블(mutable) 참조를 나타낸다. 즉, 함수 내에서 이 변수를 변경할 수 있다.

간단하게 예를 들면, v는 정수를 원소로 가지는 벡터에 대한 뮤터블 참조라고 생각하면 돼. 이 뮤터블 참조 때문에 함수 내부에서 v의 원소를 변경할 수 있다.


그렇다면, 여기서 &mut 대신 mut라고 쓰면 어떻게 되나?

  • &mut: 뮤터블 참조를 의미한다. 즉, 변수에 대한 참조를 전달하면서 해당 참조를 통해 원래 변수를 수정할 수 있도록 허용한다.
  • mut: 변수 자체가 뮤터블하다는 것을 의미한다. 즉, 해당 변수의 값을 변경할 수 있다.

예를 들어, fn triple(v: mut Vec<i32>) { ... }라고 쓰면, 컴파일러가 이 문법을 이해하지 못해서 에러가 발생한다. mut는 변수의 뮤터블성을 설정할 때 사용하는 키워드인데, 함수의 매개변수 타입으로 사용될 수 없기 때문이다.

fn triple(v: &mut Vec<i32>) { ... } 형태로 써야 참조를 전달하면서도 그 참조를 통해 원래의 변수를 변경할 수 있게 된다.

간단하게 말하면, &mut은 뮤터블 참조를 만들고, mut은 변수 자체를 뮤터블하게 만든다. 따라서 함수의 매개변수로는 &mut을 사용해야 적절한 뮤터블 참조를 전달할 수 있다.


위의 main.rs에서 상단에 선언된 mod, 즉 모듈들은 이렇게 같은 src 폴더 내에 다른 파일들로 정의가 되어있다.



그럼 이제부터 아래의 모듈들에 대해서 하나씩 분석을 해보자.

arrays.rs


pub fn run() {
    let a = [3, 2, 7, 6, 8];
    print(a);
    print2(a);

    for x in a {
        // x is each element (may move)
        println!("{}", x);
    }

    for x in &a {
        // x is a reference to each element
        println!("{}", x);
    }
}

fn print(a: [i32; 5]) {
    println!("{:?}", a);
}

fn print2(a: [i32; 5]) {
    for i in 0..5 {
        println!("{}", a[i]);
    }
}



두개의 for문

여기서 위쪽의 두개의 for문을 비교해 보자. 이 두개의 for문의 차이점은 변수 x가 어떤 타입의 값으로 채워지느냐에 있다.

for x in a { ... }
이 경우, x는 배열 a의 각 요소의 값을 직접 가져와서 사용한다. 즉, 배열의 각 요소가 x로 이동(move)될 수 있다. 이는 Rust에서 소유권의 개념과 관련이 있다. 배열 a의 타입이 복사 가능한 타입(Copy trait을 구현한 타입)일 경우에는 문제가 없지만, 그렇지 않다면 이 코드는 소유권 문제로 컴파일 에러가 발생할 수 있다.

for x in &a { ... }
여기서 x는 배열 a의 각 요소에 대한 레퍼런스(참조)이다. &a는 배열의 레퍼런스를 만들어서 for문이 배열의 원본을 변경하지 않도록 한다. 즉, 소유권을 이동시키지 않고 값을 읽을 수 있다. 따라서 이 경우에는 원본 배열 a가 변경되지 않고 안전하게 각 요소를 읽을 수 있다.

간단한 예시로, 아래와 같은 코드가 있다고 하자:

let a = [1, 2, 3];

for x in a {
    println!("{}", x);
} // 이 경우 x는 각 요소의 값이다.

for x in &a {
    println!("{}", x);
} // 이 경우 x는 각 요소에 대한 레퍼런스이다.



첫 번째 for문에서 x는 정수값이며, 두 번째 for문에서 x는 정수값에 대한 레퍼런스(&i32)이다. 이 차이를 알아야 상황에 따라 적절한 for문을 사용할 수 있다.

여기서 두 for문 모두 배열 a의 각 요소를 출력하는데 사용되므로, 출력 결과는 동일하다. 다만, 내부적으로 x가 어떤 값을 가지는 지가 다를 뿐이다.

첫 번째 for x in a 경우에는 x가 배열의 각 요소의 값을 직접 가지므로, println!("{}", x);에서 x는 해당 요소의 값이 된다.

두 번째 for x in &a 경우에는 x가 배열의 각 요소에 대한 레퍼런스를 가지게 되지만, println! 매크로가 이를 자동으로 역참조하여 출력한다. 따라서 println!("{}", x); 문장에서 x는 레퍼런스이지만, 출력되는 값은 해당 레퍼런스가 가리키는 값이 된다.

이런 이유로 두 for문의 출력 결과는 동일하다.

두개의 함수

두 함수는 배열 a를 출력하는 작업을 수행하지만, 출력 형식과 접근 방식에 차이가 있다.

  • fn print(a: [i32; 5])는 println!("{:?}", a);을 사용하여 배열 a를 디버깅 형식으로 한 번에 출력한다.
  • fn print2(a: [i32; 5])는 for문을 사용하여 배열 a의 각 요소를 순회하면서 하나씩 출력한다.
{:?}와 {}의 차이는 다음과 같다:

  • {:?}: 디버깅 형식으로 출력. 주로 디버깅을 위한 출력이나, 구조체나 열거형과 같은 복잡한 데이터 타입을 간편하게 출력할 때 사용된다. Debug 트레잇이 구현되어 있어야 사용할 수 있다.
  • {}: 보통 형식으로 출력. 기본 데이터 타입들은 대부분 이 형식으로 쉽게 출력할 수 있다. Display 트레잇이 구현되어 있어야 사용할 수 있다.
예를 들어, 배열 [1, 2, 3]을 출력할 때:

  • println!("{:?}", [1, 2, 3]);을 사용하면 [1, 2, 3]이 출력된다.
  • for i in [1, 2, 3].iter() { println!("{}", i); }을 사용하면 1, 2, 3이 각각 다른 줄에 출력된다.

numbers.rs


pub fn f(x: i32) -> i32 {
    super::hello();
    x * x
}

pub fn run() {
    println!("NUMBERS");
    let x = 1;
    println!("{}", x * x);
    println!("{}", x + 1u8);
    let y: i32 = x as i32;
    println!("{}", &y);
    println!("{}", &&&y);
    println!("{}", 1 + &2); // Add trait
    let mut a = 1;
    print(&a);
    a = 2;
    double(&mut a);
    print(&a);
    let r = &a;
    a = 1;
    print(&a);
    // println!("{}", r);

    let r2 = &mut a;
    println!("{}", r2);
    println!("{}", a);
    // println!("{}", r2);
}

fn print(x: &i32) {
    println!("{}", x);
}

fn double(x: &mut i32) {
    *x *= 2;
}



첫번째 함수 f


첫번째 함수 f는 pub fn f(x: i32) -> i32 형태로 정의되어 있다. pub은 이 함수가 공개(public)되어 외부에서도 접근할 수 있다는 것을 의미한다. 함수 f는 하나의 정수형 매개변수 x를 받아서, 정수형을 반환한다.

함수 내부에서는 두 가지 작업을 수행한다:

super::hello();: super 키워드는 현재 모듈의 부모 모듈을 가리킨다. 이 부분은 부모 모듈에 정의된 hello() 함수를 호출한다는 것을 의미한다. 즉, 현재 모듈이 child라고 하면, 이 함수는 child의 부모 모듈에 있는 hello() 함수를 호출한다.

x * x: 이 부분은 매개변수 x의 제곱을 계산한다. 그리고 그 결과값을 반환한다.

이 함수는 총 3가지의 작업을 한다고 볼 수 있다: 부모 모듈의 hello() 함수를 호출하고, 매개변수 x의 제곱을 계산하여 반환한다.

    let y: i32 = x as i32;

x를 i32 타입으로 변환하여 y에 저장한다.

    println!("{}", &y);
    println!("{}", &&&y);

&y: 이 경우, y 변수의 참조를 생성하여 빌림을 표현한다. 이 참조는 불변이며, 이를 통해 y의 값에 접근할 수 있지만 수정은 할 수 없다.

&&&y: 이 경우는 y의 참조의 참조의 참조를 생성하는 것이다. 이것도 결국은 y를 빌림을 표현하는 또 다른 방식이다. 이렇게 여러 번 중첩된 참조도 결국은 원본 변수 y에 대한 빌림이라고 볼 수 있다.

자동 역참조 기능 덕분에, 이러한 중첩된 참조도 결국은 원본 변수에 대한 접근을 가능하게 한다. 이렇게 & 기호를 사용하여 생성된 참조는 함수나 메서드가 원본 데이터를 소유하지 않고 일시적으로 사용할 수 있도록 해준다.

그렇다면, &y는 &&&y와 같은 것인가?

&y와 &&&y는 같은 원본 변수 y를 가리키긴 하지만, 타입이 다르다. &y는 y의 참조이고, &&&y는 y의 참조의 참조의 참조다. 이 두 개는 서로 다른 레벨의 "간접성"을 가진다.

예를 들어, y가 i32 타입이라면:

  • &y는 &i32 타입
  • &&&y는 &&&i32 타입
이 될 것이다.

그렇지만 Rust의 "자동 역참조" 덕분에, 둘 다 원본 변수 y에 대한 접근을 가능하게 한다. 즉, 실제 코드에서는 둘 사이에 큰 차이를 느끼지 못할 수도 있다. 하지만 타입이 다르기 때문에, 함수의 인자로 전달할 때는 이 차이가 중요하다. 함수가 &i32를 요구한다면 &&&i32 타입을 넣을 수 없다.

조금 복잡하고 어려운 개념이므로 더 쉽게 설명을 해본다.

참조(&)를 쓰는 것은 마치 지도를 쓰는 것과 비슷하다고 생각하면 된다.

예를 들어, 집이 있고 그 집을 찾아가려면 지도가 필요하다고 해보자.

  • y는 집이다.
  • &y는 그 집까지 가는 첫 번째 지도이다.
  • &&&y는 그 집까지 가는 방법을 알려주는 지도를 찾아가려면 다른 지도들을 봐야하는 상황이다. 즉, 지도를 찾아가려면 또 다른 지도를 봐야 하는 것이다. 총 세 개의 지도를 봐야 최종 목적지에 도착하는 것이 된다.
하지만 Rust에서는 좀 똑똑해서, 목적지에 도달하기 위해 필요한 모든 지도를 알아서 봐준다. 그래서 첫 번째 지도든, 세 번째 지도든 결국은 같은 집(y)을 가리키게 된다.

하지만 이 지도들은 다른 종류의 지도라서, 지도를 요구하는 다른 사람(함수)에게 줄 때는 그에 맞는 종류의 지도를 줘야 한다.

    println!("{}", 1 + &2); // Add trait

이 코드에서 1 + &2를 볼 때, 이 덧셈 연산이 어떻게 작동하는지 알기 위해 Rust는 "Add trait"을 찾아본다. Trait는 러스트에서 특정 기능을 정의하는 일종의 템플릿이라고 생각하면 돼. 예를 들어, 덧셈을 할 수 있는 객체들은 "Add trait"을 가지고 있어야 하고, 이 trait은 어떻게 덧셈을 해야 하는지 알려주는 역할을 한다.

1 + &2에서 1은 i32 타입의 정수, &2는 i32 타입의 정수에 대한 참조다. Rust의 "Add trait"은 이렇게 다양한 타입의 덧셈을 지원한다. 즉, 정수와 정수 참조(&)가 더해져도 잘 처리할 수 있다는 것이다.

여기서 Add trait이라고 하는 것은 덧셈을 가능하게 해주는 기능을 모아둔 일종의 '규칙 모음'이라고 볼 수 있다. Rust에서는 이런 연산자를 어떻게 처리할지 정의하기 위해서 trait을 사용한다.

Trait이란?


Trait는 Rust에서 인터페이스와 유사한 개념이다. 특정 기능이나 동작을 정의하고, 이를 다른 타입에 적용할 수 있게 해준다. Trait을 정의하고 나면, 이 Trait을 "구현(implement)"한 타입만이 해당 Trait의 메서드나 연관 함수를 사용할 수 있다.

Trait을 정의하는 기본 문법은 다음과 같다:

trait SomeTrait {
    fn some_method(&self);
}


다른 타입에서 이 Trait을 구현하기 위해서는 impl 키워드를 사용한다:

struct MyStruct;

impl SomeTrait for MyStruct {
    fn some_method(&self) {
        println!("Doing something!");
    }
}


이렇게 하면 MyStruct 타입은 SomeTrait을 구현하게 되며, SomeTrait의 some_method를 호출할 수 있다:

let my_instance = MyStruct;
my_instance.some_method(); // 출력: "Doing something!"


Trait은 제네릭을 사용해서 여러 타입에 대해 동작을 정의할 수도 있고, 더 복잡한 제약 조건을 추가할 수도 있다. 이렇게 함으로써 코드의 재사용성과 유연성을 높일 수 있다.

trait는 러스트에서 특정한 '기술'이나 '능력'을 나타내는 것이다. 예를 들어, "축구를 할 수 있는"이라는 능력을 모두 모아 '축구 선수'라고 부르는 것처럼, 러스트에서는 특정 기능들을 모아 하나의 trait으로 만들어서 이름을 붙인다.

쉽게 말해, 어떤 동물이 '날 수 있는' 능력이 있다면, 그 동물은 '날 수 있는' trait을 가진 것이라고 할 수 있다. 같은 방식으로, 러스트의 프로그래밍 세계에서 어떤 코드가 특정 작업을 할 수 있다면, 그 코드는 그 작업을 나타내는 trait을 가지고 있다고 말한다.

이렇게 해서 러스트는 여러 코드 조각들이 어떤 일을 할 수 있는지, 어떤 능력을 가지고 있는지 쉽게 알 수 있다. 이게 바로 trait의 역할이다.

쉬운 예를 들어보겠다. 먼저, "소리를 낼 수 있는" 것들에 대한 trait을 만들어보자. 이 trait의 이름을 Speak라고 하겠다. 그리고 이 Speak라는 능력을 가진 동물들이 어떤 소리를 내는지 알려주는 기능을 추가하자.

// "소리를 낼 수 있는" 능력을 나타내는 trait
trait Speak {
    // 어떤 소리를 내는지 알려주는 함수
    fn speak(&self) -> String;
}

// 개라는 동물이 있고, 개는 "멍멍"이라고 소리를 낸다.
struct Dog;

// 개가 "Speak"라는 능력을 가진다고 알려줌
impl Speak for Dog {
    fn speak(&self) -> String {
        String::from("멍멍")
    }
}

// 고양이라는 동물이 있고, 고양이는 "야옹"이라고 소리를 낸다.
struct Cat;

// 고양이가 "Speak"라는 능력을 가진다고 알려줌
impl Speak for Cat {
    fn speak(&self) -> String {
        String::from("야옹")
    }
}

// 메인 함수에서는 개와 고양이가 어떤 소리를 내는지 출력한다.
fn main() {
    let my_dog = Dog;
    let my_cat = Cat;

    println!("개는 이렇게 소리를 낸다: {}", my_dog.speak());
    println!("고양이는 이렇게 소리를 낸다: {}", my_cat.speak());
}


이 예제에서 Speak라는 trait는 speak라는 함수를 가지고 있다. 그리고 Dog와 Cat 이라는 구조체(동물들을 나타내는 코드)는 이 Speak trait를 구현(impl)해서 각각 "멍멍", "야옹"이라는 소리를 내는 것을 보여준다.

main 함수에서는 개와 고양이가 실제로 어떤 소리를 내는지 출력해본다. 이렇게 trait을 사용하면 다양한 동물들이나 물건들이 공통의 능력을 어떻게 사용하는지 쉽게 관리할 수 있다.

    let mut a = 1;
    print(&a);
    a = 2;
    double(&mut a);
    print(&a);
    let r = &a;
    a = 1;
    print(&a);

이 코드에서는 정수형 변수 a를 선언하고 여러 가지 작업을 해본다. 코드를 순서대로 살펴보자.

  1. let mut a = 1;: a라는 이름의 mutable(변경 가능한) 변수를 생성하고 1로 초기화한다.
  2. print(&a);: a의 값을 읽을 수 있는 참조(reference)를 만들어 print 함수에 전달한다. 함수 내에서 a의 값을 출력한다.
  3. a = 2;: a의 값을 2로 변경한다.
  4. double(&mut a);: a의 값을 변경할 수 있는 mutable 참조를 만들어 double 함수에 전달한다. 함수 내에서 a의 값이 두 배가 된다.
  5. print(&a);: a의 변경된 값을 출력한다. 이 시점에서는 4가 출력될 것이다.
  6. let r = &a;: a의 값을 읽을 수 있는 참조를 만들어 r 변수에 저장한다.
  7. a = 1;: a의 값을 다시 1로 변경한다.
  8. print(&a);: a의 최종 변경된 값을 출력한다. 이 시점에서는 1이 출력될 것이다.
이렇게 보면 Rust의 참조와 mutable 참조, 그리고 소유권의 개념이 어떻게 작동하는지 알 수 있다. &는 값을 빌려 읽기만 할 수 있게 하고, &mut은 값을 빌려 변경도 할 수 있게 한다. 이러한 방식으로 Rust는 메모리 안전성을 유지한다. 

그런데, 이 다음에 아래의 코드를 comment를 제거하고 실행하면 컴파일 에러가 난다.

    // println!("{}", r);

let r = &a; 이라는 코드는 "r이 a의 정보를 빌린다"라고 이해할 수 있다. 빌린 정보는 원래 '바꿀 수 없다'고 생각되지만, Rust에서는 특별한 상황에서는 바꿀 수 있다.

이 특별한 상황은 바로 '빌린 정보(r)가 실제로는 어디에서도 사용되지 않을 때'다. 이렇게 되면, 원래 정보(a)를 바꿔도 된다고 Rust가 판단한다.

그래서 println!("{}", r); 같은 코드가 없어서 r이 실제로 사용되지 않았다면, a를 바꿀 수 있다.

println!("{}", r);를 마지막에 추가하면 Rust는 이렇게 생각한다:

  1. let r = &a;에서 r은 a의 정보를 빌린다.
  2. a = 1;에서 a의 값을 변경하려고 한다.
  3. println!("{}", r);에서 r을 실제로 사용하려고 한다.


r이 a를 빌렸고, 그 빌린 정보(r)를 실제로 사용하려고 하기 때문에, a의 값을 변경하는 것은 불가능하다. 이 때문에 컴파일 에러가 발생한다.

Rust는 이런 식으로 '빌린 정보가 어떻게 사용되는지'를 계속 확인한다. 그래서 빌린 정보가 실제로 사용되면 원본 데이터(a)를 변경할 수 없게 막는다.

tuple_structs.rs


#[derive(Debug, Copy, Clone)]
struct S(i32, f64);

pub fn run() {
    println!("TUPLE STRUCTS");

    let x = S(1, 3.14);
    print(x);
    println!("{}", x.0);
    print2(&x);
    println!("{}", x.0);
    debug_print(&x);
}

fn print(s: S) {
    println!("{} {}", s.0, s.1);
}

fn print2(s: &S) {
    println!("{} {}", s.0, s.1);
}

fn debug_print(s: &S) {
    println!("{:?}", s);
}



#[derive(Debug, Copy, Clone)]는 Rust의 attribute 중 하나로, 컴파일러에게 특정 트레이트(trait)을 자동으로 구현하도록 지시한다. 이 경우에는 Debug, Copy, Clone 세 가지 트레이트에 대한 구현을 자동으로 생성하라는 뜻이다.

  • Debug
    • 이 트레이트는 객체를 디버깅하기 쉽게 출력할 수 있도록 한다. 이 트레이트를 구현한 타입은 println!("{:?}", obj);와 같이 디버깅 정보를 출력할 수 있다.
  • Copy
    • 이 트레이트는 객체가 얕은 복사를 지원해야 함을 나타낸다. 일반적으로 간단한 타입(정수, 실수, 불리언 등)은 Copy 트레이트를 구현한다. Copy 트레이트가 구현된 타입의 변수를 다른 변수에 할당하거나 함수에 전달할 때, 데이터가 자동으로 복사된다.
  • Clone
    • 이 트레이트는 객체가 깊은 복사를 지원해야 함을 나타낸다. Clone 트레이트를 구현한 타입은 .clone() 메서드를 사용해서 명시적으로 복사할 수 있다.

예를 들어, 아래와 같이 #[derive(Debug, Copy, Clone)]를 사용한 경우,

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


Point 타입은 이제 Debug, Copy, Clone 트레이트를 자동으로 구현하게 된다. 이로 인해 Point 객체를 디버깅 출력할 수 있으며, 얕은 복사와 깊은 복사를 수행할 수 있다.


이 코드는 Rust에서 'Tuple Struct'라는 개념을 다루고 있다. Tuple Struct는 일반적인 구조체와 비슷하지만, 각 필드에 이름이 없고 타입만 정의된다.

  1. struct S(i32, f64);: S라는 이름의 Tuple Struct를 정의한다. 첫 번째 요소는 i32 타입, 두 번째 요소는 f64 타입이다.
  2. pub fn run() { ... }: 이것은 run이라는 public 함수를 정의하고 있다. 이 함수는 다음을 수행한다:
    1. let x = S(1, 3.14);: S 구조체의 인스턴스 x를 생성하고, 첫 번째 요소에 1, 두 번째 요소에 3.14를 할당한다.
    2. print(x);: x를 print 함수에 전달해서 출력한다.
    3. println!("{}", x.0);: x의 첫 번째 요소(x.0)를 출력한다.
    4. print2(&x);: x의 참조(&x)를 print2 함수에 전달해서 출력한다.
    5. println!("{}", x.0);: x의 첫 번째 요소(x.0)를 다시 출력한다.
    6. debug_print(&x);: x의 참조(&x)를 debug_print 함수에 전달해서 디버그 형식으로 출력한다.
  3. fn print(s: S) { ... }: S 타입의 값을 인자로 받아 그 값을 출력하는 함수이다.
  4. fn print2(s: &S) { ... }: S 타입의 참조를 인자로 받아 그 값을 출력하는 함수이다. 여기서는 소유권을 넘기지 않고 빌림만 하기 때문에 원본 x에 영향을 미치지 않는다.
  5. fn debug_print(s: &S) { ... }: S 타입의 참조를 인자로 받아 디버그 형식({:?})으로 출력하는 함수이다. 하지만, 주의할 점은 {:?}를 사용하려면 해당 타입이 Debug trait을 구현해야 한다. 여기서는 그 부분이 빠져 있어 실제로 컴파일 에러가 발생할 것이다.

이런 식으로 코드는 Tuple Struct와 그를 사용하는 여러 함수를 정의하고 있다.


tuples.rs


pub fn run() {
    println!("TUPLES");
    let x = (1, 3.14);
    println!("{} {}", x.0, x.1);
    print(x);

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

fn print(t: (i32, f64)) {
    println!("{:?}", t);
}



이 코드는 Rust에서 튜플(tuple)을 사용하는 예시다. 튜플은 여러 타입의 값들을 하나로 묶을 수 있는 데이터 구조다.

  1. let x = (1, 3.14);: (1, 3.14)는 i32와 f64 타입의 값을 가진 튜플이고, 이를 x에 저장한다.
  2. println!("{} {}", x.0, x.1);: 튜플의 첫 번째 값(x.0, 1)과 두 번째 값(x.1, 3.14)을 출력한다.
  3. print(x);: print 함수를 호출하여 튜플 x를 출력한다. print 함수는 튜플을 받아 println!("{:?}", t);로 출력한다. {:?}는 Debug 트레이트를 사용하여 디버깅 정보를 출력한다.
  4. let (a, b) = x;: 튜플 x의 값을 a와 b에 분해(destructuring)하여 저장한다. a에는 1, b에는 3.14가 저장된다.
  5. println!("{} {}", a, b);: 변수 a와 b의 값을 출력한다.
  6. fn print(t: (i32, f64)): 이 함수는 (i32, f64) 타입의 튜플을 매개변수로 받아, 그 값을 디버깅 정보로 출력한다.

이 코드를 통해 튜플의 기본적인 사용법과 값의 분해, 그리고 함수에서 튜플을 매개변수로 사용하는 방법을 확인할 수 있다.



vectors.rs


pub fn run() {
    println!("VECTORS");
    let mut v = vec![3, 2, 7, 6, 8];

    for x in &mut v {
        *x *= 2;
    }
    println!("{:?}", v);

    triple(&mut v);
    println!("{:?}", v);
}

fn triple(v: &mut Vec<i32>) {
    for x in v {
        *x *= 3;
    }
}



이 코드는 Rust의 벡터(vector)에 대한 예시다. 벡터는 동적 배열로 크기가 변경될 수 있고, 같은 타입의 여러 개의 값을 저장할 수 있다.

  1. let mut v = vec![3, 2, 7, 6, 8];: 초기 값을 [3, 2, 7, 6, 8]로 가지는 mutable 벡터 v를 생성한다.
  2. 첫 번째 for 루프에서는 벡터의 각 원소를 두 배로 만든다.
    • for x in &mut v: v의 각 원소에 대한 mutable 참조를 가져온다.
    • *x *= 2;: 각 원소를 두 배로 만든다.
  3. println!("{:?}", v);: 두 배로 만든 벡터를 출력한다.
  4. triple(&mut v);: triple 함수를 호출해서 벡터의 각 원소를 세 배로 만든다.
  5. 두 번째 println!("{:?}", v);: 세 배로 만든 벡터를 출력한다.
  6. fn triple(v: &mut Vec<i32>): triple 함수는 mutable 참조를 통해 벡터를 받아, 각 원소를 세 배로 만든다.
    • for x in v: v의 각 원소에 대한 참조를 가져온다.
    • *x *= 3;: 각 원소를 세 배로 만든다.
이 코드를 통해 mutable 벡터를 다루는 방법과 벡터에 대한 참조를 함수에 전달하는 방법을 알 수 있다. 이는 배열이나 리스트를 다룰 때 흔히 쓰이는 패턴 중 하나다.


for x in &mut v에서 x는 뮤터블 참조(&mut)를 가져. 즉, x는 실제 벡터 v의 각 요소에 대한 뮤터블 참조다. 따라서 x는 참조임이 맞고, 그 참조를 따라가서 실제 값을 변경하려면 *x를 써야 한다.

x는 참조니까 직접 값을 변경할 수 없고, 그래서 *x *= 2;라고 써서 참조가 가리키는 실제 값을 변경한다. *x는 "참조가 가리키는 실제 값"을 의미하기 때문에, 그 실제 값에 2를 곱해서 다시 저장한다는 의미가 된다.


&mut v와 mut v의 차이

&mut v는 "뮤터블한 참조를 빌려온다"라고 생각하면 돼. 여기서 '참조'는 그냥 다른 변수에게 뭔가를 '빌려주는' 행위를 말해. 예를 들어서 네가 친구에게 연필을 빌려주면, 친구는 그 연필로 그림을 그릴 수 있어. 그리고 네가 뮤터블한 참조(&mut)로 빌려줬으니까, 친구는 연필의 색깔을 바꿀 수도 있어.

for x in &mut v는 "벡터 v의 모든 요소에 대한 뮤터블한 참조를 하나씩 x에 넣어서 반복해"라는 의미야. 그래서 x는 벡터 v의 각 요소를 뮤터블하게 빌려오는 것이고, 이걸 *x로 표현해서 실제 값을 바꿀 수 있는 것이다.

즉, &mut v는 벡터 v를 뮤터블하게 빌려와서 그 안의 값들을 바꿀 수 있게 해주는 역할을 하는 것이다.

mut v는 "변수 v는 변할 수 있는(mutatable) 값이다"라고 선언하는 것이다. 즉, 이 변수의 값은 프로그램이 실행되는 동안 바뀔 수 있다는 뜻이 된다.

예를 들면,

let v = vec![1, 2, 3];  // v는 변경 불가능한(immutable) 벡터
let mut w = vec![1, 2, 3];  // w는 변경 가능한(mutable) 벡터

여기서 v는 한 번 값이 주어지면 바뀌지 않아. 하지만 mut w로 선언된 w는 나중에 값을 바꿀 수 있어. 예를 들어 w.push(4); 이런 식으로 말이다.

&mut v와 mut v는 비슷해 보이지만, 다르다는 것을 알아두면 좋겠다.

  • mut v: v 자체가 변할 수 있는 변수라는 의미
  • &mut v: v 에 대한 변할 수 있는 참조를 만들어서 빌려오는 것
둘 다 '변할 수 있다'는 공통점은 있지만, 하나는 변수 자체의 성격을, 다른 하나는 빌려온 참조의 성격을 바꾸는 것이라고 이해하면 되겠다.







댓글 없음:

댓글 쓰기

관련 포스팅