Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

타입 안전성

newtype은 정적 구분을 제공합니다 (C-NEWTYPE)

newtype은 기반 타입의 서로 다른 해석을 정적으로 구분할 수 있습니다.

예를 들어, f64 값은 마일 단위의 양을 나타낼 수도 있고 킬로미터 단위의 양을 나타낼 수도 있습니다. newtype을 사용하면 의도한 해석을 추적할 수 있습니다:

#![allow(unused)]
fn main() {
struct Miles(pub f64);
struct Kilometers(pub f64);

impl Miles {
    fn to_kilometers(self) -> Kilometers { /* ... */ }
}
impl Kilometers {
    fn to_miles(self) -> Miles { /* ... */ }
}
}

이 두 타입을 분리하고 나면, 둘을 혼동하지 않도록 정적으로 보장할 수 있습니다. 예를 들어, 다음 함수는

#![allow(unused)]
fn main() {
fn are_we_there_yet(distance_travelled: Miles) -> bool { /* ... */ }
}

실수로 Kilometers 값으로 호출될 수 없습니다. 컴파일러가 변환을 수행하라고 알려 주므로, 일부 치명적인 버그를 피할 수 있습니다.

인수는 bool이나 Option이 아닌 타입을 통해 의미를 전달합니다 (C-CUSTOM-TYPE)

다음 방식이

#![allow(unused)]
fn main() {
let w = Widget::new(Small, Round)
}

다음 방식보다 낫습니다

#![allow(unused)]
fn main() {
let w = Widget::new(true, false)
}

bool, u8, Option 같은 핵심 타입은 여러 가지로 해석될 수 있습니다.

해석과 불변 조건을 전달하려면 의도적으로 정한 타입(열거형, 구조체, 튜플 중 무엇이든)을 사용하십시오. 위 예시에서는 인수 이름을 찾아보지 않으면 true와 false가 무엇을 뜻하는지 바로 알기 어렵지만, Small과 Round는 의미가 훨씬 잘 드러납니다.

사용자 정의 타입을 사용하면 나중에 ExtraLarge 배리언트를 추가하는 것처럼 선택지를 확장하기가 더 쉬워집니다.

기존 타입을 구별되는 이름으로 비용 없이 감싸는 방법은 newtype 패턴(C-NEWTYPE)을 참고하십시오.

플래그 집합을 나타내는 타입은 열거형이 아니라 bitflags입니다 (C-BITFLAG)

Rust는 판별값을 명시적으로 지정한 enum 타입을 지원합니다:

#![allow(unused)]
fn main() {
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}
}

사용자 지정 판별값은 enum 타입을 다른 시스템이나 언어와 호환되는 정수 값으로 직렬화해야 할 때 유용합니다. 이러한 판별값은 “타입 안전한” API를 지원합니다. 함수가 정수 대신 Color를 받으면, 나중에 그 입력을 정수로 보더라도 올바른 형식의 입력을 받는다는 것이 보장됩니다.

enum을 사용하면 API가 여러 선택지 중 정확히 하나를 요청할 수 있습니다. 그런데 API의 입력이 플래그 집합의 유무인 경우도 있습니다. C 코드에서는 흔히 각 플래그를 특정 비트에 대응시켜, 하나의 정수로 예컨대 32개나 64개의 플래그를 나타내는 방식으로 이를 처리합니다. Rust의 bitflags 크레이트는 이 패턴을 타입 안전하게 표현하는 방법을 제공합니다.

use bitflags::bitflags;

bitflags! {
    struct Flags: u32 {
        const FLAG_A = 0b00000001;
        const FLAG_B = 0b00000010;
        const FLAG_C = 0b00000100;
    }
}

fn f(settings: Flags) {
    if settings.contains(Flags::FLAG_A) {
        println!("doing thing A");
    }
    if settings.contains(Flags::FLAG_B) {
        println!("doing thing B");
    }
    if settings.contains(Flags::FLAG_C) {
        println!("doing thing C");
    }
}

fn main() {
    f(Flags::FLAG_A | Flags::FLAG_C);
}

빌더를 사용하면 복잡한 값을 생성할 수 있습니다 (C-BUILDER)

일부 데이터 구조는 생성하는 데 다음이 필요하므로 생성하기가 복잡합니다:

  • 많은 수의 입력
  • 복합 데이터(예: 슬라이스)
  • 선택적인 설정 데이터
  • 여러 형태 중 하나의 선택

이로 인해 각각 많은 인수를 받는 서로 다른 생성자가 많이 생기기 쉽습니다.

T가 이러한 데이터 구조라면 T 빌더의 도입을 고려하십시오.

  1. T 값을 점진적으로 설정하기 위한 별도의 데이터 타입 TBuilder를 도입하십시오. 가능하다면 더 나은 이름을 선택하십시오. 예를 들어 Command는 자식 프로세스의 빌더이며, Url은 ParseOptions로부터 생성할 수 있습니다.
  2. 빌더 생성자는 T를 만드는 데 반드시 필요한 데이터만 매개변수로 받아야 합니다.
  3. 빌더는 복합 입력(슬라이스 등)을 점진적으로 설정하는 기능을 포함하여, 설정을 위한 편리한 메서드 모음을 제공해야 합니다. 이러한 메서드는 체이닝이 가능하도록 self를 반환해야 합니다.
  4. 빌더는 실제로 T를 만드는 하나 이상의 “종결” 메서드를 제공해야 합니다.

빌더 패턴은 T를 만드는 과정에 태스크 생성이나 프로세스 실행 같은 부수 효과가 수반될 때 특히 적합합니다.

Rust에는 아래에서 설명하는 것처럼 소유권을 다루는 방식이 서로 다른 두 가지 빌더 패턴 배리언트가 있습니다.

소비하지 않는 빌더(권장)

어떤 경우에는 최종 T를 생성할 때 빌더 자체를 소비할 필요가 없습니다. 다음과 같은 std::process::Command의 배리언트가 한 예입니다:

#![allow(unused)]
fn main() {
// NOTE: the actual Command API does not use owned Strings;
// this is a simplified version.

pub struct Command {
    program: String,
    args: Vec<String>,
    cwd: Option<String>,
    // etc
}

impl Command {
    pub fn new(program: String) -> Command {
        Command {
            program: program,
            args: Vec::new(),
            cwd: None,
        }
    }

    /// Add an argument to pass to the program.
    pub fn arg(&mut self, arg: String) -> &mut Command {
        self.args.push(arg);
        self
    }

    /// Add multiple arguments to pass to the program.
    pub fn args(&mut self, args: &[String]) -> &mut Command {
        self.args.extend_from_slice(args);
        self
    }

    /// Set the working directory for the child process.
    pub fn current_dir(&mut self, dir: String) -> &mut Command {
        self.cwd = Some(dir);
        self
    }

    /// Executes the command as a child process, which is returned.
    pub fn spawn(&self) -> io::Result<Child> {
        /* ... */
    }
}
}

빌더 설정을 실제로 사용하여 프로세스를 생성하는 spawn 메서드가 빌더를 공유 참조로 받는다는 점에 유의하십시오. 이것이 가능한 이유는 프로세스를 생성하는 데 설정 데이터의 소유권이 필요하지 않기 때문입니다.

종결 메서드인 spawn은 참조만 필요로 하므로, 설정 메서드들은 self의 가변 빌림을 받고 반환합니다.

이점

전체적으로 빌림을 사용하므로, Command는 한 줄짜리 생성과 더 복잡한 생성 모두에 편리하게 사용할 수 있습니다.

#![allow(unused)]
fn main() {
// One-liners
Command::new("/bin/cat").arg("file.txt").spawn();

// Complex configuration
let mut cmd = Command::new("/bin/ls");
if size_sorted {
    cmd.arg("-S");
}
cmd.arg(".");
cmd.spawn();
}

소비하는 빌더

때로는 빌더가 최종 타입 T를 생성할 때 소유권을 넘겨야 하는데, 이는 종결 메서드가 &self가 아닌 self를 받아야 한다는 뜻입니다.

#![allow(unused)]
fn main() {
impl TaskBuilder {
    /// Name the task-to-be.
    pub fn named(mut self, name: String) -> TaskBuilder {
        self.name = Some(name);
        self
    }

    /// Redirect task-local stdout.
    pub fn stdout(mut self, stdout: Box<io::Write + Send>) -> TaskBuilder {
        self.stdout = Some(stdout);
        self
    }

    /// Creates and executes a new child task.
    pub fn spawn<F>(self, f: F) where F: FnOnce() + Send {
        /* ... */
    }
}
}

여기서 stdout 설정에는 io::Write의 소유권을 넘기는 과정이 수반되며, 이 소유권은 생성 시(spawn에서) 태스크로 이전되어야 합니다.

빌더의 종결 메서드가 소유권을 필요로 하는 경우에는 기본적인 트레이드오프가 존재합니다.

  • 다른 빌더 메서드들이 가변 빌림을 받고 반환한다면, 복잡한 설정의 경우에는 잘 동작하지만 한 줄짜리 설정은 불가능해집니다.

  • 다른 빌더 메서드들이 소유된 self를 받고 반환한다면, 한 줄짜리 설정은 계속 잘 동작하지만 복잡한 설정은 덜 편리해집니다.

쉬운 일은 쉽게, 어려운 일은 가능하게 만든다는 원칙에 따라, 소비하는 빌더의 모든 빌더 메서드는 소유된 self를 받고 반환해야 합니다. 그러면 클라이언트 코드는 다음과 같이 동작합니다.

#![allow(unused)]
fn main() {
// One-liners
TaskBuilder::new("my_task").spawn(|| { /* ... */ });

// Complex configuration
let mut task = TaskBuilder::new();
task = task.named("my_task_2"); // must re-assign to retain ownership
if reroute {
    task = task.stdout(mywriter);
}
task.spawn(|| { /* ... */ });
}

한 줄짜리 설정은 이전과 같이 동작하는데, 이는 소유권이 spawn에 의해 소비될 때까지 각 빌더 메서드를 거쳐 전달되기 때문입니다. 그러나 복잡한 설정은 더 장황해집니다. 각 단계마다 빌더를 다시 할당해야 하기 때문입니다.