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

상호 운용성

타입은 공통 트레이트를 적극적으로 구현합니다 (C-COMMON-TRAITS)

Rust의 트레이트 시스템은 고아(orphan)를 허용하지 않습니다. 대략적으로 말하면, 모든 impl은 트레이트를 정의하는 크레이트나 구현 대상 타입을 정의하는 크레이트 중 하나에 있어야 합니다. 따라서 새 타입을 정의하는 크레이트는 적용 가능한 모든 공통 트레이트를 적극적으로 구현해야 합니다.

그 이유를 알아보기 위해 다음 상황을 생각해 봅시다.

  • 크레이트 std는 트레이트 Display를 정의합니다.
  • 크레이트 url은 Display를 구현하지 않은 채 타입 Url을 정의합니다.
  • 크레이트 webapp은 std와 url 양쪽에서 가져옵니다.

webapp은 Display와 Url 중 어느 것도 정의하지 않으므로, Url에 Display를 추가할 방법이 없습니다. (참고: newtype 패턴을 사용하면 효율적이지만 불편한 우회 방법을 쓸 수 있습니다.)

std에서 구현해야 할 가장 중요한 공통 트레이트는 다음과 같습니다:

타입이 Default와 인수가 없는 new 생성자를 모두 구현하는 것은 흔한 일이며 당연히 기대되는 일입니다. new는 Rust의 생성자 관례이고 사용자는 이것이 존재하기를 기대합니다. 따라서 기본 생성자가 인수를 받지 않는 것이 합리적이라면, 기능적으로 default와 동일하더라도 그렇게 해야 합니다.

변환에는 표준 트레이트 From, AsRef, AsMut를 사용합니다 (C-CONV-TRAITS)

다음 변환 트레이트는 타당한 경우에 구현해야 합니다.

다음 변환 트레이트는 절대 구현해서는 안 됩니다.

이 트레이트들은 From과 TryFrom을 기반으로 하는 포괄 구현(blanket impl)을 가지고 있습니다. 대신 그 트레이트들을 구현하십시오.

표준 라이브러리의 예

  • 작은 정수는 항상 더 큰 정수로 변환할 수 있으므로 u32에는 From<u16>이 구현되어 있습니다.
  • 정수가 너무 크면 변환이 불가능할 수 있으므로 u16에는 From<u32>이 구현되어 있지 않습니다.
  • u16에는 TryFrom<u32>이 구현되어 있으며, 정수가 너무 커서 u16에 들어가지 않으면 오류를 반환합니다.
  • v4와 v6 IP 주소를 모두 표현할 수 있는 타입인 IpAddr에는 From<Ipv6Addr>이 구현되어 있습니다.

컬렉션은 FromIterator와 Extend를 구현합니다 (C-COLLECT)

FromIterator와 Extend를 구현하면 컬렉션을 다음 이터레이터 메서드와 함께 편리하게 사용할 수 있습니다.

FromIterator는 이터레이터의 항목들을 담은 새 컬렉션을 만들기 위한 것이고, Extend는 이터레이터의 항목들을 기존 컬렉션에 추가하기 위한 것입니다.

표준 라이브러리의 예

  • Vec<T>는 FromIterator<T>와 Extend<T>를 모두 구현합니다.

데이터 구조는 Serde의 Serialize, Deserialize를 구현합니다 (C-SERDE)

데이터 구조 역할을 하는 타입은 Serialize와 Deserialize를 구현해야 합니다.

명백히 데이터 구조인 것과 명백히 데이터 구조가 아닌 것 사이에는 타입들이 연속적으로 분포하며, 그 사이에는 회색 지대가 있습니다. LinkedHashMap과 IpAddr는 데이터 구조입니다. 누군가 JSON 파일에서 LinkedHashMap이나 IpAddr를 읽어 들이거나, IPC를 통해 다른 프로세스로 보내고 싶어 하는 것은 지극히 합당합니다. LittleEndian은 데이터 구조가 아닙니다. 이는 byteorder 크레이트가 특정 순서의 바이트에 대해 컴파일 타임에 최적화하기 위해 사용하는 마커이며, 실제로 LittleEndian의 인스턴스는 런타임에 결코 존재할 수 없습니다. 따라서 이들은 명확한 예이며, 더 모호한 경우에는 필요하다면 #rust 또는 #serde IRC 채널에서 판단에 도움을 받을 수 있습니다.

크레이트가 다른 이유로 이미 Serde에 의존하고 있지 않다면, Serde 구현을 Cargo cfg 뒤에 두어 선택적으로 활성화하도록 할 수 있습니다. 이렇게 하면 다운스트림 라이브러리는 해당 구현이 필요한 경우에만 Serde 컴파일 비용을 치르면 됩니다.

다른 Serde 기반 라이브러리와의 일관성을 위해 Cargo cfg의 이름은 단순히 "serde"여야 합니다. "serde_impls"나 "serde_serialization"처럼 cfg에 다른 이름을 사용하지 마십시오.

derive를 사용하지 않을 때의 표준적인 구현은 다음과 같습니다.

[dependencies]
serde = { version = "1.0", optional = true }
#![allow(unused)]
fn main() {
pub struct T { /* ... */ }

#[cfg(feature = "serde")]
impl Serialize for T { /* ... */ }

#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for T { /* ... */ }
}

그리고 derive를 사용할 때는 다음과 같습니다.

[dependencies]
serde = { version = "1.0", optional = true, features = ["derive"] }
#![allow(unused)]
fn main() {
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct T { /* ... */ }
}

타입은 가능한 경우 Send와 Sync입니다 (C-SEND-SYNC)

Send와 Sync는 컴파일러가 적절하다고 판단하면 자동으로 구현됩니다.

원시 포인터를 다루는 타입에서는 타입의 Send 및 Sync 상태가 스레드 안전성 특성을 정확히 반영하는지 주의 깊게 살펴야 합니다. 다음과 같은 테스트는 타입이 Send 또는 Sync를 구현하는지에 관한 의도치 않은 퇴행을 잡아내는 데 도움이 됩니다.

#![allow(unused)]
fn main() {
#[test]
fn test_send() {
    fn assert_send<T: Send>() {}
    assert_send::<MyStrangeType>();
}

#[test]
fn test_sync() {
    fn assert_sync<T: Sync>() {}
    assert_sync::<MyStrangeType>();
}
}

오류 타입은 의미 있고 올바르게 동작합니다 (C-GOOD-ERR)

오류 타입이란 크레이트의 공개 함수가 반환하는 Result<T, E>에 쓰이는 모든 타입 E를 말합니다. 오류 타입은 항상 std::error::Error 트레이트를 구현해야 합니다. 이 트레이트는 error-chain 같은 오류 처리 라이브러리가 서로 다른 종류의 오류를 추상화하는 메커니즘이며, 오류를 다른 오류의 source()로 사용할 수 있게 해 줍니다.

또한 오류 타입은 Send 및 Sync 트레이트를 구현해야 합니다. Send가 아닌 오류는 thread::spawn으로 실행한 스레드에서 반환할 수 없습니다. Sync가 아닌 오류는 Arc를 사용해 스레드 간에 전달할 수 없습니다. 이는 멀티스레드 애플리케이션에서 기본적인 오류 처리를 할 때 흔히 요구되는 조건입니다.

Send와 Sync는 std::io::Error::new를 사용해 사용자 정의 오류를 IO 오류로 감쌀 때에도 중요합니다. 이 함수는 Error + Send + Sync 트레이트 경계를 요구합니다.

이 가이드라인과 관련해 특히 주의해야 할 곳은 오류(Error) 트레이트 객체를 반환하는 함수입니다. 예를 들면 reqwest::Error::get_ref가 있습니다. 일반적으로 호출자에게는 Error + Send + Sync + 'static이 가장 유용합니다. 'static을 추가하면 트레이트 객체를 Error::downcast_ref와 함께 사용할 수 있습니다.

오류가 담을 유용한 추가 정보가 없는 경우라도 ()를 오류 타입으로 사용하지 마십시오.

  • ()는 Error를 구현하지 않으므로 error-chain 같은 오류 처리 라이브러리와 함께 사용할 수 없습니다.
  • ()는 Display를 구현하지 않으므로, 사용자가 이 오류로 인해 실패 처리를 하려면 오류 메시지를 직접 작성해야 합니다.
  • ()의 Debug 표현은 오류를 unwrap()하기로 한 사용자에게 아무런 도움이 되지 않습니다.
  • 다운스트림 라이브러리가 자신의 오류 타입에 대해 From<()>를 구현하는 것은 의미상 타당하지 않으므로, 오류 타입으로 쓰인 ()는 ? 연산자와 함께 사용할 수 없습니다.

대신 크레이트나 개별 함수에 특화된 의미 있는 오류 타입을 정의하십시오. 적절한 Error 및 Display 구현을 제공하십시오. 오류가 담을 유용한 정보가 없다면 유닛 구조체로 구현할 수 있습니다.

#![allow(unused)]
fn main() {
use std::error::Error;
use std::fmt::Display;

// Instead of this...
fn do_the_thing() -> Result<Wow, ()>

// Prefer this...
fn do_the_thing() -> Result<Wow, DoError>

#[derive(Debug)]
struct DoError;

impl Display for DoError { /* ... */ }
impl Error for DoError { /* ... */ }
}

오류 타입의 Display 표현이 제공하는 오류 메시지는 소문자로 쓰고 끝에 구두점을 붙이지 않아야 하며, 대체로 간결해야 합니다.

Error::description()은 구현하지 않아야 합니다. 이 메서드는 더 이상 사용되지 않으며(deprecated), 사용자는 오류를 출력할 때 항상 description() 대신 Display를 사용해야 합니다.

표준 라이브러리의 예

  • ParseBoolError는 문자열을 bool로 파싱하는 데 실패했을 때 반환됩니다.

오류 메시지의 예

  • “unexpected end of file”
  • “제공된 문자열이 true 또는 false가 아닙니다”
  • “invalid IP address syntax”
  • “second time provided was later than self”
  • “잘못된 UTF-8 시퀀스: {}바이트, 인덱스 {}부터”
  • “environment variable was not valid unicode: {:?}”

이진 숫자 타입은 Hex, Octal, Binary 서식을 제공합니다 (C-NUM-FMT)

이 트레이트들은 {:X}, {:x}, {:o}, {:b} 서식 지정자에서 타입이 어떻게 표현될지를 결정합니다.

|나 & 같은 비트 연산을 할 만한 숫자 타입이라면 이 트레이트들을 구현하십시오. 특히 비트플래그 타입에 잘 맞습니다. struct Nanoseconds(u64)처럼 수량을 나타내는 숫자 타입에는 대개 필요하지 않습니다.

제네릭 리더/라이터 함수는 R: Read와 W: Write를 값으로 받습니다 (C-RW-VALUE)

표준 라이브러리에는 다음 두 impl이 있습니다:

#![allow(unused)]
fn main() {
impl<'a, R: Read + ?Sized> Read for &'a mut R { /* ... */ }

impl<'a, W: Write + ?Sized> Write for &'a mut W { /* ... */ }
}

따라서 R: Read나 W: Write 제네릭 매개변수를 값으로 받는 함수는 필요하면 가변 참조로도 호출할 수 있습니다.

이런 함수의 문서에는 가변 참조를 넘길 수 있다는 점을 사용자에게 짧게 알려 주십시오. Rust를 처음 쓰는 사용자는 이 부분에서 자주 어려움을 겪습니다. 예를 들어 파일을 하나 연 다음 거기서 데이터를 여러 번 나누어 읽으려고 하는데, 한 번 읽는 함수가 reader를 값으로 받아 소비해 버려서 더 진행하지 못할 수 있습니다. 이럴 때는 위의 impl 중 하나를 활용해 reader 매개변수로 f 대신 &mut f를 넘기면 됩니다.

예시