Zaawansowane Traity
Po raz pierwszy omówiliśmy traity w sekcji „Definiowanie wspólnego zachowania za pomocą traitów” w Rozdziale 10, ale nie omówiliśmy bardziej zaawansowanych szczegółów. Teraz, gdy wiesz więcej o Rust, możemy przejść do sedna.
Definiowanie Traitów z Typami Stowarzyszonymi
Typy stowarzyszone łączą typ zastępczy z traitem w taki sposób, że definicje metod traitów mogą używać tych typów zastępczych w swoich sygnaturach. Implementator traitu określi konkretny typ, który ma być użyty zamiast typu zastępczego dla konkretnej implementacji. W ten sposób możemy zdefiniować trait, który używa pewnych typów, bez konieczności dokładnego poznania tych typów, dopóki trait nie zostanie zaimplementowany.
Większość zaawansowanych funkcji w tym rozdziale opisaliśmy jako rzadko potrzebne. Typy stowarzyszone znajdują się gdzieś pośrodku: są używane rzadziej niż funkcje wyjaśnione w pozostałej części książki, ale częściej niż wiele innych funkcji omówionych w tym rozdziale.
Jednym z przykładów traitu z typem stowarzyszonym jest trait Iterator, który udostępnia biblioteka standardowa. Typ stowarzyszony nazywa się Item i zastępuje typ wartości, po których iteruje typ implementujący trait Iterator. Definicja traitu Iterator jest pokazana na Liście 20-13.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Iterator, który ma stowarzyszony typ ItemTyp Item jest zastępczy, a definicja metody next pokazuje, że zwróci ona wartości typu Option<Self::Item>. Implementatorzy traitu Iterator określą konkretny typ dla Item, a metoda next zwróci Option zawierający wartość tego konkretnego typu.
Typy stowarzyszone mogą wydawać się podobną koncepcją do generyków, w tym sensie, że te ostatnie pozwalają nam zdefiniować funkcję bez określania, jakie typy może obsługiwać. Aby zbadać różnicę między tymi dwoma koncepcjami, przyjrzymy się implementacji traitu Iterator na typie o nazwie Counter, który określa, że typ Item to u32:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
Ta składnia wydaje się porównywalna do składni generyków. Więc dlaczego nie zdefiniować cechy Iterator za pomocą generyków, jak pokazano na Liście 20-14?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
Iterator używającego generykówRóżnica polega na tym, że używając generyków, jak na Liście 20-14, musimy adnotować typy w każdej implementacji; ponieważ możemy również zaimplementować Iterator<String> for Counter lub dowolny inny typ, moglibyśmy mieć wiele implementacji Iterator dla Counter. Innymi słowy, gdy trait ma parametr generyczny, może być implementowany dla typu wiele razy, zmieniając konkretne typy generycznych parametrów typu za każdym razem. Kiedy używamy metody next na Counter, musielibyśmy podać adnotacje typów, aby wskazać, którą implementację Iterator chcemy użyć.
Z typami stowarzyszonymi nie musimy adnotować typów, ponieważ nie możemy zaimplementować traitu na typie wiele razy. Na Liście 20-13 z definicją używającą typów stowarzyszonych, możemy wybrać typ Item tylko raz, ponieważ może istnieć tylko jedna impl Iterator for Counter. Nie musimy określać, że chcemy iteratora wartości u32 wszędzie tam, gdzie wywołujemy next na Counter.
Typy stowarzyszone stają się również częścią kontraktu traitu: Implementatorzy traitu muszą dostarczyć typ, który zastąpi symbol zastępczy typu stowarzyszonego. Typy stowarzyszone często mają nazwę opisującą sposób użycia typu, a dokumentowanie typu stowarzyszonego w dokumentacji API jest dobrą praktyką.
Używanie Domyślnych Parametrów Typów Generycznych i Przeciążanie Operatorów
Kiedy używamy generycznych parametrów typu, możemy określić domyślny konkretny typ dla typu generycznego. Eliminuje to potrzebę określania konkretnego typu przez implementatorów cechy, jeśli domyślny typ działa. Domyślny typ określa się podczas deklarowania typu generycznego za pomocą składni <PlaceholderType=ConcreteType>.
Świetnym przykładem sytuacji, w której ta technika jest przydatna, jest przeciążanie operatorów, w którym dostosowujesz zachowanie operatora (takiego jak +) w określonych sytuacjach.
Rust nie pozwala na tworzenie własnych operatorów ani na przeciążanie dowolnych operatorów. Możesz jednak przeciążać operacje i odpowiadające im traity wymienione w std::ops, implementując traity powiązane z operatorem. Na przykład, na Liście 20-15 przeciążamy operator +, aby dodawać dwie instancje Point. Robimy to, implementując trait Add na strukturze Point.
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Add w celu przeciążenia operatora + dla instancji PointMetoda add dodaje wartości x dwóch instancji Point i wartości y dwóch instancji Point, aby stworzyć nową Point. Trait Add ma stowarzyszony typ o nazwie Output, który określa typ zwracany przez metodę add.
Domyślny typ generyczny w tym kodzie znajduje się wewnątrz traitu Add. Oto jego definicja:
#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
}
Ten kod powinien być ogólnie znany: trait z jedną metodą i stowarzyszonym typem. Nowa część to Rhs=Self: ta składnia nazywa się domyślnymi parametrami typu. Generyczny parametr typu Rhs (skrót od „right-hand side” – prawa strona) definiuje typ parametru rhs w metodzie add. Jeśli nie określimy konkretnego typu dla Rhs podczas implementacji traitu Add, typ Rhs domyślnie przyjmie Self, czyli typ, na którym implementujemy Add.
Kiedy implementowaliśmy Add dla Point, użyliśmy domyślnego Rhs, ponieważ chcieliśmy dodać dwie instancje Point. Przyjrzyjmy się przykładowi implementacji traitu Add, gdzie chcemy dostosować typ Rhs, zamiast używać wartości domyślnej.
Mamy dwie struktury, Millimeters i Meters, przechowujące wartości w różnych jednostkach. To cienkie opakowanie istniejącego typu w inną strukturę jest znane jako wzorzec newtype, który szczegółowo opisujemy w sekcji „Implementowanie zewnętrznych traitów za pomocą wzorca newtype”. Chcemy dodawać wartości w milimetrach do wartości w metrach i chcemy, aby implementacja Add poprawnie wykonywała konwersję. Możemy zaimplementować Add dla Millimeters z Meters jako Rhs, jak pokazano na Liście 20-16.
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Add na Millimeters w celu dodawania Millimeters i MetersAby dodać Millimeters i Meters, określamy impl Add<Meters>, aby ustawić wartość parametru typu Rhs zamiast używać domyślnego Self.
Parametry typu domyślnego będziesz używać na dwa główne sposoby:
- Aby rozszerzyć typ bez naruszania istniejącego kodu
- Aby umożliwić dostosowanie w konkretnych przypadkach, których większość użytkowników nie będzie potrzebować
Trait Add z biblioteki standardowej jest przykładem drugiego celu: zazwyczaj dodajesz dwa podobne typy, ale trait Add zapewnia możliwość dostosowania wykraczającego poza to. Użycie domyślnego parametru typu w definicji traitu Add oznacza, że nie musisz określać dodatkowego parametru przez większość czasu. Innymi słowy, nie jest potrzebna pewna ilość kodu boilerplate, co ułatwia używanie traitu.
Pierwszy cel jest podobny do drugiego, ale odwrotnie: jeśli chcesz dodać parametr typu do istniejącego traitu, możesz nadać mu wartość domyślną, aby umożliwić rozszerzenie funkcjonalności traitu bez naruszania istniejącego kodu implementacji.
Rozróżnianie Metod o Identycznych Nazwach
Nic w Rust nie zapobiega temu, aby trait miał metodę o tej samej nazwie co metoda innego traitu, ani Rust nie zapobiega implementowaniu obu traitów na jednym typie. Możliwe jest również zaimplementowanie metody bezpośrednio na typie z tą samą nazwą co metody z traitów.
Podczas wywoływania metod o tej samej nazwie, będziesz musiał powiedzieć Rust, której chcesz użyć. Rozważ kod na Liście 20-17, gdzie zdefiniowaliśmy dwa traity, Pilot i Wizard, które oba mają metodę fly. Następnie implementujemy oba traity na typie Human, który już ma zaimplementowaną metodę fly. Każda metoda fly robi coś innego.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {}
fly i zaimplementowane na typie Human, oraz metoda fly zaimplementowana bezpośrednio na Human.Kiedy wywołujemy fly na instancji Human, kompilator domyślnie wywołuje metodę, która jest bezpośrednio zaimplementowana na typie, jak pokazano na Liście 20-18.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
person.fly();
}
fly na instancji HumanUruchomienie tego kodu wydrukuje *waving arms furiously*, pokazując, że Rust wywołał metodę fly zaimplementowaną bezpośrednio na Human.
Aby wywołać metody fly z traitu Pilot lub traitu Wizard, musimy użyć bardziej jawnej składni, aby określić, którą metodę fly mamy na myśli. Lista 20-19 demonstruje tę składnię.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
fly z traitu chcemy wywołaćOkreślenie nazwy traitu przed nazwą metody wyjaśnia Rust, którą implementację fly chcemy wywołać. Moglibyśmy również napisać Human::fly(&person), co jest równoważne person.fly(), którego użyliśmy na Liście 20-19, ale jest to nieco dłuższe do napisania, jeśli nie musimy rozróżniać.
Uruchomienie tego kodu wypisuje następujące informacje:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
Ponieważ metoda fly przyjmuje parametr self, gdybyśmy mieli dwa typy, które oba implementują jeden trait, Rust mógłby ustalić, którą implementację traitu użyć na podstawie typu self.
Jednak funkcje stowarzyszone, które nie są metodami, nie mają parametru self. Gdy istnieje wiele typów lub cech, które definiują funkcje niestowarzyszone z tą samą nazwą funkcji, Rust nie zawsze wie, o który typ chodzi, chyba że użyjesz w pełni kwalifikowanej składni. Na przykład na Liście 20-20 tworzymy cechę dla schroniska dla zwierząt, które chce nazwać wszystkie szczenięta Spot. Tworzymy cechę Animal z powiązaną funkcją niestowarzyszoną baby_name. Cecha Animal jest zaimplementowana dla struktury Dog, dla której również udostępniamy bezpośrednio powiązaną funkcję niestowarzyszoną baby_name.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
Implementujemy kod do nazywania wszystkich szczeniąt Spot w funkcji stowarzyszonej baby_name, która jest zdefiniowana na Dog. Typ Dog implementuje również trait Animal, który opisuje cechy, które mają wszystkie zwierzęta. Młode psy nazywane są szczeniętami, co jest wyrażone w implementacji traitu Animal na Dog w funkcji baby_name stowarzyszonej z traitem Animal.
W main wywołujemy funkcję Dog::baby_name, która wywołuje bezpośrednio funkcję stowarzyszoną zdefiniowaną na Dog. Ten kod wypisuje następujące informacje:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
Ten wynik nie jest tym, czego chcieliśmy. Chcemy wywołać funkcję baby_name, która jest częścią cechy Animal, którą zaimplementowaliśmy na Dog, tak aby kod wypisywał A baby dog is called a puppy. Technika określania nazwy cechy, której użyliśmy na Liście 20-19, nie pomaga tutaj; jeśli zmienimy main na kod z Listy 20-21, otrzymamy błąd kompilacji.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
baby_name z traitu Animal, ale Rust nie wie, której implementacji użyćPonieważ Animal::baby_name nie ma parametru self, a mogą istnieć inne typy, które implementują cechę Animal, Rust nie może ustalić, którą implementację Animal::baby_name chcemy. Otrzymamy ten błąd kompilacji:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
Aby rozróżnić i powiedzieć Rust, że chcemy użyć implementacji Animal dla Dog w przeciwieństwie do implementacji Animal dla jakiegoś innego typu, musimy użyć w pełni kwalifikowanej składni. Lista 20-22 demonstruje, jak używać w pełni kwalifikowanej składni.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
baby_name z traitu Animal zaimplementowanego na DogDostarczamy Rust adnotację typu w nawiasach ostrych, która wskazuje, że chcemy wywołać metodę baby_name z cechy Animal zaimplementowanej na Dog, mówiąc, że chcemy traktować typ Dog jako Animal dla tego wywołania funkcji. Ten kod wydrukuje teraz to, czego chcemy:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
Ogólnie, w pełni kwalifikowana składnia jest zdefiniowana następująco:
<Typ jako Trait>::funkcja(odbiorca_jeśli_metoda, następny_argument, ...);
Dla funkcji stowarzyszonych, które nie są metodami, nie byłoby odbiorcy: byłaby tylko lista innych argumentów. Można by używać w pełni kwalifikowanej składni wszędzie tam, gdzie wywołuje się funkcje lub metody. Jednakże, można pominąć dowolną część tej składni, którą Rust może ustalić na podstawie innych informacji w programie. Tę bardziej rozbudowaną składnię trzeba używać tylko w przypadkach, gdy istnieje wiele implementacji, które używają tej samej nazwy, a Rust potrzebuje pomocy w zidentyfikowaniu, którą implementację chcesz wywołać.
Używanie Supertraitów
Czasami możesz napisać definicję traitu, która zależy od innego traitu: Aby typ implementował pierwszy trait, chcesz wymagać, aby ten typ również implementował drugi trait. Robisz to po to, aby Twoja definicja traitu mogła korzystać ze stowarzyszonych elementów drugiego traitu. Trait, na którym opiera się Twoja definicja traitu, nazywany jest supertraitem Twojego traitu.
Na przykład, powiedzmy, że chcemy stworzyć cechę OutlinePrint z metodą outline_print, która będzie drukować podaną wartość sformatowaną tak, aby była obramowana gwiazdkami. Oznacza to, że biorąc pod uwagę strukturę Point, która implementuje standardową cechę Display, aby uzyskać (x, y), gdy wywołamy outline_print na instancji Point, która ma 1 dla x i 3 dla y, powinna ona wydrukować następujące:
**********
* *
* (1, 3) *
* *
**********
W implementacji metody outline_print chcemy użyć funkcjonalności cechy Display. Dlatego musimy określić, że cecha OutlinePrint będzie działać tylko dla typów, które również implementują Display i dostarczają funkcjonalność, której potrzebuje OutlinePrint. Możemy to zrobić w definicji cechy, określając OutlinePrint: Display. Ta technika jest podobna do dodawania ograniczenia cechy do cechy. Lista 20-23 pokazuje implementację cechy OutlinePrint.
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
fn main() {}
OutlinePrint, który wymaga funkcjonalności z DisplayPonieważ określiliśmy, że OutlinePrint wymaga traitu Display, możemy użyć funkcji to_string, która jest automatycznie implementowana dla każdego typu, który implementuje Display. Gdybyśmy spróbowali użyć to_string bez dodania dwukropka i określenia traitu Display po nazwie traitu, otrzymalibyśmy błąd mówiący, że w bieżącym zakresie nie znaleziono metody o nazwie to_string dla typu &Self.
Zobaczmy, co się stanie, gdy spróbujemy zaimplementować OutlinePrint na typie, który nie implementuje Display, takim jak struktura Point:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
Otrzymujemy błąd mówiący, że Display jest wymagane, ale nie zaimplementowane:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
Aby to naprawić, implementujemy Display na Point i spełniamy ograniczenie, którego wymaga OutlinePrint, w ten sposób:
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
Wtedy implementacja cechy OutlinePrint na Point skompiluje się pomyślnie, i będziemy mogli wywołać outline_print na instancji Point, aby wyświetlić ją w obramowaniu z gwiazdek.
Implementowanie Zewnętrznych Traitów za pomocą Wzorca Newtype
W sekcji „Implementowanie traitu na typie” w Rozdziale 10 wspomnieliśmy o regule sieroty, która stanowi, że możemy implementować trait na typie tylko wtedy, gdy albo trait, albo typ, albo oba, są lokalne dla naszego crate’u. Możliwe jest obejście tego ograniczenia za pomocą wzorca newtype, który polega na utworzeniu nowego typu w strukturze krotkowej. (Struktury krotkowe omówiliśmy w sekcji „Tworzenie różnych typów za pomocą struktur krotkowych” w Rozdziale 5.) Struktura krotkowa będzie miała jedno pole i będzie cienkim opakowaniem wokół typu, dla którego chcemy zaimplementować trait. Wtedy typ opakowujący jest lokalny dla naszego crate’u, i możemy zaimplementować trait na opakowaniu. Newtype to termin, który pochodzi z języka programowania Haskell. Nie ma kary za wydajność w czasie wykonania za użycie tego wzorca, a typ opakowujący jest pomijany w czasie kompilacji.
Na przykład, załóżmy, że chcemy zaimplementować Display na Vec<T>, czego reguła sieroty uniemożliwia nam bezpośrednio, ponieważ trait Display i typ Vec<T> są zdefiniowane poza naszym crate. Możemy stworzyć strukturę Wrapper, która przechowuje instancję Vec<T>; następnie możemy zaimplementować Display na Wrapper i użyć wartości Vec<T>, jak pokazano na Liście 20-24.
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Wrapper wokół Vec<String> w celu implementacji DisplayImplementacja Display używa self.0 do dostępu do wewnętrznego Vec<T>, ponieważ Wrapper jest strukturą krotkową, a Vec<T> jest elementem o indeksie 0 w krotce. Następnie możemy użyć funkcjonalności cechy Display na Wrapper.
Wadą stosowania tej techniki jest to, że Wrapper jest nowym typem, więc nie ma metod wartości, którą przechowuje. Musielibyśmy zaimplementować wszystkie metody Vec<T> bezpośrednio na Wrapper, tak aby metody delegowały do self.0, co pozwoliłoby nam traktować Wrapper dokładnie tak jak Vec<T>. Gdybyśmy chcieli, aby nowy typ miał każdą metodę, którą ma typ wewnętrzny, zaimplementowanie cechy Deref na Wrapper, aby zwracała typ wewnętrzny, byłoby rozwiązaniem (omówiliśmy implementację cechy Deref w sekcji „Traktowanie inteligentnych wskaźników jak zwykłych referencji” w Rozdziale 15). Gdybyśmy nie chcieli, aby typ Wrapper miał wszystkie metody typu wewnętrznego — na przykład, aby ograniczyć zachowanie typu Wrapper — musielibyśmy ręcznie zaimplementować tylko te metody, które chcemy.
Ten wzorzec newtype jest również użyteczny nawet wtedy, gdy traity nie są zaangażowane. Zmieńmy fokus i przyjrzyjmy się niektórym zaawansowanym sposobom interakcji z systemem typów Rust.