Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Przykładowy program używający struktur

Aby zrozumieć, kiedy warto używać struktur, napiszmy program, który oblicza pole prostokąta. Zaczniemy od użycia pojedynczych zmiennych, a następnie zrefaktoryzujemy program, aż będziemy używać struktur.

Stwórzmy nowy projekt binarny za pomocą Cargo o nazwie rectangles, który będzie przyjmował szerokość i wysokość prostokąta podane w pikselach i obliczał pole prostokąta. Listing 5-8 przedstawia krótki program z jednym sposobem na zrobienie tego w pliku src/main.rs naszego projektu.

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "Pole prostokąta wynosi {} pikseli kwadratowych.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Teraz uruchom ten program za pomocą cargo run:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

Ten kod z powodzeniem oblicza pole prostokąta, wywołując funkcję area z każdym wymiarem, ale możemy zrobić więcej, aby uczynić ten kod bardziej przejrzystym i czytelnym.

Problem z tym kodem jest widoczny w sygnaturze area:

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "Pole prostokąta wynosi {} pikseli kwadratowych.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

Funkcja area ma za zadanie obliczyć pole jednego prostokąta, ale napisana przez nas funkcja ma dwa parametry i nigdzie w naszym programie nie jest jasne, że te parametry są ze sobą powiązane. Byłoby bardziej czytelne i łatwiejsze w zarządzaniu, gdybyśmy zgrupowali szerokość i wysokość. Omówiliśmy już jeden sposób, w jaki moglibyśmy to zrobić w sekcji „Typ krotki” w Rozdziale 3: używając krotek.

Refaktoryzacja za pomocą krotek

Listing 5-9 przedstawia inną wersję naszego programu, która używa krotek.

fn main() {
    let rect1 = (30, 50);

    println!(
        "Pole prostokąta wynosi {} pikseli kwadratowych.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

Pod pewnym względem ten program jest lepszy. Krotki pozwalają nam dodać trochę struktury, a teraz przekazujemy tylko jeden argument. Ale pod innym względem ta wersja jest mniej przejrzysta: krotki nie nazywają swoich elementów, więc musimy indeksować części krotki, co sprawia, że nasze obliczenia są mniej oczywiste.

Pomylenie szerokości i wysokości nie miałoby znaczenia dla obliczania powierzchni, ale gdybyśmy chcieli narysować prostokąt na ekranie, miałoby to znaczenie! Musielibyśmy pamiętać, że width to indeks krotki 0, a height to indeks krotki 1. Byłoby to jeszcze trudniejsze do zrozumienia i zapamiętania dla kogoś innego, kto używałby naszego kodu. Ponieważ nie przekazaliśmy znaczenia naszych danych w naszym kodzie, łatwiej jest teraz wprowadzić błędy.

Refaktoryzacja za pomocą struktur

Używamy struktur, aby dodać znaczenie poprzez etykietowanie danych. Możemy przekształcić krotkę, której używamy, w strukturę z nazwą dla całości, a także nazwami dla części, jak pokazano w Listingu 5-10.

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "Pole prostokąta wynosi {} pikseli kwadratowych.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

Tutaj zdefiniowaliśmy strukturę i nazwaliśmy ją Rectangle. W nawiasach klamrowych zdefiniowaliśmy pola jako width i height, z których oba mają typ u32. Następnie, w main, utworzyliśmy konkretną instancję Rectangle o szerokości 30 i wysokości 50.

Nasza funkcja area jest teraz zdefiniowana z jednym parametrem, który nazwaliśmy rectangle, którego typem jest niezmienne pożyczenie instancji struktury Rectangle. Jak wspomniano w Rozdziale 4, chcemy pożyczyć strukturę, a nie przejmować jej własności. W ten sposób main zachowuje własność i może nadal używać rect1, co jest powodem, dla którego używamy & w sygnaturze funkcji i tam, gdzie wywołujemy funkcję.

Funkcja area uzyskuje dostęp do pól width i height instancji Rectangle (zauważ, że dostęp do pól pożyczonej instancji struktury nie przenosi wartości pól, dlatego często spotyka się pożyczanie struktur). Nasza sygnatura funkcji area mówi teraz dokładnie, co mamy na myśli: Oblicz pole Rectangle, używając jego pól width i height. Przekazuje to, że szerokość i wysokość są ze sobą powiązane, i nadaje wartościom opisowe nazwy, zamiast używać wartości indeksu krotki 0 i 1. Jest to korzyść dla przejrzystości.

Dodawanie funkcjonalności za pomocą wyprowadzonych cech (derived traits)

Przydałoby się móc wyświetlić instancję Rectangle podczas debugowania programu i zobaczyć wartości wszystkich jej pól. Listing 5-11 próbuje użyć makra println!, tak jak używaliśmy w poprzednich rozdziałach. Jednak to nie zadziała.

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 to {rect1}");
}

Kiedy skompilujemy ten kod, otrzymamy błąd z następującym głównym komunikatem:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

Makro println! potrafi wiele rodzajów formatowania, a domyślnie nawiasy klamrowe mówią println!, aby użyć formatowania znanego jako Display: wyjście przeznaczone do bezpośredniego użytku przez użytkownika końcowego. Prymitywne typy, które widzieliśmy do tej pory, domyślnie implementują Display, ponieważ istnieje tylko jeden sposób, w jaki chciałbyś pokazać 1 lub inny typ prymitywny użytkownikowi. Ale w przypadku struktur sposób, w jaki println! powinien sformatować wyjście, jest mniej jasny, ponieważ istnieje więcej możliwości wyświetlania: Czy chcesz przecinków, czy nie? Czy chcesz drukować nawiasy klamrowe? Czy wszystkie pola powinny być wyświetlane? Z powodu tej niejednoznaczności Rust nie próbuje zgadywać, czego chcemy, a struktury nie mają dostarczonej implementacji Display do użycia z println! i symbolem zastępczym {}.

Jeśli będziemy dalej czytać błędy, znajdziemy tę pomocną notatkę:

   |                        |`Rectangle` nie może być sformatowany za pomocą domyślnego formatatora
   |                        wymaganego przez ten parametr formatowania

Spróbujmy! Wywołanie makra println! będzie teraz wyglądać tak: println!("rect1 to {rect1:?}");. Umieszczenie specyfikatora :? w nawiasach klamrowych mówi println!, że chcemy użyć formatu wyjściowego o nazwie Debug. Cecha Debug umożliwia nam wyświetlanie naszej struktury w sposób użyteczny dla programistów, dzięki czemu możemy zobaczyć jej wartość podczas debugowania kodu.

Skompiluj kod z tą zmianą. Cholera! Nadal otrzymujemy błąd:

error[E0277]: `Rectangle` nie implementuje `Debug`

Ale znowu, kompilator daje nam pomocną notatkę:

   |                        wymagany przez ten parametr formatowania
   |

Rust zawiera funkcjonalność do wyświetlania informacji debugujących, ale musimy jawnie ją włączyć, aby ta funkcjonalność była dostępna dla naszej struktury. Aby to zrobić, dodajemy zewnętrzny atrybut #[derive(Debug)] tuż przed definicją struktury, jak pokazano w Listingu 5-12.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 to {rect1:?}");
}

Teraz, gdy uruchomimy program, nie otrzymamy żadnych błędów i zobaczymy następujące wyjście:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Pięknie! To nie jest najładniejsze wyjście, ale pokazuje wartości wszystkich pól dla tej instancji, co z pewnością pomogłoby podczas debugowania. Kiedy mamy większe struktury, przydatne jest, aby wyjście było nieco łatwiejsze do odczytania; w takich przypadkach możemy użyć {:#?} zamiast {:?} w ciągu println!. W tym przykładzie użycie stylu {:#?} spowoduje wyświetlenie następującego komunikatu:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

Innym sposobem wydrukowania wartości za pomocą formatu Debug jest użycie makra dbg!, które przejmuje własność wyrażenia (w przeciwieństwie do println!, które przyjmuje referencję), drukuje plik i numer linii, w której występuje wywołanie makra dbg! wraz z wynikową wartością tego wyrażenia i zwraca własność wartości.

Uwaga: Wywołanie makra dbg! drukuje do standardowego strumienia błędów konsoli (stderr), w przeciwieństwie do println!, które drukuje do standardowego strumienia wyjściowego konsoli (stdout). Więcej o stderr i stdout omówimy w sekcji „Przekierowywanie błędów do standardowego strumienia błędów” w Rozdziale 12.

Oto przykład, w którym interesuje nas wartość przypisana do pola width, a także wartość całej struktury w rect1:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

Możemy umieścić dbg! wokół wyrażenia 30 * scale i, ponieważ dbg! zwraca własność wartości wyrażenia, pole width otrzyma tę samą wartość, jakbyśmy nie mieli tam wywołania dbg!. Nie chcemy, aby dbg! przejmowało własność rect1, więc w następnym wywołaniu używamy referencji do rect1. Oto jak wygląda wyjście z tego przykładu:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Widzimy, że pierwsza część wyjścia pochodzi z pliku src/main.rs w linii 10, gdzie debugujemy wyrażenie 30 * scale, a jego wynikowa wartość to 60 (formatowanie Debug zaimplementowane dla liczb całkowitych polega na wydrukowaniu tylko ich wartości). Wywołanie dbg! w linii 14 pliku src/main.rs wyprowadza wartość &rect1, czyli struktury Rectangle. To wyjście wykorzystuje ładne formatowanie Debug typu Rectangle. Makro dbg! może być naprawdę pomocne, gdy próbujesz dowiedzieć się, co robi twój kod!

Oprócz cechy Debug, Rust dostarczył szereg cech, których możemy używać z atrybutem derive, które mogą dodawać użyteczne zachowanie do naszych niestandardowych typów. Te cechy i ich zachowania są wymienione w Dodatku C. Omówimy, jak zaimplementować te cechy z niestandardowym zachowaniem, a także jak tworzyć własne cechy w Rozdziale 10. Istnieje również wiele innych atrybutów niż derive; więcej informacji można znaleźć w sekcji „Atrybuty” w dokumentacji Rust Reference.

Nasza funkcja area jest bardzo specyficzna: oblicza tylko pole prostokątów. Pomocne byłoby powiązanie tego zachowania bliżej z naszą strukturą Rectangle, ponieważ nie będzie ono działać z żadnym innym typem. Przyjrzyjmy się, jak możemy kontynuować refaktoryzację tego kodu, zamieniając funkcję area w metodę area zdefiniowaną dla naszego typu Rectangle.