Dane oryginału
Copyright © 2018 by Steve Klabnik and Carol Nichols, with contributions from the Rust Community. Title of English-language original: The Rust Programming Language, ISBN 978-1- 59327-828-1, published by No Starch Press. Polish-language edition copyright © 2019 by Polish Scientifi c Publishers PWN Wydawnictwo Naukowe PWN Spółka Akcyjna. All rights reserved.

Przekład Małgorzata Dąbkowska-Kowalik i Witold Sikorski
na zlecenie WITKOM Witold Sikorski

Projekt okładki polskiego wydania INT-MEDIA Dawid Mazur na podstawie oryginału

Wydawca Edyta Kawala

Redaktor prowadzący Jolanta Kowalczuk

Redaktor Irena Puchalska

Koordynator produkcji Anna Bączkowska

Skład wersji elektronicznej na zlecenie Wydawnictwa Naukowego PWN

Katarzyna Ossowska/Woblink

Zastrzeżonych nazw firm i produktów użyto w książce wyłącznie w celu identyfikacji.

Copyright © for the Polish edition by Wydawnictwo Naukowe PWN SA

Warszawa 2019

ISBN 978-83-01-20639-0

eBook został przygotowany na podstawie wydania papierowego z 2019 r., (wyd. I)

Warszawa 2019

Wydawnictwo Naukowe PWN SA

02-460 Warszawa, ul. Gottlieba Daimlera 2

tel. 22 69 54 321, faks 22 69 54 288

infolinia 801 33 33 88

e-mail: pwn@pwn.com.pl; reklama@pwn.pl

www.pwn.pl

Patroni medialni

O autorach

O autorach

Steve Klabnik jest szefem zespołu dokumentacji Rusta oraz jednym z najważniejszych deweloperów tego języka. Często zabiera głos i aktywnie uczestniczy w tworzeniu open source. Poprzednio pracował przy takich projektach jak Ruby i Ruby on Rails. Klabnik pracuje w firmie Mozilla.

Carol Nichols jest członkinią podstawowego zespołu Rusta, a także współzałożycielką pierwszej na świecie firmy konsultingu programistycznego skupionego na języku Rust, Integer 32, LLC. Nichols organizuje konferencje Rust Belt Rust Conference.

Spis treści

Spis treści

Wstęp

Podziękowania

Wprowadzenie

Dla kogo jest Rust

Zespoły deweloperskie

Studenci

Firmy

Deweloperzy open source

Ludzie ceniący szybkość i stabilność

Dla kogo jest ta książka

Jak korzystać z tej książki

Źródła i jak uczestniczyć w tworzeniu tej książki

1.
Rozpoczynamy

Instalacja

Instalacja rustup w systemach Linux i macOS

Instalacja rustup w systemie Windows

Aktualizacja i odinstalowywanie

Usuwanie błędów

Lokalna dokumentacja

Hello, world!

Tworzenie katalogu projektu

Pisanie i uruchamianie programu w języku Rust

Anatomia programu w języku Rust

Kompilacja i uruchomienie to oddzielne kroki

Witaj Cargo!

Tworzenie projektu za pomocą Cargo

Tworzenie i uruchamianie projektu w Cargo

Tworzenie gotowej wersji

Cargo jako konwencja

Podsumowanie

2.
Programowanie zgadywanki

Tworzenie nowego projektu

Przetwarzanie odpowiedzi

Zapisywanie wartości w zmiennych

Obsługa potencjalnych błędów za pomocą typu Result

Wyświetlanie wartości za pomocą symboli zastępczych w println!

Testowanie pierwszej części

Generowanie tajnej liczby

Korzystanie ze skrzynki, aby rozszerzyć funkcjonalność

Generowanie liczby losowej

Porównanie liczby odgadniętej z tajną

Dopuszczenie wielu prób za pomocą pętli

Kończenie programu po poprawnej odpowiedzi

Obsługa niepoprawnych danych na wejściu

Podsumowanie

3.
Typowe pojęcia z zakresu programowania

Zmienne i mutowalność

Różnice między zmiennymi a stałymi

Zasłanianie

Typy danych

Typy skalarne

Typy złożone

Funkcje

Parametry funkcji

Instrukcje i wyrażenia w treści funkcji

Funkcje z wartościami zwrotnymi

Komentarze

Sterowanie przepływem

Wyrażenie if

Powtarzanie w pętlach

Podsumowanie

4.
Pojęcie posiadania

Czym jest posiadanie?

Reguły posiadania

Zakres zmiennej

Typ String

Pamięć i jej przydział

Posiadanie i funkcje

Zwracane wartości i zakres

Odwołania i pożyczki

Odwołania mutowalne

Wiszące odwołania

Reguły odwołań

Typ wycinek

Wycinki łańcucha

Inne wycinki

Podsumowanie

5.
Używanie struktur do konstruowania powiązanych danych

Definiowanie struktur i tworzenie ich instancji

Używanie skrótu do inicjacji pola, gdy zmienne i pola mają takie same nazwy

Tworzenie instancji z innych instancji za pomocą składni aktualizacji struktury

Użycie struktur krotki bez nazywanych pól do tworzenia różnych typów

Struktury jako jednostki bez żadnych pól

Przykład programu z użyciem struktury

Refaktoryzacja za pomocą krotek

Refaktoryzacja za pomocą struktur

Dodawanie użytecznej funkcjonalności za pomocą wyprowadzonych cech

Składnia metody

Definiowanie metod

Metody z większą liczbą parametrów

Funkcje powiązane

Wiele bloków impl

Podsumowanie

6.
Typ wyliczeniowy i dopasowanie wzorca

Definiowanie wyliczenia

Wartości wyliczeń

Wyliczenie Option i jego zalety w porównaniu z wartościami null

Operator sterowania przepływem match

Wzorce, które są związane z wartościami

Dopasowywanie za pomocą Option <T>

Dopasowania wyczerpują wszystkie przypadki

Symbol zastępczy _

Sterowanie przepływem za pomocą if let

Podsumowanie

7.
Wykorzystanie modułów do porządkowania kodu i jego ponownego wykorzystywania

mod i Filesystem

Definicje modułów

Przenoszenie modułów do innych plików

Reguły systemów plików modułów

Sterowanie widocznością za pomocą pub

Ustawienie funkcji jako publicznej

Reguły prywatności

Przykłady prywatności

Odwoływanie się do nazw w różnych modułach

Wprowadzanie nazw do zakresu za pomocą słowa kluczowego use

Wprowadzanie wszystkich nazw do zakresu za pomocą glob

Użycie super do uzyskiwania dostępu do modułu nadrzędnego

Podsumowanie

8.
Typowe kolekcje

Przechowywanie list wartości za pomocą wektorów

Tworzenie nowego wektora

Uaktualnianie wektora

Usunięcie wektora usuwa jego elementy

Czytanie elementów wektora

Iterowanie po wartościach w wektorze

Użycie wyliczania do przechowywania wielu typów

Przechowywanie w łańcuchach tekstu zakodowanego za pomocą UTF-8

Czym jest łańcuch?

Tworzenie nowego łańcucha

Aktualizacja łańcucha

Indeksowanie łańcuchów

Dzielenie łańcuchów na wycinki

Metody iterowania po łańcuchach

Łańcuchy nie są takie proste

Przechowywanie kluczy z powiązanymi wartościami w mapach skrótów

Tworzenie nowej mapy skrótów

Mapy skrótów a posiadanie

Dostęp do wartości w mapie skrótów

Aktualizacja mapy skrótów

Funkcje skrótu

Podsumowanie

9.
Obsługa błędów

Błędy nienaprawialne z makrem panic!

Użycie śladu panic!

Błędy do naprawienia za pomocą Result

Dopasowywanie przy różnych błędach

Skróty do paniki przy błędzie: unwrap i expect

Propagowanie błędów

Panikować czy nie panikować

Przykłady, prototypowy kod i testy

Przypadki, w których mamy więcej informacji niż kompilator

Wskazówki dotyczące obsługi błędów

Tworzenie niestandardowych typów do celów sprawdzania

Podsumowanie

10.
Typy generyczne, cechy i czasy życia

Usuwanie duplikacji przez wyodrębnienie funkcji

Generyczne typy danych

W definicjach funkcji

Definicje w strukturze

W definicjach wyliczeń

W definicjach metod

Wydajność kodu z użyciem typów generycznych

Cechy – definiowanie wspólnego zachowania

Definiowanie cechy

Implementowanie cechy na typie

Implementacje domyślne

Granice cech

Naprawa funkcji largest za pomocą granic cech

Używanie granic cech do metod implementowanych warunkowo

Sprawdzanie odwołań za pomocą czasów życia

Zapobieganie „wiszącym” odwołaniom za pomocą czasów życia

Kontroler pożyczek

Generyczne czasy życia w funkcjach

Składnia adnotacji dla czasu życia

Adnotacje o czasie życia w sygnaturach funkcji

Myślenie w kategoriach czasów życia

Adnotacje o czasie życia w definicjach struktur

Pominięcie czasu życia

Adnotacje o czasie życia w definicjach metod

Statyczny czas życia

Parametry generycznego typu, granice cech i czas życia w połączeniu

Podsumowanie

11.
Pisanie automatycznych testów

Jak pisać testy

Anatomia funkcji testowania

Sprawdzanie wyników za pomocą makra assert!

Testowanie równości za pomocą makr assert_eq! i assert_ne!

Dodawanie niestandardowych komunikatów

Testowanie kodu pod kątem paniki za pomocą should_panic

Sterowanie sposobem uruchamiania testów

Uruchamianie testów równolegle lub po kolei

Pokazywanie wyników funkcji

Uruchomienie podzbioru testów według nazwy

Pomijanie niektórych testów, jeśli nie zostaną konkretnie wymienione

Organizacja testów

Testy jednostkowe

Testy integracyjne

Podsumowanie

12.
Projekt we/wy – budowa programu wiersza poleceń

Akceptowanie argumentów wiersza poleceń

Czytanie wartości argumentów

Zapisywanie wartości argumentów w zmiennych

Czytanie pliku

Refaktoryzacja w celu poprawienia modułowości i obsługi błędów

Oddzielanie problemów w projektach binarnych

Naprawa obsługi błędów

Wyodrębnianie kodu z main

Podział kodu do skrzynki bibliotecznej

Tworzenie funkcjonalności biblioteki przy użyciu TDD

Pisanie testu zakończonego niepowodzeniem

Pisanie testu, który przejdzie

Praca ze zmiennymi środowiskowymi

Pisanie testu zakończonego niepowodzeniem dla funkcji nierozróżniających wielkości liter

Implementacja funkcji search_case_insensitive

Pisanie komunikatów o błędach do standardowego błędu zamiast standardowego wyjścia

Sprawdzanie, gdzie są zapisywane błędy

Zapisywanie błędów do standardowego błędu

Podsumowanie

13.
Funkcje języka funkcyjnego: iteratory i zamknięcia

Zamknięcia – anonimowe funkcje, które mogą przechwycić swoje środowisko

Tworzenie abstrakcji zachowania za pomocą zamknięć

Wnioskowanie o typie zamknięcia i adnotacje

Zapisywanie zamknięć z użyciem parametrów generycznych i cech Fn

Ograniczenia implementacji Cacher

Przechwytywanie środowiska za pomocą zamknięć

Przetwarzanie ciągów elementów za pomocą iteratorów

Cecha Iterator i metoda next

Metody, które zużywają iterator

Metody tworzące inne iteratory

Wykorzystanie zamknięć, które przechwytują swoje środowisko

Tworzenie własnych iteratorów za pomocą cechy Iterator

Ulepszanie naszego projektu we/wy

Usuwanie klonu za pomocą iteratora

Bardziej przejrzysty kod dzięki adapterom iteratora

Porównywanie wydajności – pętle a iteratory

Podsumowanie

14.
Więcej informacji o Cargo i Crates.io

Wersje niestandardowe z profilami wydania

Publikacja skrzynki w Crates.io

Dokonywanie użytecznych komentarzy dokumentujących

Eksport wygodnego publicznego API za pomocą pub use

Tworzenie konta Crates.io

Dodawanie metadanych do nowej skrzynki

Publikowanie w Crates.io

Publikowanie nowej wersji istniejącej skrzynki

Usuwanie wersji z Crates.io za pomocą cargo yank

Przestrzenie robocze Cargo

Tworzenie przestrzeni roboczej

Tworzenie drugiej skrzynki w przestrzeni roboczej

Instalowanie wersji binarnych z Crates.io za pomocą cargo install

Rozszerzanie Cargo za pomocą niestandardowych poleceń

Podsumowanie

15.
Inteligentne wskaźniki

Używanie Box<T> do wskazywania danych na kopcu

Używanie Box<T> do zapisu danych na kopcu

Włączenie typów rekurencyjnych z pudełkami

Traktowanie inteligentnych wskaźników jak zwykłych odwołań z cechą Deref

Podążanie za wskaźnikiem do wartości z użyciem operatora wyłuskiwania

Używanie Box<T> jak odwołania

Definiowanie własnego inteligentnego wskaźnika

Traktowanie typu jak odwołania dzięki implementacji cechy Deref

Niejawne wymuszanie Deref z funkcjami i metodami

Jak wymuszanie Deref współdziała z mutowalnością

Uruchamianie kodu czyszczącego z cechą Drop

Wczesne odrzucanie wartości z zastosowaniem std::mem::drop

Rc<T> – inteligentny wskaźnik ze zliczaniem odwołań

Używanie Rc<T> do współdzielenia danych

Klonowanie Rc<T> zwiększa licznik odwołań

RefCell<T> oraz wzorzec wewnętrznej mutowalności

Wymuszanie reguł pożyczania podczas wykonania za pomocą RefCell<T>

Mutowalność wewnętrzna – mutowalne pożyczanie niemutowalnej wartości

Wielu posiadaczy zmiennych danych dzięki połączeniu Rc<T> i RefCell<T>

Odwołania cykliczne mogą prowadzić do wycieku pamięci

Tworzenie odwołań cyklicznych

Zapobieganie odwołaniom cyklicznym – zmiana Rc<T> na Weak<T>

Podsumowanie

16.
Współbieżność bez obaw

Użycie wątków do równoległego uruchamiania kodu

Tworzenie nowego wątku za pomocą spawn

Oczekiwanie, aż wszystkie wątki się zakończą, z użyciem JoinHandle

Korzystanie z zamknięcia move z wątkami

Używanie przekazywania komunikatów do transferu danych między wątkami

Kanały i przeniesienie własności

Wysyłanie wielu wartości i oczekujący nadajnik

Tworzenie wielu producentów przez klonowanie nadajnika

Współbieżność ze współdzieleniem zasobów

Wykorzystanie muteksów w celu zezwolenia na dostęp do danych z jednego wątku naraz

Podobieństwa między RefCell<T>/Rc<T> a Mutex<T>/Arc<T>

Elastyczna współbieżność z cechami Sync i Send

Pozwolenie na przenoszenie posiadania między wątkami za pomocą Send

Pozwolenie na dostęp z wielu wątków za pomocą Sync

Ręczne implementowanie Send i Sync nie jest bezpieczne

Podsumowanie

17.
Własności programowania obiektowego w języku Rust

Charakterystyka języków obiektowych

Obiekty zawierają dane i działanie

Enkapsulacja, która ukrywa szczegóły implementacji

Dziedziczenie jako system typów i jako współdzielenie kodu

Wykorzystywanie obiektów cech, które dopuszczają wartości różnych typów

Definiowanie cechy dla jednolitego zachowania

Implementowanie cechy

Obiekty cech wykonują dynamiczne wysyłki

Bezpieczeństwo obiektowe jest wymagane dla obiektów cech

Implementowanie wzorca projektu obiektowego

Definiowanie postu i tworzenie nowej instancji w stanie Draft

Przechowywanie tekstu treści postu

Gwarantowanie, że treść projektu postu jest pusta

Żądanie oceny postu zmienia jego stan

Dodawanie metody approve, która zmienia zachowanie treści

Kompromisy wzorca stanu

Podsumowanie

18.
Wzorce i dopasowywanie

Wszystkie miejsca, w których można korzystać ze wzorców

Gałęzie match

Wyrażenia warunkowe if let

Warunkowe pętle while let

Pętle for

Instrukcja let

Parametry funkcji

Podważalność – czy dopasowanie do wzorca może się nie udać

Składnia wzorca

Dopasowywanie literałów

Dopasowywanie nazwanych zmiennych

Wiele wzorców

Dopasowywanie zakresów wartości za pomocą składni ...

Destrukturyzacja w celu rozdzielenia wartości

Pomijanie wartości we wzorcu

Tworzenie odwołań we wzorcach za pomocą ref i ref mut

Dodatkowe instrukcje warunkowe ze strażnikami dopasowania

Wiązania @

Podsumowanie

19.
Funkcje zaawansowane

Niebezpieczny Rust

Niebezpieczne super możliwości

Wyłuskiwanie pierwotnego wskaźnika

Wywoływanie niebezpiecznej funkcji lub metody

Uzyskiwanie dostępu do modyfikowalnej zmiennej statycznej lub jej modyfikowanie

Implementacja niebezpiecznej cechy

Kiedy korzystać z kodu, który nie jest bezpieczny

Zaawansowane czasy życia

Podtypy czasów życia jako gwarancja, że jeden czas życia jest dłuższy od innego

Granice czasu życia w odwołaniach do typów generycznych

Wnioskowanie o czasach życia obiektów cech

Zaawansowane cechy

Określanie typów symboli zastępczych w definicjach cech z powiązanymi typami

Domyślne parametry typów generycznych i przeciążenie operatora

Składnia w pełni kwalifikowana dla jednoznaczności – wywoływanie metod z taką samą nazwą

Użycie super cech, aby wymagać funkcjonalności jednej cechy wewnątrz innej cechy

Użycie wzorca newtype do implementacji zewnętrznych cech na zewnętrznych typach

Typy zaawansowane

Użycie wzorca newtype dla bezpieczeństwa typów i abstrakcji

Tworzenie synonimów typu z aliasami typów

Typ „nigdy”, który nigdy niczego nie zwraca

Typy o dynamicznie określanych rozmiarach i cecha Sized

Zaawansowane funkcje i zamknięcia

Wskaźniki funkcji

Zwracanie zamknięć

Podsumowanie

20.
Ostatni projekt – budowanie wielowątkowego serwera WWW

Budowanie jednowątkowego serwera WWW

Nasłuchiwanie połączeń TCP

Czytanie żądania

Bliższe spojrzenie na żądanie HTTP

Pisanie odpowiedzi

Zwracanie rzeczywistego HTML

Sprawdzanie poprawności żądania i selektywne odpowiadanie

Nieco refaktoryzacji

Zamiana serwera jednowątkowego na wielowątkowy

Symulowanie powolnego żądania w bieżącej implementacji serwera

Poprawienie przepustowości za pomocą puli wątków

Płynne zakończenie i czyszczenie

Implementacja cechy Drop na ThreadPool

Sygnalizowanie wątkom, aby przestały nasłuchiwać zadań

Podsumowanie

A.
Słowa kluczowe

Słowa kluczowe obecnie stosowane

Słowa kluczowe zarezerwowane do użycia w przyszości

B.
Operatory i symbole

Operatory

Symbole inne niż operatory

C.
Cechy wyprowadzone

Debugowanie wyjścia dla programisty

PartialEq i Eq do porównań równościowych

PartialOrd i Ord do porównywania kolejności

Klonowanie i kopiowanie zduplikowanych wartości

Cecha Hash do odwzorowania wartości na wartość o ustalonym rozmiarze

Cecha Default dla wartości domyślnych

D.
Makra

Różnica między makrami a funkcjami

Makra deklaratywne z macro_rules! do celów ogólnego metaprogramowania

Makra proceduralne do celów niestandardowych wyprowadzeń

Przyszłość makr

Przypisy

WSTĘP

Nie zawsze było to oczywiste, lecz język programowania Rust ma przede wszystkim związek z upoważnianiem – niezależnie od tego, jaki rodzaj kodu piszecie, Rust daje upoważnienie do sięgania dalej, do programowania z zaufaniem w szerszym zestawie dziedzin, niż to robiliśmy dotychczas.

Weźmy jako przykład pracę „na poziomie systemowym”, która dotyczy szczegółów zarządzania pamięcią na niskim poziomie, reprezentacji danych i współbieżności. Tradycyjnie ten obszar programowania jest postrzegany jako arkana dostępne tylko dla niewielkiej grupy wtajemniczonych, którzy poświęcili lata na naukę unikania wstydliwych wpadek. A nawet osoby, które mają w tym praktykę, robią to ostrożnie, aby ich kod nie był otwarty na eksploity, katastrofy i uszkodzenia.

Rust przełamuje te bariery, eliminując stare pułapki i zapewniając nam przyjazny, elegancki zestaw narzędzi. Programiści, którzy muszą się zagłębić w sterowanie niskiego poziomu, mogą to zrobić w języku Rust, bez typowego ryzyka katastrofy lub luk w zabezpieczeniach, a także bez konieczności nauki szczegółów kapryśnych narzędzi. Co więcej, język jest zaprojektowany tak, aby w naturalny sposób kierować nas do niezawodnego kodu, który jest efektywny w zakresie szybkości i wykorzystania pamięci.

Programiści, którzy pracują już w kodzie niskiego poziomu, mogą użyć Rusta, aby zmierzyć się z czymś nowym. Na przykład wprowadzenie równoległości to w języku Rust działanie dość niskiego ryzyka – kompilator wychwyci za nas klasyczne błędy. Możemy więc wziąć się za bardziej agresywną optymalizację kodu, mając pewność, że nie wprowadzimy przypadkiem awarii ani eksploitów.

Ale Rust nie ogranicza się do programowania niskiego poziomu. Jest wystarczająco wyrazisty i ergonomiczny, aby tworzyć aplikacje wiersza poleceń, serwery sieciowe i wiele innych rodzajów kodu, który jest przyjemny w pisaniu – proste przykłady obu elementów znajdziecie w dalszej części książki. Praca z językiem Rust pozwala budować umiejętności, które można przenosić między dziedzinami. Możemy nauczyć się Rusta, pisząc aplikację sieciową, a potem wykorzystać te umiejętności do celów naszego Raspberry Pi.

Książka ta obejmuje cały potencjał Rusta, jaki daje on swoim użytkownikom. Ma przyjazną i dostępnie napisaną treść, która nie tylko pomaga podnieść poziom wiedzy o języku Rust, lecz także zwiększyć zaufanie do siebie jako programisty. Zagłębmy się więc w lekturze i zacznijmy naukę – witamy w społeczności Rusta!

Nicholas Matsakis i Aaron Turon

PODZIĘKOWANIA

Chcielibyśmy podziękować wszystkim, którzy pracowali z nami nad językiem Rust, tworząc wspaniały język warty napisania o nim książki. Jesteśmy wdzięczni każdemu ze społeczności Rusta, że są otwarci i tworzą środowisko, do którego należałoby zaprosić większą liczbę osób.

Szczególnie wdzięczni jesteśmy tym, którzy przeczytali początkowe wersje tej książki online oraz przekazali nam uwagi, raporty o błędach i prośby dotyczące zmian. Specjalne podziękowania kierujemy do Eduarda-Mihaiła Burtescu i Alexa Crichtona za opinię techniczną, a Karen Rustad Tölvy za grafikę na okładce. Dziękujemy naszemu zespołowi z No Starch, w tym Billowi Pollockowi, Liz Chadwick i Janelle Ludowise, za poprawienie tej książki i doprowadzenie jej do druku.

Steve chciałby podziękować Carol za to, że była wspaniałą współautorką. Bez niej ta książka miałaby znacznie gorszą jakość, a jej pisanie zajęłoby znacznie więcej czasu. Ponadto dziękuję Ashley Williams, która udzialała nam ogromnego wsparcia od początku aż do końca pisania książki. Carol chciałaby podziękować Steve’owi za rozbudzenie jej zainteresowania Rustem i za możliwość pracy nad tą książką. Jest wdzięczna swojej rodzinie, zwłaszcza mężowi Jake’owi Gouldingowi i córce Vivian za nieustającą miłość i wsparcie.

WPROWADZENIE

Witamy w książce Programowanie w języku Rust będącej wprowadzeniem do tego języka. Rust to język programowania, który pomaga szybciej pisać niezawodne oprogramowanie. Ergonomia wysokiego poziomu i sterowanie na niskim poziomie to często sprzeczności w projektach języków programowania. Rust stawia wyzwanie temu konfliktowi. Dzięki zrównoważeniu silnych możliwości technicznych i dużego doświadczenia deweloperskiego, Rust daje możliwość kontrolowania szczegółów niskiego poziomu (jak wykorzystanie pamięci) bez kłopotów tradycyjnie związanych z taką kontrolą.

Dla kogo jest Rust

Rust jest idealny dla wielu ludzi z różnych powodów. Przejrzyjmy najważniejsze ich grupy.

Zespoły deweloperskie

Rust sprawdza się jako produktywne narzędzie pracy dużych zespołów deweloperów na różnym poziomie wiedzy o programowaniu systemów. Kod niskiego poziomu jest podatny na wiele wyrafinowanych błędów, które w większości języków można wychwycić tylko dzięki intensywnemu testowaniu i uważnemu sprawdzaniu kodu przez doświadczonych deweloperów. W języku Rust kompilator gra rolę bramkarza, odmawiając skompilowania kodu z tymi problematycznymi błędami, w tym z błędami współbieżności. Dzięki pracy z kompilatorem zespół może poświęcić czas na skupienie się na schemacie logiczym programu, a nie na wyszukiwanie błędów.

Rust oferuje także współczesne narzędzia programistyczne do programowania systemowego:

Cargo, menedżer zależności i narzędzie do tworzenia oprogramowania, sprawia, że dodawanie, kompilowanie i zarządzenia zależnościami jest „bezbolesne” i spójne w całym ekosystemie Rusta;

Rustfmt zapewnia spójny styl kodowania różnych deweloperów;

Rust Language Server zasila integrację IDE (Integrated Development Environment) w celu tworzenia kodu i wewnętrznych komunikatów o błędach.

Korzystając z tych i innych narzędzi w ekosystemie Rusta, deweloperzy mogą być wydajni, pisząc na poziomie systemowym.

Studenci

Rust jest przeznaczony dla studentów i tych, którzy są zainteresowani nauką pojęć systemowych. Korzystając z Rusta, wiele osób poznało takie pojęcia, jak tworzenie systemów operacyjnych. Społeczność Rusta chętnie przyjmuje studentów i z radością odpowiada na ich pytania. Dzięki takim przedsięwzięciom jak ta książka zespoły Rusta chcą, aby pojęcia systemowe były bardziej dostępne dla większej liczby osób, zwłaszcza tych rozpoczynających programowanie.

Firmy

Setki firm, małych i dużych, używa Rusta do wielu zadań. Zadania te obejmują narzędzia wiersza poleceń, usługi sieciowe, narzędzia DevOps, wbudowane urządzenia, analizę i transkrypcje audio o wideo, kryptowaluty, bioinformatykę, mechanizmy wyszukiwania, Internet rzeczy, a nawet duże fragmenty przeglądarki Firefox.

Deweloperzy open source

Rust jest dla osób, które chcą budować język programowania, jego społeczność, narzędzia deweloperskie i biblioteki. Chcielibyśmy, aby każdy dokładał swoją cegiełkę do języka Rust.

Ludzie ceniący szybkość i stabilność

Rust jest dla tych, którzy wymagają od języka szybkości i stabilności. Przez szybkość rozumiemy szybkość programów, które możemy tworzyć, i szybkość, z jaką Rust pozwala nam je pisać. Sprawdzenie dokonywane przez kompilator Rusta zapewnia stabilność dzięki dodawaniu funkcji i refaktoryzacji. To kontrastuje z wątłym starszym kodem w językach niemających takiej kontroli, a które deweloperzy często boją się modyfikować. Dzięki dążeniu do abstrakcji o zerowych kosztach, funkcjom wyższego poziomu, które kompilują się do kodu niskiego poziomu, a także szybkiemu kodowi pisanemu ręcznie twórcy Rusta starają się tworzyć bezpieczniejszy kod, który jednocześnie jest szybszy.

Społeczność Rusta chce też wspierać innych użytkowników. Wymienieni tu są po prostu najważniejszymi interesariuszami. Ogólnie największą ambicją twórców Rusta jest wyeliminowanie kompromisów, które programiści akceptowali przez całe dekady, zapewniając bezpieczeństwo i produktywność, szybkość i ergonomię. Wypróbujcie Rusta i sprawdźcie, czy będzie wam odpowiadać.

Dla kogo jest ta książka

W książce założono, że czytelnicy pisali dotyczczas kod w innym języku programowania, ale nie ma to znaczenia, w jakim. Chcieliśmy, aby materiał był szeroko dostępny dla osób z różnym doświadczeniem programistycznym. Nie poświęcamy wiele czasu na tłumaczenie, czym jest programowanie lub jak o nim myśleć. Osoby, które są nowicjuszami w programowaniu, powinny raczej sięgnąć po książkę poświęconą wprowadzeniu do programowania.

Jak korzystać z tej książki

Ogólnie przyjęto, że książka będzie czytana od początku do końca. Kolejne rozdziały są oparte na pojęciach z rozdziałów wcześniejszych, a te wcześniejsze mogą nie zawierać szczegółów danego tematu. Zwykle wracamy do niego w dalszych rozdziałach.

W książce są dwa rodzaje rozdziałów: rozdziały dotyczące pojęć oraz projektów. W rozdziałach dotyczących pojęć uczymy się danego aspektu języka Rust. W rozdziałach z projektami budujemy razem niewielki program, korzystając z tego, czego nauczyliśmy się do tej pory. Rozdziały 1, 12 i 20 to rozdziały zawierające projekty. Pozostałe to rozdziały poświęcone pojęciom.

W rozdziale 1 wyjaśniamy, jak zainstalować Rusta, jak napisać program Hello, world! oraz jak używać Cargo – menedżera pakietów i systemu tworzenia oprogramowania w języku Rust. Rozdział 2 to praktyczne wprowadzenie do języka. Omawiamy tu pojęcia wysokiego poziomu, a kolejne rozdziały zawierają potrzebne szczegóły. Jeśli od razu chcemy zająć się praktyką, rozdział 2 świetnie się do tego nadaje. Z początku można mieć ochotę przeskoczyć rozdział 3, w którym przedstawiamy funkcje Rusta, podobne jak innych języków programowania, i od razu przejść do rozdziału 4, aby poznać system posiadania Rusta. Uczniowie metodyczni, którzy wolą poznać szczegóły zanim przejdą dalej, mogą pominąć rozdział 2 i przejść do rozdziału 3, wracając do rozdziału 2, gdy zechcą pracować nad projektem, stosując poznane szczegóły.

W rozdziale 5 omawiamy struktury i metody, a w rozdziale 6 – wyliczenia, wyrażenia match i konstrukcję sterującą if let. Struktur i wyliczeń będziemy używać do tworzenia niestandardowych typów w języku Rust.

W rozdziale 7 poznamy system modułów Rusta i reguły prywatności do celów organizacji kodu i jego interfejsu API (Application Programming Interface). W rozdziale 8 omawiamy typowe struktury danych kolekcji, które zapewnia standardowa biblioteka, takie jak wektory, łańcuchy oraz mapy skrótów. Rozdział 9 dotyczy filozofii i techniki obsługi błędów.

W rozdziale 10 zagłębiamy się w elementy generyczne, cechy i czas życia, co daje nam możliwość definiowania kodu, który stosuje się do wielu typów. Rozdział 11 dotyczy testowania, co nawet przy gwarancjach bezpieczeństwa Rusta jest konieczne do stwierdzenia, czy kod programu jest poprawny. W rozdziale 12 tworzymy własną implementację podzbioru funkcjonalności z narzędzia wiersza poleceń grep, które wyszukuje tekst w plikach. W tym celu korzystamy z wielu pojęć omawianych w poprzednich rozdziałach.

Rozdział 13 zawiera analizę zamknięć i iteratorów – funkcji Rusta, które wywodzą się z funkcyjnych języków programowania. W rozdziale 14 analizujemy dokładniej Cargo i omawiamy najlepsze praktyki współdzielenia naszych bibliotek z innymi. W rozdziale 15 przedstawiamy inteligentne wskaźniki, których dostarcza standardowa biblioteka, oraz cechy, które umożliwiają ich funkcjonalność.

W rozdziale 16 analizujemy różne modele programowania współbieżnego i mówimy o tym, jak Rust pomaga nam bez obaw programować wiele wątków. W rozdziale 17 porównujemy idiomy Rusta z zasadami programowania obiektowego, które możecie znać.

Treść rozdziału 18 odwołuje się do wzorców i do porównywania z nimi, co stanowi dający wiele możliwości sposób wyrażania idei w programach języka Rust. Rozdział 19 zawiera duży wybór zaawansowanych tematów, w tym niebezpieczny (unsafe) Rust i więcej informacji o czasach życia, cechach, typach, funkcjach i zamknięciach.

W rozdziale 20 tworzymy projekt, w którym implementujemy na niskim poziomie wielowątkowy serwer sieciowy!

Wreszcie dodatki zawierają użyteczne informacje o języku w postaci zestawień. W dodatku A omawiamy słowa kluczowe Rusta, w dodatku B – operatory i symbole, w dodatku C – wyprowadzane cechy dostępne w bibliotece standardowej, a dodatek D dotyczy makr.

Nie ma złego sposobu czytania tej książki – jeśli chcecie przeskoczyć do przodu, proszę bardzo! Może będziecie musieli wrócić do wcześniejszych rozdziałów, jeśli coś będzie niejasne. Róbcie to, co jest dla was najlepsze.

Ważną częścią nauki języka Rust jest wiedza o tym, jak czytać komunikaty o błędach wyświetlane przez kompilator – one prowadzą nas do działającego kodu. Dlatego podajemy wiele przykładów kodu, który nie kompiluje się wraz z komunikatem o błędzie pokazywanym przez kompilator w każdej sytuacji. Trzeba wiedzieć, że jeśli napiszemy i uruchomimy przypadkowy przykład, może się on nie skompilować! Trzeba przeczytać sąsiadujący tekst, aby sprawdzić, czy przykład, który próbujemy uruchomić, może prowadzić do błędu. W większości sytuacji doprowadzimy was do poprawnej wersji każdego kodu, który się nie kompiluje.

Źródła i jak uczestniczyć w tworzeniu tej książki

Książka ta jest open source. Jeśli znajdziecie błąd, nie wahajcie się go opisać lub wysłać żądanie uzupełnienia na GitHub pod adresem https://github.com/rust-lang/book/. Sprawdźcie CONTRIBUTING.md na stronie https://github.com/rust-lang/book/blob/master/CONTRIBUTING.md, aby poznać tam więcej szczegółów.

Kod źródłowy do przykładów z tej książki, errata i inne informacje są dostępne pod adresem https://www.nostarch.com/Rust/.

1

ROZPOCZYNAMY

Zacznijmy naszą podróż po języku Rust! Mamy wiele do nauki, ale każda podróż ma swój początek. W tym rozdziale omówimy:

• instalację języka Rust w systemach Linux, macOS i Windows;

• pisanie programu, który wyświetla nam Hello, world!;

• wykorzystanie Cargo – menedżera pakietu i systemu tworzenia oprogramowania w języku Rust.

Instalacja

Pierwszym krokiem jest instalacja Rusta. Pobierzemy go przez rustup, narzędzie wiersza poleceń do zarządzania wersjami Rusta i powiązanymi z nim narzędziami. Pobranie wymaga połączenia z Internetem.

UWAGA

Jeśli z jakiegoś powodu nie chcecie korzystać z rustup, należy zajrzeć na stronę instalacyjną Rusta, pod adresem https://www.rust-lang.org/install.html, gdzie można znaleźć inne opcje.

Podane niżej kroki pozwalają zainstalować stabilną wersję kompilatora Rust. Wszystkie przykłady i wyniki pokazane w książce są oparte na stabilnej wersji Rust 1.21.0. Stabilność Rusta gwarantuje, że wszystkie przykłady z książki dadzą się skompilować także w nowszych wersjach języka. Wyniki mogą się nieco różnić, zależnie od wersji, gdyż Rust często ma wprowadzane poprawki komunikatów o błędach i ostrzeżenia. Innymi słowy, każda nowsza, stabilna wersja Rusta, którą zainstalujemy, korzystając z podanych tu kroków, powinna działać zgodnie z oczekiwaniami przedstawionymi w książce.

Notacja wiersza poleceń

W tym rozdziale oraz w całej książce pokażemy niektóre polecenia używane z poziomu terminalu. Wiersze poleceń, które wprowadzamy z terminalu, muszą się zaczynać od znaku $. Nie musimy go wpisywać, gdyż wskazuje on początek każdego polecenia. Wiersze, które nie rozpoczynają się od znaku $, pokazują zwykle wyniki poprzedniego polecenia. Ponadto przykłady specyficzne dla Power- Shell będą używać znaku > zamiast znaku $.

Instalacja rustup w systemach Linux i macOS

Jeśli używamy systemów Linux lub macOS, musimy otworzyć terminal i wprowadzić poniższe polecenie:

Polecenie pobiera skrypt i rozpoczyna instalację narzędzia rustup, które instaluje najnowszą stabilną wersję Rusta. Możemy zostać poproszeni o hasło. Jeśli instalacja się powiedzie, pojawi się pokazany niżej wiersz:

(Rust został zainstalowany. Wspaniale!)

Jeśli chcemy, możemy pobrać skrypt i sprawdzić go przed uruchomieniem.

Przy kolejnym logowaniu skrypt instalacyjny automatycznie dodaje Rust do naszej systemowej ścieżki PATH. Jeśli chcemy zacząć korzystać z Rusta od razu, bez restartowania terminalu, w naszej powłoce musimy uruchomić poniższe polecenie, aby ręcznie dodać Rust do naszej ścieżki systemowej PATH:

Alternatywnie możemy dodać następujący wiersz do naszego ~/.bash_profile:

Ponadto potrzebny nam będzie jakiegoś rodzaju konsolidator. Być może macie już jakiś zainstalowany, ale przy próbie kompilacji programu w języku Rust mogą się pojawić błędy mówiące, że konsolidator nie może działać, co będzie oznaczać brak odpowiedniej instalacji, i wtedy trzeba zainstalować go ręcznie. Kompilatory języka C są zwykle dostarczane z poprawnym konsolidatorem. Należy sprawdzić w dokumentacji, jak zainstalować kompilator języka C. Ponadto niektóre popularne pakiety języka Rust są oparte na kodzie C i kompilator C będzie nam potrzebny. Dlatego warto zainstalować go od razu.

Instalacja rustup w systemie Windows

W systemie Windows należy przejść do witryny https://www.rust-lang.org/install.html i postępować zgodnie z instrukcjami instalacji języka Rust. W jakimś punkcie instrukcji otrzymamy komunikat informujący, że potrzebne są nam także narzędzia instalacyjne C++ dla Visual Studio 2013 lub w wersji nowszej. Najprostszym sposobem pozyskania narzędzi instalacyjnych jest zainstalowanie Build Tools for Visual Studio 2017 z witryny https://www.visualstudio.com/downloads/. Narzędzia te są dostępne w części Other Tools and Frameworks (Inne narzędzia i schematy). W dalszej części książki są używane polecenia, które działają zarówno z cmd.exe, jak i z PowerShell. Jeśli wystąpią jakieś różnice, wyjaśnimy, której wersji używać.

Aktualizacja i odinstalowywanie

Po zainstalowaniu Rusta za pośrednictwem rustup aktualizacja do najnowszej wersji jest łatwa. Z naszej powłoki uruchamiamy następujący skrypt aktualizacyjny:

Aby odinstalować Rusta i rustup, uruchamiamy w naszej powłoce poniższy skrypt dezinstalacyjny:

Usuwanie błędów

Aby sprawdzić, czy Rust został poprawnie zainstalowany, otwieramy powłokę i wpisujemy wiersz polecenia:

Powinniśmy zobaczyć numer wersji, skrót wydania oraz datę wydania ostatniej stabilnej wersji. Format komunikatu jest następujący:

Jeśli zobaczymy taką informację, to znaczy, że Rust został poprawnie zainstalowany. Jeśli informacja się nie pojawi, a jesteśmy w systemie Windows, musimy sprawdzić, czy Rust znajduje się w naszej zmiennej systemowej %PATH%. Jeśli wszystko jest tu poprawne, a Rust nadal nie działa, to pomoc można znaleźć w kilku miejscach. Najprostsze jest skorzystanie z kanału IRC #rust na witrynie irc.mozilla.org, do której można dotrzeć przez Mibbit pod adresem http://chat.mibbit.com/?server=irc.mozilla.org&channel=%23rust/. Pod tym adresem można pogawędzić z Rustarianami (to przydomek, jaki sobie nadaliśmy), którzy pomogą nam rozwiązać problem. Innym świetnym zasobem jest forum Users pod adresem https://users.rust-lang.org/ oraz Stack Overflow pod adresem http://stackoverflow.com/questions/tagged/rust/.

Lokalna dokumentacja

Instalator obejmuje także lokalną kopię dokumentacji, więc można ją czytać w trybie offline. Aby otworzyć lokalną dokumentację, musimy w naszej przeglądarce uruchomić rustup doc.

Za każdym razem, gdy typ lub funkcja są dostarczone przez standardową bibliotekę, a my nie mamy pewności, co ona robi lub jak z niej korzystać, możemy użyć dokumentacji interfejsu programowania aplikacji (API), aby się tego dowiedzieć.

Hello, world!

Gdy już mamy zainstalowany Rust, pora na napisanie pierwszego programu. Tradycją przy nauce nowego języka jest napisanie małego programu, który wyświetla na ekranie tekst Hello, world!.

UWAGA

W książce przyjęto, że czytelnicy umieją posługiwać się wierszem poleceń. Rust nie ma szczególnych wymagań dotyczących edycji, narzędzi ani miejsca położenia kodu, więc można też bez przeszkód korzystać z zintegrowanego środowiska deweloperskiego (integrated development environment, IDE) zamiast z wiersza poleceń. Wiele IDE zawiera obecnie elementy obsługi Rusta. Szczegóły można sprawdzić w dokumentacji. Obecnie zespół Rusta skupia się na umożliwieniu szerszej obsługi w IDE i w tym obszarze następuje szybki postęp.

Tworzenie katalogu projektu

Zaczniemy od utworzenia katalogu, w którym będzie przechowywany nasz kod program. Dla języka Rust nie ma znaczenia, gdzie znajduje się kod, ale na potrzeby ćwiczeń i projektów zawartych w tej książce zalecamy utworzenie katalogu projects w podstawowym katalogu i przechowywanie tam wszystkich naszych projektów.

Aby utworzyć katalog projects oraz katalog dla projektu Hello, world! w obrębie tego katalogu, otwieramy terminal i wprowadzamy podane niżej polecenia. Dla systemów Linux i macOS wprowadzamy:

Dla wiersza poleceń systemu Windows polecenia mają postać:

Z kolei w Windows PowerShell wprowadzamy:

Pisanie i uruchamianie programu w języku Rust

Teraz utworzymy nowy plik źródłowy i nazwiemy go main.rs. Pliki w języku Rust zawsze mają rozszerzenie .rs. Jeśli w nazwie pliku używamy więcej niż jednego słowa, oddzielamy je znakiem podkreślenia. Na przykład piszemy hello_world.rs, a nie helloworld.rs.

Otwórzmy teraz utworzony przed chwilą plik hello_world.rs i wprowadźmy do niego kod pokazany na listingu 1.1.

Listing 1.1. Program, który wyświetla tekst Hello, world!

Zapisujemy plik i wracamy do okna terminalu. Aby skompilować i uruchomić program w systemach Linux lub macOS, wprowadzamy następujące polecenia:

W systemie Windows wprowadzamy polecenie .\main.exe zamiast ./main:

Niezależnie od używanego systemu operacyjnego, ciąg znaków Hello, world! powinien wyświetlić się na terminalu. Jeśli nie widzimy takiego wyniku, musimy odwołać się do punktu „Usuwanie błędów” w rozdz. 1, aby znaleźć sposób rozwiązania problemu.

Jeśli tekst Hello, world! się wyświetlił, to świetnie. Można oficjalnie stwierdzić, że napisaliśmy pierwszy program w języku Rust, czyli staliście się programistami Rusta – witamy!

Anatomia programu w języku Rust

Przyjrzyjmy się szczegółowo, co się dzieje w naszym programie Hello, world!. Oto pierwszy kawałek puzzla:

Te wiersze definiują funkcję w języku Rust. Funkcja main jest funkcją specjalną – zawsze stanowi pierwszy element kodu, który jest uruchamiany w wykonywalnym programie Rusta. Pierwszy wiersz deklaruje funkcję o nazwie main, która nie ma żadnego parametru i niczego nie zwraca. Gdyby miała ona parametry, byłyby one umieszczone w nawiasach ( ).

Zauważmy też, że treść funkcji znajduje się w nawiasach klamrowych { }. Rust wymaga, aby takie nawiasy otaczały treści funkcji. W dobrym stylu jest umieszczenie otwierającego nawiasu klamrowego w tym samym wierszu, w którym jest deklaracja funkcji, z odstępem między nimi.

W czasie pisania tej książki tworzone było automatyczne narzędzie formatujące o nazwie rustfmt. Jeśli chcemy trzymać się standardowego stylu pisania w projektach Rusta, rustfmt sformatuje nasz kod w określonym stylu. Zespól Rusta planuje ostatecznie dołączyć to narzędzie do standardowej dystrybucji języka, takiej jak rustc. Tak więc zależnie od tego, kiedy czytacie tę książkę, być może macie już to zainstalowane na swoim komputerze! Więcej szczegółów można znaleźć online.

Wewnątrz funkcji main znajduje się następujący kod:

Ten wiersz wykonuje całe działanie tego małego programu – wyświetla tekst na ekranie. Warto zwrócić uwagę na cztery niewielkie szczegóły. Po pierwsze, styl Rusta polega na robieniu wcięć za pomocą czterech odstępów, a nie tabulatora.

Po drugie, println! wywołuje makro Rusta. Gdyby było to wywołanie funkcji, podalibyśmy samą nazwę w postaci println (bez znaku !). Makra języka Rust omawiamy bardziej szczegółowo w dodatku D. Teraz wystarczy wiedzieć, że użycie znaku ! oznacza, że wywołujemy makro, a nie zwykłą funkcję.

Po trzecie, widzimy łańcuch tekstowy "Hello, world!". Przekazujemy go jako argument do println!, a wtedy tekst jest wyświetlany na ekranie.

Po czwarte, kończymy wiersz znakiem średnika (;), który wskazuje, że wyrażenie jest zakończone i można zacząć pisać następne. Większość wierszy w kodzie języka Rust kończy się średnikiem.

Kompilacja i uruchomienie to oddzielne kroki

Właśnie uruchomiliśmy nowo utworzony program. Przeanalizujmy więc wszystkie kroki tego procesu.

Przed uruchomieniem programu w języku Rust musimy go skompilować za pomocą kompilatora Rusta, co robimy, korzystając z polecenia rustc, po którym wpisujemy nazwę naszego pliku źródłowego, co wygląda tak:

Osoby, które umieją programować w C lub C++, zauważą, że przypomina to gcc lub clang. Po udanej kompilacji Rust daje na wyjściu binarny plik wykonywalny. W przypadku systemów Linux, MacOS i PowerShell w Windows zobaczymy plik wykonywalny, wykonując polecenie ls w naszej powłoce:

W przypadku CMD w systemie Windows musimy podać poniższe polecenie:

Pokazuje to plik z kodem źródłowym o rozszerzeniu .rs, plik wykonywalny (w systemie Windows main.exe, a main w pozostałych platformach) oraz, jeśli używamy CMD, plik zawierający informacje debugera o rozszerzeniu .pdb. W tym miejscu uruchamiamy plik main.exe lub main w następujący sposób:

Jeśli nasz plik main.rs zawierał nasz program Hello, world!, to na terminalu pojawi się taki właśnie napis.

Osoby, które znają dynamiczny język jak Ruby, Python lub JavaScript, mogą nie być przyzwyczajone do tego, że kompilacja i uruchamianie programu to dwa oddzielne kroki. Rust jest kompilowanym językiem programowania. Oznacza to, że możemy skompilować program i przekazać komuś wersję wykonywalną, aby osoba ta mogła uruchomić program bez instalacji języka Rust. Jeśli damy komuś plik .rb, .py lub .js, to musi on mieć zainstalowaną implementację języka Ruby, Python lub JavaScript. W tych językach potrzebujemy tylko jednego polecenia, aby skompilować i uruchomić nasz program. Wszystko jest kompromisem w projekcie języka.

Sama kompilacja za pomocą rustc wystarcza dla prostszych programów, ale w miarę jak nasz projekt się rozrasta, będziemy chcieli zarządzać wszystkimi opcjami i łatwo dzielić się kodem. Omówimy teraz narzędzie Cargo, które pomoże nam pisać prawdziwe programy w języku Rust.

Witaj Cargo!

Cargo to wbudowany w Rust menedżer systemu i pakietów. Większość Rustarian używa tego narzędzia, aby zarządzać swoimi projektami w języku Rust, gdyż Cargo obsługuje za nas wiele zadań, takich jak tworzenie kodu, pobieranie bibliotek, od których nasz kod jest zależny, oraz tworzenie tych bibliotek. (Biblioteki, których potrzebuje nasz kod, nazywamy zależnościami).

Najprostsze programy w języku Rust, jak ten, który już napisaliśmy, nie mają żadnych zależności. Tak więc, gdybyśmy utworzyli nasz projekt Hello, world! za pomocą Cargo, obsługuje tworzenie kodu. W miarę pisania coraz bardziej skomplikowanych programów w języku Rust będziemy dodawać zależności, a jeśli zaczniemy projekt przy użyciu Cargo, dodawanie zależności stanie się dużo łatwiejsze.

Ponieważ większość projektów w Rust wykorzystuje Cargo, w książce także go używamy. Cargo jest instalowane razem z Rust, więc ma go każdy, kto użył oficjalnych instalatorów omówionych w podrozdziale „Instalacja” w rozdz. 1. Jeśli instalowaliśmy Rusta za pomocą innych środków, trzeba sprawdzić, czy Cargo jest zainstalowane, wprowadzając z terminalu następujące polecenie:

Jeśli zobaczymy numer wersji, to go mamy! Jeśli pojawi się błąd, jak command not found (nie znaleziono polecenia), to musimy znaleźć w dokumentacji sposób oddzielnej instalacji Cargo.

Tworzenie projektu za pomocą Cargo

Utwórzmy nowy projekt, korzystając z Cargo, i popatrzmy, czym się różni od naszego oryginalnego projektu Hello, world!. Cofnijmy się do naszego katalogu projects (lub innego, w którym zdecydowaliśmy się przechowywać nasz kod). Następnie w dowolnym systemie operacyjnym wykonajmy następujące polecenia:

Pierwsze polecenie tworzy nowy wykonywalny plik binarny o nazwie hello_cargo. Argument --bin przekazany do cargo new tworzy nową wykonywalną aplikację (często nazywaną po prostu binarną), w przeciwieństwie do biblioteki. Nasz projekt nazwaliśmy hello_cargo, a Cargo tworzy swoje pliki katalogu o tej samej nazwie.

Wejdźmy do katalogu hello_cargo i pokażmy listę plików. Zobaczymy, że Cargo wygenerował dwa pliki i jeden katalog: plik Cargo.toml oraz katalog src zawierający wewnątrz plik main.rs. Zainicjowane zostało także nowe repozytorium Git wraz z plikiem gitignore.

UWAGA

Git to popularny system kontroli wersji. Możemy zmienić cargo new tak, aby używało innego systemu kontroli wersji za pomocą flagi --vcs. Uruchommy cargo new --help tak, aby zobaczyć dostępne opcje.

W naszym edytorze tekstów otwieramy Cargo.toml. Plik powinien wyglądać podobnie jak kod na listingu 1.2.

Listing 1.2. Zawartość pliku Cargo.toml wygenerowana przez polecenie cargo new

Plik ten jest zapisany w formacie TOML (Tom’s Obvious, Minimal Language), który jest formatem konfiguracyjnym Cargo.

Pierwszy wiersz, [package], to nagłówek części, który wskazuje, że kolejne instrukcje stanowią pakiet konfiguracyjny. W miarę jak będziemy dodawać więcej informacji do tego pliku, pojawią się inne części.

Kolejne trzy wiersze określają informacje konfiguracyjne potrzebne Cargo do skompilowania naszego programu: nazwę, wersję i autora. Cargo otrzymuje nasze nazwisko (nazwę) oraz e-mail z naszego środowiska pracy, więc jeśli podane informacje są niepoprawne, to musimy je w tym momencie poprawić, a następnie zapisać plik.

Ostatni wiersz, [dependencies], to początek części, w której podajemy listę ewentualnych elementów zależnych naszego projektu. W języku Rust pakiety kodu są określane jako skrzynki (crates). W tym projekcie nie potrzebujemy żadnych dodatkowych skrzynek, ale w pierwszym projekcie z rozdziału 2 wykorzystamy część dependencies. Otwórzmy teraz plik src/main.rs i przyjrzyjmy mu się:

Cargo wygenerował nam program Hello, world! taki jak ten, który napisaliśmy w listingu 1.1. Jak dotąd różnica między poprzednim projektem a projektem generowanym przez Cargo polega na tym, że Cargo umieścił kod w katalogu src oraz że w katalogu głównym mamy plik konfiguracyjny Cargo.toml. Cargo oczekuje, że nasze pliki źródłowe będą się znajdowały w katalogu src. Katalog na najwyższym poziomie projektu jest przeznaczony tylko na pliki README, informacje o licencji, pliki konfiguracyjne i wszystkie inne elementy, które nie są związane z naszym kodem. Cargo pomaga w organizowaniu naszych projektów. Wszystko ma tu swoje miejsce i my mamy miejsce na wszystko.

Jeśli rozpoczęliśmy projekt, który nie używa Cargo, jak to zrobiliśmy przy naszym projekcie Hello, world!, możemy go przekształcić na projekt z użyciem Cargo. Kod projektu przenosimy do katalogu src i tworzymy odpowiedni plik Cargo.toml.

Tworzenie i uruchamianie projektu w Cargo

Popatrzmy teraz, jakie będą różnice po utworzeniu i uruchomieniu programu Hello, world! za pomocą Cargo! Z naszego katalogu hello_cargo budujemy nasz projekt, wprowadzając następujące polecenie:

Polecenie to tworzy plik wykonywalny w target/debug/hello_cargo (lub w target\debug\hello_cargo.exe w systemie Windows) zamiast korzystania z naszego bieżącego katalogu. Plik wykonywalny możemy uruchomić za pomocą następującego polecenia:

Jeśli wszystko pójdzie dobrze, Hello, world! powinien wyświetlić tekst na terminalu. Pierwsze uruchomienie programu Cargo spowoduje też, że Cargo utworzy nowy plik Cargo.lock w katalogu na najwyższym poziomie. Plik ten przechowuje dokładne wersje zależności z naszego projektu. Ten projekt nie ma zależności, więc plik jest prawie pusty. Nie musimy zmieniać go ręcznie, gdyż Cargo zarządza jego zawartością.

My tworzymy tylko projekt za pomocą cargo build i uruchamiamy za pomocą ./target/debug/hello_cargo, ale możemy też użyć cargo run, aby skompilować kod, a następnie uruchomić wynikowy plik wykonywalny w jednym poleceniu:

Tym razem nie widzimy wyników wskazujących, że w kompilacji hello_cargo pomaga Cargo. Cargo określił, że pliki nie uległy zmianie, więc uruchomił wersję binarną. Gdybyśmy zmodyfikowali nasz kod źródłowy, Cargo przed uruchomieniem przebudowałby projekt, a wtedy wyniki miałyby postać jak niżej:

Cargo zapewnia także polecenie o nazwie cargo check. Polecenie to szybko sprawdza nasz kod, aby się upewnić, że można go skompilować, ale nie wykonuje pliku wykonywalnego:

Dlaczego nie tworzyć pliku wykonywalnego? cargo check często jest znacznie szybsze od cargo build, gdyż pomija krok tworzenia pliku wykonywalnego. Jeśli podczas pisania kodu stale sprawdzamy swoją pracę, korzystanie z cargo check przyspieszy ten proces! Dlatego wielu Rustarian co jakiś czas uruchamia cargo check podczas pisania programu, aby mieć pewność, że uda się go skompilować. Uruchamiają cargo build dopiero wtedy, gdy są gotowi do użycia wersji wykonywalnej.

Podsumujmy więc, czego dowiedzieliśmy się o Cargo:

możemy tworzyć projekt za pomocą cargo build lub cargo check;

możemy utworzyć i uruchomić projekt w jednym kroku za pomocą cargo run;

zamiast zapisywania wyników kompilacji w tym samym katalogu co nasz kod, Cargo przechowuje go w katalogu target/debug.

Dodatkową zaletą korzystania z Cargo jest fakt, że polecenia są takie same, niezależnie od tego, w jakim systemie operacyjnym pracujemy. Dlatego nie będą już podawane instrukcje specyficzne dla systemów Linux i macOS oraz dla systemu Windows.

Tworzenie gotowej wersji

Gdy nasz projekt jest wreszcie gotowy, możemy użyć polecenia cargo build --release, aby skompilować go z optymalizacją. To polecenie utworzy plik wynikowy w katalogu target/release zamiast target/debug. Optymalizacja powoduje, że kod w języku Rust działa szybciej, ale włączenie jej wydłuża czas potrzebny na skompilowanie programu. Dlatego mamy dwa różne profile: jeden na potrzeby tworzenia programu, gdy chcemy szybko ponownie budować program, i często drugi do tworzenia końcowej wersji programu, który przekażemy użytkownikowi i który nie będzie wielokrotnie przebudowywany, a za to będzie działał tak szybko jak to tylko możliwe. Jeśli robimy badania porównawcze (benchmarking) czasu działania naszego kodu, koniecznie musimy wykorzystywać cargo build --release i zrobić badanie porównawcze dla pliku wykonywalnego z katalogu target/release.

Cargo jako konwencja

Przy prostych projektach Cargo nie daje wielkich korzyści w stosunku do korzystania z rustc, ale będzie się sprawdzał w miarę jak nasze programy staną się bardziej skomplikowane. Przy projektach złożonych z wielu modułów znacznie prostsze jest zrzucenie na Cargo koordynacji budowy wyniku.

Wprawdzie projekt hello_cargo jest prosty, wykorzystuje jednak wiele narzędzi, z których będziemy korzystać w dalszej części naszej kariery z językiem Rust. W istocie praca przy jakimkolwiek istniejącym projekcie pozwala na korzystanie z podanych niżej poleceń, aby sprawdzić kod za pomocą Git, zmienić katalog zawierający projekt oraz na budowę wyniku:

Więcej informacji na temat Cargo można znaleźć w dokumentacji pod adresem https://doc.rust-lang.org/cargo/.

Podsumowanie

Świetnie rozpoczęliśmy naszą podróż z językiem Rust! W tym rozdziale nauczyliśmy się, jak:

instalować najnowszą stabilną wersję języka Rust za pomocą rustup;

aktualizować Rust do nowszej wersji;

otwierać zainstalowaną lokalnie dokumentację;

pisać i uruchamiać program Hello, world! bezpośrednio za pomocą rustc;

tworzyć i uruchamiać nowy projekt, wykorzystując konwencje Cargo.

To dobry moment na budowę bardziej konkretnego programu, aby przyzwyczaić się do czytania i zapisu w kodzie języka Rust. Dlatego w rozdziale 2 zbudujemy program zgadywankę. Jeśli ktoś od razu chce przejść do opisu działania popularnych pojęć programowania w języku Rust, może najpierw przeczytać rozdział 3, a potem wrócić do rozdziału 2.

2

PROGRAMOWANIE ZGADYWANKI

Wykonajmy skok na głęboką wodę, pracując w języku Rust nad działającym projektem. W tym rozdziale wprowadzamy kilka popularnych pojęć języka Rust, pokazując ich działanie w prawdziwym programie. Nauczymy się o let, match, o metodach, o powiązanych funkcjach, skrzynkach zewnętrznych, modułach i wiele więcej. Kolejne rozdziały są poświęcone szczegółom tych pojęć. W tym rozdziale poćwiczymy podstawy.

Zaimplementujemy klasyczny program dla początkujących programistów – zgadywankę. Działa ona tak: program generuje liczbę losową z przedziału od 1 do 100, potem pyta użytkownika o odgadnięcie liczby, a po jej wprowadzeniu program pokazuje, czy wartość jest za mała, czy za duża. Jeśli strzał jest poprawny, gra wyświetla komunikat z gratulacjami i zakończy działanie.

Tworzenie nowego projektu

Aby utworzyć nowy projekt, przechodzimy do katalogu projects, który zrobiliśmy w rozdziale 1, i za pomocą Cargo tworzymy nowy projekt. Będzie to wyglądać mniej więcej tak:

Pierwsze polecenie, cargo new, przyjmuje jako pierwszy argument nazwę projektu (tu: guessing_game). Dalej flaga --bin mówi Cargo, aby utworzył projekt binarny, podobny do tego z rozdziału 1. Drugie polecenie zmienia bieżący katalog na katalog nowego projektu. Oto jak wygląda utworzony plik Cargo.toml:

Jeśli informacja o autorze pobrana z naszego środowiska nie jest poprawna, to musimy poprawić plik i ponownie go zapisać.

Jak widzieliśmy w rozdziale 1, cargo new generuje program „Hello, world!”. Sprawdźmy, jak wygląda plik src/main.rs:

Teraz skompilujmy program „Hello, world!” i uruchommy w tym samym kroku za pomocą polecenia cargo run:

Polecenie run jest wygodne, gdy szybko chcemy powtórzyć projekt, co będziemy robić w naszej zgadywance, testując każdą iterację przed przejściem do kolejnej. Ponownie otwieramy plik src/main.rs. Będziemy teraz w tym pliku pisać cały nasz kod.

Przetwarzanie odpowiedzi

Pierwsza część programu zgadywanki to pytanie do użytkownika o dane, przetwarzanie podanych danych oraz sprawdzenie, czy mają one poprawną postać. Zaczniemy od tego, że pozwolimy użytkownikowi wprowadzić liczbę. Do pliku src/main.rs wprowadzamy kod pokazany na listingu 2.1.

Listing 2.1. Kod, który pobiera liczbę od użytkownika i ją wyświetla

Ten kod zawiera wiele informacji, więc przeanalizujmy go wiersz po wierszu. Aby uzyskać dane od użytkownika, a potem wyświetlić wyniki, musimy wprowadzić do zakresu działania bibliotekę io (input/output). Biblioteka io znajduje się w standardowej bibliotece (która jest znana jako std):

Domyślnie Rust ma w zakresie tylko kilka typów zawartych w części wstępnej każdego programu. Jeśli potrzebny nam typ nie znajduje się w części wstępnej, musimy go jawnie wprowadzić do zakresu działania za pomocą odpowiedniej instrukcji. Zastosowanie biblioteki std::io zapewnia nam wiele użytecznych funkcji, w tym możliwość przyjmowania danych wejściowych użytkownika.

Jak widzieliśmy w rozdziale 1, funkcja main stanowi punkt wejścia do programu:

Składnia fn deklaruje nową funkcję, nawiasy ( ) wskazują, że nie ma ona żadnych parametrów, a nawias klamrowy { rozpoczyna treść funkcji. Z rozdziału 1 wiemy, że println! to makro, które wyświetla na ekranie ciąg znaków (łańcuch):

Ten kod wyświetla zgłoszenie stwierdzające, jaka jest to gra, oraz żądające, aby użytkownik podał dane wejściowe.

Zapisywanie wartości w zmiennych

Teraz utworzymy miejsce na zapisanie danych wprowadzanych przez użytkownika:

Program zaczyna być ciekawy! W tym niewielkim wierszu dzieje się bardzo dużo. Zauważmy, że mamy tu instrukcję let, która została użyta do utworzenia zmiennej. Oto kolejny przykład:

Ten wiersz tworzy nową zmienną o nazwie foo i wiąże ją z paskiem wartości. W języku Rust zmienne są domyślnie niemutowalne. Omawiamy szczegółowo tę koncepcję w podrozdziale „Zmienne i mutowalność” w rozdz. 3. Kolejny przykład pokazuje, jak użyć mut przed nazwą zmiennej, aby zmienna była mutowalna:

UWAGA

Składnia // rozpoczyna komentarz, który rozciąga się do końca wiersza. Rust ignoruje całą treść komentarza, co bardziej szczegółowo jest omówione w rozdziale 3.

Wróćmy do programu zgadywanki. Wiemy teraz, że wiersz let mut guess wprowadza mutowalną zmienną o nazwie guess. Z drugiej strony znaku równości = znajduje się wartość, która jest przywiązana do zmiennej guess, stanowiąc wynik wywołania String::new, funkcji, która zwraca nową instancję String. String jest typem łańcuchowym znajdującym się w standardowej bibliotece i jest możliwym do powiększenia fragmentem tekstu zakodowanym w kodzie UTF-8.

Składnia :: w wierszu ::new wskazuje, że funkcja new jest funkcją powiązaną (associated function) typu łańcuchowego (String). Funkcja powiązana jest implementowana dla danego typu, w tym przypadku String, a nie dla konkretnej instancji typu String. W niektórych językach nazywa się to metodą statyczną (static method).

Ta funkcja new tworzy pusty łańcuch. Znajdziemy funkcję new dla wielu typów, ponieważ jest to popularna nazwa funkcji tworzącej nową wartość określonego rodzaju.

Podsumowując, w wierszu let mut guess = String::new(); została utworzona zmienna mutowalna, która jest obecnie powiązana z nową, pustą instancją typu String. Uff!

Przypomnijmy sobie, że w pierwszym wierszu programu, use std::io;, dołączyliśmy funkcje wejścia/wyjścia (input/output) ze standardowej biblioteki. Teraz możemy wywołać związaną funkcję, stdin, dla io:

Gdybyśmy pominęli wiersz use std::io na początku programu, nie moglibyśmy napisać wywołania tej funkcji w postaci std::io::stdin. Funkcja stdin zwraca instancję std::io::Stdin, która jest typem reprezentującym uchwyt do standardowego wejścia z naszego terminalu.

Kolejna część kodu, .read_line(&mut guess), wywołuje metodę read_line dla standardowego uchwytu wejściowego, aby uzyskać dane wejściowe od użytkownika. Przekazujemy także jeden argument do read_line – jest to &mut guess.

Zadaniem read_line jest pobranie tego, co wpisze użytkownik na standardowym wejściu, i umieszczenie tego w łańcuchu, dlatego argumentem jest łańcuch (string). Argument typu łańcuchowego musi mieć możliwość zmiany, aby metoda mogła zmienić zawartość łańcucha przez dodanie do niego danych wprowadzanych przez użytkownika.

Znak & wskazuje, że argument ten jest odwołaniem (reference), które pozwala nam na to, aby wiele części naszego kodu miało dostęp do jednego wycinka danych, bez konieczności wielokrotnego kopiowania tych danych do pamięci. Odwołania są złożoną własnością, a jedną z najważniejszych zalet języka Rust jest bezpieczne i łatwe z nich korzystanie. Nie musimy w tej chwili znać szczegółów, aby ukończyć program. Teraz musimy tylko wiedzieć, że podobnie jak zmienne, odwołania są domyślnie niemutowalne. Dlatego musimy napisać &mut guess, a nie tylko &guess, aby sprawić, że staną się mutowalne. (W rozdziale 4 tematyka odwołań jest omówiona bardziej szczegółowo).

Obsługa potencjalnych błędów za pomocą typu Result

Jeszcze nie skończyliśmy z tym wierszem kodu. Wprawdzie omówiliśmy pojedynczy wiersz tekstu, ale jest to tylko pierwsza część jednego logicznego wiersza kodu. Drugą część stanowi ta metoda:

Gdy wywołujemy metodę ze składnią .foo(), często warto dodać nowy wiersz i inne puste miejsca, aby pomóc w rozdzieleniu długich wierszy. Moglibyśmy zapisać ten kod w postaci:

Jednak taki długi wiersz jest trudny do przeczytania, więc lepiej podzielić go na dwa – dwa wiersze dla dwóch wywołań metod. Teraz przeanalizujmy, co robi ten wiersz.

Jak wspomniano, read_line umieszcza to, co pisze użytkownik w łańcuchu, który jej przekazujemy, ale zwraca ona także wartość – w tym przypadku wynikiem jest io::Result. Rust ma w swojej standardowej bibliotece typy liczbowe o nazwie Result – zarówno generyczny Result, jak i konkretne wersje dla podmodułów, jak io::Result.

Typy Result są wyliczeniami (enumerations). Wyliczenie jest typem, który musi mieć ustalony zbiór wartości, nazywanych wariantami (variants). Typ wyliczany szczegółowo omawiamy w rozdziale 6.

Wariantami dla Result są Ok lub Err. Wariant Ok wskazuje, że działanie zakończyło się powodzeniem i wewnątrz Ok jest pomyślnie wygenerowana wartość. Wariant Err oznacza, że działanie się nie powiodło, Err zaś zawiera informację o tym, jak i dlaczego ona się nie powiodła.

Celem typów Result jest zakodowanie informacji związanych z obsługą błędów. Wartości typu Result, podobnie jak wartości każdego innego typu, mają w sobie zdefiniowane metody. Instancja io::Result ma metodę expect, którą możemy wywołać. Jeśli instancją io::Result jest wartość Err, to expect spowoduje, że program „padnie” i wyświetli komunikat, który został przekazany expect jako argument. Jeśli metoda read_line zwróci Err, to będzie to najpewniej wynikiem błędu pochodzącego od działającego systemu operacyjnego. Jeśli instancją io::Result jest wartość Ok, to expect pobierze wartość wynikową, którą przechowuje Ok, i zwróci nam tę wartość, abyśmy mogli jej używać. W tym przypadku wartość jest liczbą bajtów, które użytkownik wprowadził ze standardowego wejścia.

Jeśli nie możemy wywołać expect, program się skompiluje, ale otrzymamy ostrzeżenie:

Rust ostrzega nas, że nie wykorzystaliśmy wartości Result zwróconej przez read_line, która wskazuje, że program nie obsłużył ewentualnego błędu.

Prawidłowym sposobem zlikwidowania ostrzeżenia jest napisanie kodu obsługi błędu. Ale ponieważ chcemy, aby w takiej sytuacji program po prostu przestał działać, możemy użyć expect. W rozdziale 9 jest mowa o tym, jak wracamy do działania po błędzie.

Wyświetlanie wartości za pomocą symboli zastępczych w println!

Oprócz zamykających nawiasów klamrowych do omówienia pozostał nam jeszcze jeden wiersz kodu:

Ten wiersz wyświetla łańcuch, w którym zapisaliśmy dane wejściowe użytkownika. Para nawiasów klamrowych { } to symbol zastępczy – można sobie wybrazić, że to szczypce kraba, które utrzymują wartość na miejscu. W ten sposób możemy wyświetlić więcej niż jedną wartość – pierwszy zestaw nawiasów klamrowych zawiera pierwszą wartość podaną po łańcuchu formatu. Drugi zestaw obejmuje drugą wartość i tak dalej. Wyświetlenie wielu wartości przy jednym wywołaniu println! będzie wyglądać tak:

Ten kod wyświetli x = 5 and y = 10.

Testowanie pierwszej części

Przetestujmy pierwszą część zgadywanki. Uruchamiamy ją za pomocą cargo run:

W tym miejscu pierwsza część gry jest gotowa – otrzymujemy dane z klawiatury, a następnie je wyświetlamy.

Generowanie tajnej liczby

Następnie musimy wygenerować tajną liczbę, którą użytkownik będzie próbował odgadnąć. Liczba ta powinna być za każdym razem inna, aby w grę można było zagrać więcej niż raz. Użyjemy liczby losowej z zakresu od 1 do 100, aby gra nie była zbyt trudna. Rust nie zawiera jeszcze w swojej standardowej bibliotece funkcji liczb losowych. Jednak zespół Rusta udostępnia skrzynkę rand pod adresem https://crates.io/crates/rand/.

Korzystanie ze skrzynki, aby rozszerzyć funkcjonalność

Skrzynka (crate) to pakiet zawierający kod Rusta. Projekt, który tworzymy, to skrzynka binarna (binary crate), której plik jest wykonywalny. Skrzynka rand jest skrzynką biblioteczną (library crate), która zawiera kod przeznaczony do wykorzystania w innych programach.

Cargo wykorzystuje skrzynki zewnętrzne i to jest jego jasny punkt. Zanim będziemy mogli napisać kod, który wykorzystuje rand, musimy tak zmodyfikować plik Cargo.toml, aby zawierał skrzynkę rand jako element zależny. Otwieramy teraz ten plik i dodajemy do niego następujący wiersz poniżej nagłówka części [dependencies], którą Cargo dla nas utworzył:

W pliku Cargo.toml wszystko, co jest pod nagłówkiem, stanowi fragment części, który ciągnie się aż do początku kolejnej części. Część [dependencies] jest miejscem, w którym informujemy Cargo, od których zewnętrznych skrzynek Cargo jest zależne i która wersja skrzynek jest wymagana. W tym przypadku wymieniamy skrzynkę rand o wersji semantycznej 0.3.14. Cargo rozumie określenie Semantic Versioning (czasem określane jako SemVer), co stanowi standard zapisu numerów wersji. Numer 0.3.14 stanowi skrót od ^0.3.14, który oznacza „każda wersja, która ma publiczny API zgodny z wersją 0.3.14”.

Teraz, nie zmieniając niczego w kodzie, zbudujemy projekt pokazany na listingu 2.2.

Listing 2.2. Wyniki uruchomienia cargo build po dodaniu skrzynki rand jako zależności

Możemy zobaczyć różne numery wersji (ale dzięki SemVer będą one kompatybilne z kodem!), a wiersze mogą mieć różną kolejność. Teraz gdy mamy zewnętrzną zależność, Cargo pobiera z registry najnowszą wersję wszystkiego, co stanowi kopię danych z https://crates.io/. Adres Crates.io to miejsce, w którym osoby należące do ekosystemu Rusta umieszczają projekty open source w języku Rust, aby umożliwić korzystanie z nich innym.

Po aktualizacji rejestru Cargo sprawdza część [dependencies] i pobiera wszystkie skrzynki, których nam jeszcze brakuje. W tym przypadku, choć mamy podane rand jako zależność, Cargo pobrał także kopię libc, gdyż rand jest w działaniu zależny od libc. Po pobraniu skrzynek Rust je kompiluje, a następnie kompiluje projekt wraz z dostępnymi zależnościami.

Jeśli od razu uruchomimy cargo build bez jakichkolwiek zmian, to nie otrzymamy żadnych wyników, poza wierszem z napisem Finished. Cargo wie, że już pobrał i skompilował zależności, a my nie zmieniliśmy niczego w pliku Cargo.toml. Cargo wie także, że nie zmieniliśmy niczego w naszym kodzie, więc nie kompiluje go ponownie. Nie mając nic do zrobienia, po prostu kończy działanie.

Jeśli otworzymy plik src/main.rs, zrobimy jakąś trywialną zmianę, zapiszemy i ponownie uruchomimy kompilacje, otrzymamy dwa wiersze wyników:

Wiersze te pokazują, że Cargo jedynie zaktualizował skompilowany plik z naszą niewielką zmianą, zapisując to w pliku src/main.rs. Nasze zależności się nie zmieniły, więc Cargo wie, że może ponownie wykorzystać to, co już pobrał i skompilował. Tworzy ponownie jedynie naszą część kodu.

Zapewnienie powtarzalnych kompilacji za pomocą pliku Cargo.lock

Cargo ma mechanizm, który pozwala odbudować ten sam artefakt za każdym razem, gdy tworzymy nowy kod – Cargo będzie wykorzystywał tylko podaną przez nas wersję, aż do momentu, gdy podamy coś innego. Co się na przykład stanie, jeśli w następnym tygodniu pojawi się wersja skrzynki rand 0.3.15, która będzie zawierać ważną poprawkę błędu, ale będzie tam też regresja, która zmieni nasz kod?

Odpowiedzią na ten problem jest plik Cargo.lock, który został utworzony po raz pierwszy podczas wykonywania cargo build i znajduje się teraz w naszym katalogu guessing_game. Gdy po raz pierwszy kompilujemy projekt, Cargo określa wersje wszystkich zależności, które odpowiadają podanym kryteriom, a następnie zapisuje je w pliku Cargo.lock. Gdy w przyszłości ponownie skompilujemy nasz projekt, Cargo zobaczy, że istnieje plik Cargo.lock i wykorzysta podaną tam wersję zamiast powtarzać czynności związanie z ustalaniem wersji. To pozwala nam na automatyczne powtórzenie kompilacji. Innymi słowy, dzięki Cargo.lock nasz projekt pozostanie w wersji 0.3.14 do momentu, w którym jawnie nie zaktualizujemy tej wersji.

Aktualizacja skrzynki w celu otrzymania nowej wersji

Jeśli świadomie chcemy zaktualizować skrzynkę, Cargo ma dla nas kolejne polecenie, update, które zignoruje pliki Cargo.lock i znajdzie wszystkie najnowsze wersje pasujące do naszej specyfikacji w Cargo.tom1. Jeśli to zadziała, to Cargo zapisze te wersje w nowym pliku Cargo.lock.

Jednak domyślnie Cargo będzie szukał nowej wersji tylko wersji większych od 0.3.0 i mniejszych od 0.4.0. Jeśli skrzynka rand będzie wypuszczona w dwóch wersjach, 0.3.15 oraz 0.4.0, to przy uruchomieniu aktualizacji (update) zobaczymy następujący wynik:

W tym miejscu zauważymy, że nastąpiła zmiana w naszym pliku Cargo.lock, z uwagą, że obecnie używana wersja skrzynki to 0.3.15.

Jeśli chcielibyśmy korzystać z wersji 0.4.0 rand lub z jakiejkolwiek wersji 0.4.x, to musielibyśmy zaktualizować plik Cargo.tom1 tak, aby szukał w inny sposób:

Przy następnym uruchomieniu cargo build Cargo zaktualizuje rejestr dostępnych skrzynek i ponownie oceni nasze wymagania względem rand, zgodnie z podaną przez nas nową wersją.

Można powiedzieć o wiele więcej na temat Cargo i jego ekosystemu, o czym piszemy w rozdziale 14, ale teraz wystarczy nam to, co już wiemy. Cargo sprawia, że możemy łatwo korzystać z bibliotek, więc Rustarianie mogą pisać niewielkie projekty, które będą składane z wielu gotowych pakietów.

Generowanie liczby losowej

Ponieważ dodaliśmy już skrzynkę rand do Cargo.toml, możemy zacząć z niej korzystać. Kolejnym krokiem jest aktualizacja pliku src/main.rs, jak to pokazano na listingu 2.3.

Listing 2.3. Dodawanie kodu generowania liczby losowej

Najpierw dodajemy wiersz, który pozwala, aby Rust wiedział, że używamy skrzynki rand jako zewnętrznej zależności  1 . To wykonuje też ekwiwalent wywołania use rand, więc teraz możemy wywołać wszystko, co znajduje się w skrzynce rand, pisząc przed tym rand::.

Następnie dodajemy kolejny wiersz use rand::Rng  2 . Cecha Rng definiuje metody, które implementują generatory liczb losowych i musi mieć ona taki zakres, żebyśmy mogli tych metod używać. W rozdziale 10 szczegółowo omawiamy cechy.

Dodajemy jeszcze dwa wiersze w połowie kodu  3 . Funkcja rand::thread_rng da nam konkretny generator liczb losowych, z którego będziemy korzystać – lokalny w stosunku do bieżącego wątku działania, pobierający ziarno z systemu operacyjnego. Następnie dla generatora liczb losowych wywołujemy metodę gen_range. Metoda ta jest zdefiniowana przez cechę Rng, która została wprowadzona do zakresu poleceniem rand::thread_rng. Metoda gen_range pobiera jako argumenty dwie liczby i generuje liczbę losową znajdującą się między nimi. Granica dolna wchodzi w zakres liczb, a górna nie, więc musimy podać 1 i 101, aby zażądać liczby z zakresu od 1 do 100.

UWAGA

To, z jakich cech korzystać i które metody oraz funkcje ze skrzynki wywoływać, nie jest podane bezpośrednio. Instrukcje korzystania ze skrzynki są zawarte w jego dokumentacji. Inna wygodna cecha cargo to możliwość uruchomienia polecenia cargo doc --open, które lokalnie utworzy dokumentację dostępną dla wszystkich naszych zależności i otworzy ją w naszej przeglądarce. Zainteresowani innymi funkcjami skrzynki rand mogą uruchomić cargo doc --open i kliknąć rand z paska po lewej stronie.

Drugi dodany do kodu wiersz wyświetla tajną liczbę. Jest to wygodne podczas tworzenia programu w celu testowania, ale usuniemy to z wersji końcowej programu. To nie będzie zabawa, jeśli program wyświetli nam odpowiedź bezpośrednio po uruchomieniu!

Spróbujmy kilka razy wykonać program.

Powinniśmy otrzymać różne liczby losowe należące do przedziału od 1 do 100. Dobra robota!

Porównanie liczby odgadniętej z tajną

Gdy mamy już dane wejściowe użytkownika, możemy je porównać. Ten krok jest pokazany na listingu 2.4. Zauważmy, że ten kod jeszcze się nie skompiluje, co zaraz wyjaśnimy.

Listing 2.4. Obsługa możliwych wyników porównania dwóch liczb

Pierwszy nowy fragment to instrukcja use, która wprowadza do zakresu typ std::cmp::Ordering z biblioteki standardowej. Podobnie jak Result, Ordering jest kolejnym typem wyliczeniowym, ale obejmuje warianty Less, Greater i Equal. Są to trzy możliwe wyniki, gdy porównujemy dwie wartości.

Następnie u dołu dodajemy pięć nowych wierszy, które wykorzystują typ Ordering. Metoda cmp  3  porównuje dwie wartości i można ją wywołać dla wszystkich wartości, które mogą być porównywane. Odwołuje się do tego, z czym chcemy porównywać – tu porównuje wartość guess z secret_number. Następnie zwraca jeden z wariantów typu wyliczanego Ordering, który wprowadziliśmy do zakresu za pomocą instrukcji use. Stosujemy wyrażenie porównania  2 , aby zdecydować co należy robić na podstawie tego, który wariant Ordering został zwrócony z wywołania cmp przy argumentach guess i secret_number.

Wyrażenie match jest zbudowane z gałęzi. Gałąź składa się ze wzorca oraz kodu, który ma zostać uruchomiony, jeśli wartość podana na początku wyrażenia match pasuje do wzorca gałęzi. Rust pobiera wartość podaną w match i po kolei przegląda wzorce we wszystkich gałęziach. Konstrukcja i wzorce match to znaczące funkcje Rusta, pozwalające opisać wiele sytuacji, które może napotkać nasz kod oraz poprawnie je obsłużyć. Funkcje te zostaną szczegółowo omówione w rozdziałach 6 i 18.

Prześledźmy ten przykład, żeby zobaczyć co się stanie, gdy zastosowane zostanie wyrażenie match. Powiedzmy, że użytkownik podał liczbę 50, a wybrana losowo tajna liczba to 38. Gdy kod porówna 50 z 38, metoda cmp zwróci Ordering::Greater, gdyż 50 jest większe od 38. Wyrażenie match otrzyma wartość Ordering::Greater i zacznie sprawdzać szablon w każdej gałęzi. Sprawdzi szablon w pierwszej gałęzi, Ordering::Less, i zobaczy, że wartość Ordering::Greater nie pasuje do niego, więc zignoruje kod tej gałęzi i przejdzie do następnej. Wzorzec w kolejnej gąłęzi, Ordering:: Greater, pasuje do Ordering::Greater! Powiązany z nim kod zostanie wykonany i na ekranie zostanie wyświetlony napis Too big!. W tym miejscu wyrażenie match kończy działanie, gdyż w tym scenariuszu nie ma potrzeby sprawdzania ostatniej gałęzi.

Jednak kod na listingu 2.4 nie da się jeszcze skompilować. Spróbujmy:

(błąd [E0308]: niezgodność typów)*
(błąd: przerwanie z powodu poprzedniego błędu)

Istotą błędu jest tekst mismatched values (niepasujące wartości). Rust ma silny, statyczny system typów. Jednak ma też wnioskowanie typów. Gdy napisaliśmy let guess = String::new(), Rust potrafił wywnioskować, że zmienna guess powinna być typu String i nie kazał nam wpisywać typu. Zmienna secret_number ma typ liczbowy. Wartość od 1 do 100 może przyjąć kilka typów liczbowych: i32 – liczba 32-bitowa, u32 – 32-bitowa liczba bez znaku, i64 – liczba 64-bitowa, a także kilka innych. Rust domyślnie wybiera i32, która jest typem secret_number, o ile dodamy w innym miejscu informację, że Rust powinien wywnioskować inny typ liczbowy. Powodem błędu jest fakt, że Rust nie może porównać łańcucha tekstowego (String) z typem liczbowym.

W efekcie chcemy więc przekształcić String, który jest czytany przez program na wejściu, na typ liczby rzeczywistej, abyśmy mogli porównać wartości liczbowe. Możemy tego dokonać, dodając do treści funkcji main dwa następujące wiersze:

Tworzymy zmienną o nazwie guess. Ale zaraz, czy program nie ma już zmiennej o nazwie guess? Tak, ma, ale Rust pozwala nam na zasłonięcie poprzedniej wartości guess przez nową. Ta funkcja jest często używana w sytuacjach, gdy wartość jednego typu chcemy przekształcić na inny. Zasłanianie pozwala na ponowne wykorzystanie nazwy zmiennej guess zamiast wymuszać tworzenie dwóch różnych nazw, jak na przykład guess_str i guess. (W rozdz. 3 zasłanianie będzie omówione szczegółowo).

Łączymy guess z wyrażeniem guess.trim().parse(). W tym wyrażeniu guess odnosi się do oryginalnej zmiennej guess, która była typu String i która zawiera wartość wejściową. Metoda trim zastosowana do instancji String wyeliminuje wszelkie puste odstępy na początku i na końcu. Typ u32 może wprawdzie zawierać tylko wartości liczbowe, ale użytkownik musi nacisnąć enter, aby zadowolić read_line. Gdy użytkownik naciska enter, do łańcucha jest dodawany znak nowego wiersza. Jeśli, na przykład, użytkownik wpisze 5 i naciśnie enter, to guess ma postać: 5\n. Symbole \n oznaczają „nowy wiersz”, co jest wynikiem naciśnięcia klawisza enter. Metoda trim eliminuje \n, co pozostawia samo 5.

Metoda parse w zastosowaniu do łańcucha analizuje łańcuch, przekształcając go w liczbę. Ponieważ metoda może analizować różne typy liczbowe, musimy poinformować Rust, jaki dokładnie typ chcemy otrzymać, a do tego służy let guess: u32. Znak dwukropka (:) po zmiennej guess mówi, że jest to adnotacja dotycząca typu zmiennej. Rust ma kilka wbudowanych typów liczbowych, a zastosowany tu u32 to 32-bitowa liczba całkowita bez znaku. Jest to właściwy wybór domyślny dla małych liczb dodatnich. O innych typach liczbowych jest mowa w rozdziale 3. Ponadto przypisanie u32 w tym przykładowym programie i porównanie z secret_number oznacza, że Rust będzie wnioskował, że secret_number także ma typ u32. Tak więc będziemy porównywać dwie wartości tego samego typu!

Wywołanie parse łatwo może spowodować błąd. Jeśli przykładowo łańcuch zawierałby Areka%, to nie byłoby sposobu przekształcenia go w liczbę. Ponieważ może wystąpić błąd, metoda parse zwraca typ Result, podobnie jak robi to metoda read_line (co omówiono w punkcie „Obsługa potencjalnych błędów za pomocą typu Result”). Potraktujemy tu typ Result w podobny sposób, wykorzystując ponownie metodę expect. Jeśli parse zwróci wariant Err Result, gdyż nie może utworzyć z łańcucha liczby, to wywołanie expect przerwie grę i wyświetli podany komunikat. Jeśli parse potrafi z powodzeniem przekształcić łańcuch w liczbę, zwróci wariant Ok dla Result, więc expect da w wyniku oczekiwaną liczbę z wartości Ok.

Uruchommy teraz program!

Ładnie! Choć przy wpisywaniu liczby guess podano spacje, program i tak poprawnie odczytał, że użytkownik podał 76. Możemy uruchomić program kilka razy, aby sprawdzić jego różne zachowania przy różnych danych – podajmy poprawną wartość, liczbę za dużą i liczbę za małą. Działa nam już większa część gry, ale użytkownik może zgadywać tylko raz. Zmieńmy to teraz, dodając pętlę!

Dopuszczenie wielu prób za pomocą pętli

Słowo kluczowe loop tworzy nieskończoną pętlę. Dodamy ją teraz, aby dać użytkownikom więcej szans na odgadnięcie liczby:

Jak widzimy, wszystko zostało umieszczone w pętli, od odgadywania liczby do końca. Trzeba koniecznie wciąć wiersze wewnątrz pętli o kolejne cztery spacje i ponownie można uruchomić program. Zauważmy, że pojawi się nowy problem, gdyż program będzie robił dokładnie to, co mu kazaliśmy – zawsze będzie pytał o kolejną liczbę! Wygląda na to, że użytkownik nie może przestać grać!

Użytkownik może zawsze zatrzymać program, używając skrótu klawiaturowego ctrl-C. Jest jednak inny sposób ucieczki od tego potwora, jak wspomniano przy omawianiu parse w podrozdziale „Porównanie liczby odgadniętej z tajną” w rozdz. 2 – jeśli użytkownik wpisze wartość nieliczbową, program przerwie się awaryjnie. Użytkownik może skorzystać z tej możliwości w celu przerwania, jak pokazano poniżej:

(wątek ‘main’ wpadł w panikę przy ‘Proszę podać liczbę!’: )

Wpisanie quit rzeczywiście przerywa grę, ale podobnie stanie się, gdy wprowadzimy inną wartość niebędącą liczbą. Jest to jednak rozwiązywanie delikatnie mówiąc suboptymalne. Chcemy, aby gra kończyła się automatycznie, gdy podamy prawidłową liczbę.

Kończenie programu po poprawnej odpowiedzi

Zaprogramujmy grę tak, aby kończyła się, gdy użytkownik wygra. Zastosujemy polecenie break:

Dodanie break po wierszu You win! sprawia, że program wyjdzie z pętli, gdy użytkownik odgadnie tajną liczbę. Wyjście z pętli oznacza także wyjście z program, gdyż pętla jest ostatnią częścią modułu main.

Obsługa niepoprawnych danych na wejściu

Aby jeszcze ulepszyć zachowanie gry, wprowadźmy zamiast pojawiania się błędu, gdy użytkownik poda wartość różną od liczby, ignorowanie takiej wartości, aby użytkownik mógł kontynuować zgadywanie. Możemy to zrobić, zmieniając wiersz, w którym guess jest przekształcana z typu String na u32, jak to pokazano na listingu 2.5.

Listing 2.5. Ignorowanie nieliczbowych danych i powrót do żądania kolejnej liczby zamiast przerywania programu

Zmiana z wywołania expect na wyrażenie match to sposób na przejście od awarii programu przy błędzie do obsługi błędu. Przypomnijmy, że parse zwraca typ Result, a Result jest wyliczeniem, które ma warianty Ok lub Err. Użyjemy wyrażenia match, podobnie jak robiliśmy to przy wynikach Ordering w metodzie cmp.

Jeśli parse potrafi z powodzeniem przekształcić łańcuch w liczbę, to zwraca wartość Ok, która zawiera liczbę będącą wynikiem przekształcenia. Ta wartość będzie pasować do wzorca pierwszej gałęzi, a wyrażenie match zwróci wartość num, którą funkcja parse wytworzyła i umieściła wewnątrz wartości Ok. Liczba ta znajdzie się dokładnie tam, gdzie powinna – w nowej, utworzonej przez nas, zmiennej guess. Jeśli parse nie może zamienić łańcucha na liczbę, zwróci wartość Err, która zawiera więcej informacji o błędzie. Wartość Err nie jest zgodna ze wzorcem Ok(num) w pierwszej gałęzi match, ale pasuje do wzorca Err(_) w drugiej gałęzi. Podkreślenie (_) to wartość pasująca do wszystkiego (catchall). W tym przykładzie mówimy, że chcemy uzyskać zgodność z dowolną wartością Err, bez względu na to, jaka informacja się tam znajduje. Tak więc program wykona kod z drugiej gałęzi, continue, który mówi, że program powinien przejść do kolejnej iteracji pętli (loop) i zapytać o kolejną liczbę. W efekcie program zignoruje wszystkie błędy, które napotka instrukcja parse!

Teraz cały program powinien działać zgodnie z oczekiwaniem. Wypróbujmy go:

Świetnie! Po jednej niewielkiej końcowej korekcie ukończymy naszą zgadywankę. Przypomnijmy sobie, że program nadal wyświetla tajną liczbę. To było w porządku podczas testowania programu, ale psuje grę. Usuńmy więc instrukcję println!, która pokazuje tajną liczbę. Listing 2.6 pokazuje końcowy kod programu.

Listing 2.6. Gotowy kod zgadywanki

Podsumowanie

Zatem zbudowaliśmy poprawną zgadywankę. Gratulacje!

Projekt stanowi praktyczny sposób wprowadzenia do wielu nowych pojęć języka Rust: let, match, metod, powiązanych funkcji, skrzynek zewnętrznych i wielu innych. W kilku kolejnych rozdziałach poznamy więcej szczegółów dotyczących tych pojęć. W rozdziale 3 przedstawimy pojęcia, które występują w większości języków programowania, takie jak zmienne, typy danych i funkcje, oraz pokażemy, jak z nich korzystać w języku Rust. Rozdział 4 zawiera analizę własności, funkcji, która odróżnia język Rust od innych języków. Rozdział 5 to omómienie składni struktur i metod, a w rozdziale 6 tłumaczymy działanie wyliczenia.

PRZYPISY

Pod niektórymi listingami umieszczono tłumaczenie błędów i uwag. Pominięto je przy powtórzeniach oraz tam, gdzie wszystko dokładnie objaśniono w tekście (przyp. tłum.).