Devin.KR

Rust · 심화

트레이트와 동시성으로 깊어지는 Rust

제네릭 심화 - 연관 타입·newtype·PhantomData

연관 타입과 제네릭 매개변수 비교, where 절, 기본 메서드, newtype 패턴과 단위 안전성, PhantomData 로 상태 표시, 연산자 오버로딩(std::ops)

개발자KR · 원고 갱신

이 장에서 배우는 것

기본서에서 제네릭과 트레이트 경계를 배웠다면 fn largest<T: PartialOrd> 같은 함수는 이미 써 봤을 것이다. 이 장은 거기서 한 걸음 더 들어가, 타입 시스템이 값 대신 의미를 구분하게 만드는 방법을 다룬다. 창고 프로그램에서는 개수로 세는 부품과 무게로 재는 원료가 섞이지 않게 하고, 주문이 아직 재고를 확정하지 않았는데 출고되는 일을 컴파일 단계에서 막는 데 쓴다.

  • 연관 타입(associated type)과 제네릭 매개변수(generic parameter)의 차이를 구분하고 상황에 맞게 고른다.
  • where 절과 기본 메서드(default method)로 트레이트 경계를 읽기 쉽게 쓰고 공통 동작을 한 번만 구현한다.
  • newtype 패턴으로 같은 u32 를 단위별 타입으로 나누고, PhantomData 로 주문의 진행 상태를 타입에 표시한다.
  • std::ops 의 연산자 트레이트를 구현해 새 타입에 +, +=, * 를 붙인다.

문제 상황

창고에는 두 종류의 품목이 있다. 볼트와 너트는 개수로 세고, 곡물 사일로의 재고는 kg 으로 잰다. 지금까지는 두 경우 모두 u32 하나로 수량을 표현했다. 이 방식은 컴파일러가 두 수량의 차이를 알 수 없다는 문제가 있다. 사일로의 600 을 볼트 재고에 더하는 코드도 문법상 아무 문제 없이 컴파일된다.

주문 처리도 비슷하다. 초안 상태의 주문, 재고를 빼서 확정한 주문, 출고가 끝난 주문을 하나의 구조체에 status 필드로 구분하면 확정하지 않은 주문을 출고하는 실수는 실행해 봐야 드러난다. 재고 저장소도 부품은 SKU 문자열, 사일로는 번호로 찾기 때문에 저장소마다 함수 시그니처가 조금씩 달라지고, 공통 보고 함수를 만들기 어렵다.

이 장에서는 세 가지 도구로 이 문제를 푼다. 연관 타입은 저장소마다 달라지는 식별자와 단위를 트레이트에 담는다. newtype 과 PhantomData 는 단위와 상태를 타입으로 옮긴다. 연산자 오버로딩(operator overloading)은 그렇게 만든 타입을 평범한 수처럼 쓸 수 있게 한다.

연관 타입과 where 절

연관 타입과 제네릭 매개변수

트레이트가 다루는 타입을 하나 더 늘리는 방법은 두 가지다. trait Convert<T> 처럼 트레이트 자체에 매개변수를 달거나, 트레이트 안에 type Item; 으로 연관 타입을 선언한다. 둘의 차이는 한 타입이 같은 트레이트를 몇 번 구현할 수 있는가에 있다.

연관 타입은 구현 하나당 값이 하나로 정해진다. 표준 라이브러리의 Iterator 가 대표적이다. 하나의 타입은 Iterator 를 한 번만 구현하고, 그때 Item 도 하나로 정해진다. 그래서 호출하는 쪽은 Item 을 따로 적을 필요가 없다. 반면 제네릭 매개변수는 같은 타입이 From<u8>, From<char> 처럼 서로 다른 매개변수로 여러 번 구현할 수 있다. 호출하는 쪽의 인자 타입이 어느 구현을 쓸지 정한다.

연관 타입은 구현 하나에 값 하나를, 제네릭 매개변수는 구현 여러 개를 허용한다.
연관 타입과 제네릭 매개변수를 고르는 기준
기준연관 타입제네릭 매개변수예
구현 개수타입당 하나매개변수마다 여러 개Iterator / From
누가 타입을 고르는가구현하는 쪽사용하는 쪽Item / Rhs
호출 시 타입 표기대개 필요 없다모호하면 필요하다next() / from()
경계에서 쓰는 모양I: Iterator<Item = u32>T: From<u32>제약 표기

이 장의 재고 저장소는 연관 타입이 맞는 경우다. PieceStock 의 식별자는 항상 SKU 문자열이고 단위는 항상 개수다. 같은 저장소가 어떤 호출에서는 kg 을 받는 일은 없다. 이처럼 구현마다 답이 하나뿐이면 연관 타입으로 쓴다. 반대로 Won 에 곱할 수 있는 것이 정수일 수도 있고 개수 타입일 수도 있다면, 구현이 여러 개이므로 Mul<Rhs> 처럼 매개변수로 둔다. Mul 은 둘을 함께 쓴다. 우변 타입은 매개변수 Rhs 이고 결과는 연관 타입 Output 이다.

연관 타입에도 경계를 붙일 수 있다. type Unit: Measure; 라고 쓰면 이 트레이트를 구현하는 모든 타입의 Unit 이 Measure 를 만족해야 한다. 그리고 S::Unit 을 쓰는 제네릭 코드는 경계를 다시 적지 않아도 이 사실을 쓸 수 있다.

where 절과 기본 메서드

경계가 길어지면 꺾쇠 안이 읽기 어려워진다. where 절은 같은 경계를 시그니처 뒤로 옮겨 적는 문법이다. 연관 타입에 경계를 걸 때는 where 절이 사실상 필수다. 꺾쇠 안에서는 S::Id: Display 같은 조건을 쓸 수 없기 때문이다.

fn report<S>(title: &str, stock: &S, ids: &[S::Id])
where
    S: Inventory,
    S::Id: fmt::Display,
{
    // ...
}

기본 메서드는 트레이트 안에 본문까지 써 두는 메서드다. 구현하는 타입은 필수 메서드만 채우면 되고, 나머지는 공짜로 얻는다. 이 장의 Inventory 는 on_hand, receive, take 세 개만 요구하고 has_enough 는 on_hand 를 이용해 기본 구현으로 제공한다. 저장소가 더 빠른 방법을 안다면 기본 메서드를 덮어쓸 수도 있다.

newtype 과 PhantomData

newtype 으로 단위 구분하기

newtype 패턴은 값 하나를 감싼 튜플 구조체를 새 타입으로 쓰는 방법이다. 이 장의 Won(u64) 이 그 예다. 원화 금액은 내부적으로 u64 이지만, 개수와 더할 수 없는 별개의 타입이다. 감싸는 비용은 없다. 컴파일된 코드에서는 내부 값과 같은 표현이 된다.

개수와 kg 은 구조가 같고 단위만 다르다. 구조체를 두 개 만들어 같은 메서드를 복사하는 대신, 단위를 타입 매개변수로 받는 Qty<U> 하나를 만든다. 그런데 U 는 필드에 쓰이지 않는다. Rust 는 쓰이지 않는 타입 매개변수를 허용하지 않으므로 PhantomData<U> 필드를 둔다. 이것은 크기가 0인 타입(zero-sized type)이어서 메모리를 쓰지 않으면서 U 를 타입의 일부로 만든다.

단위를 섞으면 컴파일러가 막는다.

let a = Qty::<Piece>::new(3);
let b = Qty::<Kg>::new(5);
let c = a + b;

오류 요지는 error[E0308]: mismatched types 이고 expected `Qty<Piece>`, found `Qty<Kg>` 라고 나온다. Add 를 같은 Qty<U> 끼리만 구현했기 때문이다.

단위 이름을 화면에 쓰는 일은 Measure 트레이트의 연관 상수 LABEL 이 맡는다. 연관 상수는 연관 타입처럼 구현마다 하나로 정해지는 값이다. 단위 표지는 enum Piece {} 처럼 변형이 없는 열거형으로 만들었다. 값을 만들 일이 없는 표지이므로 인스턴스가 존재할 수 없는 타입이 의도에 맞고, 사용하지 않는 생성자에 대한 경고도 생기지 않는다.

PhantomData 로 상태 표시하기

같은 기법을 주문의 진행 상태에 쓴다. Order<S> 의 S 가 Draft, Confirmed, Shipped 중 하나다. 상태를 타입에 올려 두는 방식을 타입 상태(typestate) 패턴이라 부른다. 메서드는 상태별 impl 블록에 나눠 둔다. add_line 과 confirm 은 Order<Draft> 에만, ship 은 Order<Confirmed> 에만 있다.

주문은 Draft, Confirmed, Shipped 순서로만 넘어가며 건너뛰는 호출은 컴파일되지 않는다.

전이 메서드는 self 를 값으로 받는다. 그래야 옛 상태의 주문이 이동되어 더 쓸 수 없다. 초안 상태에서 바로 출고를 시도하면 이렇게 막힌다.

let order = Order::new(1);
let shipped = order.ship();

오류 요지는 error[E0599]: no method named `ship` found for struct `Order<Draft>` in the current scope 이다. 상태 검사가 실행 시점의 if 가 아니라 메서드 존재 여부로 바뀐 것이다.

한 가지 주의할 점이 있다. #[derive(Clone, Copy)] 를 Qty<U> 에 붙이면 U: Clone, U: Copy 경계가 함께 생긴다. 표지 타입에 그 트레이트가 없으면 Qty 가 복사되지 않는다. 그래서 이 장에서는 Clone 과 Copy 를 직접 구현했다.

연산자 오버로딩

연산자는 std::ops 의 트레이트로 정의된다. a + b 는 Add::add(a, b) 로 번역된다. 따라서 자기 타입에 이 트레이트를 구현하면 연산자를 쓸 수 있다. 앞에서 본 Mul 처럼 우변 타입은 제네릭 매개변수이고 결과 타입은 연관 타입이다.

이 장에서 구현하는 std::ops 트레이트
연산자트레이트메서드이 장에서의 구현
a + bAddadd(self, rhs)같은 단위의 Qty 끼리
a += bAddAssignadd_assign(&mut self, rhs)Qty, Won
a * bMul<Rhs>mul(self, rhs)Qty * u32, Won * Qty

뺄셈은 일부러 연산자로 만들지 않았다. Sub 는 결과 타입이 Output 하나이므로 재고보다 많이 빼는 경우를 표현할 방법이 패닉뿐이다. 그 대신 checked_sub 메서드가 Option 을 돌려준다. 연산자는 실패하지 않는 연산에만 붙이는 것이 안전하다.

완성 코드

cargo new warehouse 로 만든 프로젝트의 src/main.rs 를 아래 내용으로 바꾼다.

use std::collections::HashMap;
use std::fmt;
use std::marker::PhantomData;
use std::ops::{Add, AddAssign, Mul};

// 단위 표지: 값을 만들 수 없는 타입이라 실행 시점 비용이 없다
enum Piece {}
enum Kg {}

trait Measure {
    const LABEL: &'static str;
}

impl Measure for Piece {
    const LABEL: &'static str = "개";
}

impl Measure for Kg {
    const LABEL: &'static str = "kg";
}

struct Qty<U> {
    value: u32,
    unit: PhantomData<U>,
}

// derive 는 U: Clone 경계를 요구하므로 직접 구현한다
impl<U> Clone for Qty<U> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<U> Copy for Qty<U> {}

impl<U> Qty<U> {
    const fn new(value: u32) -> Self {
        Qty { value, unit: PhantomData }
    }

    fn value(self) -> u32 {
        self.value
    }

    fn checked_sub(self, rhs: Self) -> Option<Self> {
        self.value.checked_sub(rhs.value).map(Self::new)
    }
}

impl<U: Measure> fmt::Display for Qty<U> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}{}", self.value, U::LABEL)
    }
}

impl<U> Add for Qty<U> {
    type Output = Qty<U>;

    fn add(self, rhs: Self) -> Self {
        Qty::new(self.value + rhs.value)
    }
}

impl<U> AddAssign for Qty<U> {
    fn add_assign(&mut self, rhs: Self) {
        self.value += rhs.value;
    }
}

impl<U> Mul<u32> for Qty<U> {
    type Output = Qty<U>;

    fn mul(self, k: u32) -> Self {
        Qty::new(self.value * k)
    }
}

#[derive(Clone, Copy)]
struct Won(u64);

impl fmt::Display for Won {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}원", self.0)
    }
}

impl AddAssign for Won {
    fn add_assign(&mut self, rhs: Self) {
        self.0 += rhs.0;
    }
}

impl Mul<Qty<Piece>> for Won {
    type Output = Won;

    fn mul(self, rhs: Qty<Piece>) -> Won {
        Won(self.0 * u64::from(rhs.value()))
    }
}

trait Inventory {
    type Id;
    type Unit: Measure;

    fn on_hand(&self, id: &Self::Id) -> Qty<Self::Unit>;
    fn receive(&mut self, id: Self::Id, qty: Qty<Self::Unit>);
    fn take(&mut self, id: &Self::Id, qty: Qty<Self::Unit>) -> bool;

    fn has_enough(&self, id: &Self::Id, need: Qty<Self::Unit>) -> bool {
        self.on_hand(id).checked_sub(need).is_some()
    }
}

#[derive(Default)]
struct PieceStock {
    items: HashMap<String, Qty<Piece>>,
}

impl Inventory for PieceStock {
    type Id = String;
    type Unit = Piece;

    fn on_hand(&self, id: &String) -> Qty<Piece> {
        self.items.get(id).copied().unwrap_or(Qty::new(0))
    }

    fn receive(&mut self, id: String, qty: Qty<Piece>) {
        *self.items.entry(id).or_insert(Qty::new(0)) += qty;
    }

    fn take(&mut self, id: &String, qty: Qty<Piece>) -> bool {
        let Some(cur) = self.items.get_mut(id) else {
            return false;
        };
        match cur.checked_sub(qty) {
            Some(rest) => {
                *cur = rest;
                true
            }
            None => false,
        }
    }
}

#[derive(Default)]
struct BulkStock {
    items: HashMap<u32, Qty<Kg>>,
}

impl Inventory for BulkStock {
    type Id = u32;
    type Unit = Kg;

    fn on_hand(&self, id: &u32) -> Qty<Kg> {
        self.items.get(id).copied().unwrap_or(Qty::new(0))
    }

    fn receive(&mut self, id: u32, qty: Qty<Kg>) {
        *self.items.entry(id).or_insert(Qty::new(0)) += qty;
    }

    fn take(&mut self, id: &u32, qty: Qty<Kg>) -> bool {
        let Some(cur) = self.items.get_mut(id) else {
            return false;
        };
        match cur.checked_sub(qty) {
            Some(rest) => {
                *cur = rest;
                true
            }
            None => false,
        }
    }
}

fn report<S>(title: &str, stock: &S, ids: &[S::Id])
where
    S: Inventory,
    S::Id: fmt::Display,
{
    for id in ids {
        println!("{title} {id}: {}", stock.on_hand(id));
    }
}

enum Draft {}
enum Confirmed {}
enum Shipped {}

trait Stage {
    const NAME: &'static str;
}

impl Stage for Draft {
    const NAME: &'static str = "초안";
}

impl Stage for Confirmed {
    const NAME: &'static str = "확정";
}

impl Stage for Shipped {
    const NAME: &'static str = "출고";
}

struct Line {
    sku: String,
    qty: Qty<Piece>,
    price: Won,
}

struct Order<S> {
    id: u32,
    lines: Vec<Line>,
    state: PhantomData<S>,
}

impl Order<Draft> {
    fn new(id: u32) -> Self {
        Order { id, lines: Vec::new(), state: PhantomData }
    }

    fn add_line(&mut self, sku: &str, qty: Qty<Piece>, price: Won) {
        self.lines.push(Line { sku: sku.to_string(), qty, price });
    }

    fn confirm<S>(self, stock: &mut S) -> Result<Order<Confirmed>, String>
    where
        S: Inventory<Id = String, Unit = Piece>,
    {
        for (i, line) in self.lines.iter().enumerate() {
            if !stock.take(&line.sku, line.qty) {
                for done in &self.lines[..i] {
                    stock.receive(done.sku.clone(), done.qty);
                }
                return Err(format!("{} 재고 부족", line.sku));
            }
        }
        Ok(Order { id: self.id, lines: self.lines, state: PhantomData })
    }
}

impl Order<Confirmed> {
    fn ship(self) -> Order<Shipped> {
        Order { id: self.id, lines: self.lines, state: PhantomData }
    }
}

impl<S> Order<S> {
    fn total(&self) -> Won {
        let mut sum = Won(0);
        for line in &self.lines {
            sum += line.price * line.qty;
        }
        sum
    }
}

impl<S: Stage> fmt::Display for Order<S> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "주문 #{} [{}] {}줄, 합계 {}",
            self.id,
            S::NAME,
            self.lines.len(),
            self.total()
        )
    }
}

fn main() {
    let batch = Qty::<Kg>::new(40) + Qty::new(10);
    println!("묶음 {batch}, 두 배 {}", batch * 2);

    let mut pieces = PieceStock::default();
    pieces.receive("bolt".to_string(), Qty::new(120));
    pieces.receive("nut".to_string(), Qty::new(80));
    pieces.receive("bolt".to_string(), Qty::new(30));

    let mut silos = BulkStock::default();
    silos.receive(1, Qty::new(500));
    silos.receive(2, Qty::new(250));
    silos.receive(1, Qty::new(100));

    let skus = ["bolt".to_string(), "nut".to_string(), "washer".to_string()];
    report("부품", &pieces, &skus);
    report("사일로", &silos, &[1, 2, 3]);
    println!("nut 100개 가능: {}", pieces.has_enough(&"nut".to_string(), Qty::new(100)));

    let mut order = Order::new(1);
    order.add_line("bolt", Qty::new(100), Won(50));
    order.add_line("nut", Qty::new(40), Won(30));
    println!("{order}");

    let order = match order.confirm(&mut pieces) {
        Ok(confirmed) => confirmed,
        Err(msg) => {
            println!("확정 실패: {msg}");
            return;
        }
    };
    println!("{order}");
    let order = order.ship();
    println!("{order}");

    let mut big = Order::new(2);
    big.add_line("bolt", Qty::new(10), Won(50));
    big.add_line("nut", Qty::new(100), Won(30));
    match big.confirm(&mut pieces) {
        Ok(confirmed) => println!("{confirmed}"),
        Err(msg) => println!("확정 실패: {msg}"),
    }

    report("부품", &pieces, &skus[..2]);
}

줄별 해설

표지와 Measure. enum Piece {} 와 enum Kg {} 는 타입 이름만 필요한 표지다. Measure 의 LABEL 은 연관 상수이고, Display 구현에서 U::LABEL 로 읽는다. 따라서 Qty<Kg> 는 단위 문자열을 값으로 들고 다니지 않는다.

Qty 정의. value 는 숫자이고 unit 은 PhantomData<U> 다. 이 필드는 읽을 일이 없지만 컴파일러가 미사용 경고를 내지 않는다. Clone 은 *self 로 구현하는데, 같은 파일의 Copy 구현이 있어서 가능하다. new 가 const fn 이어도 PhantomData 는 상수 문맥에서 만들 수 있다.

연산자 구현. Add, AddAssign, Mul<u32> 는 모두 impl<U> 로 단위에 무관하게 쓴다. Add 의 우변은 기본값 Self 이므로 같은 Qty<U> 만 받는다. Won 은 Mul<Qty<Piece>> 로 단가 곱하기 개수를 표현한다. kg 에 단가를 곱하는 식은 구현이 없으므로 컴파일되지 않는다. u64::from 은 손실 없는 확장 변환이다.

Inventory. Id 와 Unit 이 연관 타입이다. has_enough 는 on_hand 와 checked_sub 만 써서 모든 저장소에 공통으로 동작한다. PieceStock 은 Id = String 이고 BulkStock 은 Id = u32 로 서로 다른 식별자를 쓰지만 같은 트레이트를 구현한다. take 는 let ... else 로 키가 없는 경우를 먼저 돌려보내고, 모자라면 값을 건드리지 않고 false 를 낸다.

report. S::Id: fmt::Display 가 where 절에 있어서 {id} 출력이 가능하다. Qty<S::Unit> 를 출력하는 데 필요한 S::Unit: Measure 는 트레이트 선언에서 이미 보장된다. 그래서 같은 함수가 부품 저장소와 사일로 저장소에 모두 쓰인다.

Order. confirm 의 경계 S: Inventory<Id = String, Unit = Piece> 는 연관 타입을 구체적으로 고정하는 문법이다. 줄 단위로 재고를 빼다가 실패하면 앞서 뺀 줄을 receive 로 되돌리고 오류 문자열을 낸다. 성공하면 self.lines 를 새 Order<Confirmed> 로 옮긴다. ship 도 같은 방식이다. total 은 impl<S> 에 있어 모든 상태에서 쓸 수 있고, Display 는 S: Stage 일 때 상태 이름을 붙여 출력한다.

main. Qty::<Kg>::new(40) + Qty::new(10) 에서 두 번째 값의 단위는 Add 가 요구하는 Self 로 추론된다. 두 번째 주문은 볼트 줄을 먼저 빼고 너트 줄에서 실패하므로, 되돌림이 동작하는지 마지막 보고로 확인한다.

실행 결과

$ cargo run
묶음 50kg, 두 배 100kg
부품 bolt: 150개
부품 nut: 80개
부품 washer: 0개
사일로 1: 600kg
사일로 2: 250kg
사일로 3: 0kg
nut 100개 가능: false
주문 #1 [초안] 2줄, 합계 6200원
주문 #1 [확정] 2줄, 합계 6200원
주문 #1 [출고] 2줄, 합계 6200원
확정 실패: nut 재고 부족
부품 bolt: 50개
부품 nut: 40개

마지막 두 줄에서 볼트가 50개로 돌아와 있다. 두 번째 주문이 볼트 10개를 뺐다가 되돌린 결과다. 컴파일 과정에서 경고는 나오지 않는다.

실무에서 자주 틀리는 것

표지 타입 때문에 Copy 가 사라진다

틀린 코드는 derive 로 한 번에 처리하는 경우다.

#[derive(Clone, Copy)]
struct Qty<U> {
    value: u32,
    unit: PhantomData<U>,
}

let a = Qty::<Piece>::new(1);
let b = a + a;

derive 는 impl<U: Copy> Copy for Qty<U> 를 만든다. Piece 가 Copy 가 아니므로 Qty<Piece> 도 아니고, error[E0382]: use of moved value: `a` 가 나온다. 이 장처럼 impl<U> Clone 과 impl<U> Copy 를 직접 쓰면 U 에 경계가 붙지 않는다.

Deref 로 단위 보호를 풀어 버린다

편하다는 이유로 Deref 를 구현하는 경우가 있다.

impl<U> std::ops::Deref for Qty<U> {
    type Target = u32;
    fn deref(&self) -> &u32 { &self.value }
}

let mixed = *kg + *pieces; // 컴파일된다

이러면 단위가 다른 값이 다시 u32 로 섞인다. Deref 는 스마트 포인터처럼 가리키는 값에 투명하게 접근할 때 쓰는 것이다. newtype 의 목적이 구분이라면 value() 같은 명시적 접근자만 열고 필요한 연산자만 구현한다.

Sub 로 빼기를 구현해 패닉을 만든다

impl<U> Sub for Qty<U> {
    type Output = Qty<U>;
    fn sub(self, rhs: Self) -> Self {
        Qty::new(self.value - rhs.value)
    }
}

재고보다 많이 빼면 디버그 빌드에서 attempt to subtract with overflow 패닉이 난다. 이 장의 checked_sub 처럼 Option 을 돌려주는 메서드로 바꾸고, 호출하는 쪽이 부족한 경우를 처리하게 한다.

전이 메서드가 &self 를 받는다

impl Order<Confirmed> {
    fn ship(&self) -> Order<Shipped> { /* lines 복제 */ }
}

컴파일은 되지만 같은 확정 주문을 두 번 출고할 수 있다. 옛 상태 값이 그대로 남아 있기 때문이다. fn ship(self) 로 바꾸면 첫 호출 뒤 두 번째 호출은 error[E0382]: use of moved value 로 막힌다.

한눈에 보기

이 장의 개념 정리
개념쓰는 곳핵심 문법주의
연관 타입구현마다 답이 하나인 타입type Id;한 타입이 구현을 한 번만 가진다
제네릭 매개변수구현이 여러 개인 트레이트Mul<Rhs>모호하면 타입 표기가 필요하다
where 절긴 경계, 연관 타입 경계S::Id: Display연관 타입 조건은 where 에 쓴다
기본 메서드구현자가 공유하는 동작트레이트 안 본문필수 메서드만으로 구현할 수 있어야 한다
newtype의미가 다른 같은 값 구분struct Won(u64)Deref 로 구분을 풀지 않는다
PhantomData단위와 상태 표시PhantomData<U>derive 는 U 에 경계를 건다
연산자 오버로딩새 타입의 산술impl Add for ...실패할 수 있는 연산은 메서드로 둔다

연습 문제

  1. Qty<Piece> 와 Qty<Kg> 를 += 로 합치는 코드를 쓰면 어떤 오류가 나는지 예측하고, 그 이유를 AddAssign 구현과 연결해 설명하라.
  2. Qty<U> 에 Div<u32> 를 구현하라. 0으로 나누면 어떻게 되는지도 적어라.
  3. Order<Confirmed> 에 취소 전이 cancel 을 추가하라. 뺐던 재고를 돌려주고 Order<Draft> 를 돌려주어야 한다.
  4. where 절을 사용해 fn total_on_hand<S>(stock: &S, ids: &[S::Id]) -> Qty<S::Unit> 를 작성하라. 부품 저장소와 사일로 저장소 모두에 쓸 수 있어야 한다.

정답과 해설

  1. 컴파일 오류 E0308 mismatched types 가 나며 expected `Qty<Piece>`, found `Qty<Kg>` 와 같은 요지다. AddAssign 은 impl<U> AddAssign for Qty<U> 이고 우변 기본값이 Self 이므로 좌변과 같은 U 의 값만 받는다. 단위가 다르면 대응하는 구현이 없다.
  2. 구현은 다음과 같다.
    impl<U> Div<u32> for Qty<U> {
        type Output = Qty<U>;
    
        fn div(self, k: u32) -> Self {
            Qty::new(self.value / k)
        }
    }
    정수 나눗셈이라 나머지는 버려진다. k 가 0이면 attempt to divide by zero 패닉이 난다. 실패할 수 있는 연산이므로 실무에서는 checked_div 같은 메서드가 더 안전하다. 사용하려면 use std::ops::Div 를 추가한다.
  3. 구현은 다음과 같다.
    impl Order<Confirmed> {
        fn cancel<S>(self, stock: &mut S) -> Order<Draft>
        where
            S: Inventory<Id = String, Unit = Piece>,
        {
            for line in &self.lines {
                stock.receive(line.sku.clone(), line.qty);
            }
            Order { id: self.id, lines: self.lines, state: PhantomData }
        }
    }
    self 를 값으로 받으므로 취소한 뒤에는 확정 주문을 다시 쓸 수 없다. 반복문의 빌림이 끝난 다음에 self.lines 를 옮긴다.
  4. 구현은 다음과 같다.
    fn total_on_hand<S>(stock: &S, ids: &[S::Id]) -> Qty<S::Unit>
    where
        S: Inventory,
    {
        let mut sum = Qty::new(0);
        for id in ids {
            sum += stock.on_hand(id);
        }
        sum
    }
    Qty 의 AddAssign 이 단위에 무관한 impl<U> 이므로 S::Unit 에도 적용된다. 반환 타입에서 S::Unit 이 정해지므로 Qty::new(0) 의 단위도 추론된다.

연산자 트레이트의 전체 목록은 std::ops 문서에서 확인할 수 있다. 다음 장에서는 이 장의 impl 처럼 모양이 같은 코드가 반복될 때 선언형 매크로로 줄이는 방법을 다룬다.

오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.