Metody
Metody są podobne do funkcji: deklarujemy je słowem kluczowym fn i nazwą, mogą mieć parametry i zwracać wartość, oraz zawierają kod, który jest uruchamiany po wywołaniu metody z innego miejsca. W przeciwieństwie do funkcji, metody są definiowane w kontekście struktury (lub typu wyliczeniowego albo obiektu cechy, które omówimy odpowiednio w Rozdziale 6 i Rozdziale 18), a ich pierwszym parametrem jest zawsze self, które reprezentuje instancję struktury, na której wywoływana jest metoda.
Składnia metody
Zmieńmy funkcję area, która ma instancję Rectangle jako parametr, i zamiast tego stwórzmy metodę area zdefiniowaną w strukturze Rectangle, jak pokazano w Listingu 5-13.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"Pole prostokąta wynosi {} pikseli kwadratowych.",
rect1.area()
);
}
Aby zdefiniować funkcję w kontekście Rectangle, rozpoczynamy blok impl (implementacji) dla Rectangle. Wszystko w tym bloku impl będzie powiązane z typem Rectangle. Następnie przenosimy funkcję area do nawiasów klamrowych impl i zmieniamy pierwszy (a w tym przypadku jedyny) parametr na self w sygnaturze i wszędzie w treści. W main, gdzie wywołaliśmy funkcję area i przekazaliśmy rect1 jako argument, możemy zamiast tego użyć składni metody do wywołania metody area na naszej instancji Rectangle. Składnia metody występuje po instancji: dodajemy kropkę, po której następuje nazwa metody, nawiasy i wszelkie argumenty.
W sygnaturze area używamy &self zamiast rectangle: &Rectangle. &self to w rzeczywistości skrót od self: &Self. W bloku impl typ Self jest aliasem dla typu, dla którego jest blok impl. Metody muszą mieć parametr o nazwie self typu Self jako swój pierwszy parametr, więc Rust pozwala na skrócenie tego do samej nazwy self w miejscu pierwszego parametru. Zauważ, że nadal musimy używać & przed skrótem self, aby wskazać, że ta metoda pożycza instancję Self, tak jak zrobiliśmy to w rectangle: &Rectangle. Metody mogą przejmować własność self, pożyczać self niezmiennie, jak to zrobiliśmy tutaj, lub pożyczać self mutowalnie, tak jak każdy inny parametr.
Wybraliśmy &self z tego samego powodu, dla którego użyliśmy &Rectangle w wersji funkcyjnej: nie chcemy przejmować własności i chcemy tylko odczytywać dane ze struktury, a nie do niej zapisywać. Gdybyśmy chcieli zmienić instancję, na której wywołaliśmy metodę, w ramach działania metody, użylibyśmy &mut self jako pierwszego parametru. Posiadanie metody, która przejmuje własność instancji, używając tylko self jako pierwszego parametru, jest rzadkie; ta technika jest zazwyczaj używana, gdy metoda przekształca self w coś innego i chcemy uniemożliwić wywołującemu używanie oryginalnej instancji po transformacji.
Głównym powodem używania metod zamiast funkcji, oprócz zapewnienia składni metody i braku konieczności powtarzania typu self w sygnaturze każdej metody, jest organizacja. Umieściliśmy wszystkie rzeczy, które możemy zrobić z instancją typu, w jednym bloku impl, zamiast zmuszać przyszłych użytkowników naszego kodu do szukania możliwości Rectangle w różnych miejscach dostarczanej przez nas biblioteki.
Zauważ, że możemy nadać metodzie taką samą nazwę jak jednemu z pól struktury. Na przykład, możemy zdefiniować metodę w Rectangle, która również nazywa się width:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("Prostokąt ma szerokość różną od zera; wynosi {}", rect1.width);
}
}
Tutaj, decydujemy, aby metoda width zwracała true, jeśli wartość w polu width instancji jest większa niż 0, a false, jeśli wartość jest 0: możemy użyć pola o tej samej nazwie w metodzie do dowolnego celu. W main, gdy po rect1.width umieścimy nawiasy, Rust wie, że chodzi nam o metodę width. Gdy nie używamy nawiasów, Rust wie, że chodzi nam o pole width.
Często, choć nie zawsze, gdy nadajemy metodzie taką samą nazwę jak polu, chcemy, aby zwracała tylko wartość z pola i nic więcej. Takie metody nazywane są getterami, a Rust nie implementuje ich automatycznie dla pól struktur, tak jak robią to niektóre inne języki. Gettery są użyteczne, ponieważ można uczynić pole prywatnym, ale metodę publiczną, a tym samym umożliwić dostęp tylko do odczytu do tego pola jako części publicznego API typu. Omówimy, czym są publiczne i prywatne, oraz jak oznaczyć pole lub metodę jako publiczną lub prywatną w Rozdziale 7.
Gdzie jest operator
->?W C i C++ do wywoływania metod używa się dwóch różnych operatorów:
.jeśli wywołuje się metodę bezpośrednio na obiekcie, oraz->jeśli wywołuje się metodę na wskaźniku do obiektu i trzeba najpierw dereferencjować wskaźnik. Innymi słowy, jeśliobjectjest wskaźnikiem,object->something()jest podobne do(*object).something().Rust nie ma odpowiednika operatora
->; zamiast tego, Rust ma funkcję zwaną automatycznym referencjowaniem i dereferencjowaniem. Wywoływanie metod jest jednym z niewielu miejsc w Rust z takim zachowaniem.Działa to w następujący sposób: Kiedy wywołujesz metodę
object.something(), Rust automatycznie dodaje&,&mutlub*, tak abyobjectpasował do sygnatury metody. Innymi słowy, poniższe są takie same:#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }Pierwsza z nich wygląda znacznie czyściej. To automatyczne zachowanie referencjonowania działa, ponieważ metody mają wyraźny odbiornik — typ
self. Biorąc pod uwagę odbiornik i nazwę metody, Rust może jednoznacznie określić, czy metoda odczytuje (&self), mutuje (&mut self), czy zużywa (self). Fakt, że Rust sprawia, że pożyczanie jest niejawne dla odbiorników metod, jest dużą częścią sprawiania, że własność jest ergonomiczna w praktyce.
Metody z większą liczbą parametrów
Poćwiczmy używanie metod, implementując drugą metodę w strukturze Rectangle. Tym razem chcemy, aby instancja Rectangle przyjmowała inną instancję Rectangle i zwracała true, jeśli drugi Rectangle może całkowicie zmieścić się w self (pierwszym Rectangle); w przeciwnym razie powinna zwrócić false. Oznacza to, że po zdefiniowaniu metody can_hold, chcemy móc napisać program pokazany w Listingu 5-14.
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Czy rect1 może pomieścić rect2? {}", rect1.can_hold(&rect2));
println!("Czy rect1 może pomieścić rect3? {}", rect1.can_hold(&rect3));
}
Oczekiwane wyjście wyglądałoby następująco, ponieważ oba wymiary rect2 są mniejsze niż wymiary rect1, ale rect3 jest szerszy niż rect1:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
Wiemy, że chcemy zdefiniować metodę, więc będzie ona w bloku impl Rectangle. Nazwa metody będzie can_hold, i przyjmie niezmienne pożyczenie innego Rectangle jako parametr. Możemy stwierdzić, jaki będzie typ parametru, patrząc na kod, który wywołuje metodę: rect1.can_hold(&rect2) przekazuje &rect2, co jest niezmiennym pożyczeniem rect2, instancji Rectangle. Ma to sens, ponieważ musimy tylko odczytywać rect2 (zamiast zapisywać, co oznaczałoby, że potrzebowalibyśmy mutowalnego pożyczenia), i chcemy, aby main zachowało własność rect2, abyśmy mogli go ponownie użyć po wywołaniu metody can_hold. Wartością zwracaną can_hold będzie Boolean, a implementacja sprawdzi, czy szerokość i wysokość self są większe niż szerokość i wysokość drugiego Rectangle, odpowiednio. Dodajmy nową metodę can_hold do bloku impl z Listingu 5-13, pokazanej w Listingu 5-15.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Czy rect1 może pomieścić rect2? {}", rect1.can_hold(&rect2));
println!("Czy rect1 może pomieścić rect3? {}", rect1.can_hold(&rect3));
}
Po uruchomieniu tego kodu z funkcją main z Listingu 5-14, otrzymamy pożądane wyjście. Metody mogą przyjmować wiele parametrów, które dodajemy do sygnatury po parametrze self, a te parametry działają tak samo jak parametry w funkcjach.
Funkcje skojarzone
Wszystkie funkcje zdefiniowane w bloku impl nazywane są funkcjami skojarzonymi, ponieważ są powiązane z typem nazwanym po impl. Możemy definiować funkcje skojarzone, które nie mają self jako swojego pierwszego parametru (i dlatego nie są metodami), ponieważ nie potrzebują instancji typu do działania. Użyliśmy już jednej takiej funkcji: funkcji String::from zdefiniowanej dla typu String.
Funkcje skojarzone, które nie są metodami, są często używane jako konstruktory, które zwracają nową instancję struktury. Są one często nazywane new, ale new nie jest specjalną nazwą i nie jest wbudowane w język. Na przykład, moglibyśmy zapewnić funkcję skojarzoną o nazwie square, która miałaby jeden parametr wymiaru i używałaby go zarówno jako szerokości, jak i wysokości, ułatwiając w ten sposób tworzenie kwadratowego Rectangle zamiast konieczności dwukrotnego określania tej samej wartości:
Nazwa pliku: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
Słowa kluczowe Self w typie zwracanym i w treści funkcji są aliasami dla typu, który pojawia się po słowie kluczowym impl, czyli w tym przypadku Rectangle.
Aby wywołać tę funkcję skojarzoną, używamy składni :: z nazwą struktury; let sq = Rectangle::square(3); jest przykładem. Ta funkcja jest umieszczona w przestrzeni nazw struktury: składnia :: jest używana zarówno dla funkcji skojarzonych, jak i przestrzeni nazw utworzonych przez moduły. Omówimy moduły w Rozdziale 7.
Wiele bloków impl
Każda struktura może mieć wiele bloków impl. Na przykład, Listing 5-15 jest równoważny z kodem pokazanym w Listingu 5-16, który ma każdą metodę w swoim własnym bloku impl.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Czy rect1 może pomieścić rect2? {}", rect1.can_hold(&rect2));
println!("Czy rect1 może pomieścić rect3? {}", rect1.can_hold(&rect3));
}
Nie ma powodu, aby rozdzielać te metody na wiele bloków impl w tym przypadku, ale jest to poprawna składnia. Zobaczymy przypadek, w którym wiele bloków impl jest użytecznych w Rozdziale 10, gdzie omówimy typy generyczne i cechy.
Podsumowanie
Struktury pozwalają tworzyć niestandardowe typy, które mają znaczenie dla twojej domeny. Używając struktur, możesz zachować powiązane ze sobą fragmenty danych i nazywać każdy z nich, aby Twój kod był przejrzysty. W blokach impl możesz definiować funkcje powiązane z Twoim typem, a metody są rodzajem funkcji powiązanych, które pozwalają określać zachowanie instancji Twoich struktur.
Ale struktury to nie jedyny sposób, w jaki możesz tworzyć niestandardowe typy: przejdźmy do funkcji enum w Rust, aby dodać kolejne narzędzie do Twojego zestawu.