Przykładowy program wykorzystujący struktury

Aby pokazać, kiedy warto skorzystać ze struktur, napiszmy program, który policzy pole prostokąta. Zaczniemy od pojedynczych zmiennych, potem przekształcimy program tak, aby używał struktur.

Stwórzmy projekt aplikacji binarnej przy użyciu Cargo. Nazwijmy go prostokąty. Jako wejście przyjmie on szerokość i wysokość danego prostokąta i wyliczy jego pole. Listing 5-8 pokazuje krótki program obrazujący jeden ze sposobów, w jaki możemy to wykonać.

Plik: src/main.rs

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
}

Listing 5-8: Obliczanie pola prostokąta o szerokości i wysokości podanych jako osobne argumenty

Uruchommy program komendą cargo run:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
Pole prostokąta wynosi 1500 pikseli kwadratowych.

Pomimo że program z listingu 5-8 wygląda dobrze i poprawnie oblicza pole prostokąta wywołując funkcję area, do której podaje oba wymiary, to możemy napisać go czytelniej.

Problem w tym kodzie widnieje w sygnaturze funkcji 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 wyliczyć pole jakiegoś prostokąta, ale przecież funkcja którą my napisaliśmy ma dwa parametry. Parametry są ze sobą powiązane, ale ta zależność nie widnieje nigdzie w naszym programie. Łatwiej byłoby ten kod zrozumieć i nim się posługiwać, jeśli szerokość i wysokość byłyby ze sobą zgrupowane. Już omówiliśmy jeden ze sposobów, w jaki można to wykonać w sekcji „Krotka” rozdziału 3, czyli poprzez wykorzystanie krotek.

Refaktoryzacja z krotkami

Listing 5-9 pokazuje jeszcze jedną wersję programu wykorzystującego krotki.

Plik: src/main.rs

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
}

Listing 5-9: Określenie szerokości i wysokości prostokąta przy użyciu krotki

Ten program jest, w pewnych aspektach, lepszy. Krotki dodają odrobinę organizacji, oraz pozwalają nam podać funkcji tylko jeden argument. Z drugiej zaś strony ta wersja jest mniej czytelna: elementy krotki nie mają nazw, a nasze obliczenia stały się enigmatyczne, bo wymiary prostokąta reprezentowane są przez elementy krotki.

Ewentualne pomylenie wymiarów prostokąta nie ma znaczenia przy obliczaniu jego pola, ale sytuacja by się zmieniła gdybyśmy chcieli na przykład narysować go na ekranie. Musielibyśmy zapamiętać, że szerokość znajduje się w elemencie krotki o indeksie 0, a wysokość pod indeksem 1. Jeśli ktoś inny pracowałby nad tym kodem musiałby rozgryźć to samemu, a także to zapamiętać. Nie byłoby zaskakujące omyłkowe pomieszanie tych dwóch wartości, wynikające z braku zawarcia znaczenia danych w naszym kodzie.

Refaktoryzacja ze strukturami: ukazywanie znaczenia

Struktur używamy, aby przekazać znaczenie poprzez etykietowanie danych. Możemy przekształcić używaną przez nas krotkę nazywając zarówno całość jak i pojedyncze jej części, tak jak w listingu 5-10.

Plik: src/main.rs

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
}

Listing 5-10: Definicja struktury Rectangle.

Powyżej zdefiniowaliśmy strukturę i nazwaliśmy ją Rectangle. Wewnątrz nawiasów klamrowych zdefiniowaliśmy pola width i height, oba mające typ u32. Następnie w funkcji main stworzyliśmy konkretną instancję struktury Rectangle, w której szerokość wynosi 30, zaś wysokość 50.

Nasza funkcja area przyjmuje teraz jeden parametr, który nazwaliśmy rectangle, którego typ to niezmienne zapożyczenie instancji struktury Rectangle. Jak wspomnieliśmy w rozdziale 4, chcemy jedynie pożyczyć strukturę bez przenoszenia jej własności. Takim sposobem main pozostaje właścicielem i może dalej używać rect1, i dlatego używamy & w sygnaturze funkcji podczas jej wywołania.

Funkcja area dostaje się do pól width i height instancji struktury Rectangle. Proszę przy okazji zauważyć, że dostęp do pól pożyczonej instancji struktury nie powoduje przeniesienia wartości pól, dlatego często widuje się pożyczanie struktur. Teraz sygnatura funkcji area dobrze opisuje nasze zamiary: obliczenie pola danego prostokąta Rectangle poprzez wykorzystanie jego szerokości i wysokości. Bez niejasności przedstawiamy relację między szerokością a wysokością i przypisujemy logiczne nazwy wartościom zamiast indeksowania krotek wartościami 0 oraz 1. To wygrana dla przejrzystości.

Dodawanie przydatnej funkcjonalności za pomocą cech wyprowadzalnych

Miło byłoby móc wyświetlić instancję struktury Rectangle w trakcie debugowania naszego programu i zobaczyć wartość każdego pola. Listing 5-11 próbuje użyć makra println!, którego używaliśmy w poprzednich rozdziałach. To jednakowoż nie zadziała.

Plik: src/main.rs

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

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

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

Listing 5-11: Próba wyświetlenia instancji Rectangle

Podczas próby kompilacji tego kodu wyświetlany jest błąd z poniższym komunikatem:

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

Makro println! może formatować na wiele sposobów, a domyślnie para nawiasów klamrowych daje println! znać, że chcemy wykorzystać formatowanie Display (ang. wyświetlenie). Jest to tekst przeznaczony dla docelowego użytkownika. Widziane przez nas wcześniej prymitywne typy implementują Display automatycznie, bo przecież jest tylko jeden sposób wyświetlenia użytkownikowi symbolu 1 czy jakiegokolwiek innego prymitywnego typu. Ale kiedy w grę wchodzą struktury, sposób w jaki println! powinno formatować tekst jest mniej oczywiste, bo wyświetlać strukturę można na wiele sposobów: z przecinkami, czy bez? Chcesz wyświetlić nawiasy klamrowe? Czy każde pole powinno być wyświetlone? Przez tę wieloznaczność Rust nie zakłada z góry co jest dla nas najlepsze, więc z tego powodu struktury nie implementują automatycznie cechy Display wykorzystywanej przez println!.

Jeśli będziemy czytać dalej znajdziemy taką przydatną informację:

   = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

Jesteśmy poinformowani, że podana przez nas struktura nie może być użyta z domyślnym formaterem i zasugerowane jest nam użycie specyfikatora formatowania :?. To tak też zróbmy! Wywołanie makra println! teraz będzie wyglądać następująco: println!("rect1 to {:?}", rect1);. Wprowadzenie specyfikatora :? wewnątrz pary nawiasów klamrowych przekazuje println!, że chcemy użyć formatu wyjścia o nazwie Debug. Cecha Debug pozwala nam wypisać strukturę w sposób użyteczny dla deweloperów, pozwalając nam na obejrzenie jej wartości podczas debugowania kodu.

Skompilujmy kod z tymi zmianami. A niech to! Nadal pojawia się komunikat o błędzie:

error[E0277]: `Rectangle` doesn't implement `Debug`

Ale kompilator ponownie daje nam pomocny komunikat:

   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`

Powyższy komunikat informuje nas, że cecha Debug nie jest zaimplementowana dla struktury Rectangle i zaleca nam dodanie adnotacji. Rust doprawdy zawiera funkcjonalność pozwalającą wyświetlić informacje pomocne w debugowaniu, ale wymaga od nas, abyśmy ręcznie i wyraźnie zaznaczyli naszą decyzję o dodaniu tej funkcjonalności do naszej struktury. W tym celu dodajemy zewnętrzny atrybut #[derive(Debug)] przed samą definicją struktury, jak w listingu 5-12.

Plik: src/main.rs

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

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

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

Listing 5-12: Dodanie atrybutu nadającego cechę Debug i wyświetlanie instancji Rectangle formatowaniem przeznaczonym do celów debugowania

Teraz kiedy uruchomimy program nie wyskoczy nam żaden błąd, a naszym oczom ukaże się poniższy tekst:

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

No nieźle! Nie jest to może najpiękniejsza reprezentacja, ale spełnia swoje zadanie i pokazuje wartości wszystkich pól tej instancji, co zdecydowanie by pomogło gdybyśmy polowali na bugi. Kiedy w grę wchodzą większe struktury miło byłoby też mieć troszkę czytelniejszy wydruk; w takich sytuacjach możemy użyć {:#?} zamiast {:?} w makrze println!. Użycie stylu {:#?} w naszym przykładzie wypisze:

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

Another way to print out a value using the Debug format is to use the dbg! macro, which takes ownership of an expression (as opposed to println!, which takes a reference), prints the file and line number of where that dbg! macro call occurs in your code along with the resultant value of that expression, and returns ownership of the value.

Note: Calling the dbg! macro prints to the standard error console stream (stderr), as opposed to println!, which prints to the standard output console stream (stdout). We’ll talk more about stderr and stdout in the „Writing Error Messages to Standard Error Instead of Standard Output” section in Chapter 12.

Here’s an example where we’re interested in the value that gets assigned to the width field, as well as the value of the whole struct in 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);
}

We can put dbg! around the expression 30 * scale and, because dbg! returns ownership of the expression’s value, the width field will get the same value as if we didn’t have the dbg! call there. We don’t want dbg! to take ownership of rect1, so we use a reference to rect1 in the next call. Here’s what the output of this example looks like:

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

We can see the first bit of output came from src/main.rs line 10 where we’re debugging the expression 30 * scale, and its resultant value is 60 (the Debug formatting implemented for integers is to print only their value). The dbg! call on line 14 of src/main.rs outputs the value of &rect1, which is the Rectangle struct. This output uses the pretty Debug formatting of the Rectangle type. The dbg! macro can be really helpful when you’re trying to figure out what your code is doing!

Oprócz cechy Debug, Rust dostarcza cały szereg innych cech, które możemy nadać za pomocą atrybutu derive, by wzbogacić nasze typy o dodatkową funkcjonalność. Te cechy i ich zachowania opisane są w Załączniku C. Jak dodawać takim cechom własne implementacje oraz także jak tworzyć własne cechy omówimy w rozdziale 10. There are also many attributes other than derive; for more information, see the „Attributes” section of the Rust Reference.

Nasza funkcja area jest dość specyficzna: oblicza pola jedynie prostokątów. Skoro i tak nie zadziała ona z żadnym innym typem, przydatne byłoby bliższe połączenie poleceń zawartych w tej funkcji z naszą strukturą Rectangle. Kontynuacja tej refaktoryzacji zmieni funkcję area w metodę area, którą zdefiniujemy w naszym typie Rectangle.