유연성
함수는 중복 작업을 피하기 위해 중간 결과를 노출합니다 (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타입을 사용할 수 없습니다.