Typy generyczne, cechy i czasy życia
Każdy język programowania posiada narzędzia do efektywnego zarządzania powtarzalnością koncepcji. W Rust, jednym z takich narzędzi są generyki: abstrakcyjne zamienniki dla konkretnych typów lub innych właściwości. Możemy wyrazić zachowanie generyków lub ich relacje z innymi generykami, nie wiedząc, co znajdzie się na ich miejscu podczas kompilacji i uruchamiania kodu.
Funkcje mogą przyjmować parametry jakiegoś typu generycznego, zamiast konkretnego typu, takiego jak i32 lub String, w ten sam sposób, w jaki przyjmują parametry o nieznanych wartościach, aby uruchamiać ten sam kod na wielu konkretnych wartościach. W rzeczywistości, już używaliśmy generyków w Rozdziale 6 z Option<T>, w Rozdziale 8 z Vec<T> i HashMap<K, V>, oraz w Rozdziale 9 z Result<T, E>. W tym rozdziale poznasz, jak definiować własne typy, funkcje i metody za pomocą generyków!
Najpierw przypomnimy, jak wyodrębnić funkcję, aby zmniejszyć duplikację kodu. Następnie użyjemy tej samej techniki, aby stworzyć funkcję generyczną z dwóch funkcji, które różnią się tylko typami swoich parametrów. Wyjaśnimy również, jak używać typów generycznych w definicjach struktur i wyliczeń.
Następnie dowiesz się, jak używać cech (traits) do definiowania zachowania w sposób generyczny. Możesz łączyć cechy z typami generycznymi, aby ograniczyć typ generyczny do akceptowania tylko tych typów, które mają określone zachowanie, w przeciwieństwie do dowolnego typu.
Na koniec omówimy czasy życia: odmianę generyków, która dostarcza kompilatorowi informacji o tym, jak referencje odnoszą się do siebie. Czasy życia pozwalają nam dostarczyć kompilatorowi wystarczających informacji o pożyczonych wartościach, aby mógł on zapewnić, że referencje będą ważne w większej liczbie sytuacji, niż byłoby to możliwe bez naszej pomocy.
Usuwanie duplikacji poprzez wyodrębnianie funkcji
Generyki pozwalają nam zastępować konkretne typy przez placeholder, który reprezentuje wiele typów, aby usunąć duplikację kodu. Zanim zagłębimy się w składnię generyków, najpierw przyjrzyjmy się, jak usunąć duplikację w sposób, który nie obejmuje typów generycznych, poprzez wyodrębnienie funkcji, która zastępuje konkretne wartości przez placeholder reprezentujący wiele wartości. Następnie zastosujemy tę samą technikę do wyodrębnienia funkcji generycznej! Patrząc na to, jak rozpoznać zduplikowany kod, który można wyodrębnić do funkcji, zaczniesz rozpoznawać zduplikowany kod, który może używać generyków.
Zaczniemy od krótkiego programu w Listing 10-1, który znajduje największą liczbę na liście.
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("Największa liczba to {largest}");
assert_eq!(*largest, 100);
}
Przechowujemy listę liczb całkowitych w zmiennej number_list i umieszczamy referencję do pierwszej liczby na liście w zmiennej o nazwie largest. Następnie iterujemy po wszystkich liczbach na liście, a jeśli bieżąca liczba jest większa niż liczba przechowywana w largest, zastępujemy referencję w tej zmiennej. Jednakże, jeśli bieżąca liczba jest mniejsza lub równa największej liczbie widzianej do tej pory, zmienna nie zmienia się, a kod przechodzi do następnej liczby na liście. Po rozważeniu wszystkich liczb na liście, largest powinien odnosić się do największej liczby, która w tym przypadku wynosi 100.
Teraz postawiono nam zadanie znalezienia największej liczby na dwóch różnych listach liczb. Aby to zrobić, możemy zdecydować się na zduplikowanie kodu z Listing 10-1 i użycie tej samej logiki w dwóch różnych miejscach w programie, jak pokazano w Listing 10-2.
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("Największa liczba to {largest}");
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("Największa liczba to {largest}");
}
Chociaż ten kod działa, duplikowanie kodu jest uciążliwe i podatne na błędy. Musimy również pamiętać o aktualizowaniu kodu w wielu miejscach, gdy chcemy go zmienić.
Aby wyeliminować tę duplikację, stworzymy abstrakcję, definiując funkcję, która operuje na dowolnej liście liczb całkowitych przekazanej jako parametr. To rozwiązanie czyni nasz kod jaśniejszym i pozwala nam abstrakcyjnie wyrazić koncepcję znajdowania największej liczby na liście.
W Listing 10-3 wyodrębniamy kod, który znajduje największą liczbę, do funkcji o nazwie largest. Następnie wywołujemy funkcję, aby znaleźć największą liczbę na dwóch listach z Listing 10-2. Moglibyśmy również użyć tej funkcji na dowolnej innej liście wartości i32, które moglibyśmy mieć w przyszłości.
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("Największa liczba to {result}");
assert_eq!(*result, 100);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("Największa liczba to {result}");
assert_eq!(*result, 6000);
}
Funkcja largest ma parametr list, który reprezentuje dowolny konkretny wycinek wartości i32, który możemy przekazać do funkcji. W rezultacie, gdy wywołujemy funkcję, kod działa na konkretnych wartościach, które przekazujemy.
Podsumowując, oto kroki, które podjęliśmy, aby zmienić kod z Listing 10-2 na Listing 10-3:
- Zidentyfikuj zduplikowany kod.
- Wyodrębnij zduplikowany kod do ciała funkcji i określ wejścia i wartości zwracane tego kodu w sygnaturze funkcji.
- Zaktualizuj dwa wystąpienia zduplikowanego kodu, aby zamiast tego wywoływały funkcję.
Następnie użyjemy tych samych kroków z generykami, aby zmniejszyć duplikację kodu. W ten sam sposób, w jaki ciało funkcji może działać na abstrakcyjnej liście zamiast na konkretnych wartościach, generyki pozwalają kodowi działać na abstrakcyjnych typach.
Na przykład, powiedzmy, że mieliśmy dwie funkcje: jedną, która znajduje największy element w wycinku wartości i32, i drugą, która znajduje największy element w wycinku wartości char. Jak wyeliminowalibyśmy tę duplikację? Dowiedzmy się!