Traktowanie inteligentnych wskaźników jak zwykłych referencji
Implementacja cechy Deref pozwala dostosować zachowanie operatora dereferencji * (nie mylić z operatorem mnożenia ani glob). Implementując Deref w taki sposób, że inteligentny wskaźnik może być traktowany jak zwykła referencja, możesz pisać kod, który działa na referencjach i używać tego kodu również z inteligentnymi wskaźnikami.
Najpierw przyjrzyjmy się, jak operator dereferencji działa ze zwykłymi referencjami. Następnie spróbujemy zdefiniować niestandardowy typ, który zachowuje się jak Box<T> i zobaczymy, dlaczego operator dereferencji nie działa jak referencja na naszym nowo zdefiniowanym typie. Zbadamy, jak implementacja cechy Deref umożliwia inteligentnym wskaźnikom działanie w sposób podobny do referencji. Następnie przyjrzymy się funkcji Rust deref coercion i temu, jak pozwala nam ona pracować zarówno z referencjami, jak i inteligentnymi wskaźnikami.
Podążanie za referencją do wartości za pomocą operatora dereferencji
Zwykła referencja jest typem wskaźnika, a jeden ze sposobów myślenia o wskaźniku to strzałka do wartości przechowywanej gdzie indziej. W Listing 15-6 tworzymy referencję do wartości i32, a następnie używamy operatora dereferencji, aby podążyć za referencją do wartości.
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
Zmienna x przechowuje wartość i32 równą 5. Ustawiamy y na referencję do x. Możemy potwierdzić, że x jest równe 5. Jednak jeśli chcemy sprawdzić wartość w y, musimy użyć *y, aby podążyć za referencją do wartości, na którą wskazuje (stąd dereferencja), tak aby kompilator mógł porównać rzeczywistą wartość. Po dereferencji y mamy dostęp do wartości całkowitej, na którą wskazuje y, którą możemy porównać z 5.
Gdybyśmy spróbowali napisać assert_eq!(5, y); zamiast tego, otrzymalibyśmy następujący błąd kompilacji:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: nie można porównać `{integer}` z `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ brak implementacji dla `{integer} == &{integer}`
|
= help: cecha `PartialEq<&{integer}>` nie jest zaimplementowana dla `{integer}`
= note: ten błąd pochodzi z makra `assert_eq` (w kompilacjach Nightly, uruchom z -Z macro-backtrace, aby uzyskać więcej informacji)
For more information about this error, try `rustc --explain E0277`.
error: nie udało się skompilować `deref-example` (bin "deref-example") z powodu 1 poprzedniego błędu
Porównywanie liczby i referencji do liczby jest niedozwolone, ponieważ są to różne typy. Musimy użyć operatora dereferencji, aby podążyć za referencją do wartości, na którą wskazuje.
Używanie Box<T> jak referencji
Możemy przepisać kod z Listing 15-6, aby używał Box<T> zamiast referencji; operator dereferencji użyty na Box<T> w Listing 15-7 działa tak samo, jak operator dereferencji użyty na referencji w Listing 15-6.
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Główna różnica między Listing 15-7 a Listing 15-6 polega na tym, że tutaj ustawiamy y jako instancję pudełka wskazującego na skopiowaną wartość x, a nie referencję wskazującą na wartość x. W ostatniej asercji możemy użyć operatora dereferencji, aby podążyć za wskaźnikiem pudełka w ten sam sposób, w jaki robiliśmy to, gdy y była referencją. Następnie zbadamy, co jest specjalnego w Box<T>, co pozwala nam używać operatora dereferencji, definiując nasz własny typ pudełka.
Definiowanie naszego własnego inteligentnego wskaźnika
Zbudujmy typ opakowujący podobny do typu Box<T> dostarczanego przez standardową bibliotekę, aby doświadczyć, jak typy inteligentnych wskaźników domyślnie zachowują się inaczej niż referencje. Następnie przyjrzymy się, jak dodać możliwość użycia operatora dereferencji.
Uwaga: Istnieje jedna duża różnica między typem
MyBox<T>, który zaraz zbudujemy, a prawdziwymBox<T>: nasza wersja nie będzie przechowywać swoich danych na stercie. W tym przykładzie skupiamy się naDeref, więc to, gdzie dane są faktycznie przechowywane, jest mniej ważne niż zachowanie podobne do wskaźnika.
Typ Box<T> jest ostatecznie zdefiniowany jako struktura tuplowa z jednym elementem, więc Listing 15-8 definiuje typ MyBox<T> w ten sam sposób. Zdefiniujemy również funkcję new pasującą do funkcji new zdefiniowanej w Box<T>.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
Definiujemy strukturę o nazwie MyBox i deklarujemy parametr generyczny T, ponieważ chcemy, aby nasz typ przechowywał wartości dowolnego typu. Typ MyBox jest strukturą tuplową z jednym elementem typu T. Funkcja MyBox::new przyjmuje jeden parametr typu T i zwraca instancję MyBox, która przechowuje przekazaną wartość.
Spróbujmy dodać funkcję main z Listing 15-7 do Listing 15-8 i zmienić ją tak, aby używała zdefiniowanego przez nas typu MyBox<T> zamiast Box<T>. Kod w Listing 15-9 nie skompiluje się, ponieważ Rust nie wie, jak dereferencyjnie traktować MyBox.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Oto wynikowy błąd kompilacji:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^ nie można dereferencyjnie traktować
For more information about this error, try `rustc --explain E0614`.
error: nie udało się skompilować `deref-example` (bin "deref-example") z powodu 1 poprzedniego błędu
Nasz typ MyBox<T> nie może zostać dereferencjonowany, ponieważ nie zaimplementowaliśmy tej możliwości dla naszego typu. Aby umożliwić dereferencjonowanie za pomocą operatora *, implementujemy cechę Deref.
Implementacja cechy Deref
Jak omówiono w sekcji „Implementacja cechy dla typu” w Rozdziale 10, aby zaimplementować cechę, musimy dostarczyć implementacje dla wymaganych metod cechy. Cecha Deref, dostarczana przez standardową bibliotekę, wymaga od nas zaimplementowania jednej metody o nazwie deref, która pożycza self i zwraca referencję do wewnętrznych danych. Listing 15-10 zawiera implementację Deref do dodania do definicji MyBox<T>.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Składnia type Target = T; definiuje typ skojarzony dla cechy Deref. Typy skojarzone to nieco inny sposób deklarowania parametru generycznego, ale na razie nie musisz się nimi martwić; omówimy je bardziej szczegółowo w Rozdziale 20.
Wypełniamy ciało metody deref za pomocą &self.0, tak aby deref zwracało referencję do wartości, do której chcemy uzyskać dostęp za pomocą operatora *; przypomnij sobie z sekcji „Tworzenie różnych typów za pomocą struktur tuplowych” w Rozdziale 5, że .0 uzyskuje dostęp do pierwszej wartości w strukturze tuplowej. Funkcja main w Listing 15-9, która wywołuje * na wartości MyBox<T>, teraz się kompiluje, a asercje przechodzą!
Bez cechy Deref kompilator może dereferencyjnie traktować tylko referencje &. Metoda deref daje kompilatorowi możliwość wzięcia wartości dowolnego typu, który implementuje Deref, i wywołania metody deref, aby uzyskać referencję, którą wie, jak dereferencyjnie traktować.
Kiedy wpisaliśmy *y w Listing 15-9, za kulisami Rust faktycznie uruchomił ten kod:
*(y.deref())
Rust zastępuje operator * wywołaniem metody deref, a następnie prostym dereferencjonowaniem, tak abyśmy nie musieli zastanawiać się, czy potrzebujemy wywoływać metodę deref, czy nie. Ta funkcja Rusta pozwala nam pisać kod, który działa identycznie, niezależnie od tego, czy mamy zwykłą referencję, czy typ implementujący Deref.
Powód, dla którego metoda deref zwraca referencję do wartości, a zwykła dereferencja poza nawiasami w *(y.deref()) jest nadal konieczna, ma związek z systemem własności. Gdyby metoda deref zwracała wartość bezpośrednio zamiast referencji do wartości, wartość zostałaby przeniesiona z self. Nie chcemy przejmować własności wewnętrznej wartości w MyBox<T> w tym przypadku ani w większości przypadków, gdy używamy operatora dereferencji.
Zauważ, że operator * jest zastępowany wywołaniem metody deref, a następnie wywołaniem operatora * tylko raz, za każdym razem, gdy używamy * w naszym kodzie. Ponieważ podstawianie operatora * nie rekursuje w nieskończoność, otrzymujemy dane typu i32, co pasuje do 5 w assert_eq! w Listing 15-9.
Używanie koercji Deref w funkcjach i metodach
Koercja Deref konwertuje referencję do typu, który implementuje cechę Deref, na referencję do innego typu. Na przykład, koercja Deref może przekonwertować &String na &str, ponieważ String implementuje cechę Deref w taki sposób, że zwraca &str. Koercja Deref jest udogodnieniem, które Rust wykonuje na argumentach funkcji i metod, i działa tylko na typach, które implementują cechę Deref. Dzieje się to automatycznie, gdy przekazujemy referencję do wartości konkretnego typu jako argument do funkcji lub metody, która nie pasuje do typu parametru w definicji funkcji lub metody. Sekwencja wywołań metody deref konwertuje dostarczony przez nas typ na typ, którego potrzebuje parametr.
Koercja Deref została dodana do Rusta, aby programiści piszący wywołania funkcji i metod nie musieli dodawać tak wielu jawnych referencji i dereferencji za pomocą & i *. Funkcja koercji Deref pozwala nam również pisać więcej kodu, który może działać zarówno z referencjami, jak i inteligentnymi wskaźnikami.
Aby zobaczyć koercję Deref w działaniu, użyjmy typu MyBox<T>, który zdefiniowaliśmy w Listing 15-8, a także implementacji Deref, którą dodaliśmy w Listing 15-10. Listing 15-11 pokazuje definicję funkcji, która ma parametr wycinka ciągu znaków.
fn hello(name: &str) {
println!("Witaj, {name}!");
}
fn main() {}
Możemy wywołać funkcję hello z wycinkiem ciągu znaków jako argumentem, na przykład hello("Rust");. Koercja Deref umożliwia wywołanie hello z referencją do wartości typu MyBox<String>, jak pokazano w Listing 15-12.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Witaj, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
Tutaj wywołujemy funkcję hello z argumentem &m, który jest referencją do wartości MyBox<String>. Ponieważ zaimplementowaliśmy cechę Deref dla MyBox<T> w Listing 15-10, Rust może przekształcić &MyBox<String> w &String poprzez wywołanie deref. Standardowa biblioteka dostarcza implementację Deref dla String, która zwraca wycinek ciągu znaków, i jest to w dokumentacji API dla Deref. Rust ponownie wywołuje deref, aby przekształcić &String w &str, co pasuje do definicji funkcji hello.
Gdyby Rust nie implementował koercji dereferencyjnej, musielibyśmy napisać kod z Listing 15-13 zamiast kodu z Listing 15-12, aby wywołać hello z wartością typu &MyBox<String>.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Witaj, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m) dereferencuje MyBox<String> do String. Następnie & i [..] pobierają wycinek ciągu znaków z String, który jest równy całemu ciągowi, aby pasował do sygnatury hello. Ten kod bez koercji dereferencyjnych jest trudniejszy do odczytania, napisania i zrozumienia ze wszystkimi zaangażowanymi symbolami. Koercja Deref pozwala Rustowi automatycznie obsługiwać te konwersje.
Kiedy cecha Deref jest zdefiniowana dla zaangażowanych typów, Rust przeanalizuje typy i użyje Deref::deref tyle razy, ile to konieczne, aby uzyskać referencję pasującą do typu parametru. Liczba razy, jaką należy wstawić Deref::deref, jest rozwiązywana w czasie kompilacji, więc nie ma kary za wydajność w czasie działania za korzystanie z koercji Deref!
Obsługa koercji Deref ze zmiennymi referencjami
Podobnie jak używasz cechy Deref do nadpisywania operatora * na niezmiennych referencjach, możesz użyć cechy DerefMut do nadpisywania operatora * na zmiennych referencjach.
Rust wykonuje koercję dereferencyjną, gdy znajdzie typy i implementacje cech w trzech przypadkach:
- Z
&Tdo&U, gdyT: Deref<Target=U> - Z
&mut Tdo&mut U, gdyT: DerefMut<Target=U> - Z
&mut Tdo&U, gdyT: Deref<Target=U>
Pierwsze dwa przypadki są takie same, z wyjątkiem tego, że drugi implementuje zmienność. Pierwszy przypadek mówi, że jeśli masz &T i T implementuje Deref do pewnego typu U, możesz w sposób przezroczysty uzyskać &U. Drugi przypadek mówi, że ta sama koercja dereferencyjna ma miejsce dla zmiennych referencji.
Trzeci przypadek jest bardziej podstępny: Rust również przekształci zmienną referencję w niezmienną. Ale odwrotna operacja nie jest możliwa: niezmienne referencje nigdy nie zostaną przekształcone w zmienne referencje. Ze względu na zasady pożyczania, jeśli masz zmienną referencję, ta zmienna referencja musi być jedyną referencją do tych danych (w przeciwnym razie program nie skompilowałby się). Konwersja jednej zmiennej referencji na jedną niezmienną referencję nigdy nie naruszy zasad pożyczania. Konwersja niezmiennej referencji na zmienną referencję wymagałaby, aby początkowa niezmienna referencja była jedyną niezmienną referencją do tych danych, ale zasady pożyczania tego nie gwarantują. Dlatego Rust nie może przyjąć założenia, że konwersja niezmiennej referencji na zmienną referencję jest możliwa.