Dodatek C: Cechy dziedziczne (Derivable Traits)
W różnych miejscach w książce omawialiśmy atrybut derive, który można zastosować do definicji struktury lub wyliczenia. Atrybut derive generuje kod, który zaimplementuje cechę z własną domyślną implementacją dla typu, który opatrzyłeś składnią derive.
W tym dodatku przedstawiamy odniesienie do wszystkich cech w bibliotece standardowej, których możesz używać z derive. Każda sekcja obejmuje:
- Jakie operatory i metody umożliwi dziedziczenie tej cechy.
- Co robi implementacja cechy dostarczana przez
derive. - Co implementacja cechy oznacza dla typu.
- Warunki, w których dozwolone lub niedozwolone jest implementowanie cechy.
- Przykłady operacji, które wymagają tej cechy.
Jeśli chcesz innego zachowania niż to, które zapewnia atrybut derive, zapoznaj się z dokumentacją biblioteki standardowej dla każdej cechy, aby uzyskać szczegółowe informacje na temat ręcznej ich implementacji.
Cechy wymienione tutaj są jedynymi zdefiniowanymi przez bibliotekę standardową, które mogą być implementowane na twoich typach za pomocą derive. Inne cechy zdefiniowane w bibliotece standardowej nie mają sensownego domyślnego zachowania, więc to od ciebie zależy, jak je zaimplementujesz, aby miały sens dla tego, co próbujesz osiągnąć.
Przykładem cechy, której nie można dziedziczyć, jest Display, która obsługuje formatowanie dla użytkowników końcowych. Zawsze powinieneś rozważyć odpowiedni sposób wyświetlania typu użytkownikowi końcowemu. Jakie części typu powinny być widoczne dla użytkownika końcowego? Jakie części byłyby dla nich istotne? Jaki format danych byłby dla nich najbardziej istotny? Kompilator Rusta nie ma tej wiedzy, więc nie może zapewnić odpowiedniego domyślnego zachowania.
Lista cech dziedzicznych podana w tym dodatku nie jest wyczerpująca: biblioteki mogą implementować derive dla własnych cech, co sprawia, że lista cech, z którymi można używać derive, jest naprawdę otwarta. Implementacja derive wiąże się z użyciem makra proceduralnego, co zostało omówione w sekcji „Niestandardowe makra derive” w Rozdziale 20.
Debug dla wyjścia programisty
Cecha Debug umożliwia formatowanie debugowania w ciągach formatujących, co sygnalizujesz, dodając :? wewnątrz symboli zastępczych {}.
Cecha Debug pozwala na drukowanie instancji typu w celach debugowania, dzięki czemu ty i inni programiści używający twojego typu możecie sprawdzić instancję w określonym punkcie wykonania programu.
Cecha Debug jest wymagana, na przykład, przy użyciu makra assert_eq!. To makro drukuje wartości instancji podanych jako argumenty, jeśli twierdzenie o równości zawiedzie, tak aby programiści mogli zobaczyć, dlaczego dwie instancje nie były równe.
PartialEq i Eq dla porównań równości
Cecha PartialEq pozwala porównywać instancje typu pod kątem równości i umożliwia użycie operatorów == i !=.
Dziedziczenie PartialEq implementuje metodę eq. Gdy PartialEq jest dziedziczone w strukturach, dwie instancje są równe tylko wtedy, gdy wszystkie pola są równe, i nie są równe, jeśli którekolwiek pola nie są równe. Gdy dziedziczone w wyliczeniach, każdy wariant jest równy sam sobie i nierówny innym wariantom.
Cecha PartialEq jest wymagana, na przykład, przy użyciu makra assert_eq!, które musi być w stanie porównać dwie instancje typu pod kątem równości.
Cecha Eq nie ma żadnych metod. Jej celem jest sygnalizowanie, że dla każdej wartości typu oznaczonego, wartość jest równa sobie. Cecha Eq może być zastosowana tylko do typów, które również implementują PartialEq, chociaż nie wszystkie typy, które implementują PartialEq, mogą implementować Eq. Jednym z przykładów są typy liczb zmiennoprzecinkowych: implementacja liczb zmiennoprzecinkowych mówi, że dwie instancje wartości not-a-number (NaN) nie są sobie równe.
Przykładem, kiedy wymagana jest cecha Eq, jest użycie kluczy w HashMap<K, V>, aby HashMap<K, V> mogło stwierdzić, czy dwa klucze są takie same.
PartialOrd i Ord dla porównań porządkowania
Cecha PartialOrd pozwala porównywać instancje typu w celach sortowania. Typ, który implementuje PartialOrd, może być używany z operatorami <, >, <=, i >=. Cechę PartialOrd można zastosować tylko do typów, które również implementują PartialEq.
Dziedziczenie PartialOrd implementuje metodę partial_cmp, która zwraca Option<Ordering>, która będzie None, gdy podane wartości nie dają porządku. Przykładem wartości, która nie daje porządku, nawet jeśli większość wartości tego typu można porównać, jest wartość zmiennoprzecinkowa NaN. Wywołanie partial_cmp z dowolną liczbą zmiennoprzecinkową i wartością zmiennoprzecinkową NaN zwróci None.
Dziedzicząc w strukturach, PartialOrd porównuje dwie instancje, porównując wartość w każdym polu w kolejności, w jakiej pola pojawiają się w definicji struktury. Dziedzicząc w wyliczeniach, warianty wyliczenia zadeklarowane wcześniej w definicji wyliczenia są uważane za mniejsze niż warianty wymienione później.
Cecha PartialOrd jest wymagana, na przykład, dla metody gen_range z crate rand, która generuje losową wartość w zakresie określonym przez wyrażenie zakresowe.
Cecha Ord pozwala wiedzieć, że dla dowolnych dwóch wartości typu opisanego adnotacją, zawsze będzie istniało prawidłowe uporządkowanie. Cecha Ord implementuje metodę cmp, która zwraca Ordering zamiast Option<Ordering>, ponieważ prawidłowe uporządkowanie zawsze będzie możliwe. Cechę Ord można zastosować tylko do typów, które również implementują PartialOrd i Eq (a Eq wymaga PartialEq). Dziedzicząc w strukturach i wyliczeniach, cmp zachowuje się tak samo jak zaimplementowane partial_cmp w PartialOrd.
Przykładem, kiedy wymagana jest cecha Ord, jest przechowywanie wartości w BTreeSet<T>, strukturze danych, która przechowuje dane na podstawie kolejności sortowania wartości.
Clone i Copy do duplikowania wartości
Cecha Clone pozwala jawnie utworzyć głęboką kopię wartości, a proces duplikacji może obejmować uruchomienie dowolnego kodu i kopiowanie danych na stercie. Aby uzyskać więcej informacji na temat Clone, zobacz sekcję „Zmienne i dane współdziałające z Clone” w Rozdziale 4.
Dziedziczenie Clone implementuje metodę clone, która, zaimplementowana dla całego typu, wywołuje clone na każdej z części typu. Oznacza to, że wszystkie pola lub wartości w typie muszą również implementować Clone, aby dziedziczyć Clone.
Przykładem, kiedy wymagana jest cecha Clone, jest wywołanie metody to_vec na wycinku. Wycinek nie jest właścicielem instancji typów, które zawiera, ale wektor zwrócony z to_vec będzie musiał być właścicielem swoich instancji, więc to_vec wywołuje clone na każdym elemencie. W ten sposób typ przechowywany w wycinku musi implementować Clone.
Cecha Copy pozwala duplikować wartość tylko poprzez kopiowanie bitów przechowywanych na stosie; nie jest potrzebny żaden arbitralny kod. Więcej informacji na temat Copy znajdziesz w sekcji „Dane tylko na stosie: Copy” w Rozdziale 4.
Cecha Copy nie definiuje żadnych metod, aby zapobiec przeciążaniu tych metod przez programistów i naruszaniu założenia, że żaden arbitralny kod nie jest wykonywany. W ten sposób wszyscy programiści mogą zakładać, że kopiowanie wartości będzie bardzo szybkie.
Możesz dziedziczyć Copy na dowolnym typie, którego wszystkie części implementują Copy. Typ, który implementuje Copy, musi również implementować Clone, ponieważ typ, który implementuje Copy, ma trywialną implementację Clone, która wykonuje to samo zadanie co Copy.
Cecha Copy jest rzadko wymagana; typy, które implementują Copy, mają dostępne optymalizacje, co oznacza, że nie musisz wywoływać clone, co sprawia, że kod jest bardziej zwięzły.
Wszystko, co jest możliwe z Copy, można również osiągnąć z Clone, ale kod może być wolniejszy lub musi używać clone w niektórych miejscach.
Hash do mapowania wartości na wartość o stałym rozmiarze
Cecha Hash pozwala na pobranie instancji typu o dowolnym rozmiarze i zmapowanie jej na wartość o stałym rozmiarze za pomocą funkcji haszującej. Dziedziczenie Hash implementuje metodę hash. Zaimplementowana metoda hash łączy wynik wywołania hash na każdej z części typu, co oznacza, że wszystkie pola lub wartości muszą również implementować Hash, aby dziedziczyć Hash.
Przykładem, kiedy wymagana jest cecha Hash, jest przechowywanie kluczy w HashMap<K, V> w celu efektywnego przechowywania danych.
Default dla wartości domyślnych
Cecha Default pozwala utworzyć domyślną wartość dla typu. Dziedziczenie Default implementuje funkcję default. Dziedziczona implementacja funkcji default wywołuje funkcję default dla każdej części typu, co oznacza, że wszystkie pola lub wartości w typie muszą również implementować Default, aby dziedziczyć Default.
Funkcja Default::default jest powszechnie używana w połączeniu ze składnią aktualizacji struktury omówioną w sekcji „Tworzenie instancji z innych instancji za pomocą składni aktualizacji struktur” w Rozdziale 5. Możesz dostosować kilka pól struktury, a następnie ustawić i użyć domyślnej wartości dla pozostałych pól, używając ..Default::default().
Cecha Default jest wymagana, gdy używasz metody unwrap_or_default na instancjach Option<T>, na przykład. Jeśli Option<T> jest None, metoda unwrap_or_default zwróci wynik Default::default dla typu T przechowywanego w Option<T>.