Definiowanie i tworzenie instancji struktur
Struktury są podobne do krotek, omówionych w sekcji „Typ krotki”, w tym, że obie przechowują wiele powiązanych wartości. Podobnie jak krotki, elementy struktury mogą być różnych typów. W przeciwieństwie do krotek, w strukturze nazwiesz każdy element danych, tak aby było jasne, co oznaczają wartości. Dodanie tych nazw sprawia, że struktury są bardziej elastyczne niż krotki: nie musisz polegać na kolejności danych, aby określić lub uzyskać dostęp do wartości instancji.
Aby zdefiniować strukturę, wpisujemy słowo kluczowe struct i nadajemy nazwę całej strukturze. Nazwa struktury powinna opisywać znaczenie grupowanych danych. Następnie, w nawiasach klamrowych, definiujemy nazwy i typy elementów danych, które nazywamy polami. Na przykład, Listing 5-1 przedstawia strukturę, która przechowuje informacje o koncie użytkownika.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {}
Aby użyć struktury po jej zdefiniowaniu, tworzymy instancję tej struktury, określając konkretne wartości dla każdego z pól. Tworzymy instancję, podając nazwę struktury, a następnie dodajemy nawiasy klamrowe zawierające pary klucz: wartość, gdzie klucze to nazwy pól, a wartości to dane, które chcemy przechowywać w tych polach. Nie musimy określać pól w tej samej kolejności, w jakiej zadeklarowaliśmy je w strukturze. Innymi słowy, definicja struktury jest jak ogólny szablon dla typu, a instancje wypełniają ten szablon konkretnymi danymi, aby utworzyć wartości tego typu. Na przykład, możemy zadeklarować konkretnego użytkownika, jak pokazano w Listingu 5-2.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
Aby uzyskać konkretną wartość ze struktury, używamy notacji kropkowej. Na przykład, aby uzyskać dostęp do adresu e-mail tego użytkownika, używamy user1.email. Jeśli instancja jest mutowalna, możemy zmienić wartość, używając notacji kropkowej i przypisując ją do konkretnego pola. Listing 5-3 pokazuje, jak zmienić wartość w polu email mutowalnej instancji User.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
Zauważ, że cała instancja musi być mutowalna; Rust nie pozwala nam oznaczać tylko niektórych pól jako mutowalne. Podobnie jak w przypadku każdego wyrażenia, możemy skonstruować nową instancję struktury jako ostatnie wyrażenie w treści funkcji, aby niejawnie zwrócić tę nową instancję.
Listing 5-4 przedstawia funkcję build_user, która zwraca instancję User z podanym adresem e-mail i nazwą użytkownika. Pole active otrzymuje wartość true, a sign_in_count wartość 1.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
Logiczne jest nazwanie parametrów funkcji tak samo jak pól struktury, ale powtarzanie nazw pól email i username oraz zmiennych jest trochę uciążliwe. Gdyby struktura miała więcej pól, powtarzanie każdej nazwy stałoby się jeszcze bardziej irytujące. Na szczęście istnieje wygodna skrócona forma!
Używanie skróconej składni inicjalizacji pola
Ponieważ nazwy parametrów i nazwy pól struktury są dokładnie takie same w Listingu 5-4, możemy użyć składni skróconej inicjalizacji pola do przepisania build_user tak, aby zachowywała się dokładnie tak samo, ale nie powtarzała username i email, jak pokazano w Listingu 5-5.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
Tutaj tworzymy nową instancję struktury User, która ma pole o nazwie email. Chcemy ustawić wartość pola email na wartość z parametru email funkcji build_user. Ponieważ pole email i parametr email mają tę samą nazwę, wystarczy napisać email zamiast email: email.
Tworzenie instancji za pomocą składni aktualizacji struktury
Często przydaje się tworzenie nowej instancji struktury, która zawiera większość wartości z innej instancji tego samego typu, ale zmienia niektóre z nich. Możesz to zrobić za pomocą składni aktualizacji struktury.
Najpierw, w Listingu 5-6, pokazujemy, jak utworzyć nową instancję User w user2 w zwykły sposób, bez składni aktualizacji. Ustawiamy nową wartość dla email, ale poza tym używamy tych samych wartości z user1, które utworzyliśmy w Listingu 5-2.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
Używając składni aktualizacji struktury, możemy osiągnąć ten sam efekt za pomocą mniejszej ilości kodu, jak pokazano w Listingu 5-7. Składnia .. określa, że pozostałe pola, które nie zostały jawnie ustawione, powinny mieć taką samą wartość jak pola w danej instancji.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
Kod w Listingu 5-7 również tworzy instancję user2, która ma inną wartość dla email, ale ma te same wartości dla pól username, active i sign_in_count co user1. Instrukcja ..user1 musi znajdować się na końcu, aby określić, że wszelkie pozostałe pola powinny otrzymać swoje wartości z odpowiednich pól w user1, ale możemy wybrać, aby określić wartości dla dowolnej liczby pól w dowolnej kolejności, niezależnie od kolejności pól w definicji struktury.
Zauważ, że składnia aktualizacji struktury używa = jak przypisania; dzieje się tak dlatego, że przenosi ona dane, tak jak widzieliśmy to w sekcji „Zmienne i dane w interakcji przez przeniesienie”. W tym przykładzie nie możemy już używać user1 po utworzeniu user2, ponieważ String z pola username w user1 zostało przeniesione do user2. Gdybyśmy podali user2 nowe wartości String dla email i username, a więc użyli tylko wartości active i sign_in_count z user1, wówczas user1 byłoby nadal ważne po utworzeniu user2. Zarówno active, jak i sign_in_count to typy implementujące cechę Copy, więc zastosowałoby się zachowanie, o którym mówiliśmy w sekcji „Dane tylko na stosie: Kopia”. W tym przykładzie nadal możemy używać user1.email, ponieważ jego wartość nie została przeniesiona z user1.
Tworzenie różnych typów za pomocą struktur krotkowych
Rust obsługuje również struktury, które wyglądają podobnie do krotek, nazywane strukturami krotkowymi. Struktury krotkowe posiadają dodatkowe znaczenie, które nadaje im nazwa struktury, ale nie mają nazw powiązanych z ich polami; zamiast tego mają tylko typy pól. Struktury krotkowe są użyteczne, gdy chcesz nadać całej krotce nazwę i uczynić ją innym typem niż inne krotki, oraz gdy nazwanie każdego pola, jak w zwykłej strukturze, byłoby zbyt rozwlekłe lub redundantne.
Aby zdefiniować strukturę krotkową, zacznij od słowa kluczowego struct i nazwy struktury, a następnie podaj typy w krotce. Na przykład, tutaj definiujemy i używamy dwóch struktur krotkowych o nazwach Color i Point:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Zauważ, że wartości black i origin są różnych typów, ponieważ są instancjami różnych struktur krotek. Każda zdefiniowana przez ciebie struktura jest swoim własnym typem, mimo że pola w strukturze mogą mieć te same typy. Na przykład, funkcja, która przyjmuje parametr typu Color, nie może przyjąć Point jako argumentu, mimo że oba typy składają się z trzech wartości i32. W przeciwnym razie, instancje struktur krotek są podobne do krotek w tym sensie, że można je dekomponować na pojedyncze elementy i można użyć . po którym następuje indeks, aby uzyskać dostęp do pojedynczej wartości. W przeciwieństwie do krotek, struktury krotki wymagają podania nazwy typu struktury podczas dekompozycji. Na przykład, napisalibyśmy let Point(x, y, z) = origin;, aby dekomponować wartości punktu origin na zmienne o nazwach x, y i z.
Definiowanie struktur podobnych do jednostek
Można również definiować struktury, które nie mają żadnych pól! Nazywa się je strukturami podobnymi do jednostek, ponieważ zachowują się podobnie do (), typu jednostkowego, o którym wspominaliśmy w sekcji „Typ krotki”. Struktury podobne do jednostek mogą być użyteczne, gdy trzeba zaimplementować cechę dla jakiegoś typu, ale nie ma się żadnych danych do przechowywania w samym typie. O cechach omówimy w Rozdziale 10. Oto przykład deklaracji i tworzenia instancji struktury jednostkowej o nazwie AlwaysEqual:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Aby zdefiniować AlwaysEqual, używamy słowa kluczowego struct, wybranej nazwy, a następnie średnika. Nie ma potrzeby używania nawiasów klamrowych ani okrągłych! Następnie możemy uzyskać instancję AlwaysEqual w zmiennej subject w podobny sposób: używając zdefiniowanej nazwy, bez nawiasów klamrowych ani okrągłych. Wyobraź sobie, że później zaimplementujemy zachowanie dla tego typu, tak aby każda instancja AlwaysEqual zawsze była równa każdej instancji dowolnego innego typu, być może w celach testowych. Nie potrzebowalibyśmy żadnych danych, aby zaimplementować to zachowanie! W Rozdziale 10 dowiesz się, jak definiować cechy i implementować je na dowolnym typie, w tym na strukturach podobnych do jednostek.
Własność Danych Struktury
W definicji struktury
Userz Listingu 5-1 użyliśmy własnego typuStringzamiast typu wycinka ciągu&str. Jest to celowy wybór, ponieważ chcemy, aby każda instancja tej struktury posiadała wszystkie swoje dane, a dane te były ważne tak długo, jak ważna jest cała struktura.Możliwe jest również, aby struktury przechowywały referencje do danych należących do czegoś innego, ale do tego wymagane jest użycie czasów życia – funkcji Rust, którą omówimy w Rozdziale 10. Czasy życia zapewniają, że dane, do których odnosi się struktura, są ważne tak długo, jak ważna jest struktura. Powiedzmy, że spróbujesz przechowywać referencję w strukturze bez określania czasów życia, tak jak poniżej w src/main.rs; to nie zadziała:
struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: "someusername123", email: "someone@example.2com", sign_in_count: 1, }; }Kompilator będzie narzekał, że potrzebuje specyfikatorów czasu życia:
$ cargo run Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | username: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 ~ username: &'a str, | error[E0106]: missing lifetime specifier --> src/main.rs:4:12 | 4 | email: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 | username: &str, 4 ~ email: &'a str, | For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` (bin "structs") due to 2 previous errorsW Rozdziale 10 omówimy, jak naprawić te błędy, aby można było przechowywać referencje w strukturach, ale na razie będziemy naprawiać takie błędy, używając typów posiadanych, takich jak
String, zamiast referencji, takich jak&str.