Related to: Personal

스터디 멤버

김태훈

김형석

유재상

참조

https://tourofrust.com/TOC_ko.html

1장 - 기초

Rust 놀이터

별도의 환경을 구성하지 않더라도, “Rust 놀이터”라고 하는 Web Service를 통해 기본적인 코드를 실행해볼 수 있다.

변수

  • ‘let’ 키워드르 통해 변수 선언, 자료형은 일반적으로 적지 않아도 유추합니다.
    • (마치 파이썬처럼..)
    • 그러나 type을 명시할 수도 있습니다.
  • variable shadowing
    • 이미 선언한 변수 이름으로 다시 선언할 수 있습니다.
    • 다시 선언하는 변수는 기존의 변수와 전혀 다른 타입일 수 있습니다.
fn main() {
    // rust가 x의 자료형을 유추합니다
    let x = 13;
    println!("{}", x);
 
    // 자료형을 명시적으로 지정할 수도 있습니다
    let x: f64 = 3.14159;
    println!("{}", x);
 
    // 선언 후 나중에 초기화도 가능하지만, 보통 그렇게 하진 않습니다
    let x;
    x = 0;
    println!("{}", x);
}

변수의 값 변경하기

  • mutable vs immutable
    • immutable(기본)

    • mutable

      • 변경 가능한 값은 mut 키워드를 추가하여 선언해야 합니다.
      fn main() {
          let mut x = 42;
          println!("{}", x);
          x = 13;
          println!("{}", x);
      }

기본 자료형

  • Rust에는 다양하지만 익숙한 자료형
    • 부울 값 - 참/거짓 값을 나타내는 bool
    • 부호가 없는 정수형 - 양의 정수를 나타내는 u8 u16 u32 u64 u128
    • 부호가 있는 정수형 - 양/음의 정수를 나타내는 i8 i16 i32 i64 i128
    • 포인터 사이즈 정수 - 메모리에 있는 값들의 인덱스와 크기를 나타내는 usize isize
    • 부동 소수점 - f32 f64
    • 튜플(tuple) - stack에 있는 값들의 고정된 순서를 전달하기 위한 (값, 값, ...)
    • 배열(array) - 컴파일 타임에 정해진 길이를 갖는 유사한 원소들의 모음(collection)인 [값, 값, ...]
    • 슬라이스(slice) - 런타임에 길이가 정해지는 유사한 원소들의 collection
    • str(문자열 slice) - 런타임에 길이가 정해지는 텍스트
      • 텍스트는 다른 익숙한 언어에서보다 복잡한데, 이는 Rust가 시스템 프로그래밍 언어이며 여러분이 지금까지 익숙하지 않았을 메모리 문제에 신경쓰기 때문입니다. 이에 대해서는 나중에 더 자세히 다루겠습니다.
  • 숫자형 자료형들은 숫자 뒤에 자료형 이름을 붙여 명시적으로 지정할 수 있습니다 (예: 13u322u8).
fn main() {
    let x = 12; // 기본적으로 i32
    let a = 12u8;
    let b = 4.3; // 기본적으로 f64
    let c = 4.3f32;
    let bv = true;
    let t = (13, false);
    let sentence = "hello world!";
    println!(
        "{} {} {} {} {} {} {} {}",
        x, a, b, c, bv, t.0, t.1, sentence
    );
}

기본 자료형 변환

  • as 키워드를 사용해 자료형을 매우 쉽게 변환할 수 있습니다.
fn main() {
    let a = 13u8;
    let b = 7u32;
    let c = a as u32 + b;
    println!("{}", c);
 
    let t = true;
    println!("{}", t as u8);
}

상수

  • const 키워드를 사용하여 상수를 선언할 수 있습니다.
  • const로 선언된 값은 컴파일 타임에 텍스트 지정자를 직접 값으로 대체합니다.
const PI: f32 = 3.14159;
 
fn main() {
    println!(
        "아무 재료 없이 애플 {}를 만들려면, 먼저 우주를 만들어야 한다.",
        PI
    );
}

배열

  • _배열(array)_은 고정된 길이로 된 모두 같은 자료형의 자료를 가진 collection입니다.
fn main() {
    let nums: [i32; 3] = [1, 2, 3];
    println!("{:?}", nums);
    println!("{}", nums[1]);
}

함수

  • fn 키워드르 메서드를 선언할 수 있습니다.
  • typing hint가 추가된 python 메서드와 같이 [변수명:타입 → 리턴 값] 타입의 형태로 선언됩니다.
fn add(x: i32, y: i32) -> i32 {
    return x + y;
}
 
fn main() {
    println!("{}", add(42, 13));
}

여러개의 리턴 값

  • 튜플 형을 리턴함으로써 여러개의 값을 리턴할 수 있습니다.
  • 또한 python 같은 destructuring 방법을 지원합니다.
fn swap(x: i32, y: i32) -> (i32, i32) {
    return (y, x);
}
 
fn main() {
    // 리턴 값의 튜플을 리턴
    let result = swap(123, 321);
    println!("{} {}", result.0, result.1);
 
    // 튜플을 두 변수명으로 분해
    let (a, b) = swap(result.0, result.1);
    println!("{} {}", a, b);
}

아무것도 리턴하지 않기

  • 아무것도 반환하지 않는 메서드는 자동으로 empty tuple = ‘()’을 반환합니다.
fn make_nothing() -> () {
    return ();
}
 
// 리턴 자료형은 ()로 암시
fn make_nothing2() {
    // 리턴할 것이 지정되지 않으면 이 함수는 ()를 리턴함
}
 
fn main() {
    let a = make_nothing();
    let b = make_nothing2();
 
    // 아무 것도 없는 것은 출력하기 힘들기 때문에
    // a와 b의 디버그 문자열을 출력한다
    println!("The value of a: {:?}", a);
    println!("The value of b: {:?}", b);
}

2장 - 기초적인 흐름 제어

if/else if/else

  • 기존의 논리 연산자를 그대로 사용 할 수 있음
    • ==!=<><=>=!||&&
  • python 처럼 조건문에 중괄호 ‘()’가 필요하지 않음
fn main() {
    let x = 42;
    if x < 42 {
        println!("42보다 작다");
    } else if x == 42 {
        println!("42와 같다");
    } else {
        println!("42보다 크다");
    }
}

loop

  • loop { … }로 반복
fn main() {
    let mut x = 0;
    loop {
        x += 1;
        if x == 42 {
            break;
        }
    }
    println!("{}", x);
}

while

  • whlie condition { … }로 반복
fn main() {
    let mut x = 0;
    while x != 42 {
        x += 1;
    }
}

for

  • for item in iterator { … }로 반복
fn main() {
    for x in 0..5 {
        println!("{}", x);
    }
 
    for x in 0..=5 {
        println!("{}", x);
    }
}

match

  • switch의 대용, 모든 case를 처리해야 한다.
fn main() {
    let x = 42;
 
    match x {
        0 => {
            println!("0 발견");
        }
        // 여러 개 값과 대조할 수 있다
        1 | 2 => {
            println!("1 또는 2 발견!");
        }
        // 범위로 대조할 수 있다
        3..=9 => {
            println!("3에서 9까지의 숫자 발견");
        }
        // 찾은 숫자를 변수에 바인딩할 수 있다
        matched_num @ 10..=100 => {
            println!("10에서 100까지의 숫자 {} 발견!", matched_num);
        }
        // 모든 케이스가 처리되지 않았을 경우 반드시 존재해야 하는 기본 match
        _ => {
            println!("뭔가 다른거 발견!");
        }
    }
}

loop에서 값 리턴하기

  • break 뒤에 값을 두면, loop 후 값이 리턴됨
fn main() {
    let mut x = 0;
    let v = loop {
        x += 1;
        if x == 13 {
            break "13 찾았다";
        }
    };
    println!("loop에서: {}", v);
}

블록 표현에서 값 리턴하기

  • 다른 언어들과 많이 다른 부분
  • ‘return’이 없더라도, ifmatch, 함수, 또는 범위 블록의 마지막 구문에 ’;‘가 없다면 Rust는 그 값을 블록의 리턴 값으로 간주
fn example() -> i32 {
    let x = 42;
    // Rust의 3항 연산 표현
    let v = if x < 42 { -1 } else { 1 };
    println!("if로부터: {}", v);
 
    let food = "햄버거";
    let result = match food {
        "핫도그" => "핫도그다",
        // 리턴문 하나 뿐이라면 중괄호는 필수가 아님
        _ => "핫도그가 아니다",
    };
    println!("음식 판별: {}", result);
 
    let v = {
        // 이 범위 블록은 함수 범위를 더럽히지 않고 값을 가져오게 해준다
        let a = 1;
        let b = 2;
        a + b
    };
    println!("block에서: {}", v);
 
    // Rust에서 함수 마지막에 값을 리턴하는 관용적 표현
    v + 4
}
 
fn main() {
    println!("function에서: {}", example());
}

2장 - 마무리

3장 - 기본 데이터 구조 자료형

구조체

  • struct는 필드(field)들의 collection
  • 이 정의는 메모리 상에 field들을 어떻게 배치할지에 대한 컴파일러의 청사진
    • 정의한 순서에 따라 memory 순서까지 달라지는 것일까?
struct SeaCreature {
    // String은 struct다
    animal_type: String,
    name: String,
    arms: i32,
    legs: i32,
    weapon: String,
}

메소드 호출하기

  • c++과 비슷한 느낌으로 호출
  • 스태틱 메소드(static methods) - 자료형 그 자체에 속하는 메소드로서, :: 연산자를 이용하여 호출
  • 인스턴스 메소드(instance methods) - 자료형의 인스턴스에 속하는 메소드로서, . 연산자를 이용하여 호출
fn main() {
    // static method를 사용하여 String의 instance를 생성
    let s = String::from("Hello world!");
    // instance의 메소드를 사용
    println!("{}의 글자 수는 {}입니다.", s, s.len());
}

메모리

  • Rust 프로그램에는 데이터가 저장되는 세 가지의 메모리 영역이 존재
  • 데이터 메모리(data memory)
    • 크기가 고정 되었으며 static한 데이터
      • 예: “Hello World!”
      • 이 텍스트의 바이트들은 오직 한 곳에서만 읽히므로 이 영역에 저장될 수 있음
      • 이런 종류의 데이터는 컴파일러가 많은 최적화를 수행
      • 위치가 알려져 있고 고정되어 있기 때문에 일반적으로 사용하기에 매우 빠름
  • 스택 메모리(stack memory)
    • 함수 내에서 변수로 선언되는 데이터용
      • 이 메모리의 위치는 함수 호출 동안에는 절대 변하지 않음
      • 때문에 컴파일러가 코드를 최적화할 수 있으며, 이로 인해 접근하기에 매우 빠름
  • 힙 메모리(heap memory)
    • 애플리케이션이 실행되는 동안 생성되는 데이터용
      • 이 영역의 데이터는 추가하거나, 이동하거나, 제거하거나, 크기를 바꾸거나, 등을 할 수 있음
      • 일반적으로 사용하기에 느리지만, 훨씬 더 창의적인 메모리 사용이 가능
      • 데이터가 이 영역에 추가되면 할당(allocation)
      • 데이터가 이 영역에서 제거되면 해제(deallocation)

메모리에 데이터 생성하기

  • 데이터가 stack에 할당되는지, heap에 할당되는지에 대한 차이는 c++과 매우 유사
  • struct의 field 값들은 . 연산자를 통해 접근
 
let ferris = SeaCreature {
		// String struct도 stack에 있지만,
		// heap에 있는 데이터에 대한 참조를 갖고 있음
		animal_type: String::from("게"),
		name: String::from("Ferris"),
		arms: 2,
		legs: 4,
		weapon: String::from("집게"),
};

Tuple 같은 구조체

  • 튜플형태로 struct를 생성할 수 있음
struct Location(i32, i32);
 
fn main() {
    // 이것도 여전히 stack에 있는 struct임
    let loc = Location(42, 32);
    println!("{}, {}", loc.0, loc.1);
}

Unit 같은 구조체

  • struct 내에 아무것도 선언하지 않는다면 빈 튜플과 같아짐
    • unit은 빈 튜플의 또 다른 이름
struct Location(i32, i32);
 
fn main() {
    // 이것도 여전히 stack에 있는 struct임
    let loc = Location(42, 32);
    println!("{}, {}", loc.0, loc.1);
}

열거형

  • 열거형(enumeration)은 enum 키워드를 통해 몇 가지 태그된 원소의 값을 가질 수 있는 새로운 자료형을 생성
enum Species {
    Crab,
    Octopus,
    Fish,
    Clam,
}

열거형과 데이터

  • 내용이 난해해서 원문 복붙..

enum의 원소들은 C의 _union_처럼 동작할 수 있도록 한 개 이상의 자료형을 가질 수 있습니다.

enum이 match를 통해 패턴 일치될 때, 각각의 데이터 값에 변수명을 붙일 수 있습니다.

enum의 메모리 상세:

  • enum 데이터 값은 가장 큰 원소의 메모리 크기와 같은 메모리 크기를 가집니다. 이는 가능한 모든 값이 동일한 메모리 공간에 들어갈 수 있게 해줍니다.
  • 원소의 자료형(있는 경우)에 더하여, 각 원소는 무슨 태그에 해당하는지 나타내는 숫자값도 갖습니다.

다른 상세 정보:

  • Rust의 enum은 _tagged union_으로도 알려져 있습니다.
  • Rust가 _대수적 자료형(algebraic types)_을 갖고 있다고 할 때 이는 자료형을 조합하여 새 자료형을 만드는 것을 의미합니다.
  • Rust의 enum은 _tagged union_으로도 알려져 있음
    • tagged union
      • https://80000coding.oopy.io/e1fcc2c2-8149-49a8-9b9b-544103481c9c
      • enum 내에 선언된 원소 중 가장 큰 data size와 같은 메모리 크기를 가짐
        • 가능한 모든 값이 동일 메모리 공간 안에 들어갈 수 있게 해줌
          • enum SomeType {
            struct_1() // size = 3 byte,
            struct_2() // size = 5 byte,
            struct_3() // size = 1 byte,
            }

          • SomeType의 data 크기는 5 byte

    • 즉, enum 내에 전혀 다른 data 크기의 값을 열거 할 수 있으며, 해당 enum의 data 크기는 열거된 원소 중 가장 큰 data size로 결정된다는 의미
enum PoisonType {
    Acidic,
    Painful,
    Lethal,
}
enum Size {
    Big,
    Small,
}
enum Weapon {
    Claw(i32, Size),
    Poison(PoisonType),
    None,
}
 
struct SeaCreature {
    species: Species,
    name: String,
    arms: i32,
    legs: i32,
    weapon: Weapon,
}
 
fn main() {
    // SeaCreature의 데이터는 stack에 있음
    let ferris = SeaCreature {
        // String struct도 stack에 있지만,
        // heap에 있는 데이터에 대한 참조를 갖고 있음
        species: Species::Crab,
        name: String::from("Ferris"),
        arms: 2,
        legs: 4,
        weapon: Weapon::Claw(2, Size::Small),
    };
 
    match ferris.species {
        Species::Crab => match ferris.weapon {
            Weapon::Claw(num_claws, size) => {
                let size_description = match size {
                    Size::Big => "큰",
                    Size::Small => "작은",
                };
                println!(
                    "ferris는 {}개의 {} 집게를 가진 게이다",
                    num_claws, size_description
                )
            }
            _ => println!("ferris는 다른 무기를 가진 게이다"),
        },
        _ => println!("ferris는 다른 동물이다"),
    }
}

3장 - 마무리

4장 - Generic 자료형

Generic 자료형이란?

  • generic 자료형은 struct나 enum을 부분적으로 정의하여, 컴파일러가 컴파일 타임에 코드 사용을 기반으로 완전히 정의된 버전을 만들 수 있게 해줍니다.
    • struct BagOfHolding<T> { item: T, }
  • Rust는 일반적으로 인스턴스화 하는 것을 보고 최종 자료형을 유추할 수 있습니다.
    • let float_bag = BagOfHolding { item: 3.14 };
  • 그러나::<T> 연산자를 사용해 언제든 명시적으로 자료형을 지정할 수 있습니다.
    • let i32_bag = BagOfHolding::<i32> { item: 42 };

아무 것도 없는 것을 표현하기 & 옵션

  • Rust에는 null이 없습니다.
  • 한 개 이상의 선택 가능한 값에 대해 None 선택지를 제공하는 방법은 null 값이 없는 Rust에서 매우 흔한 패턴입니다.
    • 실제 Rust에서 제공하는 generic enum : Option

      • 참조 : https://showx123.tistory.com/58
      • T와 Option는 다른Type
      • null pointer dereference와 같은 메모리 취약점을 없앨 수 있다.
      • Rust 컴파일러가 Option을 포인터로 컴파일 타임에 변환, 런타임 오버헤드는 사실상 0
      • Enume은 원소 중 가장 큰 type의 사이즈를 갖도록 구현되어 있기 때문에 포인터의 사이즈(32비트면 4바이트, 64비트면 8바이트)보다 작은 타입의 경우 메모리를 아주 조금 낭비하게 됨.
      enum Option<T> {
          Some(T),
          None,
      }
       
      struct BagOfHolding {
          item: Option<Inventory>,
      }

결과

  • Rust에는 실패할 가능성이 있는 값을 리턴할 수 있도록 해주는 Result라 불리는 내장된 generic enum

    • Rust에서 오류 처리를 하는 관용적인 방법
    enum Result<T, E> {
        Ok(T),
        Err(E),
    }
  • 아래 코드를 돌려보면 “Standard Error”로 결과가 출력된다.

    • 단순한 ‘표현’이 아닌듯.. throw exception 같은 표현일까?
    fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
        if i == 42 {
            Ok(13.0)
        } else {
            Err(String::from("맞는 숫자가 아닙니다"))
        }
    }
     
    fn main() {
        let result = do_something_that_might_fail(12);
     
        // match는 Result를 우아하게 분해하고, 모든 케이스를 처리하도록 해준다!
        match result {
            Ok(v) => println!("{} 발견", v),
            Err(e) => println!("오류: {}", e),
        }
    }

실패할 수 있는 메인

  • main 은 Result를 반환할 수 있음

    fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
        if i == 42 {
            Ok(13.0)
        } else {
            Err(String::from("맞는 숫자가 아닙니다"))
        }
    }
     
    // Main은 아무 값도 리턴하지 않지만, 오류를 리턴할 수 있다!
    fn main() -> Result<(), String> {
        let result = do_something_that_might_fail(12);
     
        match result {
            Ok(v) => println!("{} 발견", v),
            Err(_e) => {
                // 이 오류를 우아하게 처리한다
     
                // main으로부터 무슨 일이 발생했는지 새 오류를 리턴한다!
                return Err(String::from("main에서 뭔가 잘못 되었습니다!"));
            }
        }
     
        // 모든 일이 잘 끝났음을 표현하기 위해
        // Result Ok 안에 unit 값을 쓰고 있는걸 잘 봐두십시오
        Ok(())
    }

우아한 오류 처리

  • Result와 함께 쓸 수 있는 강력한 연산자 ?

    • match ~ 문을 이용한 예외처리를 ? 로 대체할 수 있음
    fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
        if i == 42 {
            Ok(13.0)
        } else {
            Err(String::from("맞는 숫자가 아닙니다"))
        }
    }
     
    fn main() -> Result<(), String> {
        // 얼마나 코드를 줄였는지 보세요!
        let v = do_something_that_might_fail(42)?;
        println!("{} 발견", v);
        Ok(())
    }

추한 옵션/결과 처리

  • nullable, OK/Err 반환에 대한 예외 처리를 아래와 같이 간결하게 사용 가능
    • nullable
      • 기존

        match my_option { Some(v) => v, None => panic!("some error message generated by Rust!"), }

      • unwrap 사용

        let v = my_option.unwrap()

    • OK / Err
      • 기존

        match my_result { Ok(v) => v, Err(e) => panic!("some error message generated by Rust!"), }

      • unrwap 사용

        let v = my_result.unwrap()

벡터

  • Vec
    • struct로 표현되는 가변 크기의 리스트
    • struct이나, 내부적으로는 내용물이 heap에 있는 고정 리스트에 대한 참조를 포함
    • 기본 용량을 갖고 시작
    • 용량보다 많은 내용물이 추가될 경우, 큰 용량을 가진 새 고정 리스트를 위해 heap에 데이터를 재할당
  • vec!
    • vector를 수동으로 일일이 만드는 대신, 손쉽게 생성할 수 있게 해줌
  • .iter()
    • for 반복문에 손쉽게 넣을 수 있도록 vector로부터 반복자를 생성하는 메소드
fn main() {
    // 자료형을 명시적으로 할 수 있음
    let mut i32_vec = Vec::<i32>::new(); // turbofish <3
    i32_vec.push(1);
    i32_vec.push(2);
    i32_vec.push(3);
 
    // 하지만 Rust가 얼마나 똑똑하게 자료형을 자동으로 결정하는지 보십시오
    let mut float_vec = Vec::new();
    float_vec.push(1.3);
    float_vec.push(2.3);
    float_vec.push(3.4);
 
    // 아름다운 macro입니다!
    let string_vec = vec![String::from("Hello"), String::from("World")];
 
    for word in string_vec.iter() {
        println!("{}", word);
    }
}

4장 - 마무리

5장 - 소유권과 데이터 대여

소유권

  • 할당(binding)
    • 자료형을 인스턴스화 하여 변수명에 하는 행위
    • Rust 컴파일러가 전체 **생명주기(lifetime)**동안 검증할 메모리 리소스를 생성하는 것
    • 할당된 변수는 리소스의 **소유자(owner)**라고 불립니다.
struct Foo {
    x: i32,
}
 
fn main() {
    // struct를 인스턴스화 하고 변수에 bind하여
    // 메모리 리소스를 생성함
    let foo = Foo { x: 42 };
    // foo가 owner임
}

범위 기반 리소스 관리

  • Rust는 범위(scope)가 끝나는 곳에서 리소스를 소멸하고 할당 해제합니다.
  • 이 소멸과 할당 해제를 의미하는 용어로 drop을 사용합니다.
  • Rust에는 가비지 컬렉션이 없습니다.
struct Foo {
    x: i32,
}
 
fn main() {
    let foo_a = Foo { x: 42 };
    let foo_b = Foo { x: 13 };
 
    println!("{}", foo_a.x);
 
    println!("{}", foo_b.x);
    // foo_b가 여기서 drop 됩니다
    // foo_a가 여기서 drop 됩니다
}

Dropping은 계층적이다

  • struct가 drop 될 때
    • struct 자신이 제일 먼저 drop 됩니다
    • 이후에 그 자식들이 각각 drop 되며, 등의 순서대로 처리됩니다.
  • Rust에서는 메모리를 자동으로 해제함으로써 메모리 누수가 덜 일어나도록 합니다.
    • 자식들이 포인터 형인 경우에도, heap에 있을 경우에도 알아서 해제해줄까?
  • 메모리 리소스는 단 한 번 drop 될 수 있습니다.
struct Bar {
    x: i32,
}
 
struct Foo {
    bar: Bar,
}
 
fn main() {
    let foo = Foo { bar: Bar { x: 42 } };
    println!("{}", foo.bar.x);
    // foo가 먼저 drop 되고
    // 그 다음에 foo.bar가 drop 됩니다
}

소유권 이전

  • owner가 함수의 인자로 전달되면, ownership은 그 함수의 매개변수로 이동(move)됩니다.
  • move 이후에는 원래 함수에 있던 변수는 더 이상 사용할 수 없습니다.
    • move 중에는 owner 값의 stack 메모리가 함수 호출의 매개변수 stack 메모리로 복사됩니다.
struct Foo {
    x: i32,
}
 
fn do_something(f: Foo) {
    println!("{}", f.x);
    // f가 여기서 drop 됩니다
}
 
fn main() {
    let foo = Foo { x: 42 };
    // foo가 do_something으로 move 됩니다
    do_something(foo);
    // foo는 더 이상 사용할 수 없습니다
}

소유권 리턴하기

  • ownership은 함수에서도 리턴될 수 있습니다.
    • 기존 c++, c#에서는 포인터가 아닌 이상 값의 복사가 일어 났는데, 러스트는 기본적으로 메모리 주소 자체를 아에 넘겨버리는 것으로 보임
struct Foo {
    x: i32,
}
 
fn do_something() -> Foo {
    Foo { x: 42 }
    // ownership이 밖으로 move 됩니다
}
 
fn main() {
    let foo = do_something();
    // foo가 owner가 되었습니다
    // 함수의 scope 끝에 도달했기 때문에 foo는 drop 됩니다
}

참조로 소유권 대여하기

  • & 연산자를 통해 참조로 리소스에 대한 접근권한을 대여할 수 있습니다.
    • 참조도 다른 리소스와 마찬가지로 drop 됩니다.
struct Foo {
    x: i32,
}
 
fn main() {
    let foo = Foo { x: 42 };
    let f = &foo;
    println!("{}", f.x);
    // f는 여기서 drop 됩니다
    // foo는 여기서 drop 됩니다
}

참조로 변경 가능한 소유권 대여하기

  • &mut 연산자를 통해 리소스에 대한 mutable한 접근 권한도 대여할 수 있습니다.

    • 리소스의 owner는 mutable하게 대여된 상태에서는 move 되거나 변경될 수 없습니다.
    • Rust는 데이터 경합의 가능성 때문에 소유된 값을 변경하는 방법이 여러 개 생기는 것을 방지합니다.

    → 소유권을 대여해주면, 원래의 소유권을 가졌던 참조는 소유권이 반환될 때까지 변경 권한을 잃는다.

struct Foo {
    x: i32,
}
 
fn do_something(f: Foo) {
    println!("{}", f.x);
    // f는 여기서 drop 됩니다
}
 
fn main() {
    let mut foo = Foo { x: 42 };
    let f = &mut foo;
 
    // FAILURE: do_something(foo) 은 실패할 것입니다
    // 왜냐하면 foo는 mutable하게 borrow된 상태에서는 move될 수 없기 때문입니다
 
    // FAILURE: foo.x = 13; 는 여기서 실패할 것입니다
    // 왜냐하면 foo는 mutable하게 borrow된 상태에서는 변경할 수 없기 때문입니다
 
    f.x = 13;
    // f는 이 시점 이후 더 이상 사용되지 않기 때문에 여기서 drop 됩니다
 
    println!("{}", foo.x);
 
    // 모든 mutable 참조가 drop 되었으므로 이제 문제 없이 동작합니다
    foo.x = 7;
 
    // foo의 ownership을 함수로 move 합니다
    do_something(foo);
}

역참조

  • &mut 참조를 이용해 * 연산자로 owner의 값을 설정할 수 있습니다.
  • * 연산자로 own된 값의 복사본도 가져올 수 있습니다 (복사 가능한 경우만)
fn main() {
    let mut foo = 42;
    let f = &mut foo;
    let bar = *f; // owner의 값의 복사본을 가져옴
    *f = 13; // 참조의 owner의 값을 설정함
    println!("{}", bar);
    println!("{}", foo);
}

대여한 데이터 전달하기

  • Rust의 참조 규칙은 다음과 같이 요약될 수 있습니다:
    • Rust는 단 하나의 mutable한 참조 또는 여러개의 non-mutable 참조만 허용하며, 둘 다는 안됨.
    • 참조는 그 owner보다 더 오래 살 수 없음.
  • 첫 번째 참조 규칙은 데이터 경합을 방지합니다.
    • 데이터를 읽는 행위가 동시에 데이터를 쓰는 이의 존재로 인해 동기화가 어긋날 가능성이 있을 때 일어납니다.
  • 두 번째 참조 규칙은 존재하지 않는 데이터를 바라보는 잘못된 참조를 사용하는 것을 방지
    • 이를 C에서는 허상 포인터(dangling pointers)라고 부름
struct Foo {
    x: i32,
}
 
fn do_something(f: &mut Foo) {
    f.x += 1;
    // mutable 참조 f는 여기서 drop 됩니다
}
 
fn main() {
    let mut foo = Foo { x: 42 };
    do_something(&mut foo);
    // 모든 mutable 참조가 do_something 함수 내에서 drop 되므로,
    // 하나 더 생성할 수 있습니다.
    do_something(&mut foo);
    // foo는 여기서 drop 됩니다
}

참조의 참조

  • 참조는 심지어 참조에도 사용될 수 있습니다.
struct Foo {
    x: i32,
}
 
fn do_something(a: &Foo) -> &i32 {
    return &a.x;
}
 
fn main() {
    let mut foo = Foo { x: 42 };
    let x = &mut foo.x;
    *x = 13;
    // 여기서 x가 drop 되어 non-mutable 참조를 생성할 수 있습니다
    let y = do_something(&foo);
    println!("{}", y);
    // y는 여기서 drop 됩니다
    // foo는 여기서 drop 됩니다
}

명시적인 생명주기

  • Rust 컴파일러는 모든 변수의 lifetime을 이해하며 참조가 절대로 그 owner보다 더 오래 존재하지 못하도록 검증을 시도합니다.
  • 함수에서는 어떤 매개변수와 리턴 값이 서로 같은 lifetime을 공유하는지 식별할 수 있도록 심볼로 표시하여 명시적으로 생명주기를 지정할 수 있습니다.
  • lifetime 지정자는 언제나 '로 시작합니다. (예: 'a'b'c)
struct Foo {
    x: i32,
}
 
// 매개변수 foo와 리턴 값은 동일한 lifetime을 공유함
fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
    return &foo.x;
}
 
fn main() {
    let mut foo = Foo { x: 42 };
    let x = &mut foo.x;
    *x = 13;
    // x가 여기서 drop 되어, non-mutable 참조를 생성할 수 있음
    let y = do_something(&foo);
    println!("{}", y);
    // y는 여기서 drop 됨
    // foo는 여기서 drop 됨
}

여러 개의 생명주기

  • lifetime 지정자는 컴파일러가 스스로 함수 매개변수들의 lifetime을 판별하지 못하는 경우, 이를 명시적으로 지정할 수 있게 도와줍니다.
    • 아래 코드에서 를 제외하면 에러 발생
struct Foo {
    x: i32,
}
 
// foo_b와 리턴 값은 동일한 lifetime을 공유함
// foo_a는 무관한 lifetime을 가짐
fn do_something<'a, 'b>(foo_a: &'a Foo, foo_b: &'b Foo) -> &'b i32 {
    println!("{}", foo_a.x);
    println!("{}", foo_b.x);
    return &foo_b.x;
}
 
fn main() {
    let foo_a = Foo { x: 42 };
    let foo_b = Foo { x: 12 };
    let x = do_something(&foo_a, &foo_b);
    // 여기 이후에는 foo_b의 lifetime만 존재하므로 foo_a만 drop 됨
    println!("{}", x);
    // 여기서 x가 drop 됨
    // 여기서 foo_b가 drop 됨
}

정적인 생명주기

  • static 변수는 컴파일 타임에 생성되어 프로그램의 시작부터 끝까지 존재하는 메모리 리소스입니다.
    • 이들은 명시적으로 자료형을 지정해 주어야 합니다.
  • static lifetime은 프로그램이 끝날 때까지 무한정 유지되는 메모리 리소스입니다.
    • 이 정의에 따르면, 어떤 static lifetime의 리소스는 런타임에 생성될 수도 있습니다.
    • static lifetime을 가진 리소스는 'static이라는 특별한 lifetime 지정자를 갖습니다.
    • 'static한 리소스는 절대 drop 되지 않습니다.
    • static lifetime을 가진 리소스가 참조를 포함하는 경우, 그들도 모두 'static이어야 합니다
      • 그 이하의 것들은 충분히 오래 살아남지 못합니다
  • static 변수는 어느 누구에 의해서든 전역적으로 접근 가능하기 때문에, 이를 변경하는 것은 데이터 경합을 유발하는, 본질적으로 위험한 행위입니다.
  • Rust에서는 unsafe { ... } 블록을 이용하여 특정 동작에 대해 컴파일러가 메모리 검사를 하지 않도록 할 수 있습니다.
    • 없을 경우 : error[E0133]: use of mutable static is unsafe and requires unsafe function or block
    • 아래 예제의 경우, unsafe를 없애면 SECRET = "abracadabra";println!("{}", SECRET);에 모두 오류가 발생
static PI: f64 = 3.1415;
 
fn main() {
    // static 변수는 함수 scope 안에도 넣을 수 있습니다
    static mut SECRET: &'static str = "swordfish";
 
    // string 값들은 'static lifetime을 갖습니다
    let msg: &'static str = "Hello World!";
    let p: &'static f64 = &PI;
    println!("{} {}", msg, p);
 
    // 일부 규칙은 깰 수 있으나, 반드시 명시적으로 해야 합니다
    unsafe {
        // SECRET에 string 값을 설정할 수 있는데, 이 값 역시 'static이기 때문입니다
        SECRET = "abracadabra";
        println!("{}", SECRET);
    }

데이터 자료형의 생명주기

  • 함수와 마찬가지로, 데이터 자료형의 구성원들도 lifetime 지정자로 지정할 수 있습니다.
  • Rust는 참조가 품고 있는 데이터 구조가 참조가 가리키는 owner보다 절대 오래 살아남지 못하도록 검증합니다.
struct Foo<'a> {
    i:&'a i32
}
 
fn main() {
    let x = 42;
    let foo = Foo {
        i: &x
    };
    println!("{}",foo.i);
}

5장 - 마무리

6장 - 텍스트

문자열

  • 언제나 유니코드로 되어 있음
  • 문자열의 자료형은 &'static str입니다:
    • &는 메모리 내의 장소를 참조하고 있다는 의미
    • &mut가 빠졌다는 것은 컴파일러가 값의 변경을 허용하지 않을 것이라는 뜻
    • 'static은 string 데이터가 프로그램이 끝날 때까지 유효하다는 의미(절대 drop 되지 않음)
    • str은 언제나 유효한 utf-8인 바이트 열을 가리키고 있다는 의미
  • Rust 컴파일러는 문자열을 프로그램 메모리의 데이터 세그먼트에 저장
  • let a: &'static str = "hi 🦀";

utf-8이란 무엇인가

  • utf-8은 1에서 4 바이트의 가변 길이 바이트로 도입 되었음
    • 사용 가능한 문자의 범위를 엄청나게 늘어남
    • ASCII 문자에 쓸데 없는 바이트를 필요로 하지 않음(utf-8에서도 여전히 1 바이트만 필요로 함)
    • 단순한 인덱싱(예: my_text[3]으로 네 번째 문자를 가져옴)시, O(1) 상수 시간으로 문자를 찾을 수 없음
      • 바로 앞의 글자가 가변 길이를 가질 수 있으므로, 바이트 열에서 4번째 문자가 실제로 시작하는 위치가 달라질 수도 있음
      • utf-8 바이트 열을 하나하나 돌면서 각각의 유니코드 문자가 실제로 어디에서 시작하는지 찾아야 함 (O(n) 선형 시간)

예외처리문자

  • 어떤 문자들은 시각적으로 표현하기 어려우므로, 예외처리 코드(escape code) 로 대체하여 사용

  • Rust는 C 기반 언어들의 일반적인 예외처리 코드를 지원

    • \n - 줄바꿈
    • \r - 캐리지리턴
    • \t - 탭
    • \\ - 역슬래시
    • \0 - null
    • \' - 작은 따옴표

    → 전체 목록은 이곳에서

여러 줄로 된 문자열

  • Rust의 문자열은 기본적으로 여러 줄로 되어 있음
  • 줄바꿈 문자를 원하지 않을 경우, 줄 맨 뒤에 \를 사용
fn main() {
    let haiku: &'static str = "
        나는 쓰고, 지우고, 다시 쓴다
        다시 지우고, 그러고 나면
        양귀비 꽃이 핀다.
        - 가쓰시카 호쿠사이";
    println!("{}", haiku);
 
    println!(
        "hello \
    world"
    ) // w 앞의 공백이 무시 되었음을 주의하세요
}

원시 문자열

  • r#"로 시작하고 "#로 끝남
  • 문자열을 있는 그대로 쓸 수 있음
    • 큰 따옴표나 역슬래시 같은 문자들을 바로 사용할 수 있음
fn main() {
    let a: &'static str = r#"
        <div class="advice">
            원시 문자열은 일부 상황에서 유용합니다.
        </div>
        "#;
    println!("{}", a);
}

파일에서 문자열 가져오기

  • include_str! macro를 사용하여 local에 있는 문자열을 가져올 수 있음
    • let 00_html = include_str!("00_ko.html");

문자열 슬라이스

  • 문자열 slice는 메모리 상의 바이트 열에 대한 참조
    • utf-8의 특성 상, char에 대한 index 접근과 다르다.
    • slice의 문자열 slice (sub-slice)도 역시 유효한 utf-8이어야 함
      • slice의 결과물이 문자열을 만들 지 못할 때 EROR
  • 흔히 사용되는 메소드
    • len은 문자열의 바이트 길이(글자수가 아님)
    • starts_with/ends_with는 기본적인 비교에 사용
    • is_empty는 길이가 0일 경우 true를 반환
    • find는 주어진 텍스트가 처음 등장 하는 위치인 Option<usize> 값을 반환

문자

  • utf-8 바이트 열을 char 자료형의 vector로 돌려주는 기능을 제공
  • char 하나는 4 바이트(각각의 문자를 효율적으로 찾을 수 있음)
fn main() {
    // 문자들을 char의 vector로 가져옵니다
    let chars = "hi 🦀".chars().collect::<Vec<char>>();
 
    println!("{}", chars.len()); // 4여야 합니다
 
    // char가 4 바이트이므로 u32로 변환할 수 있습니다
    println!("{}", chars[3] as u32);
}

스트링

  • String은 utf-8 바이트 열을 heap memory에 소유하는 struct
  • heap에 있기 때문에, 문자열과는 달리 늘리거나 변경하거나 기타 등등을 할 수 있음
  • push_str은 string의 맨 뒤에 utf-8 바이트들을 더 붙일 때 사용
  • replace는 utf-8 바이트 열을 다른 것으로 교체할 때 사용
  • to_lowercase/to_uppercase는 대소문자를 바꿀 때 사용
  • trim은 공백을 제거할 때 사용
fn main() {
    let mut helloworld = String::from("hello");
    helloworld.push_str(" world");
    helloworld = helloworld + "!";
    println!("{}", helloworld);
}

함수의 매개변수로서의 텍스트

  • 문자열과 string은 일반적으로 함수에 문자열 slice 형태로 전달
fn say_it_loud(msg: &str) {
    println!("{}!!!", msg.to_string().to_uppercase());
}
 
fn main() {
    // say_it_loud는 &'static str을 &str로 대여할 수 있습니다
    say_it_loud("hello");
    // say_it_loud는 또한 String을 &str로 대여할 수 있습니다
    say_it_loud(&String::from("goodbye"));
}

스트링 만들기

  • concat과 join은 string을 만드는 간단하지만 강력한 방법
fn main() {
    let helloworld = ["hello", " ", "world", "!"].concat();
    let abc = ["a", "b", "c"].join(",");
    println!("{}", helloworld);
    println!("{}",abc);
}

→ hello world!
→ a,b,c

스트링 양식 만들기

  • format! macro는 값이 어디에 어떻게 놓일지 매개변수화 된 (예: {}) string을 정의하여 string을 생성
fn main() {
    let a = 42;
    let f = format!("삶, 우주, 그리고 모든 것에 대한 해답: {}", a);
    println!("{}", f);
}

스트링 변환

  • to_string을 이용하여 string으로 변환될 수 있음(모든 자료형이 되는 것은 아님)
  • generic 함수인 parse로 string이나 문자열을 다른 자료형을 가진 값으로 변환할 수 있음
    • 실패할 수도 있기 때문에 Result를 리턴
fn main() -> Result<(), std::num::ParseIntError> {
    let a = 42;
    let a_string = a.to_string();
    let b = a_string.parse::<i32>()?;
    println!("{} {}", a, b);
    Ok(())
}

6장 - 마무리

7장 - 객체 지향 프로그래밍

OOP란 무엇인가?

  • 다음과 같은 상징적 특징을 가진 프로그래밍 언어를 뜻함
    • 캡슐화 (encapsulation) - _객체_라 불리는 단일 유형의 개념적 단위에 데이터와 함수를 연결지음.
    • 추상화 (abstraction) - 데이터와 함수를 숨겨 객체의 상세 구현 사항을 알기 어렵게 함.
    • 다형성 (polymorphism) - 다른 기능적 관점에서 객체와 상호작용하는 능력.
    • 상속 (inheritance) - 다른 객체로부터 데이터와 동작을 상속받는 능력.

Rust는 OOP가 아니다

  • Rust에서는 어떠한 방법으로도 데이터와 동작의 상속이 불가능
    • struct는 부모 struct로부터 field를 상속받을 수 없습니다.
    • struct는 부모 struct로부터 함수를 상속받을 수 없습니다.

메소드 캡슐화 하기

  • 모든 메소드의 첫번째 매개변수는 메소드 호출과 연관된 인스턴스에 대한 참조여야 함

    • &self - 인스턴스에 대한 immutable한 참조.
    • &mut self - 인스턴스에 대한 mutable한 참조.
    • ex: instanceOfObj.foo()
  • 메소드는 impl 키워드를 쓰는 구현 블록 안에 정의

    impl MyStruct {
        ...
        fn foo(&self) {
            ...
        }
    }
struct SeaCreature {
    noise: String,
}
 
impl SeaCreature {
    fn get_sound(&self) -> &str {
        &self.noise
    }
}
 
fn main() {
    let creature = SeaCreature {
        noise: String::from("blub"),
    };
    println!("{}", creature.get_sound());
}

선택적 노출을 통한 추상화

  • 기본적으로, filed와 메소드들은 그들이 속한 module에서만 접근 가능
  • pub 키워드는 struct의 field와 메소드를 module 밖으로 노출시킴
struct SeaCreature {
    pub name: String,
    noise: String,
}
 
impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}
 
fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    println!("{}", creature.get_sound());
}

다형성과 Trait

  • trait은 메소드의 집합을 struct 자료형에 연결할 수 있게 해줌
  • struct가 trait을 구현할 때, 실제 자료형이 무엇인지 알지 못하더라도 그 trait 자료형을 통해 간접적으로 struct와 상호작용할 수 있도록 (예: &dyn MyTrait) 협약을 맺게 됨
struct SeaCreature {
    pub name: String,
    noise: String,
}
 
impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}
 
trait NoiseMaker {
    fn make_noise(&self);
}
 
impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound());
    }
}
 
fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    creature.make_noise();
}

Trait에 구현된 메소드

  • trait에 메소드를 구현해 넣을 수 있음
  • struct 내부의 field에 직접 접근할 수는 없지만, trait 구현체들 사이에서 동작을 공유할 때 유용
struct SeaCreature {
    pub name: String,
    noise: String,
}
 
impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}
 
trait NoiseMaker {
    fn make_noise(&self);
    
    fn make_alot_of_noise(&self){
        self.make_noise();
        self.make_noise();
        self.make_noise();
    }
}
 
impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound());
    }
}
 
fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    creature.make_alot_of_noise();
}

Trait 상속

  • trait은 다른 trait의 메소드들을 상속 받을 수 있음
struct SeaCreature {
    pub name: String,
    noise: String,
}
 
impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}
 
trait NoiseMaker {
    fn make_noise(&self);
}
 
trait LoudNoiseMaker: NoiseMaker {
    fn make_alot_of_noise(&self) {
        self.make_noise();
        self.make_noise();
        self.make_noise();
    }
}
 
impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound());
    }
}
 
impl LoudNoiseMaker for SeaCreature {}
 
fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    creature.make_alot_of_noise();
}

동적 vs 정적 디스패치

  • 정적 디스패치 (static dispatch) - 인스턴스의 자료형을 알고 있는 경우, 어떤 함수룰 호출해야 하는지 정확히 알고 있음
  • 동적 디스패치 (dynamic dispatch) - 인스턴스의 자료형을 모르는 경우, 올바른 함수를 호출할 방법을 찾아야 함
    • trait 자료형인 &dyn MyTrait은 동적 디스패치를 통해 객체의 인스턴스들을 간접적으로 작동시킬 수 있게 함
    • 동적 디스패치를 사용할 경우, Rust에서는 사람들이 알 수 있도록 trait 자료형 앞에 dyn을 붙일 것을 권고
  • 동적 디스패치는 실제 함수 호출을 위한 포인터 추적으로 인해 조금 느릴 수 있음
struct SeaCreature {
    pub name: String,
    noise: String,
}
 
impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}
 
trait NoiseMaker {
    fn make_noise(&self);
}
 
impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound());
    }
}
 
fn static_make_noise(creature: &SeaCreature) {
    // 실제 자료형을 압니다
    creature.make_noise();
}
 
fn dynamic_make_noise(noise_maker: &dyn NoiseMaker) {
    // 실제 자료형을 모릅니다
    noise_maker.make_noise();
}
 
fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    static_make_noise(&creature);
    dynamic_make_noise(&creature);
}

Trait 객체

  • 객체의 인스턴스를 &dyn MyTrait 자료형을 가진 매개변수로 넘길 때, 이를 _trait 객체_라고 부름
  • trait 객체
    • 인스턴스의 올바른 메소드를 간접적으로 호출할 수 있게 해줌
    • 인스턴스에 대한 포인터와 인스턴스 메소드들에 대한 함수 포인터 목록을 갖고있는 struct
  • 이런 함수 목록을 C++에서는 _vtable_이라고 함

크기를 알 수 없는 데이터 다루기

  • trait을 다른 struct에 저장하는 것
    • trait은 원본 struct를 알기 어렵게 하느라 원래 크기 또한 알기 어렵게 함
  • Rust에서 크기를 알 수 없는 값이 struct에 저장될 때는 다음의 두 가지 방법으로 처리
    • generics - 매개변수의 자료형을 효과적으로 활용하여 알려진 자료형 및 크기의 struct/함수를 생성
    • indirection - 인스턴스를 heap에 올림으로써 실제 자료형의 크기 걱정 없이 그 포인터만 저장

Generic 함수

  • Rust의 generic은 trait과 함께 작동

  • 매개변수 자료형 T를 정의할 때 해당 인자가 어떤 trait을 구현해야 하는지 나열함으로써 인자에 어떤 자료형을 쓸 수 있는지 제한할 수 있음

    fn my_function<T>(foo: T)
    where
        T:Foo
    {
        ...
    }
  • generic을 이용하면 컴파일 시 자료형과 크기를 알 수 있는 정적 자료형의 함수가 만들어짐

    • 정적 디스패치와 함께 크기가 정해진 값으로 저장할 수 있게 됨
struct SeaCreature {
    pub name: String,
    noise: String,
}
 
impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}
 
trait NoiseMaker {
    fn make_noise(&self);
}
 
impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound());
    }
}
 
fn generic_make_noise<T>(creature: &T)
where
    T: NoiseMaker,
{
    // 컴파일 타임에 실제 자료형을 알게 됩니다
    creature.make_noise();
}
 
fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    generic_make_noise(&creature);
}

Generic 함수 줄여쓰기

  • trait으로 제한한 generic은 다음과 같이 줄여쓸 수 있음

    fn my_function(foo: impl Foo) {
        ...
    }
     
    // 줄여쓰기 전
    fn my_function<T>(foo: T)
    where
        T:Foo
    {
        ...
    }
struct SeaCreature {
    pub name: String,
    noise: String,
}
 
impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}
 
trait NoiseMaker {
    fn make_noise(&self);
}
 
impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound());
    }
}
 
fn generic_make_noise(creature: &impl NoiseMaker) {
    // 컴파일 타임에 실제 자료형을 알게 됩니다
    creature.make_noise();
}
 
fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    generic_make_noise(&creature);
}

Box

  • stack에 있는 데이터를 heap으로 옮길 수 있게 해주는 데이터 구조
  • _smart pointer_로도 알려진 struct이며 heap에 있는 데이터를 가리키는 포인터를 들고 있음
  • 크기가 알려져 있는 struct이므로 (왜냐하면 그저 포인터만 들고 있으므로) field의 크기를 알아야 하는 struct에 뭔가의 참조를 저장할 때 종종 사용 됨
struct SeaCreature {
    pub name: String,
    noise: String,
}
 
impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}
 
trait NoiseMaker {
    fn make_noise(&self);
}
 
impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound());
    }
}
 
struct Ocean {
    animals: Vec<Box<dyn NoiseMaker>>,
}
 
fn main() {
    let ferris = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("blub"),
    };
    let sarah = SeaCreature {
        name: String::from("Sarah"),
        noise: String::from("swish"),
    };
    let ocean = Ocean {
        animals: vec![Box::new(ferris), Box::new(sarah)],
    };
    for a in ocean.animals.iter() {
        a.make_noise();
    }
}

Generic 구조체 다시 보기

  • generic struct는 trait으로 제한된 매개변수 자료형을 가질 수 있음
struct MyStruct<T>
where
    T: MyTrait
{
    foo: T
    ...
}
 
//매개변수 자료형은 generic structure의 구현 블록 안에 표시
impl<T> MyStruct<T> {
    ...
}

7장 - 마무리

8장 - 스마트 포인터

참조 다시 보기

  • 참조는 메모리 상의 어떤 바이트들의 시작 위치를 가리키는 숫자
  • 용도
    • 특정 자료형의 데이터가 어디에 존재하는지에 대한 개념을 나타내는 것
  • 일반 숫자와의 차이점
    • Rust에서 참조가 가리키는 값보다 더 오래 살지 않도록 lifetime을 검증하는 것

원시 포인터

  • 참조는 더 원시적인 자료형인 _raw pointer_로 변환될 수 있음
  • raw pointer는 숫자와 마찬가지로 거의 제한 없이 여기저기 복사하고 이동할 수 있음
    • Rust는 raw pointer가 가리키는 메모리 위치의 유효성을 보증하지 않습니다.
  • raw pointer에는 두 종류
    • const T - 자료형 T의 데이터를 가리키는 절대 변경되지 않는 raw pointer.
    • mut T - 자료형 T의 데이터를 가리키는 변경될 수 있는 raw pointer.
  • raw pointer는 숫자와 상호 변환이 가능
    • 예: usize
  • raw pointer는 _unsafe_한 코드의 데이터에 접근할 수 있음
  • 참조 vs raw pointer
    • Rust에서의 참조
      • 사용 방법에 있어서 C의 pointer와 매우 유사하나, 저장되는 방식이나 다른 함수에 전달되는 부분에 있어 훨씬 많은 컴파일 타임의 제약을 받음
    • Rust에서의 raw pointer
      • 복사하고 전달하고 심지어 pointer 연산을 할 수 있는 숫자 자료형으로 변환할 수도 있다는 점에서 C의 pointer와 유사
fn main() {
    let a = 42;
    let memory_location = &a as *const i32 as usize;
    println!("데이터는 여기 있습니다: {}", memory_location);
}
 
-> 데이터는 여기 있습니다: 140725281338716

역참조

  • 참조 (i.e. &i32)를 통해 참조되는 데이터를 접근/변경하는 것
  • 참조로 데이터에 접근/변경하는 데에는 다음의 두 가지 방법(=역참조 방법)
    • 변수 할당 중에 참조되는 데이터에 접근
    • 참조되는 데이터의 field나 메소드에 접근

* 연산자

  • * 연산자는 참조를 역참조 하는 명시적인 방법

  • i32는 ==Copy trait을 구현하는 기본 자료형이기 때문에, stack에 있는 변수 a==의 바이트들은 변수 ==b==의 바이트들로 복사

    https://velog.io/@undefcat/Rust-T-mut-T-그리고-Copy

    • copy trait : 일종의 shallow copy? ↔ clone trait
      • trait : rust의 object
fn main() {
    let a: i32 = 42;
    let ref_ref_ref_a: &&&i32 = &&&a;
    let ref_a: &i32 = **ref_ref_ref_a;
    let b: i32 = *ref_a;
    println!("{}", b)
}
 
-> 42

. 연산자

  • 참조의 field와 메소드에 접근하는 데에 쓰임, 이건 좀 더 미묘하게 동작

    let f = Foo { value: 42 };
    let ref_ref_ref_f = &&&f;
    println!("{}", ref_ref_ref_f.value);
    • . 연산자가 참조 열을 자동으로 역참조
    • 저 마지막 줄은 컴파일러에 의해 자동적으로 다음과 같이 바뀌게 됨
      • println!("{}", (***ref_ref_ref_f).value);

스마트 포인터

  • & 연산자로 이미 존재하는 데이터의 참조를 생성하는 기능과 더불어, Rust에서는 smart pointer라 불리는 참조 같은 struct를 생성하는 기능을 제공
  • 고수준에서 보자면 참조는 다른 자료형에 대한 접근을 제공하는 자료형이라고 볼 수 있음
  • smart pointer가 일반적인 참조와 다른 점은, 프로그래머가 작성하는 내부 로직에 기반해 작동한다는 것
  • 일반적으로 smart pointer는 struct가 *와 . 연산자로 역참조될 때 무슨 일이 발생할지 지정하기 위해 DerefDerefMut, 그리고 Drop trait을 구현
use std::ops::Deref;
struct TattleTell<T> {
    value: T,
}
impl<T> Deref for TattleTell<T> {
    type Target = T;
    fn deref(&self) -> &T {
        println!("{} was used!", std::any::type_name::<T>());
        &self.value
    }
}
fn main() {
    let foo = TattleTell {
        value: "secret message",
    };
    // foo가 `len` 함수를 위해 자동참조된 후
    // 여기서 역참조가 즉시 일어납니다
    println!("{}", foo.len());
}
-> &str was used!
   14

==→ 문자열은 필드에 존재하는데, foo의 참조를 재정의해서 self.value의 참조를 반환하도록 했기 때문에 len을 호출 할 수 있게 되었다.==

위험한 스마트 코드

  • smart pointer는 _unsafe_한 코드를 꽤 자주 쓰는 경향이 있음.
  • smart pointer는 Rust에서 가장 저수준의 메모리를 다루기 위한 일반적인 도구
  • unsafe한 코드
    • Rust 컴파일러가 보증할 수 없는 몇 가지 기능이 있다는 예외사항을 제외하고는 일반적인 코드와 완전히 똑같이 동작
    • unsafe한 코드의 주기능은 _raw pointer를 역참조_하는 것
    • 이는 _raw pointer_를 메모리 상의 위치에 가져다 놓고 “데이터 구조가 여깄다!”고 선언한 뒤 사용할 수 있는 데이터 표현으로 변환하는 것을 의미
      • 예: *const u8을 u8
    • Rust에는 메모리에 쓰여지는 모든 바이트의 의미를 추적하는 방법은 없음
    • Rust는 _raw pointer_로 쓰이는 임의의 숫자에 무엇이 존재하는지 보증할 수 없기 때문에, 역참조를 unsafe { ... } 블록 안에 넣음
fn main() {
    let a: [u8; 4] = [86, 14, 73, 64];
    // 이게 원시 pointer입니다.
    // 무언가의 메모리 주소를 숫자로 가져오는 것은 완전히 안전한 일입니다
    let pointer_a = &a as *const u8 as usize;
    println!("데이터 메모리 주소: {}", pointer_a);
    // 숫자를 원시 pointer로, 다시 f32로 변환하는 것 역시
    // 안전한 일입니다.
    let pointer_b = pointer_a as *const f32;
    let b = unsafe {
        // 이건 unsafe한데,
        // 컴파일러에게 우리의 pointer가 유효한 f32라고 가정하고
        // 그 값을 변수 b로 역참조 하라고 하고 있기 때문입니다.
        // Rust는 이런 가정이 참인지 검증할 방법이 없습니다.
        *pointer_b
    };
    println!("맹세하건대 이건 파이다! {}", b);
}
 
-> 데이터 메모리 주소: 140725241194940 
   맹세하건대 이건 파이다! 3.1415

익숙한 친구들

  • Vec<T>
    • 바이트들의 메모리 영역을 소유하는 smart pointer
    • Rust 컴파일러는 이 바이트들에 뭐가 존재하는지 모름
    • smart pointer
      • 관리하는 메모리 영역에서 내용물을 꺼내기 위해 자기가 뭘 의미하는지 해석
      • 데이터 구조가 그 바이트들 내 어디에서 시작하고 끝나는지 추적
      • raw pointer를 데이터 구조로, 또 쓰기 편한 멋지고 깔끔한 인터페이스로 역참조
  • String
    • 바이트들의 메모리 영역을 추적
    • 쓰여지는 내용물이 언제나 유효한 utf-8이도록 프로그램적으로 제한
    • 메모리 영역을 &str 자료형으로 역참조할 수 있도록 도와줌
  • 이 데이터 구조들 둘 다, 자기 할 일을 하기 위해 raw pointer에 대한 unsafe한 역참조를 사용
use std::alloc::{alloc, Layout};
use std::ops::Deref;
 
struct Pie {
    secret_recipe: usize,
}
 
impl Pie {
    fn new() -> Self {
        // 4 바이트를 요청해 봅시다
        let layout = Layout::from_size_align(4, 1).unwrap();
 
        unsafe {
            // 메모리 위치를 숫자로 할당하고 저장합니다
            let ptr = alloc(layout) as *mut u8;
            // pointer 연산을 사용해 u8 값 몇 개를 메모리에 써봅시다
            ptr.write(86);
            ptr.add(1).write(14);
            ptr.add(2).write(73);
            ptr.add(3).write(64);
 
            Pie {
                secret_recipe: ptr as usize,
            }
        }
    }
}
impl Deref for Pie {
    type Target = f32;
    fn deref(&self) -> &f32 {
        // secret_recipe pointer를 f32 raw pointer로 변환합니다
        let pointer = self.secret_recipe as *const f32;
        // 역참조 하여 &f32 값으로 리턴합니다
        unsafe { &*pointer }
    }
}
fn main() {
    let p = Pie::new();
    // Pie struct의 smart pointer를 역참조 하여
    // "파이를 만듭니다"
    println!("{:?}", *p);
}
 
-> 3.1415

힙에 할당된 메모리

  • Box는 데이터를 stack에서 heap으로 옮길 수 있게 해주는 smart pointer
  • 이를 역참조하면 마치 원래 자료형이었던 것처럼 heap에 할당된 데이터를 편하게 쓸 수 있음
struct Pie;
 
impl Pie {
    fn eat(&self) {
        println!("heap에 있으니 더 맛있습니다!")
    }
}
 
fn main() {
    let heap_pie = Box::new(Pie);
    heap_pie.eat();
}
 
->heap에 있으니 더 맛있습니다!

실패할 수 있는 메인 다시 보기

  • Rust 코드에는 많고도 많은 오류 표현 방법이 있지만, 그 중에도 standard library에는 오류를 설명하기 위한 범용 trait인 std::error::Error가 있음
  • smart pointer인 Box를 사용하면 Box<dyn std::error::Error>를 오류 리턴 시 공통된 자료형으로 사용할 수 있음
  • 이는 오류를 heap에 전파하고 특정한 자료형을 몰라도 고수준에서 상호작용할 수 있도록 해주기 때문
  • Tour of Rust 초반에 main은 오류를 리턴할 수 있다고 배웠습니다.
  • 이제 우리는 오류의 데이터 구조가 Rust의 일반적인 Error trait을 구현하는 한, 프로그램에서 발생할 수 있는 거의 모든 종류의 오류를 설명할 수 있는 자료형을 리턴할 수 있습니다.
use core::fmt::Display;
use std::error::Error;
 
struct Pie;
 
#[derive(Debug)]
struct NotFreshError;
 
impl Display for NotFreshError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "이 파이는 신선하지 않군요!")
    }
}
 
impl Error for NotFreshError {}
 
impl Pie {
    fn eat(&self) -> Result<(), Box<dyn Error>> {
        Err(Box::new(NotFreshError))
    }
}
 
fn main() -> Result<(), Box<dyn Error>> {
    let heap_pie = Box::new(Pie);
    heap_pie.eat()?;
    Ok(())
}
->
Standard Error
   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/playground`
Error: NotFreshError

참조 카운팅

  • Rc
    • stack에 있는 데이터를 heap으로 옮겨주는 smart pointer
    • heap에 놓인 데이터를 immutable하게 대여하는 기능을 가진 다른 Rc smart pointer들을 복제할 수 있게 해줌
    • smart pointer가 drop 될 때에만 heap에 있는 데이터가 할당 해제
use std::rc::Rc;
 
struct Pie;
 
impl Pie {
    fn eat(&self) {
        println!("heap에 있으니 더 맛있습니다!")
    }
}
 
fn main() {
    let heap_pie = Rc::new(Pie);
    let heap_pie2 = heap_pie.clone();
    let heap_pie3 = heap_pie2.clone();
 
    heap_pie3.eat();
    heap_pie2.eat();
    heap_pie.eat();
 
    // 모든 참조 카운트 smart pointer가 여기서 drop 됩니다
    // heap 데이터인 Pie가 드디어 할당 해제됩니다
}
-> heap에 있으니 더 맛있습니다!
   heap에 있으니 더 맛있습니다!
   heap에 있으니 더 맛있습니다!

접근 공유하기

  • RefCell
    • 보통 smart pointer가 보유하는 컨테이너 데이터 구조
    • 데이터를 가져오거나 안에 있는 것에 대한 mutable 또는 immutable한 참조를 대여할 수 있게 해줌
    • 데이터를 대여할 때, Rust는 런타임에 다음의 메모리 안전 규칙을 적용하여 남용을 방지
      • 단 하나의 mutable한 참조 또는 여러개의 immutable한 참조만 허용
      • 둘 다는 불가
      • 이 규칙을 어기면 RefCell은 panic을 일으킴
use std::cell::RefCell;
 
struct Pie {
    slices: u8,
}
 
impl Pie {
    fn eat(&mut self) {
        println!("heap에 있으니 더 맛있습니다!");
        self.slices -= 1;
    }
}
 
fn main() {
    // RefCell은 런타임에 메모리 안전성을 검증합니다
    // 주의: pie_cell은 mut가 아닙니다!
    let pie_cell = RefCell::new(Pie { slices: 8 });
 
    {
        // 그렇지만 mutable 참조를 대여할 수 있습니다!
        let mut mut_ref_pie = pie_cell.borrow_mut();
        mut_ref_pie.eat();
        mut_ref_pie.eat();
 
        // mut_ref_pie는 scope의 마지막에 drop 됩니다
    }
 
    // 이제 mutable 참조가 drop 되고 나면 immutable하게 대여할 수 있습니다
    let ref_pie = pie_cell.borrow();
    println!("{} 조각 남았습니다", ref_pie.slices);
}
 
-> heap에 있으니 더 맛있습니다!
	 heap에 있으니 더 맛있습니다!
	 6 조각 남았습니다

쓰레드 간에 공유하기

  • Mutex
    • 보통 smart pointer가 보유하는 컨테이너 데이터 구조
    • 데이터를 가져오거나 내부 데이터에 대한 mutable 또는 immutable한 참조를 대여할 수 있게 해줌
    • 잠긴 대여를 통해 운영체제가 동시에 오직 하나의 CPU만 데이터에 접근 가능
    • 원래 쓰레드가 끝날 때까지 다른 쓰레드들을 막음으로써 대여가 남용되는 것을 방지Mutex는 여러 개의 CPU 쓰레드가 같은 데이터에 접근하는 것을 조율하는 근본적인 부분
  • Arc
    • 특별한 smart pointer인 Arc도 있는데, 쓰레드-안전성을 가진 참조 카운트 증가 방식을 사용한다는 것을 제외하고는 Rc와 동일
      • 동일한 Mutex에 다수의 참조를 가질 때 종종 사용
use std::sync::Mutex;
 
struct Pie;
 
impl Pie {
    fn eat(&self) {
        println!("지금은 오직 나만이 파이를 먹는다!");
    }
}
 
fn main() {
    let mutex_pie = Mutex::new(Pie);
    // 파이에 대한 잠겨있는 immutable한 참조를 빌려봅시다
    // lock은 실패할 수도 있기 때문에 그 결과는 unwrap 해야합니다
    let ref_pie = mutex_pie.lock().unwrap();
    ref_pie.eat();
    // 잠긴 참조는 여기서 drop 되며, mutex로 보호되는 값은 다른 이에 의해 쓰일 수 있습니다
}

스마트 포인터 조합하기

  • smart pointer는 한계가 있는 것처럼 보이지만, 조합해서 사용하면 매우 강력해질 수 있음
    • Rc<Vec<Foo>>
      • heap에 있는 immutable한 데이터 구조의 동일한 vector를 대여할 수 있는 복수의 smart pointer를 복제할 수 있게 해줌
    • Rc<RefCell<Foo>> 
      • 복수의 smart pointer가 동일한 Foo struct를 mutable/immutable하게 대여할 수 있게 해줌
    • Arc<Mutex<Foo>>
      • 복수의 smart pointer가 임시의 mutable/immutable한 대여를 CPU 쓰레드 독점 방식으로 잠글 수 있게 해줌
  • “내부 가변성” 패턴
    • Rust의 컴파일 타임 체크와 동일 수준의 안전성으로 런타임의 메모리 사용 규칙을 변경할 수 있는 패턴
    • 내부 데이터를 변경하기 위해 immutable한 데이터 유형(복수의 smart pointer가 소유할 수 있음)을 사용
use std::cell::RefCell;
use std::rc::Rc;
 
struct Pie {
    slices: u8,
}
 
impl Pie {
    fn eat_slice(&mut self, name: &str) {
        println!("{}가 한 조각 먹었습니다!", name);
        self.slices -= 1;
    }
}
 
struct SeaCreature {
    name: String,
    pie: Rc<RefCell<Pie>>,
}
 
impl SeaCreature {
    fn eat(&self) {
        // mutable 대여를 위해 파이에 대한 smart pointer를 사용
        let mut p = self.pie.borrow_mut();
        // 한 입 먹자!
        p.eat_slice(&self.name);
    }
}
 
fn main() {
    let pie = Rc::new(RefCell::new(Pie { slices: 8 }));
    // ferris와 sarah에겐 파이에 대한 smart pointer의 복제가 주어집니다
    let ferris = SeaCreature {
        name: String::from("ferris"),
        pie: pie.clone(),
    };
    let sarah = SeaCreature {
        name: String::from("sarah"),
        pie: pie.clone(),
    };
    ferris.eat();
    sarah.eat();
 
    let p = pie.borrow();
    println!("{} 조각 남았습니다", p.slices);
}
 
-> ferris가 한 조각 먹었습니다!
   sarah가 한 조각 먹었습니다!
   6 조각 남았습니다

8장 - 마무리

9장 - 프로젝트 구성과 구조

모듈

  • 크레이트(crate)
    • 모든 Rust 프로그램이나 라이브러리(library)
    • 모든 crate는 _모듈(module)_의 계층구조로 이루어짐
    • 모든 crate에는 최상위(root) module이 있음
  • module
    • 전역변수, 함수, struct, trait, 또는 다른 module까지도 포함될 수 있음
    • Rust에서는 파일과 module 트리 계층구조 간의 1:1 대응은 없음
    • module의 트리 구조는 코드로 직접 작성

프로그램 작성하기

  • 프로그램은 main.rs라 불리는 파일에 root module을 가짐
    • 프로그램인 경우, main.rs가 진입점

라이브러리 작성하기

  • library는 lib.rs라 불리는 파일에 root module을 가짐
    • library인 경우, lib.rs가 진입점

다른 모듈과 크레이트 참조하기

  • module 내의 항목은 전체 module 경로를 이용해 참조 가능
    • 예 : std::f64::consts::PI
  • 더 간단한 방법 : use 키워드 사용
    • module에서 쓰고자 하는 특정 항목을 전체 경로를 쓰지 않고도 코드 어디에서든 사용할 수 있음
    • use std::f64::consts::PI를 쓰면 main 함수에서 PI만으로 사용할 수 있음
  • std
    • 유용한 데이터 구조 및 OS와 상호 작용할 수 있는 함수로 가득한 **표준 라이브러리(standard library)**의 crate
  • 커뮤니티 crate
use std::f64::consts::PI;
 
fn main() {
    println!("놀이터에 오신 것을 환영합니다!");
    println!("{} 한 조각 먹고 싶군요!", PI);
}
 
-> 놀이터에 오신 것을 환영합니다!
   3.141592653589793 한 조각 먹고 싶군요!

여러 개의 항목을 참조하기

  • 복수의 항목을 하나의 module 경로로 참조하는 경우 {} 사용
    • 예: use std::f64::consts::{PI,TAU}

모듈 작성하기

  • Rust에서 module을 선언하는 데에는 두 가지 방법
    • foo.rs라는 이름의 파일
    • foo라는 이름의 디렉토리에 들어있는 파일 mod.rs

모듈 계층구조

  • module과 하위모듈(sub-module)
    • 한 module은 다른 module에 의존할 수 있음
  • 하위 모듈 만들기
    • 부모 module에 다음과 같은 코드를 작성
      • mod foo;
        • foo.rs 파일이나 foo/mod.rs 파일을 찾아 이 scope 내의 foo module안에 그 내용물을 삽입

인라인 모듈

  • sub-module

    • module의 코드 내에 직접 치환(inline)됨
  • inline module의 가장 흔한 용도

    • 단위 테스트를 만들 때
    // 이 macro는 Rust가 테스트 모드가 아닐 경우
    // 이 inline module을 제거합니다.
    #[cfg(test)]
    mod tests {
        // 부모 module에 즉시 접근이 가능하지 않다는 데에 주의하세요.
        // 반드시 명시적으로 써줘야 합니다.
        use super::*;
     
        ... tests go here ...
    }

내부 모듈 참조하기

  • use 경로에 사용할 수 있는 몇 가지 키워드
    • crate - root module
    • super - 현재 module의 부모 module
    • self - 현재 module

내보내기

  • pub 키워드
    • 사용하면 module의 구성원들을 접근 가능하게 할 수 있음
  • 기본적으로 _module_의 구성원들은 외부에서 접근이 불가능
    • 그 자식 module도 접근 불가
  • 기본적으로 _crate_의 구성원들도 외부에서 접근이 불가능
    • crate의 root module (lib.rs 또는 main.rs)
    • pub을 표시하면 구성원들을 접근 가능하게 할 수 있음

구조체 가시성

  • 함수와 마찬가지로, structure도 module 외부로 무엇을 노출할 지를 pub을 사용해 선언할 수 있음
// SeaCreature struct는 우리 module 외부에서도 사용 가능합니다
pub struct SeaCreature {
    pub animal_type: String,
    pub name: String,
    pub arms: i32,
    pub legs: i32,
    // 우리의 무기는 비밀로 남겨둡시다
    weapon: String,
}

전주곡 (Prelude)

  • prelude module
    • use로 가져오지도 않았는데 어떻게 어디서나 Vec나 Box를 쓸 수 있는 이유
    • Rust의 standard library에서는 std::prelude::*로 내보내기 된 모든 것들이 어디에서든 자동으로 사용 가능
      • Vec와 Box가 바로 이런 경우이며, 다른 것들(Option, Copy, 기타 등등)도 마찬가지

여러분만의 Prelude

  • standard library의 prelude로 인해, 흔히들 library마다 고유의 prelude module을 만듬
    • library 사용을 위해 필요한 가장 흔한 데이터 구조들을 모두 가져오는 시작점으로 사용
      • 예: use my_library::prelude::*
    • standard library와 달리 프로그램이나 library에서 자동으로 쓸 수 있는 것은 아니지만, library 사용자들이 어디서부터 시작할지 도움을 줄 좋은 습관

9장 - 마무리

Toy Project

추가 자료 조사 - Smart Pointer

c/c++의 포인터처럼 작동하지만 추가적인 메타 데이터와 기능들을 가지고 있는 “데이터 구조”

스마트 포인터를 사용하여 구현한 대표적인 자료형

  • String
  • Vec

Rust의 스마트 포인터는 Rust의 소유권 시스템의 기본 요소이며 메모리를 효율적으로 관리하는 데 사용 됨

  • 스마트 포인터는 변수의 수명을 관리하는 데 사용되는 데이터 유형
  • 데이터가 더 이상 필요하지 않을 때 자동으로 메모리를 해제
  • 복잡한 메모리 상황을 관리하고 널 포인터 참조와 메모리 누수와 같은 일반적인 버그를 방지하는 데 유용

Rust의 세 가지 내장 스마트 포인터 유형

  • Box

    • 소유권 및 힙에 대한 할당을 제공, 힙에 메모리를 할당하고 거기에 값을 저장하는 데 사용
    fn main() {
    		// i32형 데이터를 힙에 할당! 스택에는 이 힙 메모리에 대한 주소 값이 할당 됨
        let b = Box::new(5);
        println!("b = {}", b); // 주의깊게 볼 점 : 'b'는 i32 type이 아닌 Box형 type
    }
  • Rc

    • 동일한 값에 대한 여러 소유자를 허용, 코드의 여러 부분 간에 값 소유권을 공유 하려는 경우 사용
  • RefCell

    • 내부 가변성을 제공하여 그렇지 않은 경우 값의 가변 대여를 허용

참조자와 스마트 포인터 간의 추가적인 차이점은?

  • 참조자 : 데이터를 오직 빌리기만 하는 포인터
  • 스마트 포인터 : (많은 경우)그들이 가리키고 있는 데이터를 소유

스마트 포인터의 구현

  • Deref와 Drop 트레잇을 구현

  • Deref

    • 스마트 포인터 구조체의 인스턴스가 참조자처럼 동작하도록 하여 참조자나 스마트 포인터 둘 중 하나와 함께 작동하는 코드를 작성하게 해 줌

    • 우리가 (곱하기 혹은 글롭 연산자와는 반대 측에 있는) 역참조 연산자 (dereference operator) * 의 동작을 커스터마이징 하는 것을 허용

    • 기존 역참조 vs Deref

      // 기본적인 역참조 예시
      fn main() {
          let x = 5;
          let y = &x;
       
          assert_eq!(5, *y);
      }
       
      // Box로 힙에 할당한 데이터에 대한 역참조 예시
      fn main() {
          let x = 5;
          let y = Box::new(x);
       
          assert_eq!(5, x);
          assert_eq!(5, *y);
      }
    • 예시로 이해해보기

      // Box와 유사한 trait 구현
      use std::ops::Deref;
      struct MyBox<T>(T);
       
      // new 메서드 구현
      impl<T> MyBox<T> {
          fn new(x: T) -> MyBox<T> {
              MyBox(x)
          }
      }
       
      // deref 메서드 구현
      impl<T> Deref for MyBox<T> {
          type Target = T;
       
          fn deref(&self) -> &T {
      				// MyBox 타입은 T 타입의 하나의 요소를 가진 튜플 구조체
      				// 아래 코드를 통해 자신이 가지고 있는 데이터를 리턴
              &self.0
          }
      }
       
      fn main() {
          let x = 5;
          let y = MyBox::new(x);
       
          assert_eq!(5, x);
      		// 위에서 Deref를 구현했으므로 러스트는 실제로 "*(y.deref())"를 실행해 줌
          assert_eq!(5, *y);
      }
  • Drop

    • 스마트 포인터의 인스턴스가 스코프 밖으로 벗어 났을 때 실행되는 코드를 커스터마이징 가능하도록 해 줌

    • 특정한 코드는 파일이나 네트워크 연결 같은 자원을 해제하는 데에 사용될 수 있음. 일종의 Dispose

    • 예시로 이해하기

      // 인스턴스가 스코프 밖으로 벗어났을 때 Dropping CustomSmartPointer!를 출력
      // 하는 커스텀 기능만을 갖춘 CustomSmartPointer 구조체
       
      // 구조 정의
      struct CustomSmartPointer {
          data: String,
      }
       
      // Drop 구현
      impl Drop for CustomSmartPointer {
          fn drop(&mut self) {
              println!("Dropping CustomSmartPointer with data `{}`!", self.data);
          }
      }
       
      fn main() {
          let c = CustomSmartPointer { data: String::from("my stuff") };
          let d = CustomSmartPointer { data: String::from("other stuff") };
          println!("CustomSmartPointers created.");
      }

      변수들은 만들어진 순서의 역순으로 버려지므로
      출력 →
      CustomSmartPointers created.
      Dropping CustomSmartPointer with data
      other stuff!
      Dropping CustomSmartPointer with data
      my stuff!

    • 주의할 점

      • Drop은 Rust상에서 자동적으로 호출되는 메서드이므로, 우리가 직접 호출 할 수 없음

        error[E0040]: explicit use of destructor method
        src/main.rs:14:7
        |
        14 | c.drop();
        | ^^^^ explicit destructor calls not allowed

      • 만약 직접 호출할 필요가 있다면 std::mem::drop을 가져와서 구현해야 함

  • 참조

    https://rinthel.github.io/rust-lang-book-ko/ch15-00-smart-pointers.html