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

Rust API 가이드라인

이 문서는 Rust 프로그래밍 언어용 API를 설계하고 표현하는 방법에 관한 권장 사항 모음입니다. 이 가이드라인은 주로 Rust 라이브러리 팀이 Rust 표준 라이브러리와 Rust 생태계의 다른 크레이트를 만들며 얻은 경험을 바탕으로 작성했습니다.

이것은 어디까지나 가이드라인일 뿐이며, 그중 일부는 다른 것보다 더 엄격합니다. 어떤 경우에는 모호하며 아직 다듬어지는 중입니다. Rust 크레이트 작성자는 관용적이고 상호 운용 가능한 Rust 라이브러리를 개발할 때 이 가이드라인을 중요한 고려 사항의 모음으로 여기고, 적절하다고 판단하는 대로 활용하십시오. 이 가이드라인은 결코 크레이트 작성자가 반드시 따라야 하는 의무 사항으로 여겨서는 안 됩니다. 다만 이 가이드라인을 잘 따르는 크레이트가 그렇지 않은 크레이트보다 기존 크레이트 생태계와 더 잘 통합된다는 사실을 알게 될 수도 있습니다.

이 책은 두 부분으로 구성되어 있습니다. 하나는 크레이트 리뷰 중에 빠르게 훑어보기에 적합한, 모든 개별 가이드라인을 간결하게 정리한 체크리스트이고, 다른 하나는 가이드라인을 자세히 설명하는 주제별 장입니다.

API 가이드라인에 기여하는 데 관심이 있다면 contributing.md를 확인하고 Gitter 채널에 참여하십시오.

Rust API 가이드라인 체크리스트

  • 명명 (크레이트가 Rust 명명 규칙을 따릅니다)
    • 대소문자 표기가 RFC 430을 따릅니다 (C-CASE)
    • 임시 변환은 as_, to_, into_ 규칙을 따릅니다 (C-CONV)
    • 게터 이름이 Rust 규칙을 따릅니다 (C-GETTER)
    • 컬렉션에서 이터레이터를 생성하는 메서드는 iter, iter_mut, into_iter를 따릅니다 (C-ITER)
    • 이터레이터 타입 이름이 해당 이터레이터를 생성하는 메서드와 일치합니다 (C-ITER-TY)
    • 기능 이름에 의미 없는 자리 표시용 단어가 없습니다 (C-FEATURE)
    • 이름이 일관된 어순을 사용합니다 (C-WORD-ORDER)
  • 상호 운용성 (크레이트가 다른 라이브러리 기능과 잘 어우러집니다)
    • 타입이 공통 트레이트를 적극적으로 구현합니다 (C-COMMON-TRAITS)
      • Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Default
    • 변환에 표준 트레이트 From, AsRef, AsMut를 사용합니다 (C-CONV-TRAITS)
    • 컬렉션이 FromIterator와 Extend를 구현합니다 (C-COLLECT)
    • 데이터 구조가 Serde의 Serialize, Deserialize를 구현합니다 (C-SERDE)
    • 타입이 가능한 한 Send이자 Sync입니다 (C-SEND-SYNC)
    • 오류 타입이 의미 있고 올바르게 동작합니다 (C-GOOD-ERR)
    • 이진수 타입이 Hex, Octal, Binary 서식을 제공합니다 (C-NUM-FMT)
    • 제네릭 리더/라이터 함수는 R: Read와 W: Write를 값으로 받습니다 (C-RW-VALUE)
  • 매크로 (크레이트가 올바르게 동작하는 매크로를 제공합니다)
    • 입력 구문이 출력을 연상시킵니다 (C-EVOCATIVE)
    • 매크로가 속성과 잘 어우러집니다 (C-MACRO-ATTR)
    • 아이템 매크로가 아이템이 허용되는 모든 곳에서 동작합니다 (C-ANYWHERE)
    • 아이템 매크로가 가시성 지정자를 지원합니다 (C-MACRO-VIS)
    • 매크로의 타입 프래그먼트는 유연합니다 (C-MACRO-TY)
  • 문서화 (크레이트가 풍부하게 문서화되어 있습니다)
    • 크레이트 수준 문서가 충실하며 예제를 포함합니다 (C-CRATE-DOC)
    • 모든 아이템에 rustdoc 예제가 있습니다 (C-EXAMPLE)
    • 예제에서 try!나 unwrap이 아닌 ?를 사용합니다 (C-QUESTION-MARK)
    • 함수 문서에 오류, 패닉, 안전성 관련 고려 사항이 포함되어 있습니다 (C-FAILURE)
    • 본문에 관련 항목으로 연결되는 하이퍼링크가 있습니다 (C-LINK)
    • Cargo.toml에 모든 공통 메타데이터가 포함되어 있습니다 (C-METADATA)
      • authors(저자), description(설명), license(라이선스), homepage(홈페이지), documentation(문서), repository(저장소), keywords(키워드), categories(카테고리)
    • 릴리스 노트에 모든 주요 변경 사항을 기록합니다 (C-RELNOTES)
    • rustdoc에 도움이 되지 않는 구현 세부 사항을 표시하지 않습니다 (C-HIDDEN)
  • 예측 가능성 (크레이트를 쓰면 보이는 대로 동작하는 읽기 쉬운 코드를 작성할 수 있습니다)
    • 스마트 포인터는 고유 메서드를 추가하지 않습니다 (C-SMART-PTR)
    • 변환은 관련된 타입 중 가장 구체적인 타입에 둡니다 (C-CONV-SPECIFIC)
    • 명확한 리시버가 있는 함수는 메서드입니다 (C-METHOD)
    • 함수는 출력 매개변수를 받지 않습니다 (C-NO-OUT)
    • 연산자 오버로딩은 예상을 벗어나지 않습니다 (C-OVERLOAD)
    • 스마트 포인터만 Deref와 DerefMut를 구현합니다 (C-DEREF)
    • 생성자는 정적 고유 메서드입니다 (C-CTOR)
  • 유연성 (크레이트가 실제 환경의 다양한 사용 사례를 지원합니다)
    • 함수는 중복 작업을 피할 수 있도록 중간 결과를 노출합니다 (C-INTERMEDIATE)
    • 데이터를 어디에 복사하고 배치할지는 호출자가 결정합니다 (C-CALLER-CONTROL)
    • 함수는 제네릭을 사용해 매개변수에 대한 가정을 최소화합니다 (C-GENERIC)
    • 트레이트 객체로 유용하게 쓰일 수 있는 트레이트는 객체 안전합니다 (C-OBJECT)
  • 타입 안전성 (크레이트가 타입 시스템을 효과적으로 활용합니다)
    • newtype으로 정적인 구분을 제공합니다 (C-NEWTYPE)
    • 인수는 bool이나 Option이 아닌 타입으로 의미를 전달합니다 (C-CUSTOM-TYPE)
    • 플래그 집합을 나타내는 타입은 열거형이 아닌 bitflags로 만듭니다 (C-BITFLAG)
    • 빌더로 복잡한 값을 생성할 수 있습니다 (C-BUILDER)
  • 신뢰성 (크레이트가 잘못된 동작을 할 가능성이 낮습니다)
    • 함수는 인수를 검증합니다 (C-VALIDATE)
    • 소멸자는 절대 실패하지 않습니다 (C-DTOR-FAIL)
    • 블로킹될 수 있는 소멸자에는 대안이 있습니다 (C-DTOR-BLOCK)
  • 디버깅 용이성 (크레이트를 쉽게 디버깅할 수 있습니다)
    • 모든 공개 타입은 Debug를 구현합니다 (C-DEBUG)
    • Debug 표현은 절대 비어 있지 않습니다 (C-DEBUG-NONEMPTY)
  • 향후 대비 (크레이트가 사용자 코드를 깨뜨리지 않고 자유롭게 개선될 수 있습니다)
    • 봉인된 트레이트로 다운스트림 구현을 막습니다 (C-SEALED)
    • 구조체의 필드는 비공개입니다 (C-STRUCT-PRIVATE)
    • newtype으로 구현 세부 사항을 캡슐화합니다 (C-NEWTYPE-HIDE)
    • 데이터 구조체는 파생된 트레이트 바운드를 중복해서 지정하지 않습니다 (C-STRUCT-BOUNDS)
  • 필수 사항 (해당되는 경우에는 정말 중요합니다)
    • 안정 크레이트의 공개 의존성은 안정적입니다 (C-STABLE)
    • 크레이트와 그 의존성은 허용적 라이선스를 가집니다 (C-PERMISSIVE)

명명 규칙

대소문자 표기는 RFC 430을 따릅니다 (C-CASE)

Rust의 기본 명명 규칙은 RFC 430에 설명되어 있습니다.

일반적으로 Rust는 “타입 수준” 구성 요소(타입과 트레이트)에는 UpperCamelCase를, “값 수준” 구성 요소에는 snake_case를 사용하는 경향이 있습니다. 더 정확하게는 다음과 같습니다:

항목규칙
크레이트불명확함
모듈snake_case
타입UpperCamelCase
트레이트UpperCamelCase
열거형 배리언트UpperCamelCase
함수snake_case
메서드snake_case
일반 생성자new 또는 with_more_details
변환 생성자from_some_other_type
매크로snake_case!
지역 변수snake_case
정적 변수SCREAMING_SNAKE_CASE
상수SCREAMING_SNAKE_CASE
타입 매개변수간결한 UpperCamelCase, 보통 대문자 한 글자: T
라이프타임짧은 lowercase, 보통 한 글자: 'a, 'de, 'src
기능기능 이름 규칙은 불명확합니다. 다만 C-FEATURE를 참고하십시오.

UpperCamelCase에서는 약어와 복합어의 축약형을 한 단어로 취급합니다. UUID 대신 Uuid를, USize 대신 Usize를, StdIn 대신 Stdin을 사용하십시오. snake_case에서는 약어와 축약형을 소문자로 씁니다: is_xid_start.

snake_case나 SCREAMING_SNAKE_CASE에서 “단어“는 마지막 “단어“가 아닌 한 절대 한 글자로 이루어져서는 안 됩니다. 따라서 b_tree_map이 아니라 btree_map을 쓰지만, PI2가 아니라 PI_2를 씁니다.

크레이트 이름에는 -rs나 -rust를 접미사나 접두사로 사용해서는 안 됩니다. 모든 크레이트는 Rust입니다! 사용자에게 이 사실을 계속 상기시키는 것은 아무 의미가 없습니다.

표준 라이브러리의 예

표준 라이브러리 전체입니다. 이 가이드라인은 쉬울 것입니다!

임시 변환은 as_, to_, into_ 관례를 따릅니다 (C-CONV)

변환은 메서드로 제공해야 하며, 이름에는 다음과 같은 접두사를 붙입니다:

접두사비용소유권
as_무비용빌림 -> 빌림
to_높음빌림 -> 빌림
빌림 -> 소유 (Copy가 아닌 타입)
소유 -> 소유 (Copy 타입)
into_가변소유 -> 소유 (Copy가 아닌 타입)

예를 들면 다음과 같습니다.

  • str::as_bytes()는 str을 UTF-8 바이트의 슬라이스로 보는 뷰를 제공하며, 비용이 들지 않습니다. 입력은 빌린 &str이고 출력은 빌린 &[u8]입니다.
  • Path::to_str는 운영 체제 경로의 바이트에 대해 비용이 큰 UTF-8 검사를 수행합니다. 입력과 출력은 모두 빌린 것입니다. 이 메서드는 런타임에 무시할 수 없는 비용이 들기 때문에 as_str라고 부르는 것은 올바르지 않습니다.
  • str::to_lowercase()는 str에 대해 유니코드 규칙에 맞는 소문자 버전을 만들어 내는데, 이 과정에서 문자열의 문자를 순회해야 하며 메모리 할당이 필요할 수도 있습니다. 입력은 빌린 &str이고 출력은 소유된 String입니다.
  • f64::to_radians()는 부동소수점 값을 도(degree)에서 라디안으로 변환합니다. 입력은 f64입니다. f64는 복사 비용이 적으므로 참조 &f64를 전달할 필요가 없습니다. 입력이 소비되지 않으므로 이 함수를 into_radians라고 부르면 오해를 일으킬 수 있습니다.
  • String::into_bytes()는 String의 내부 Vec<u8>을 꺼내며, 비용이 들지 않습니다. 이 메서드는 String의 소유권을 가져와 소유된 Vec<u8>을 반환합니다.
  • BufReader::into_inner()는 버퍼링된 리더의 소유권을 가져와 내부 리더를 꺼내며, 비용이 들지 않습니다. 버퍼에 있던 데이터는 버려집니다.
  • BufWriter::into_inner()는 버퍼링된 라이터의 소유권을 가져와 내부 라이터를 꺼내는데, 이때 버퍼에 남은 데이터를 플러시해야 하므로 비용이 클 수 있습니다.

as_와 into_ 접두사가 붙은 변환은 일반적으로 추상화 수준을 낮추며, 내부 표현에 대한 뷰를 노출하거나(as) 데이터를 분해하여 내부 표현으로 만듭니다(into). 반면 to_ 접두사가 붙은 변환은 일반적으로 같은 추상화 수준에 머무르면서, 한 표현을 다른 표현으로 바꾸는 작업을 어느 정도 수행합니다.

어떤 타입이 단일 값을 감싸서 더 높은 수준의 의미를 부여하는 경우, 감싸진 값에 대한 접근은 into_inner() 메서드로 제공해야 합니다. 이는 BufReader처럼 버퍼링을 제공하는 래퍼, GzDecoder처럼 인코딩이나 디코딩을 제공하는 래퍼, AtomicBool처럼 원자적 접근을 제공하는 래퍼, 또는 이와 유사한 의미를 제공하는 모든 래퍼에 적용됩니다.

변환 메서드 이름의 mut 한정자가 반환 타입의 일부를 나타낸다면, 타입에 나타나는 그대로의 위치에 와야 합니다. 예를 들어 Vec::as_mut_slice는 mut 슬라이스를 반환하며, 이름 그대로의 동작을 합니다. 이 이름이 as_slice_mut보다 선호됩니다.

#![allow(unused)]
fn main() {
// Return type is a mut slice.
fn as_mut_slice(&mut self) -> &mut [T];
}
표준 라이브러리의 더 많은 예

게터 이름은 Rust 관례를 따릅니다 (C-GETTER)

몇 가지 예외를 제외하면, Rust 코드에서는 게터에 get_ 접두사를 사용하지 않습니다.

#![allow(unused)]
fn main() {
pub struct S {
    first: First,
    second: Second,
}

impl S {
    // Not get_first.
    pub fn first(&self) -> &First {
        &self.first
    }

    // Not get_first_mut, get_mut_first, or mut_first.
    pub fn first_mut(&mut self) -> &mut First {
        &mut self.first
    }
}
}

get이라는 이름은 게터로 가져올 수 있는 것이 합리적으로 보아 단 하나뿐이고 명확할 때에만 사용합니다. 예를 들어 Cell::get은 Cell의 내용에 접근합니다.

경계 검사와 같은 런타임 검증을 수행하는 게터의 경우, unsafe _unchecked 변형을 추가하는 것을 고려하십시오. 일반적으로 이러한 변형은 다음과 같은 시그니처를 가집니다.

#![allow(unused)]
fn main() {
fn get(&self, index: K) -> Option<&V>;
fn get_mut(&mut self, index: K) -> Option<&mut V>;
unsafe fn get_unchecked(&self, index: K) -> &V;
unsafe fn get_unchecked_mut(&mut self, index: K) -> &mut V;
}

게터와 변환(C-CONV)의 차이는 미묘할 수 있으며 항상 명확하게 구분되는 것은 아닙니다. 예를 들어 TempDir::path는 임시 디렉터리의 파일 시스템 경로에 대한 게터로 이해할 수 있는 반면, TempDir::into_path는 임시 디렉터리를 삭제할 책임을 호출자에게 넘기는 변환입니다. path는 게터이므로 이를 get_path나 as_path라고 부르는 것은 올바르지 않습니다.

표준 라이브러리의 예

컬렉션에서 이터레이터를 생성하는 메서드는 iter, iter_mut, into_iter를 따릅니다 (C-ITER)

RFC 199에 따릅니다.

U 타입의 요소를 담는 컨테이너의 경우, 이터레이터 메서드의 이름은 다음과 같이 지어야 합니다.

#![allow(unused)]
fn main() {
fn iter(&self) -> Iter             // Iter implements Iterator<Item = &U>
fn iter_mut(&mut self) -> IterMut  // IterMut implements Iterator<Item = &mut U>
fn into_iter(self) -> IntoIter     // IntoIter implements Iterator<Item = U>
}

이 가이드라인은 개념적으로 동질적인 컬렉션인 데이터 구조에 적용됩니다. 반례로, str 타입은 유효한 UTF-8임이 보장되는 바이트의 슬라이스입니다. 이는 개념적으로 동질적인 컬렉션보다 미묘한 차이가 있으므로, iter/iter_mut/into_iter 계열의 이터레이터 메서드를 제공하는 대신 바이트 단위로 순회하는 str::bytes와 문자 단위로 순회하는 str::chars를 제공합니다.

이 가이드라인은 함수가 아닌 메서드에만 적용됩니다. 예를 들어 url 크레이트의 percent_encode는 퍼센트 인코딩된 문자열 조각에 대한 이터레이터를 반환합니다. 여기에 iter/iter_mut/into_iter 규칙을 적용해도 더 명확해지는 것은 없습니다.

표준 라이브러리의 예

이터레이터 타입 이름은 해당 타입을 생성하는 메서드의 이름과 일치합니다 (C-ITER-TY)

into_iter()라는 메서드는 IntoIter라는 타입을 반환해야 하며, 이터레이터를 반환하는 다른 모든 메서드도 마찬가지입니다.

이 가이드라인은 주로 메서드에 적용되지만, 함수에도 적용하는 것이 타당한 경우가 많습니다. 예를 들어 url 크레이트의 percent_encode 함수는 PercentEncode라는 이터레이터 타입을 반환합니다.

이러한 타입 이름은 vec::IntoIter처럼 소속 모듈을 접두어로 붙였을 때 가장 자연스럽습니다.

표준 라이브러리의 예

기능 이름에는 의미 없는 자리 채움용 단어를 넣지 않습니다 (C-FEATURE)

use-abc나 with-abc처럼 아무 의미도 전달하지 않는 단어를 Cargo 기능의 이름에 넣지 마십시오. 기능 이름은 바로 abc로 지으십시오.

이 문제는 Rust 표준 라이브러리에 대한 선택적 의존성이 있는 크레이트에서 가장 흔하게 발생합니다. 이를 올바르게 처리하는 표준적인 방법은 다음과 같습니다.

# In Cargo.toml

[features]
default = ["std"]
std = []
#![allow(unused)]
fn main() {
// In lib.rs
#![no_std]

#[cfg(feature = "std")]
extern crate std;
}

기능 이름을 use-std나 with-std, 또는 std가 아닌 어떤 창의적인 이름으로도 짓지 마십시오. 이 명명 규칙은 Cargo가 선택적 의존성에 대해 추론하는 암시적 기능의 이름 규칙과 일치합니다. Serde와 Rust 표준 라이브러리에 대한 선택적 의존성이 있는 크레이트 x를 생각해 봅시다.

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

[features]
std = ["serde/std"]

[dependencies]
serde = { version = "1.0", optional = true }

x에 의존할 때, features = ["serde"]로 선택적 Serde 의존성을 활성화할 수 있습니다. 마찬가지로 features = ["std"]로 선택적 표준 라이브러리 의존성을 활성화할 수 있습니다. Cargo가 선택적 의존성에 대해 추론하는 암시적 기능의 이름은 use-serde나 with-serde가 아니라 serde이므로, 명시적 기능도 같은 방식으로 동작하는 것이 바람직합니다.

덧붙여, Cargo는 기능이 추가적(additive)이어야 한다고 요구하므로 no-abc처럼 부정형으로 이름 붙인 기능은 사실상 결코 올바르지 않습니다.

이름은 일관된 어순을 사용합니다 (C-WORD-ORDER)

다음은 표준 라이브러리에 있는 몇 가지 오류 타입입니다.

이들은 모두 동사-객체-오류 어순을 사용합니다. 주소 파싱 실패를 나타내는 오류를 추가한다면, 일관성을 위해 AddrParseError가 아니라 ParseAddrError처럼 동사-객체-오류 순서로 이름을 지어야 합니다.

어떤 어순을 선택하는지는 중요하지 않지만, 크레이트 내부의 일관성과 표준 라이브러리의 유사한 기능과의 일관성에 주의를 기울이십시오.

상호 운용성

타입은 공통 트레이트를 적극적으로 구현합니다 (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를 넘기면 됩니다.

예시

매크로

입력 구문은 출력을 연상시킵니다 (C-EVOCATIVE)

Rust 매크로를 사용하면 원하는 입력 구문을 사실상 무엇이든 만들어 낼 수 있습니다. 가능한 한 기존 Rust 구문을 따라 하여, 입력 구문이 사용자 코드의 나머지 부분과 친숙하고 일관되도록 하십시오. 키워드와 구두점의 선택 및 배치에 주의하십시오.

좋은 지침은 매크로의 출력에서 생성될 것과 비슷한 구문, 특히 키워드와 구두점을 사용하는 것입니다.

예를 들어 매크로가 입력으로 주어진 특정 이름으로 구조체를 선언한다면, 그 이름 앞에 struct 키워드를 붙여 주어진 이름으로 구조체가 선언된다는 것을 읽는 사람에게 알리십시오.

#![allow(unused)]
fn main() {
// Prefer this...
bitflags! {
    struct S: u32 { /* ... */ }
}

// ...over no keyword...
bitflags! {
    S: u32 { /* ... */ }
}

// ...or some ad-hoc word.
bitflags! {
    flags S: u32 { /* ... */ }
}
}

또 다른 예는 세미콜론과 쉼표입니다. Rust에서 상수 뒤에는 세미콜론이 오므로, 매크로가 일련의 상수를 선언한다면 구문이 다른 부분에서 Rust와 약간 다르더라도 상수 뒤에는 세미콜론을 붙이는 것이 좋습니다.

#![allow(unused)]
fn main() {
// Ordinary constants use semicolons.
const A: u32 = 0b000001;
const B: u32 = 0b000010;

// So prefer this...
bitflags! {
    struct S: u32 {
        const C = 0b000100;
        const D = 0b001000;
    }
}

// ...over this.
bitflags! {
    struct S: u32 {
        const E = 0b010000,
        const F = 0b100000,
    }
}
}

매크로는 매우 다양하므로 이러한 구체적인 예가 들어맞지 않을 수도 있지만, 같은 원칙을 자신의 상황에 어떻게 적용할지 생각해 보십시오.

아이템 매크로는 속성과 잘 조합됩니다 (C-MACRO-ATTR)

출력 아이템을 둘 이상 생성하는 매크로는 그중 어느 아이템에든 속성을 추가할 수 있도록 지원해야 합니다. 흔한 사용 사례 하나는 개별 아이템을 cfg 뒤에 두는 것입니다.

#![allow(unused)]
fn main() {
bitflags! {
    struct Flags: u8 {
        #[cfg(windows)]
        const ControlCenter = 0b001;
        #[cfg(unix)]
        const Terminal = 0b010;
    }
}
}

구조체나 열거형을 출력으로 생성하는 매크로는 출력에 derive를 사용할 수 있도록 속성을 지원해야 합니다.

#![allow(unused)]
fn main() {
bitflags! {
    #[derive(Default, Serialize)]
    struct Flags: u8 {
        const ControlCenter = 0b001;
        const Terminal = 0b010;
    }
}
}

아이템 매크로는 아이템이 허용되는 모든 곳에서 동작합니다 (C-ANYWHERE)

Rust에서는 아이템을 모듈 수준에 두거나 함수처럼 더 좁은 스코프 안에 둘 수 있습니다. 아이템 매크로는 이 모든 곳에서 일반 아이템과 똑같이 잘 동작해야 합니다. 테스트 스위트에는 최소한 모듈 스코프와 함수 스코프에서의 매크로 호출이 포함되어야 합니다.

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    test_your_macro_in_a!(module);

    #[test]
    fn anywhere() {
        test_your_macro_in_a!(function);
    }
}
}

문제가 생길 수 있는 경우를 보여 주는 간단한 예로, 이 매크로는 모듈 스코프에서는 잘 동작하지만 함수 스코프에서는 실패합니다.

#![allow(unused)]
fn main() {
macro_rules! broken {
    ($m:ident :: $t:ident) => {
        pub struct $t;
        pub mod $m {
            pub use super::$t;
        }
    }
}

broken!(m::T); // okay, expands to T and m::T

fn g() {
    broken!(m::U); // fails to compile, super::U refers to the containing module not g
}
}

아이템 매크로는 가시성 지정자를 지원합니다 (C-MACRO-VIS)

매크로가 생성하는 아이템의 가시성은 Rust 구문을 따르십시오. 기본적으로는 비공개이고, pub가 지정되면 공개입니다.

#![allow(unused)]
fn main() {
bitflags! {
    struct PrivateFlags: u8 {
        const A = 0b0001;
        const B = 0b0010;
    }
}

bitflags! {
    pub struct PublicFlags: u8 {
        const C = 0b0100;
        const D = 0b1000;
    }
}
}

매크로의 타입 조각은 유연하게 쓸 수 있습니다 (C-MACRO-TY)

매크로가 입력에서 $t:ty 같은 타입 프래그먼트를 받는다면, 다음 모두와 함께 사용할 수 있어야 합니다.

  • 기본 타입: u8, &str
  • 상대 경로: m::Data
  • 절대 경로: ::base::Data
  • 상위 상대 경로: super::Data
  • 제네릭: Vec<String>

문제가 생길 수 있는 간단한 예로, 이 매크로는 기본 타입과 절대 경로에서는 잘 동작하지만 상대 경로에서는 실패합니다.

#![allow(unused)]
fn main() {
macro_rules! broken {
    ($m:ident => $t:ty) => {
        pub mod $m {
            pub struct Wrapper($t);
        }
    }
}

broken!(a => u8); // okay

broken!(b => ::std::marker::PhantomData<()>); // okay

struct S;
broken!(c => S); // fails to compile
}

문서화

크레이트 수준 문서는 충실하며 예제를 포함합니다 (C-CRATE-DOC)

RFC 1687을 참고하십시오.

모든 항목에 rustdoc 예제가 있습니다 (C-EXAMPLE)

모든 공개 모듈, 트레이트, 구조체, 열거형, 함수, 메서드, 매크로, 타입 정의에는 해당 기능을 실제로 사용해 보는 예제가 있어야 합니다.

이 가이드라인은 합리적인 범위 내에서 적용해야 합니다.

다른 항목에 있는 적절한 예제로 연결하는 링크만으로도 충분할 수 있습니다. 예를 들어 특정 타입을 사용하는 함수가 정확히 하나뿐이라면, 함수나 타입 중 한쪽에만 예제를 작성하고 다른 쪽에서 그 예제로 링크하는 것이 적절할 수 있습니다.

예제의 목적이 항상 해당 항목을 사용하는 방법을 보여 주는 것은 아닙니다. 독자는 함수를 호출하는 방법, 열거형에 대해 매칭하는 방법 및 기타 기본적인 작업을 이해하고 있다고 기대할 수 있습니다. 오히려 예제는 사람들이 해당 항목을 왜 사용하려 하는지를 보여 주기 위한 경우가 많습니다.

// This would be a poor example of using clone(). It mechanically shows *how* to
// call clone(), but does nothing to show *why* somebody would want this.
fn main() {
    let hello = "hello";

    hello.clone();
}

예제는 try!나 unwrap이 아닌 ?를 사용합니다 (C-QUESTION-MARK)

좋든 싫든, 사용자는 예제 코드를 그대로 복사하는 경우가 많습니다. 오류를 언래핑하는 것은 사용자가 의식적으로 내려야 하는 결정이어야 합니다.

실패할 수 있는 예제 코드를 구성하는 일반적인 방법은 다음과 같습니다. #으로 시작하는 줄은 예제를 빌드할 때 cargo test에 의해 컴파일되지만, 사용자에게 보이는 rustdoc에는 나타나지 않습니다.

/// ```rust
/// # use std::error::Error;
/// #
/// # fn main() -> Result<(), Box<dyn Error>> {
/// your;
/// example?;
/// code;
/// #
/// #     Ok(())
/// # }
/// ```

함수 문서에는 오류, 패닉, 안전성 관련 고려 사항이 포함됩니다 (C-FAILURE)

오류 조건은 “Errors” 섹션에 문서화해야 합니다. 이는 트레이트 메서드에도 적용됩니다. 구현이 오류를 반환하는 것이 허용되거나 예상되는 트레이트 메서드는 “Errors” 섹션으로 문서화해야 합니다.

예를 들어 표준 라이브러리에서 std::io::Read::read 트레이트 메서드의 일부 구현은 오류를 반환할 수 있습니다.

/// Pull some bytes from this source into the specified buffer, returning
/// how many bytes were read.
///
/// ... lots more info ...
///
/// # Errors
///
/// If this function encounters any form of I/O or other error, an error
/// variant will be returned. If an error is returned then it must be
/// guaranteed that no bytes were read.

패닉 조건은 “Panics” 섹션에 문서화해야 합니다. 이는 트레이트 메서드에도 적용됩니다. 구현이 패닉하는 것이 허용되거나 예상되는 트레이트 메서드는 “Panics” 섹션으로 문서화해야 합니다.

표준 라이브러리에서 Vec::insert 메서드는 패닉할 수 있습니다.

/// Inserts an element at position `index` within the vector, shifting all
/// elements after it to the right.
///
/// # Panics
///
/// Panics if `index` is out of bounds.

생각할 수 있는 모든 패닉 경우를 문서화할 필요는 없으며, 특히 호출자가 제공한 로직에서 패닉이 발생하는 경우에는 더욱 그렇습니다. 예를 들어 다음 코드에서 Display 패닉을 문서화하는 것은 지나쳐 보입니다. 하지만 확신이 서지 않을 때는 패닉 경우를 더 많이 문서화하는 쪽을 택하십시오.

#![allow(unused)]
fn main() {
/// # Panics
///
/// This function panics if `T`'s implementation of `Display` panics.
pub fn print<T: Display>(t: T) {
    println!("{}", t.to_string());
}
}

unsafe 함수에는 함수를 올바르게 사용하기 위해 호출자가 지켜야 할 모든 불변 조건을 설명하는 “안전성(Safety)” 섹션을 두어 문서화해야 합니다.

unsafe std::ptr::read는 호출자에게 다음을 요구합니다.

/// Reads the value from `src` without moving it. This leaves the
/// memory in `src` unchanged.
///
/// # Safety
///
/// Beyond accepting a raw pointer, this is unsafe because it semantically
/// moves the value out of `src` without preventing further usage of `src`.
/// If `T` is not `Copy`, then care must be taken to ensure that the value at
/// `src` is not used before the data is overwritten again (e.g. with `write`,
/// `zero_memory`, or `copy_memory`). Note that `*src = foo` counts as a use
/// because it will attempt to drop the value previously at `*src`.
///
/// The pointer must be aligned; use `read_unaligned` if that is not the case.

일반 링크는 통상적인 마크다운 문법인 [text](url)을 사용하여 인라인으로 추가할 수 있습니다. 다른 타입으로의 링크는 [`text`]로 표시한 다음, 문서 주석 끝의 새 줄에 [`text`]: <target>의 형태로 링크 대상을 추가하여 넣을 수 있습니다. 여기서 <target>은 아래에서 설명합니다.

같은 타입 안의 메서드를 가리키는 링크 대상은 보통 다음과 같습니다:

[`serialize_struct`]: #method.serialize_struct

다른 타입을 가리키는 링크 대상은 보통 다음과 같습니다:

[`Deserialize`]: trait.Deserialize.html

링크 대상은 부모 모듈이나 자식 모듈을 가리킬 수도 있습니다:

[`Value`]: ../enum.Value.html
[`DeserializeOwned`]: de/trait.DeserializeOwned.html

이 가이드라인은 RFC 1574의 “Link all the things” 항목에서 공식적으로 권장하는 사항입니다.

Cargo.toml에 모든 일반 메타데이터가 포함됩니다 (C-METADATA)

Cargo.toml의 [package] 섹션에는 다음 값들이 포함되어야 합니다:

  • authors
  • description
  • license
  • repository
  • keywords
  • categories

이 밖에 선택적인 메타데이터 필드가 두 가지 있습니다:

  • documentation
  • homepage

기본적으로 crates.io는 docs.rs에 있는 해당 크레이트의 문서로 링크합니다. documentation 메타데이터는 문서가 docs.rs가 아닌 다른 곳에 호스팅되는 경우에만 설정하면 됩니다. 예를 들어 크레이트가 docs.rs의 빌드 환경에서 사용할 수 없는 공유 라이브러리에 링크하는 경우가 이에 해당합니다.

homepage 메타데이터는 소스 저장소나 API 문서 외에 크레이트를 위한 별도의 웹사이트가 있는 경우에만 설정해야 합니다. homepage가 documentation 값이나 repository 값과 중복되지 않도록 하십시오. 예를 들어 Serde는 전용 웹사이트인 https://serde.rs 주소를 homepage로 설정합니다.

릴리스 노트에 모든 중요한 변경 사항이 기록됩니다 (C-RELNOTES)

크레이트 사용자는 릴리스 노트를 읽고 크레이트의 각 배포 릴리스에서 무엇이 바뀌었는지 요약을 확인할 수 있습니다. 릴리스 노트로 가는 링크나 릴리스 노트 자체를 크레이트 수준 문서와 Cargo.toml에 링크된 저장소 중 한쪽 또는 양쪽에 포함해야 합니다.

호환성을 깨뜨리는 변경(RFC 1105에서 정의한 것)은 릴리스 노트에 명확하게 표시해야 합니다.

Git으로 크레이트의 소스를 관리하는 경우, crates.io에 배포된 모든 릴리스에는 배포된 커밋을 식별하는 태그가 하나씩 있어야 합니다. Git이 아닌 VCS 도구를 사용하는 경우에도 비슷한 절차를 따라야 합니다.

# Tag the current commit
GIT_COMMITTER_DATE=$(git log -n1 --pretty=%aD) git tag -a -m "Release 0.3.0" 0.3.0
git push --tags

일부 Git 명령은 주석 태그가 하나라도 존재하면 주석이 없는 태그를 무시하므로 주석 태그를 사용하는 것이 좋습니다.

예시

rustdoc은 도움이 되지 않는 구현 세부 사항을 보여 주지 않습니다 (C-HIDDEN)

rustdoc에는 사용자가 크레이트를 온전히 사용하는 데 필요한 모든 것이 포함되어야 하며, 그 이상은 포함되지 않아야 합니다. 관련된 구현 세부 사항을 본문에서 설명하는 것은 괜찮지만, 그것이 문서의 실제 항목이 되어서는 안 됩니다.

특히 rustdoc에 어떤 impl을 보이게 할지 신중하게 고르십시오. 사용자가 크레이트를 온전히 사용하는 데 필요한 impl은 모두 보이게 하되, 그 밖의 것은 보이지 않게 하십시오. 다음 코드에서 PublicError의 rustdoc은 기본적으로 From<PrivateError> impl을 보여 줍니다. 사용자는 자신의 코드에서 PrivateError를 가질 수 없으므로 이 impl은 사용자와 전혀 관련이 없습니다. 그래서 #[doc(hidden)]으로 이를 숨기기로 합니다.

#![allow(unused)]
fn main() {
// This error type is returned to users.
pub struct PublicError { /* ... */ }

// This error type is returned by some private helper functions.
struct PrivateError { /* ... */ }

// Enable use of `?` operator.
#[doc(hidden)]
impl From<PrivateError> for PublicError {
    fn from(err: PrivateError) -> PublicError {
        /* ... */
    }
}
}

pub(crate)는 공개 API에서 구현 세부 사항을 제거하는 또 하나의 훌륭한 도구입니다. 이를 사용하면 항목을 자신이 속한 모듈 바깥에서는 사용할 수 있지만, 같은 크레이트 바깥에서는 사용할 수 없습니다.

예측 가능성

스마트 포인터는 고유 메서드를 추가하지 않습니다 (C-SMART-PTR)

예를 들어, Box::into_raw 함수가 지금과 같은 방식으로 정의된 이유가 바로 이것입니다.

#![allow(unused)]
fn main() {
impl<T> Box<T> where T: ?Sized {
    fn into_raw(b: Box<T>) -> *mut T { /* ... */ }
}

let boxed_str: Box<str> = /* ... */;
let ptr = Box::into_raw(boxed_str);
}

만약 이것이 고유 메서드로 정의되었다면, 호출 지점에서 호출되는 메서드가 Box<T>의 메서드인지 T의 메서드인지 혼란스러울 것입니다.

#![allow(unused)]
fn main() {
impl<T> Box<T> where T: ?Sized {
    // Do not do this.
    fn into_raw(self) -> *mut T { /* ... */ }
}

let boxed_str: Box<str> = /* ... */;

// This is a method on str accessed through the smart pointer Deref impl.
boxed_str.chars()

// This is a method on Box<str>...?
boxed_str.into_raw()
}

변환은 관련된 타입 중 가장 구체적인 타입에 둡니다 (C-CONV-SPECIFIC)

확신이 서지 않을 때는 from_보다 to_/as_/into_를 선호하십시오. 이쪽이 사용하기에 더 편리하기 때문입니다(그리고 다른 메서드와 연쇄하여 호출할 수도 있습니다).

두 타입 간의 많은 변환에서, 한쪽 타입이 분명히 더 “구체적“입니다. 즉, 다른 타입에는 없는 추가적인 불변 조건이나 해석을 제공합니다. 예를 들어, str은 UTF-8로 인코딩된 바이트 시퀀스이므로 &[u8]보다 더 구체적입니다.

변환은 관련된 타입 중 더 구체적인 타입에 두어야 합니다. 따라서 str은 &[u8] 값으로 변환하거나 그로부터 변환하기 위해 as_bytes 메서드와 from_utf8 생성자를 모두 제공합니다. 이 관례는 직관적일 뿐만 아니라, &[u8]와 같은 구체 타입이 끝없는 변환 메서드로 오염되는 것을 방지합니다.

명확한 리시버가 있는 함수는 메서드입니다 (C-METHOD)

다음 방식이

#![allow(unused)]
fn main() {
impl Foo {
    pub fn frob(&self, w: widget) { /* ... */ }
}
}

다음 방식보다 낫습니다

#![allow(unused)]
fn main() {
pub fn frob(foo: &Foo, w: widget) { /* ... */ }
}

특정 타입과 명확하게 연관된 모든 연산에 해당합니다.

메서드는 함수에 비해 많은 장점이 있습니다.

  • 메서드를 사용하기 위해 임포트하거나 경로를 한정할 필요가 없습니다. 적절한 타입의 값만 있으면 됩니다.
  • 메서드 호출 시 자동 차용(가변 차용 포함)이 수행됩니다.
  • “T 타입의 값으로 무엇을 할 수 있는가“라는 질문에 쉽게 답할 수 있게 해 줍니다(특히 rustdoc을 사용할 때).
  • self 표기법을 제공하는데, 이는 더 간결하며 소유권의 구분을 더 명확하게 전달하는 경우가 많습니다.

함수는 출력 매개변수를 받지 않습니다 (C-NO-OUT)

다음 방식이

#![allow(unused)]
fn main() {
fn foo() -> (Bar, Bar)
}

다음 방식보다 낫습니다

#![allow(unused)]
fn main() {
fn foo(output: &mut Bar) -> Bar
}

여러 개의 Bar 값을 반환하는 경우에 해당합니다.

튜플이나 구조체 같은 복합 반환 타입은 효율적으로 컴파일되며 힙 할당이 필요하지 않습니다. 함수가 여러 값을 반환해야 한다면, 이러한 타입 중 하나를 통해 반환해야 합니다.

주요 예외는 다음과 같습니다. 때로는 함수가 호출자가 이미 소유한 데이터를 수정하기 위한 것일 수 있습니다. 예를 들어 버퍼를 재사용하는 경우입니다.

#![allow(unused)]
fn main() {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>
}

연산자 오버로드는 예상을 벗어나지 않습니다 (C-OVERLOAD)

내장 구문이 있는 연산자(*, | 등)는 std::ops의 트레이트를 구현하여 타입에 제공할 수 있습니다. 이러한 연산자에는 강한 기대가 따릅니다. Mul은 곱셈과 어느 정도 유사하고 (결합 법칙과 같은 기대되는 성질을 공유하는) 연산에 대해서만 구현하십시오. 다른 트레이트도 마찬가지입니다.

스마트 포인터만 Deref와 DerefMut를 구현합니다 (C-DEREF)

Deref 트레이트는 여러 상황에서 컴파일러에 의해 암묵적으로 사용되며, 메서드 해석과 상호작용합니다. 관련 규칙은 스마트 포인터를 수용하도록 특별히 설계되었으므로, 이 트레이트는 그 목적으로만 사용해야 합니다.

표준 라이브러리의 예

생성자는 정적 고유 메서드입니다 (C-CTOR)

Rust에서 “생성자“는 관례에 불과합니다. 생성자 이름 짓기에는 다양한 관례가 있으며, 그 차이는 미묘한 경우가 많습니다.

가장 기본적인 형태의 생성자는 인수가 없는 new 메서드입니다.

#![allow(unused)]
fn main() {
impl<T> Example<T> {
    pub fn new() -> Example<T> { /* ... */ }
}
}

생성자는 자신이 생성하는 타입의 정적(self가 없는) 고유 메서드입니다. 타입 이름을 완전히 임포트하는 관행과 결합하면, 이 관례는 정보를 충분히 담으면서도 간결한 생성 코드로 이어집니다.

#![allow(unused)]
fn main() {
use example::Example;

// Construct a new Example.
let ex = Example::new();
}

일반적으로 타입의 인스턴스를 만드는 주된 메서드에는 new라는 이름을 사용해야 합니다. 위의 예처럼 인수를 받지 않는 경우도 있습니다. Box::new처럼 Box에 넣을 값을 전달받는 등 인수를 받는 경우도 있습니다.

일부 타입의 생성자, 특히 I/O 리소스 타입의 생성자는 File::open, Mmap::open, TcpStream::connect, UdpSocket::bind처럼 생성자에 별도의 명명 규칙을 사용합니다. 이러한 경우 이름은 해당 도메인에 적합하게 선택됩니다.

타입을 생성하는 방법이 여러 가지인 경우가 많습니다. 이러한 경우 Mmap::open_with_offset처럼 보조 생성자에 _with_foo 접미사를 붙이는 것이 일반적입니다. 그러나 타입에 생성 옵션이 많다면, 대신 빌더 패턴(C-BUILDER)을 고려하십시오.

일부 생성자는 “변환 생성자“로, 다른 타입의 기존 값으로부터 새로운 타입을 만드는 메서드입니다. 이러한 생성자는 보통 std::io::Error::from_raw_os_error처럼 from_으로 시작하는 이름을 가집니다. 다만 이와 매우 유사한 From 트레이트(C-CONV-TRAITS)도 참고하십시오. from_ 접두사가 붙은 변환 생성자와 From<T> 구현 사이에는 세 가지 차이점이 있습니다.

  • from_ 생성자는 unsafe일 수 있지만, From 구현은 그럴 수 없습니다. 그 한 예로 Box::from_raw가 있습니다.
  • from_ 생성자는 u64::from_str_radix처럼 원본 데이터의 의미를 명확히 하기 위해 추가 인수를 받을 수 있습니다.
  • From 구현은 원본 데이터 타입만으로 출력 데이터 타입의 인코딩을 결정할 수 있을 때에만 적절합니다. u64::from_be나 String::from_utf8처럼 입력이 단지 비트 덩어리일 때는 변환 생성자의 이름으로 그 의미를 나타낼 수 있습니다.

타입이 Default와 new 생성자를 모두 구현하는 것은 흔하며 권장되는 일입니다. 둘 다 갖춘 타입이라면 두 가지가 같은 동작을 해야 합니다. 어느 한쪽을 다른 쪽을 이용해 구현해도 됩니다.

표준 라이브러리의 예

유연성

함수는 중복 작업을 피하기 위해 중간 결과를 노출합니다 (C-INTERMEDIATE)

어떤 질문에 답하는 함수는 대부분 그와 관련된 유용한 데이터도 함께 계산합니다. 이 데이터가 클라이언트에게 유용할 수 있다면 API에서 노출하는 것을 고려하십시오.

표준 라이브러리의 예

  • Vec::binary_search는 값을 찾았는지 여부를 나타내는 bool을 반환하지 않으며, 값이 있을 수도 있는 인덱스를 담은 Option<usize>도 반환하지 않습니다. 대신 값을 찾은 경우에는 그 인덱스에 대한 정보를 반환하고, 찾지 못한 경우에는 값을 삽입해야 할 인덱스도 반환합니다.

  • String::from_utf8은 입력 바이트가 UTF-8이 아니면 실패할 수 있습니다. 오류가 발생하면 입력이 어느 바이트 오프셋까지 유효한 UTF-8이었는지 알려 주는 중간 결과를 반환하고, 입력 바이트의 소유권도 되돌려 줍니다.

  • HashMap::insert는 주어진 키에 이미 값이 있으면 그 값을 담은 Option<T>를 반환합니다. 사용자가 이 값을 되찾고 싶을 때 삽입 연산이 값을 반환해 주므로, 해시 테이블을 한 번 더 조회하지 않아도 됩니다.

데이터를 어디에 복사하고 배치할지는 호출자가 결정합니다 (C-CALLER-CONTROL)

함수가 인수의 소유권을 필요로 한다면, 인수를 빌림한 뒤 복제하지 말고 인수의 소유권을 가져가야 합니다.

#![allow(unused)]
fn main() {
// Prefer this:
fn foo(b: Bar) {
    /* use b as owned, directly */
}

// Over this:
fn foo(b: &Bar) {
    let b = b.clone();
    /* use b as owned after cloning */
}
}

함수가 인수의 소유권을 필요로 하지 않는다면, 소유권을 가져간 뒤 인수를 드롭하지 말고 인수의 공유 빌림이나 배타적 빌림을 받아야 합니다.

#![allow(unused)]
fn main() {
// Prefer this:
fn foo(b: &Bar) {
    /* use b as borrowed */
}

// Over this:
fn foo(b: Bar) {
    /* use b as borrowed, it is implicitly dropped before function returns */
}
}

Copy 트레이트는 꼭 필요한 경우에만 바운드로 사용해야 하며, 복사 비용이 저렴해야 한다는 신호를 보내는 수단으로 사용해서는 안 됩니다.

함수는 제네릭을 사용하여 매개변수에 대한 가정을 최소화합니다 (C-GENERIC)

함수가 입력에 대해 하는 가정이 적을수록 더 널리 사용할 수 있습니다.

다음 방식이

#![allow(unused)]
fn main() {
fn foo<I: IntoIterator<Item = i64>>(iter: I) { /* ... */ }
}

아래의 어떤 방식보다도 낫습니다

#![allow(unused)]
fn main() {
fn foo(c: &[i64]) { /* ... */ }
fn foo(c: &Vec<i64>) { /* ... */ }
fn foo(c: &SomeOtherCollection<i64>) { /* ... */ }
}

함수가 데이터를 순회하기만 하면 되는 경우에 해당합니다.

더 일반적으로는, 함수가 인수에 대해 해야 하는 가정을 정확히 짚어 내기 위해 제네릭을 사용하는 것을 고려하십시오.

제네릭의 장점

  • 재사용성. 제네릭 함수는 열린 타입 집합에 적용할 수 있으며, 동시에 그 타입들이 제공해야 하는 기능에 대해 명확한 계약을 제시합니다.

  • 정적 디스패치와 최적화. 제네릭 함수는 사용될 때마다 트레이트 바운드를 구현하는 특정 타입에 맞게 특수화(“단형화”)됩니다. 따라서 (1) 트레이트 메서드 호출은 구현을 직접 호출하는 정적 호출이 되고, (2) 컴파일러가 이 호출들을 인라인하거나 다른 방식으로 최적화할 수 있습니다.

  • 인라인 레이아웃. struct나 enum 타입이 어떤 타입 매개변수 T에 대해 제네릭이면, T 타입의 값은 간접 참조 없이 struct/enum 안에 인라인으로 배치됩니다.

  • 추론. 제네릭 함수의 타입 매개변수는 대개 추론될 수 있으므로, 제네릭 함수를 사용하면 보통 명시적 변환이나 다른 메서드 호출이 필요한 코드의 장황함을 줄일 수 있습니다.

  • 정확한 타입. 제네릭은 트레이트를 구현하는 특정 타입에 이름을 부여하므로, 바로 그 타입이 요구되거나 생성되는 위치를 정확하게 지정할 수 있습니다. 예를 들어, 다음 함수는

    #![allow(unused)]
    fn main() {
    fn binary<T: Trait>(x: T, y: T) -> T
    }

    정확히 같은 타입 T의 요소를 소비하고 생성함이 보장됩니다. 즉, 둘 다 Trait를 구현하는 서로 다른 타입의 매개변수로는 이 함수를 호출할 수 없습니다.

제네릭의 단점

  • 코드 크기. 제네릭 함수를 특수화한다는 것은 함수 본문이 복제된다는 것을 의미합니다. 코드 크기의 증가와 정적 디스패치의 성능상 이점을 저울질해야 합니다.

  • 동종 타입. 이는 “정확한 타입“이라는 동전의 반대쪽 면입니다. T가 타입 매개변수라면, 이는 하나의 실제 타입을 나타냅니다. 따라서 예를 들어 Vec<T>는 하나의 구체적인 타입의 요소만 포함합니다(실제로 벡터 표현은 이 요소들을 인라인으로 배치하도록 특수화됩니다). 때로는 이종 컬렉션이 유용합니다. 트레이트 객체를 참고하십시오.

  • 시그니처의 장황함. 제네릭을 많이 사용하면 함수 시그니처를 읽고 이해하기가 더 어려워질 수 있습니다.

표준 라이브러리의 예

  • std::fs::File::open은 제네릭 타입 AsRef<Path>의 인수를 받습니다. 이 덕분에 문자열 리터럴 "f.txt", Path, OsString 및 몇 가지 다른 타입으로부터 파일을 편리하게 열 수 있습니다.

트레이트 객체로 유용할 수 있는 트레이트는 객체 안전합니다 (C-OBJECT)

트레이트 객체에는 몇 가지 중요한 제약이 있습니다. 트레이트 객체를 통해 호출되는 메서드는 제네릭을 사용할 수 없으며, 리시버 위치 외에는 Self를 사용할 수 없습니다.

트레이트를 설계할 때는 해당 트레이트를 객체로 사용할지, 제네릭의 바운드로 사용할지를 일찍 결정하십시오.

트레이트를 객체로 사용하려는 경우, 그 메서드는 제네릭을 사용하기보다 트레이트 객체를 받고 반환해야 합니다.

Self: Sized의 where 절을 사용하면 특정 메서드를 트레이트의 객체에서 제외할 수 있습니다. 다음 트레이트는 제네릭 메서드 때문에 객체 안전하지 않습니다.

#![allow(unused)]
fn main() {
trait MyTrait {
    fn object_safe(&self, i: i32);

    fn not_object_safe<T>(&self, t: T);
}
}

제네릭 메서드에 Self: Sized 요구 사항을 추가하면 해당 메서드가 트레이트 객체에서 제외되어 트레이트가 객체 안전해집니다.

#![allow(unused)]
fn main() {
trait MyTrait {
    fn object_safe(&self, i: i32);

    fn not_object_safe<T>(&self, t: T) where Self: Sized;
}
}

트레이트 객체의 장점

  • 이종성. 이것이 필요할 때는 정말로 필요합니다.
  • 코드 크기. 제네릭과 달리 트레이트 객체는 특수화된(단형화된) 버전의 코드를 생성하지 않으므로 코드 크기를 크게 줄일 수 있습니다.

트레이트 객체의 단점

  • 제네릭 메서드 없음. 트레이트 객체는 현재 제네릭 메서드를 제공할 수 없습니다.
  • 동적 디스패치와 팻 포인터. 트레이트 객체는 본질적으로 간접 참조와 vtable 디스패치를 수반하므로 성능 저하가 발생할 수 있습니다.
  • Self 없음. 메서드 리시버 인수를 제외하면, 트레이트 객체의 메서드는 Self 타입을 사용할 수 없습니다.

표준 라이브러리의 예

  • io::Read 트레이트와 io::Write 트레이트는 객체로 자주 사용됩니다.
  • Iterator 트레이트에는 Iterator를 객체로 사용할 수 있도록 where Self: Sized가 표시된 제네릭 메서드가 여럿 있습니다.

타입 안전성

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에 의해 소비될 때까지 각 빌더 메서드를 거쳐 전달되기 때문입니다. 그러나 복잡한 설정은 더 장황해집니다. 각 단계마다 빌더를 다시 할당해야 하기 때문입니다.

신뢰성

함수는 인수를 검증합니다 (C-VALIDATE)

Rust API는 일반적으로 “보낼 때는 보수적으로, 받을 때는 관대하게“라는 견고성 원칙을 따르지 않습니다.

대신 Rust 코드는 실용적으로 가능한 경우 언제나 입력의 유효성을 강제해야 합니다.

강제는 다음과 같은 메커니즘을 통해 달성할 수 있습니다(선호하는 순서대로 나열합니다).

정적 강제

잘못된 입력을 배제하는 인수 타입을 선택하십시오.

예를 들어, 다음을

#![allow(unused)]
fn main() {
fn foo(a: Ascii) { /* ... */ }
}

아래보다 선호하십시오.

#![allow(unused)]
fn main() {
fn foo(a: u8) { /* ... */ }
}

여기서 Ascii는 최상위 비트가 0임을 보장하는 u8의 래퍼입니다. 타입 안전한 래퍼를 만드는 방법에 대한 자세한 내용은 newtype 패턴(C-NEWTYPE)을 참고하십시오.

정적 강제는 보통 런타임 비용이 거의 들지 않습니다. 비용을 경계 지점으로 밀어내기 때문입니다(예: u8이 처음 Ascii로 변환될 때). 또한 런타임 실패를 통해서가 아니라 컴파일 중에 버그를 일찍 잡아냅니다.

반면에 어떤 속성은 타입으로 표현하기 어렵거나 불가능합니다.

동적 강제

입력을 처리하면서(또는 필요하다면 미리) 입력을 검증하십시오. 동적 검사는 정적 검사보다 구현하기 쉬운 경우가 많지만, 몇 가지 단점이 있습니다:

  1. 런타임 오버헤드(입력 처리의 일부로 검사를 수행할 수 있는 경우는 제외합니다).
  2. 버그 발견이 늦어집니다.
  3. panic! 또는 Result/Option 타입을 통해 실패 사례가 생기며, 클라이언트 코드는 이를 처리해야 합니다.

debug_assert!를 사용한 동적 강제

동적 강제와 같지만, 프로덕션 빌드에서 비용이 큰 검사를 쉽게 끌 수 있습니다.

옵트아웃이 가능한 동적 강제

동적 강제와 같지만, 검사를 생략하는 형제 함수를 추가합니다.

관례적으로 이러한 옵트아웃 함수에는 _unchecked 같은 접미사를 붙이거나 raw 하위 모듈에 둡니다.

검사하지 않는 함수는 (1) 성능상 검사를 피해야 하고 (2) 클라이언트가 다른 방법으로 입력이 유효하다고 확신할 수 있는 경우에 신중하게 사용할 수 있습니다.

소멸자는 절대 실패하지 않습니다 (C-DTOR-FAIL)

소멸자는 패닉 중에도 실행되며, 그 상황에서 소멸자가 실패하면 프로그램이 중단(abort)됩니다.

소멸자에서 실패하는 대신, 깔끔한 정리 여부를 확인하는 별도의 메서드(예: close 메서드)를 제공하고 이 메서드가 문제를 알리는 Result를 반환하도록 하십시오. 그 close 메서드가 호출되지 않으면 Drop 구현이 정리를 수행해야 하며, 이때 발생하는 오류는 무시하거나 로그/트레이스로 남겨야 합니다.

블로킹될 수 있는 소멸자에는 대안이 있습니다 (C-DTOR-BLOCK)

마찬가지로 소멸자는 블로킹 작업을 호출해서는 안 됩니다. 블로킹 작업은 디버깅을 훨씬 더 어렵게 만들 수 있습니다. 이번에도 실패하지 않고 블로킹되지 않는 해체를 준비하는 별도의 메서드를 제공하는 것을 고려하십시오.

디버깅 용이성

모든 공개 타입은 Debug를 구현합니다 (C-DEBUG)

예외가 있더라도 드뭅니다.

Debug 표현은 결코 비어 있지 않습니다 (C-DEBUG-NONEMPTY)

개념적으로 비어 있는 값이라도 Debug 표현은 결코 비어 있어서는 안 됩니다.

#![allow(unused)]
fn main() {
let empty_str = "";
assert_eq!(format!("{:?}", empty_str), "\"\"");

let empty_vec = Vec::<bool>::new();
assert_eq!(format!("{:?}", empty_vec), "[]");
}

미래 대비

봉인된 트레이트는 다운스트림 구현으로부터 보호합니다 (C-SEALED)

어떤 트레이트는 그것을 정의한 크레이트 안에서만 구현되도록 의도됩니다. 이런 경우 봉인된 트레이트 패턴을 사용하면 호환성을 깨지 않고 트레이트를 변경할 수 있는 여지를 유지할 수 있습니다.

#![allow(unused)]
fn main() {
/// This trait is sealed and cannot be implemented for types outside this crate.
pub trait TheTrait: private::Sealed {
    // Zero or more methods that the user is allowed to call.
    fn ...();

    // Zero or more private methods, not allowed for user to call.
    #[doc(hidden)]
    fn ...();
}

// Implement for some types.
impl TheTrait for usize {
    /* ... */
}

mod private {
    pub trait Sealed {}

    // Implement for those same types, but no others.
    impl Sealed for usize {}
}
}

비어 있는 비공개 Sealed 슈퍼트레이트는 다운스트림 크레이트에서 이름으로 참조할 수 없으므로, Sealed(따라서 TheTrait)의 구현은 현재 크레이트 안에만 존재함이 보장됩니다. 봉인되지 않은 트레이트라면 일반적으로 호환성을 깨는 변경이 되겠지만, TheTrait에는 호환성을 깨지 않는 릴리스에서도 자유롭게 메서드를 추가할 수 있습니다. 또한 공개적으로 문서화되지 않은 메서드의 시그니처도 자유롭게 변경할 수 있습니다.

다만 봉인된 트레이트에서 공개 메서드를 제거하거나 공개 메서드의 시그니처를 변경하는 것은 여전히 호환성을 깨는 변경이라는 점에 유의하십시오.

사용자가 트레이트를 구현하려다 좌절하는 일이 없도록, 해당 트레이트가 봉인되어 있으며 현재 크레이트 밖에서 구현하도록 의도된 것이 아니라는 점을 rustdoc에 문서화해야 합니다.

예시

구조체는 비공개 필드를 가집니다 (C-STRUCT-PRIVATE)

필드를 공개하는 것은 강한 약속입니다. 이는 표현 방식의 선택을 고정하며, 또한 클라이언트가 필드를 임의로 변경할 수 있으므로 타입이 필드 내용에 대해 어떠한 검증도 제공하거나 불변 조건을 유지할 수 없게 만듭니다.

공개 필드는 C의 정신을 따르는 struct 타입, 즉 복합적이고 수동적인 데이터 구조에 가장 적합합니다. 그렇지 않은 경우에는 필드를 숨기고 대신 게터/세터 메서드를 제공하는 것을 고려하십시오.

newtype은 구현 세부 사항을 캡슐화합니다 (C-NEWTYPE-HIDE)

newtype을 사용하면 클라이언트에게 정확한 약속을 하면서도 표현의 세부 사항을 숨길 수 있습니다.

예를 들어, 복합 이터레이터 타입을 반환하는 함수 my_transform을 생각해 봅시다.

#![allow(unused)]
fn main() {
use std::iter::{Enumerate, Skip};

pub fn my_transform<I: Iterator>(input: I) -> Enumerate<Skip<I>> {
    input.skip(3).enumerate()
}
}

클라이언트에게는 이 타입을 숨겨서, 클라이언트가 보는 반환 타입이 대략 Iterator<Item = (usize, T)>가 되도록 하고 싶습니다. newtype 패턴을 사용하면 이렇게 할 수 있습니다.

#![allow(unused)]
fn main() {
use std::iter::{Enumerate, Skip};

pub struct MyTransformResult<I>(Enumerate<Skip<I>>);

impl<I: Iterator> Iterator for MyTransformResult<I> {
    type Item = (usize, I::Item);

    fn next(&mut self) -> Option<Self::Item> {
        self.0.next()
    }
}

pub fn my_transform<I: Iterator>(input: I) -> MyTransformResult<I> {
    MyTransformResult(input.skip(3).enumerate())
}
}

이렇게 newtype을 사용하면 시그니처가 단순해질 뿐 아니라 클라이언트에게 더 적게 약속할 수 있습니다. 클라이언트는 결과 이터레이터가 어떻게 구성되거나 표현되는지 알지 못하므로, 앞으로 클라이언트 코드를 깨뜨리지 않고 표현을 변경할 수 있습니다.

Rust 1.26에서는 impl Trait 기능도 도입되었는데, 이는 newtype 패턴보다 간결하지만 몇 가지 추가적인 트레이드오프가 있습니다. 즉, impl Trait를 사용하면 표현할 수 있는 것이 제한됩니다. 예를 들어, Debug나 Clone, 또는 다른 이터레이터 확장 트레이트의 조합을 구현하는 이터레이터를 반환하는 것은 문제가 될 수 있습니다. 요약하면, 반환 타입으로서의 impl Trait는 내부 API에는 아주 적합할 것이고 공개 API에도 적절할 수 있지만, 모든 경우에 그렇지는 않을 것입니다. 자세한 내용은 Edition Guide의 “impl Trait로 복잡한 타입을 손쉽게 반환하기” 절을 참고하십시오.

#![allow(unused)]
fn main() {
pub fn my_transform<I: Iterator>(input: I) -> impl Iterator<Item = (usize, I::Item)> {
    input.skip(3).enumerate()
}
}

데이터 구조체는 파생된 트레이트 바운드를 중복하지 않습니다 (C-STRUCT-BOUNDS)

제네릭 데이터 구조는 파생될 수 있거나 그 밖에 의미상의 가치를 더하지 않는 트레이트 경계를 사용해서는 안 됩니다. derive 속성에 있는 각 트레이트는 별도의 impl 블록으로 확장되며, 이 블록은 해당 트레이트를 구현하는 제네릭 인수에만 적용됩니다.

#![allow(unused)]
fn main() {
// Prefer this:
#[derive(Clone, Debug, PartialEq)]
struct Good<T> { /* ... */ }

// Over this:
#[derive(Clone, Debug, PartialEq)]
struct Bad<T: Clone + Debug + PartialEq> { /* ... */ }
}

파생된 트레이트를 Bad의 경계로 중복해서 지정하는 것은 불필요하며 하위 호환성을 위협합니다. 이를 설명하기 위해, 앞의 예제에 나온 구조체에 PartialOrd를 파생하는 경우를 생각해 봅시다.

#![allow(unused)]
fn main() {
// Non-breaking change:
#[derive(Clone, Debug, PartialEq, PartialOrd)]
struct Good<T> { /* ... */ }

// Breaking change:
#[derive(Clone, Debug, PartialEq, PartialOrd)]
struct Bad<T: Clone + Debug + PartialEq + PartialOrd> { /* ... */ }
}

일반적으로 데이터 구조에 트레이트 경계를 추가하는 것은 호환성을 깨는 변경입니다. 해당 구조체를 사용하는 모든 쪽에서 추가된 경계를 새로 충족해야 하기 때문입니다. derive 속성을 사용해 표준 라이브러리의 트레이트를 더 파생하는 것은 호환성을 깨는 변경이 아닙니다.

다음 트레이트는 데이터 구조의 경계에 절대 사용해서는 안 됩니다.

  • Clone
  • PartialEq
  • PartialOrd
  • Debug
  • Display
  • Default
  • Error
  • Serialize
  • Deserialize
  • DeserializeOwned

Read나 Write처럼 파생할 수 없으면서 구조체 정의에 반드시 필요하지는 않은 그 밖의 트레이트 경계는 애매한 영역에 있습니다. 이러한 경계는 타입의 정의에서 의도한 동작을 더 잘 드러낼 수 있지만, 향후 확장성을 제한하기도 합니다. 그래도 데이터 구조에 의미상 유용한 트레이트 경계를 포함하는 것은 파생 가능한 트레이트를 경계로 포함하는 것보다는 문제가 덜합니다.

예외

구조체에 트레이트 경계가 필요한 예외는 세 가지입니다.

  1. 데이터 구조가 트레이트의 연관 타입을 참조하는 경우입니다.
  2. 경계가 ?Sized인 경우입니다.
  3. 데이터 구조에 트레이트 경계가 필요한 Drop impl이 있는 경우입니다. 현재 Rust에서는 Drop impl에 있는 모든 트레이트 경계가 데이터 구조에도 있어야 합니다.

표준 라이브러리의 예

필수 사항

안정 크레이트의 공개 의존성은 안정적입니다 (C-STABLE)

크레이트의 모든 공개 의존성이 안정적이지 않으면 그 크레이트는 안정(>=1.0.0) 상태가 될 수 없습니다.

공개 의존성이란 현재 크레이트의 공개 API에서 그 타입이 사용되는 크레이트를 말합니다.

#![allow(unused)]
fn main() {
pub fn do_my_thing(arg: other_crate::TheirThing) { /* ... */ }
}

이 함수를 포함하는 크레이트는 other_crate도 안정적이지 않으면 안정 상태가 될 수 없습니다.

공개 의존성은 예상치 못한 곳에 슬며시 끼어들 수 있으므로 주의하십시오.

#![allow(unused)]
fn main() {
pub struct Error {
    private: ErrorImpl,
}

enum ErrorImpl {
    Io(io::Error),
    // Should be okay even if other_crate isn't
    // stable, because ErrorImpl is private.
    Dep(other_crate::Error),
}

// Oh no! This puts other_crate into the public API
// of the current crate.
impl From<other_crate::Error> for Error {
    fn from(err: other_crate::Error) -> Self {
        Error { private: ErrorImpl::Dep(err) }
    }
}
}

크레이트와 그 의존성은 허용적 라이선스를 가집니다 (C-PERMISSIVE)

Rust 프로젝트에서 만든 소프트웨어는 MIT 또는 Apache 2.0 라이선스 중 하나를 선택할 수 있는 이중 라이선스로 배포됩니다. Rust 생태계와 최대한의 호환성만 필요한 크레이트는 여기에 설명한 방식으로 똑같이 하는 것을 권장합니다. 다른 선택지는 아래에서 설명합니다.

이 API 가이드라인은 Rust 라이선스를 자세히 설명하지 않지만, Rust FAQ에 이에 관한 내용이 조금 있습니다. 이 가이드라인은 Rust와의 상호 운용성 문제를 다루며, 라이선스 선택지를 포괄적으로 다루지는 않습니다.

프로젝트에 Rust 라이선스를 적용하려면 Cargo.toml의 license 필드를 다음과 같이 정의하십시오.

[package]
name = "..."
version = "..."
authors = ["..."]
license = "MIT OR Apache-2.0"

그런 다음 저장소 루트에 라이선스 본문을 담은 LICENSE-APACHE 파일과 LICENSE-MIT 파일을 추가하십시오(라이선스 본문은 예를 들어 choosealicense.com에서 Apache-2.0과 MIT용으로 구할 수 있습니다).

그리고 README.md 끝부분에는 다음 내용을 추가하십시오.

## License

Licensed under either of

 * Apache License, Version 2.0
   ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
 * MIT license
   ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)

at your option.

## Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.

MIT/Apache-2.0 이중 라이선스 외에 Rust 크레이트 작성자가 흔히 사용하는 또 다른 라이선스 방식은 MIT나 BSD 같은 단일 허용적 라이선스를 적용하는 것입니다. 이 라이선스 방식은 Rust의 MIT 라이선스가 부과하는 최소한의 제약만 부과하므로 Rust의 라이선스와도 완전히 호환됩니다.

Rust와 완벽한 라이선스 호환성을 원하는 크레이트는 Apache 라이선스만 선택하는 것을 권장하지 않습니다. Apache 라이선스는 허용적 라이선스이기는 하지만 MIT 및 BSD 라이선스보다 더 많은 제약을 부과하므로 일부 상황에서는 사용을 꺼리게 하거나 사용할 수 없게 만들 수 있습니다. 따라서 Rust 런타임 스택 대부분은 사용할 수 있는 일부 상황에서도 Apache 단독 라이선스 소프트웨어는 사용할 수 없습니다.

크레이트 의존성의 라이선스는 크레이트 자체의 배포 제약에 영향을 줄 수 있으므로, 허용적 라이선스를 가진 크레이트는 일반적으로 허용적 라이선스를 가진 크레이트에만 의존해야 합니다.

외부 링크