Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

რიცხვის გამოცნობის თამაში

მოდით პირდაპირ დავიწყოთ Rust-ში პრაქტიკულ პროექტზე მუშაობა! ეს თავი გაგაცნობთ Rust-ის რამდენიმე ზოგად კონცეფციას რეალურ პროგრამაში მათი გამოყენების ჩვენებით. თქვენ ისწავლით let-ის, match-ის, მეთოდების, ასოცირებული ფუნქციების, გარეგანი crate-ების და სხვა საკითხების შესახებ! მომდევნო თავებში ამ იდეებს უფრო დეტალურად განვიხილავთ. ამ თავში კი უბრალოდ ივარჯიშებთ საფუძვლებში.

ჩვენ განვახორციელებთ დამწყებთათვის განკუთვნილ კლასიკურ პროგრამირების ამოცანას: რიცხვის გამოცნობის თამაშს. აი, როგორ მუშაობს ის: პროგრამა დააგენერირებს შემთხვევით მთელ რიცხვს 1-დან 100-მდე. შემდეგ ის სთხოვს მოთამაშეს შეიყვანოს ნავარაუდევი რიცხვი. რიცხვის შეყვანის შემდეგ, პროგრამა მიუთითებს, ნავარაუდევი რიცხვი ძალიან დაბალია თუ ძალიან მაღალი. თუ ნავარაუდევი რიცხვი სწორია, თამაში დაბეჭდავს მისალოც შეტყობინებას და დასრულდება.

ახალი პროექტის გამართვა

ახალი პროექტის გასამართად, გადადით projects დირექტორიაში, რომელიც შექმენით პირველ თავში, და შექმენით ახალი პროექტი Cargo-ს გამოყენებით, ასე:

cargo new guessing_game
cd guessing_game

პირველი ბრძანება, cargo new, იღებს პროექტის სახელებს (guessing_game) პირველ არგუმენტად. მეორე ბრძანება გადადის ახალი პროექტის დირექტორიაში.

გადახედეთ დაგენერირებულ Cargo.toml ფაილს:

ფაილის სახელი: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"

[dependencies]

როგორც პირველ თავში ნახეთ, cargo new თქვენთვის აგენერირებს “Hello, world!” პროგრამას. შეამოწმეთ src/main.rs ფაილი:

ფაილის სახელი: src/main.rs

fn main() {
    println!("Hello, world!");
}

ახლა დავაკომპილიროთ ეს “Hello, world!” პროგრამა და გავუშვათ ის იმავე ნაბიჯში cargo run ბრძანების გამოყენებით:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/guessing_game`
Hello, world!

run ბრძანება ძალიან გამოსადეგია, როდესაც გჭირდებათ პროექტზე სწრაფი იტერაცია, როგორც ამას გავაკეთებთ ამ თამაშში, თითოეული იტერაციის სწრაფად ტესტირებით მომდევნოზე გადასვლამდე.

ხელახლა გახსენით src/main.rs ფაილი. თქვენ მთელ კოდს ამ ფაილში დაწერთ.

ნავარაუდევი რიცხვის დამუშავება

რიცხვის გამოცნობის თამაშის პირველი ნაწილი მოითხოვს მომხმარებლის შეყვანილ მონაცემებს (input), დაამუშავებს ამ მონაცემებს და შეამოწმებს, არის თუ არა შეყვანილი მონაცემები მოსალოდნელ ფორმატში. დასაწყისისთვის, მოთამაშეს მივცემთ საშუალებას შეიყვანოს ნავარაუდევი რიცხვი. შეიყვანეთ კოდი 2-1 ლისტინგიდან src/main.rs-ში.

Filename: src/main.rs
use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}
Listing 2-1: კოდი, რომელიც იღებს ნავარაუდევ რიცხვს მომხმარებლისგან და ბეჭდავს მას

ეს კოდი შეიცავს ბევრ ინფორმაციას, ასე რომ განვიხილოთ ის სტრიქონ-სტრიქონ. მომხმარებლის შეყვანილი მონაცემების მისაღებად და შემდეგ შედეგის გამონატანის სახით დასაბეჭდად, ჩვენ გვაქვს საჭიროება შემოვიტანოთ io შეყვანა/გამოყვანის (input/output) ბიბლიოთეკა Scope-ში. io ბიბლიოთეკა მოდის სტანდარტული ბიბლიოთეკიდან, რომელიც ცნობილია როგორც std:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

ნაგულისხმევად, Rust-ს აქვს ელემენტების ნაკრები განსაზღვრული სტანდარტულ ბიბლიოთეკაში, რომელიც შემოაქვს თითოეული პროგრამის Scope-ში. ამ ნაკრებს ეწოდება prelude, და შეგიძლიათ ნახოთ ყველაფერი მასში სტანდარტული ბიბლიოთეკის დოკუმენტაციაში.

თუ ტიპი, რომლის გამოყენებაც გსურთ, არ არის prelude-ში, ეს ტიპი მკაფიოდ უნდა შემოიტანოთ Scope-ში use ინსტრუქციით. std::io ბიბლიოთეკის გამოყენება გთავაზობთ მრავალ სასარგებლო შესაძლებლობას, მათ შორის მომხმარებლის შეყვანილი მონაცემების მიღების უნარს.

როგორც პირველ თავში ნახეთ, main ფუნქცია არის პროგრამაში შესვლის წერტილი (entry point):

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

fn სინტაქსი აცხადებს ახალ ფუნქციას; ფრჩხილები, (), მიუთითებს, რომ პარამეტრები არ არის; ხოლო ფიგურული ფრჩხილი, {, იწყებს ფუნქციის ტანს.

როგორც ასევე ისწავლეთ პირველ თავში, println! არის მაკროსი, რომელიც ბეჭდავს სტრიქონს ეკრანზე:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

ეს კოდი ბეჭდავს შეტყობინებას, სადაც ნათქვამია, თუ რა არის თამაში, და ითხოვს მონაცემების შეყვანას მომხმარებლისგან.

მნიშვნელობების შენახვა ცვლადებით

შემდეგ, შევქმნით ცვლადს მომხმარებლის შეყვანილი მონაცემების შესანახად, ასე:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

ახლა პროგრამა საინტერესო ხდება! ამ პატარა სტრიქონში ბევრი რამ ხდება. ცვლადის შესაქმნელად ვიყენებთ let ინსტრუქციას. აი კიდევ ერთი მაგალითი:

let apples = 5;

ეს სტრიქონი ქმნის ახალ ცვლადს სახელად apples და აკავშირებს მას 5 მნიშვნელობასთან. Rust-ში ცვლადები ნაგულისხმევად არის immutable (შეუცვლელი), რაც ნიშნავს, რომ მას შემდეგ, რაც ცვლადს მივცემთ მნიშვნელობას, ეს მნიშვნელობა არ შეიცვლება. ამ კონცეფციას დეტალურად განვიხილავთ “ცვლადები და Mutability” სექციაში მე-3 თავში. ცვლადის mutable (ცვლადი) გახდომისთვის, ცვლადის სახელის წინ ვამატებთ mut-ს:

let apples = 5; // immutable
let mut bananas = 5; // mutable

შენიშვნა: // სინტაქსი იწყებს კომენტარს, რომელიც გრძელდება სტრიქონის ბოლომდე. Rust-ი იგნორირებას უკეთებს ყველაფერს კომენტარებში. კომენტარებს უფრო დეტალურად განვიხილავთ მე-3 თავში.

დავუბრუნდეთ რიცხვის გამოცნობის თამაშის პროგრამას. ახლა უკვე იცით, რომ let mut guess შემოიტანს mutable ცვლადს სახელად guess. ტოლობის ნიშანი (=) ეუბნება Rust-ს, რომ ახლა გვსურს რაღაცის დაკავშირება ცვლადთან. ტოლობის ნიშნის მარჯვნივ არის მნიშვნელობა, რომელთანაც დაკავშირებულია guess, რაც არის String::new-ის გამოძახების შედეგი — ფუნქციისა, რომელიც აბრუნებს String-ის ახალ ინსტანციას. String არის სტანდარტული ბიბლიოთეკის მიერ მოწოდებული სტრიქონის ტიპი, რომელიც წარმოადგენს გაფართოებად, UTF-8 კოდირებულ ტექსტს.

:: სინტაქსი ::new სტრიქონში მიუთითებს, რომ new არის String ტიპის ასოცირებული ფუნქცია (associated function). ასოცირებული ფუნქცია არის ფუნქცია, რომელიც იმპლემენტირებულია ტიპზე, ამ შემთხვევაში String-ზე. ეს new ფუნქცია ქმნის ახალ, ცარიელ სტრიქონს. new ფუნქციას შეხვდებით მრავალ ტიპზე, რადგან ეს არის გავრცელებული სახელი ფუნქციისთვის, რომელიც ქმნის რაიმე სახის ახალ მნიშვნელობას.

სრულად, let mut guess = String::new(); სტრიქონმა შექმნა mutable ცვლადი, რომელიც ამჟამად დაკავშირებულია String-ის ახალ, ცარიელ ინსტანციასთან.

მომხმარებლის შეყვანილი მონაცემების მიღება

გახსოვდეთ, რომ სტანდარტული ბიბლიოთეკიდან შეყვანა/გამოყვანის ფუნქციონალი შემოვიტანეთ use std::io;-ით პროგრამის პირველ სტრიქონზე. ახლა გამოვიძახებთ stdin ფუნქციას io მოდულიდან, რაც საშუალებას მოგვცემს დავამუშაოთ მომხმარებლის შეყვანილი მონაცემები:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

პროგრამის დასაწყისში io მოდული use std::io;-ით რომ არ შემოგვეტანა, მაინც შეგვეძლო ფუნქციის გამოყენება ამ ფუნქციის გამოძახების ჩწერით როგორც std::io::stdin. stdin ფუნქცია აბრუნებს std::io::Stdin-ის ინსტანციას — ტიპს, რომელიც წარმოადგენს სახელურს (handle) თქვენი ტერმინალის სტანდარტულ შეყვანაზე (standard input).

შემდეგ, სტრიქონი .read_line(&mut guess) იძახებს read_line მეთოდს სტანდარტული შეყვანის სახელურზე მომხმარებლისგან მონაცემების მისაღებად. ჩვენ ასევე გადავცემთ &mut guess-ს არგუმენტად read_line-ს, რათა ავუხსნათ, რომელ სტრიქონში შეინახოს მომხმარებლის შეყვანილი მონაცემები. read_line-ის სრული ამოცანაა აიღოს ყველაფერი, რასაც მომხმარებელი აკრეფს სტანდარტულ შეყვანაში და მიაწეროს ის სტრიქონს (მისი შინაარსის გადაწერის გარეშე), ასე რომ, ჩვენ ამ სტრიქონს გადავცემთ არგუმენტად. სტრიქონის არგუმენტი უნდა იყოს mutable, რათა მეთოდმა შეძლოს სტრიქონის შინაარსის შეცვლა.

& სიმბოლო მიუთითებს, რომ ეს არგუმენტი არის reference (მიმართვა/რეფერენსი), რაც გაძლევთ საშუალებას თქვენი კოდის მრავალ ნაწილს ჰქონდეს წვდომა მონაცემთა ერთ ნაწილთან მეხსიერებაში ამ მონაცემების მრავალჯერ კოპირების საჭიროების გარეშე. Reference-ები რთული შესაძლებლობაა, და Rust-ის ერთ-ერთი მთავარი უპირატესობა ის არის, თუ რამდენად უსაფრთხო და მარტივია reference-ების გამოყენება. ამ პროგრამის დასასრულებლად ბევრი ამ დეტალის ცოდნა არ გჭირდებათ. ახლა მხოლოდ ის უნდა იცოდეთ, რომ ცვლადების მსგავსად, reference-ებიც ნაგულისხმევად immutable-ია. ამდენად, თქვენ უნდა დაწეროთ &mut guess და არა &guess, რათა ის mutable გახადოთ. (მე-4 თავი reference-ებს უფრო სიღრმისეულად განმარტავს.)

შესაძლო შეცდომის დამუშავება Result-ით

ჩვენ კვლავ ვმუშაობთ კოდის ამ სტრიქონზე. ახლა განვიხილავთ ტექსტის მესამე სტრიქონს, თუმცა გაითვალისწინეთ, რომ ის მაინც კოდის ერთი ლოგიკური სტრიქონის ნაწილია. შემდეგი ნაწილი არის ეს მეთოდი:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

ეს კოდი შეგვეძლო ასეც დაგვეწერა:

io::stdin().read_line(&mut guess).expect("Failed to read line");

თუმცა, ერთი გრძელი სტრიქონი ძნელი წასაკითხია, ასე რომ ჯობია ის დავყოთ. ხშირად გონივრულია ახალი სტრიქონისა და დაშორებების შემოტანა გრძელი სტრიქონების დასაშლელად, როდესაც მეთოდს იძახებთ .method_name() სინტაქსით. ახლა განვიხილოთ, რას აკეთებს ეს სტრიქონი.

როგორც ადრე აღვნიშნეთ, read_line ათავსებს ყველაფერს, რასაც მომხმარებელი შეიყვანს, იმ სტრიქონში, რომელსაც მას გადავცემთ, მაგრამ ის ასევე აბრუნებს Result მნიშვნელობას. Result არის enumeration (ჩამონათვალი), რომელსაც ხშირად enum-ს უწოდებენ — ტიპი, რომელიც შეიძლება იყოს მრავალი შესაძლო მდგომარეობიდან ერთ-ერთში. თითოეულ შესაძლო მდგომარეობას variant-ს (ვარიანტს) ვუწოდებთ.

მე-6 თავი დაფარავს enum-ებს უფრო დეტალურად. ამ Result ტიპების მიზანია შეცდომების დამუშავების ინფორმაციის კოდირება.

Result-ის ვარიანტებია Ok და Err. Ok ვარიანტი მიუთითებს, რომ ოპერაცია წარმატებული იყო, და ის შეიცავს წარმატებით დაგენერირებულ მნიშვნელობას. Err ვარიანტი ნიშნავს, რომ ოპერაცია ჩაიშალა, და ის შეიცავს ინფორმაციას იმის შესახებ, თუ როგორ ან რატომ ჩაიშალა ოპერაცია.

Result ტიპის მნიშვნელობებს, ისევე როგორც ნებისმიერი ტიპის მნიშვნელობებს, მათზე განსაზღვრული მეთოდები აქვთ. Result-ის ინსტანციას აქვს expect მეთოდი, რომლის გამოძახებაც შეგიძლიათ. თუ Result-ის ეს ინსტანცია არის Err მნიშვნელობა, expect გამოიწვევს პროგრამის ავარიულ გათიშვას (crash) და აჩვენებს შეტყობინებას, რომელიც არგუმენტად გადაეცით expect-ს. თუ read_line მეთოდი აბრუნებს Err-ს, ეს სავარაუდოდ იქნება ოპერაციული სისტემიდან მოსული შეცდომის შედეგი. თუ Result-ის ეს ინსტანცია არის Ok მნიშვნელობა, expect აიღებს იმ დაბრუნებულ მნიშვნელობას, რომელსაც Ok შეიცავს და დაგიბრუნებთ მხოლოდ ამ მნიშვნელობას, რათა შეძლოთ მისი გამოყენება. ამ შემთხვევაში, ეს მნიშვნელობა არის ბაიტების რაოდენობა მომხმარებლის შეყვანილ მონაცემებში.

თუ არ გამოიძახებთ expect-ს, პროგრამა დაკომპილირდება, მაგრამ მიიღებთ გაფრთხილებას (warning):

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
   |
10 |     let _ = io::stdin().read_line(&mut guess);
   |     +++++++

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s

Rust-ი გაფრთხილებთ, რომ არ გამოგიყენებიათ read_line-დან დაბრუნებული Result მნიშვნელობა, რაც მიუთითებს, რომ პროგრამას არ დაუმუშავებია შესაძლო შეცდომა.

გაფრთხილების ჩახშობის სწორი გზაა შეცდომის დამუშავების რეალური კოდის დაწერა, მაგრამ ჩვენს შემთხვევაში უბრალოდ გვსურს პროგრამის გათიშვა პრობლემის წარმოქმნისას, ასე რომ შეგვიძლია გამოვიყენოთ expect. შეცდომებისგან აღდგენის შესახებ ისწავლით მე-9 თავში.

მნიშვნელობების დაბეჭდვა println! Placeholder-ებით

დამხურავი ფიგურული ფრჩხილის გარდა, აქამდე არსებულ კოდში განსახილველი დარჩა მხოლოდ ერთი სტრიქონი:

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

ეს სტრიქონი ბეჭდავს სტრიქონს, რომელიც ახლა შეიცავს მომხმარებლის შეყვანილ მონაცემებს. {} ფიგურული ფრჩხილების წყვილი არის placeholder-ი (ადგილის დამკავებელი): წარმოიდგინეთ {} როგორც პატარა მარწუხები, რომლებიც იჭერენ მნიშვნელობას თავის ადგილზე. ცვლადის მნიშვნელობის დაბეჭდვისას, ცვლადის სახელი შეიძლება ჩაიწეროს ფიგურული ფრჩხილების შიგნით. გამოსახულების შეფასების შედეგის დაბეჭდვისას, განათავსეთ ცარიელი ფიგურული ფრჩხილები ფორმატის სტრიქონში, შემდეგ ფორმატის სტრიქონს მოაყოლეთ მძიმით გამოყოფილი გამოსახულებების სია თითოეულ ცარიელ ფიგურულ ფრჩხილში დასაბეჭდად იმავე თანმიმდევრობით. ცვლადისა და გამოსახულების შედეგის დაბეჭდვა println!-ის ერთ გამოძახებაში ასე გამოყურება:

#![allow(unused)]
fn main() {
let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);
}

ეს კოდი დაბეჭდავს x = 5 and y + 2 = 12.

პირველი ნაწილის ტესტირება

გამოვცადოთ რიცხვის გამოცნობის თამაშის პირველი ნაწილი. გაუშვით ის cargo run-ის გამოყენებით:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
      Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

ამ ეტაპზე, თამაშის პირველი ნაწილი დასრულებულია: ჩვენ ვიღებთ მონაცემებს კლავიატურიდან და შემდეგ ვბეჭდავთ მათ.

საიდუმლო რიცხვის გენერირება

შემდეგ, ჩვენ უნდა დავაგენერიროთ საიდუმლო რიცხვი, რომლის გამოცნობასაც მომხმარებელი შეეცდება. საიდუმლო რიცხვი ყოველ ჯერზე განსხვავებული უნდა იყოს, რათა თამაში საინტერესო იყოს ერთზე მეტჯერ სათამაშოდ. გამოვიყენებთ შემთხვევით რიცხვს 1-დან 100-მდე, რათა თამაში არ იყოს ძალიან რთული. Rust-ი თავის სტანდარტულ ბიბლიოთეკაში ჯერ არ მოიცავს შემთხვევითი რიცხვების ფუნქციონალს. თუმცა, Rust-ის გუნდი უზრუნველყოფს rand crate-ს აღნიშნული ფუნქციონალით.

ფუნქციონალის გაზრდა Crate-ით

გახსოვდეთ, რომ crate არის Rust-ის საწყისი კოდის ფაილების კოლექცია. პროექტი, რომელსაც ჩვენ ვაგებთ, არის binary crate (ბინარული crate), რომელიც წარმოადგენს შესრულებად პროგრამას. rand crate არის library crate (ბიბლიოთეკის crate), რომელიც შეიცავს კოდს, რომელიც განკუთვნილია სხვა პროგრამებში გამოსაყენებლად და დამოუკიდებლად ვერ შესრულდება.

გარეგანი crate-ების კოორდინაცია არის ის, სადაც Cargo ჭეშმარიტად ბრწყინავს. სანამ დავწერთ კოდს, რომელიც იყენებს rand-ს, ჩვენ უნდა შევცვალოთ Cargo.toml ფაილი, რათა შევიყვანოთ rand crate დამოკიდებულებად (dependency). გახსენით ეს ფაილი ახლა და დაამატეთ შემდეგი სტრიქონი ბოლოში, [dependencies] სექციის სათაურის ქვეშ, რომელიც Cargo-მ შექმნა თქვენთვის. დარწმუნდით, რომ მიუთითეთ rand ზუსტად ისე, როგორც აქ გვაქვს, ამ ვერსიის ნომრით, თორემ ამ სახელმძღვანელოს კოდის მაგალითებმა შეიძლება არ იმუშაოს:

ფაილის სახელი: Cargo.toml

[dependencies]
rand = "0.10.1"

Cargo.toml ფაილში ყველაფერი, რაც სათაურს მოჰყვება, არის იმ სექციის ნაწილი, რომელიც გრძელდება მანამ, სანამ სხვა სექცია არ დაიწყება. [dependencies]-ში თქვენ ეუბნებით Cargo-ს, თუ რომელ გარეგან crate-ებზეა დამოკიდებული თქვენი პროექტი და ამ crate-ების რომელ ვერსიებს მოითხოვთ. ამ შემთხვევაში, ჩვენ ვუთითებთ rand crate-ს სემანტიკური ვერსიის სპეციფიკატორით 0.10.1. Cargo-ს ესმის Semantic Versioning (ზოგჯერ უწოდებენ SemVer-ს), რაც არის ვერსიის ნომრების დაწერის სტანდარტი. სპეციფიკატორი 0.10.1 სინამდვილეში არის ^0.10.1-ის მოკლე ჩანაწერი, რაც ნიშნავს ნებისმიერ ვერსიას, რომელიც არის სულ მცირე 0.10.1, მაგრამ 0.11.0-ზე ნაკლები.

Cargo მიიჩნევს, რომ ამ ვერსიებს აქვთ საჯარო API-ები, რომლებიც თავსებადია 0.10.1 ვერსიასთან, და ეს სპეციფიკაცია უზრუნველყოფს იმას, რომ მიიღებთ უახლეს patch გამოშვებას, რომელიც კვლავ დაკომპილირდება ამ თავის კოდთან. 0.11.0 ან უფრო მაღალ ვერსიას არ აქვს გარანტირებული, რომ ექნება იგივე API, რასაც შემდგომი მაგალითები იყენებენ.

ახლა, კოდის შეცვლის გარეშე, ავაგოთ პროექტი, როგორც ნაჩვენებია 2-2 ლისტინგში.

$ cargo build
    Updating crates.io index
      Locking 8 packages to latest Rust 1.96.0 compatible versions
  Downloaded rand_core v0.10.1
  Downloaded chacha20 v0.10.1
  Downloaded rand v0.10.1
  Downloaded 3 crates (162.9KiB) in 0.59s
   Compiling libc v0.2.186
   Compiling rand_core v0.10.1
   Compiling getrandom v0.4.3
   Compiling cfg-if v1.0.4
   Compiling chacha20 v0.10.1
   Compiling rand v0.10.1
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.03s
Listing 2-2: cargo build-ის გაშვების გამონატანი rand crate-ის დამოკიდებულებად დამატების შემდეგ

შეიძლება დაინახოთ ვერსიის განსხვავებული ნომრები (მაგრამ ისინი ყველა თავსებადი იქნება კოდთან SemVer-ის წყალობით!) და განსხვავებული სტრიქონები (ოპერაციული სისტემის მიხედვით), და სტრიქონები შეიძლება იყოს განსხვავებული თანმიმდევრობით.

როდესაც შემოგვაქვს გარეგანი დამოკიდებულება, Cargo წამოიღებს ყველაფრის უახლეს ვერსიებს, რაც ამ დამოკიდებულებას სჭირდება რეესტრიდან (registry), რაც არის მონაცემთა ასლი Crates.io-დან. Crates.io არის ადგილი, სადაც Rust ეკოსისტემის ადამიანები აქვეყნებენ თავიანთ ღია კოდის Rust პროექტებს სხვების გამოსაყენებლად.

რეესტრის განახლების შემდეგ, Cargo ამოწმებს [dependencies] სექციას და ჩამოტვირთავს ჩამოთვლილ crate-ებს, რომლებიც ჯერ არ არის ჩამოტვირთული. ამ შემთხვევაში, თუმცა ჩვენ მხოლოდ rand ჩამოვთვალეთ დამოკიდებულებად, Cargo-მ ასევე წამოიღო სხვა crate-ები, რომლებზეც rand არის დამოკიდებული სამუშაოდ. crate-ების ჩამოტვირთვის შემდეგ, Rust-ი აკომპილირებს მათ და შემდეგ აკომპილირებს პროექტს ხელმისაწვდომი დამოკიდებულებებით.

თუ დაუყოვნებლივ გაუშვებთ cargo build-ს ხელახლა ცვლილებების შეტანის გარეშე, ვერ მიიღებთ ვერანაირ გამონატანს Finished სტრიქონის გარდა. Cargo-მ იცის, რომ მან უკვე ჩამოტვირთა და დააკომპილირა დამოკიდებულებები, და თქვენ არაფერი შეგიცვლიათ მათ შესახებ თქვენს Cargo.toml ფაილში. Cargo-მ ასევე იცის, რომ არაფერი შეგიცვლიათ თქვენს კოდში, ასე რომ ის არც მას აკომპილირებს ხელახლა. გასაკეთებელი რომ არაფერია, ის უბრალოდ სრულდება.

თუ გახსნით src/main.rs ფაილს, შეიტანთ მცირე ცვლილებას, შეინახავთ მას და ხელახლა ააგებთ, დაინახავთ გამონატანის მხოლოდ ორ სტრიქონს:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s

ეს სტრიქონები აჩვენებს, რომ Cargo ანახლებს build-ს მხოლოდ თქვენი მცირე ცვლილებით src/main.rs ფაილში. თქვენი დამოკიდებულებები არ შეცვლილა, ასე რომ Cargo-მ იცის, რომ შეუძლია ხელახლა გამოიყენოს ის, რაც უკვე ჩამოტვირთა და დააკომპილირა მათთვის.

აღწარმოებადი Build-ების უზრუნველყოფა

Cargo-ს აქვს მექანიზმი, რომელიც უზრუნველყოფს იმას, რომ შეგიძლიათ ხელახლა ააგოთ იგივე არტეფაქტი ყოველ ჯერზე, როდესაც თქვენ ან ვინმე სხვა აგებს თქვენს კოდს: Cargo გამოიყენებს მხოლოდ იმ დამოკიდებულებების ვერსიებს, რომლებიც თქვენ მიუთითეთ, სანამ სხვაგვარად არ მიუთითებთ. მაგალითად, ვთქვათ, რომ მომდევნო კვირაში გამოვა rand crate-ის 0.10.2 ვერსია, და ეს ვერსია შეიცავს მნიშვნელოვან შეცდომის გასწორებას (bug fix), მაგრამ ის ასევე შეიცავს რეგრესიას, რომელიც გააფუჭებს თქვენს კოდს. ამის დასამუშავებლად, Rust-ი ქმნის Cargo.lock ფაილს cargo build-ის პირველად გაშვებისას, ასე რომ ახლა ეს ფაილი გვაქვს guessing_game დირექტორიაში.

როდესაც პროექტს პირველად აგებთ, Cargo ადგენს დამოკიდებულებების ყველა ვერსიას, რომლებიც შეესაბამება კრიტერიუმებს და შემდეგ წერს მათ Cargo.lock ფაილში. როდესაც პროექტს აგებთ მომავალში, Cargo დაინახავს, რომ Cargo.lock ფაილი არსებობს და გამოიყენებს იქვე მითითებულ ვერსიებს, ნაცვლად იმისა, რომ ხელახლა შეასრულოს ვერსიების დადგენის მთელი სამუშაო. ეს საშუალებას გაძლევთ ავტომატურად გქონდეთ აღწარმოებადი build-ი (reproducible build). სხვა სიტყვებით რომ ვთქვათ, თქვენი პროექტი დარჩება 0.10.1 ვერსიაზე, სანამ მკაფიოდ არ განაახლებთ მას, Cargo.lock ფაილის წყალობით. რადგან Cargo.lock ფაილი მნიშვნელოვანია აღწარმოებადი build-ებისთვის, ის ხშირად შეჰყავთ ვერსიების კონტროლში (source control) თქვენი პროექტის დანარჩენ კოდთან ერთად.

Crate-ის განახლება ახალი ვერსიის მისაღებად

როდესაც ნამდვილად გსურთ crate-ის განახლება, Cargo გვაწვდის update ბრძანებას, რომელიც იგნორირებას გაუკეთებს Cargo.lock ფაილს და დაადგენს ყველა უახლეს ვერსიას, რომლებიც შეესაბამება თქვენს სპეციფიკაციებს Cargo.toml-ში. Cargo შემდეგ ჩაწერს ამ ვერსიებს Cargo.lock ფაილში. წინააღმდეგ შემთხვევაში, ნაგულისხმევად, Cargo მოძებნის მხოლოდ 0.10.1-ზე მეტ და 0.11.0-ზე ნაკლებ ვერსიებს. თუ rand crate-ს გამოშვებული ექნება ორი ახალი ვერსია, 0.10.2 და 0.999.0, cargo update-ის გაშვებისას დაინახავდით შემდეგს:

$ cargo update
    Updating crates.io index
      Locking 1 package to latest Rust 1.96.0 compatible version
    Updating rand v0.10.1 -> v0.10.2 (available: v0.999.0)

Cargo იგნორირებას უკეთებს 0.999.0 გამოშვებას. ამ ეტაპზე, თქვენ ასევე შეამჩნევდით ცვლილებას თქვენს Cargo.lock ფაილში, სადაც აღნიშნულია, რომ rand crate-ის ვერსია, რომელსაც ახლა იყენებთ, არის 0.10.2. rand-ის 0.999.0 ვერსიის ან 0.999.x სერიის ნებისმიერი ვერსიის გამოსაყენებლად, თქვენ მოგიწევთ განაახლოთ Cargo.toml ფაილი, რათა ის ასე გამოიყურებოდეს (სინამდვილეში ნუ შეიტანთ ამ ცვლილებას, რადგან შემდგომი მაგალითები გულისხმობს, რომ იყენებთ rand 0.10-ს):

[dependencies]
rand = "0.999.0"

შემდეგ ჯერზე, როდესაც გაუშვებთ cargo build-ს, Cargo განაახლებს ხელმისაწვდომი crate-ების რეესტრს და ხელახლა შეაფასებს თქვენს rand მოთხოვნებს თქვენ მიერ მითითებული ახალი ვერსიის შესაბამისად.

ბევრი რამ არის სათქმელი Cargo-სა და მისი ეკოსისტემის შესახებ, რასაც განვიხილავთ მე-14 თავში, მაგრამ ჯერჯერობით ეს არის ყველაფერი, რაც უნდა იცოდეთ. Cargo ძალიან მარტივს ხდის ბიბლიოთეკების ხელახლა გამოყენებას, ასე რომ Rustacean-ებს შეუძლიათ დაწერონ უფრო მცირე პროექტები, რომლებიც აწყობილია რამდენიმე პაკეტისგან.

შემთხვევითი რიცხვის გენერირება

დავიწყოთ rand-ის გამოყენება საიდუმლო რიცხვის დასაგენერირებლად. შემდეგი ნაბიჯია src/main.rs-ის განახლება, როგორც ნაჩვენებია 2-3 ლისტინგში.

Filename: src/main.rs
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}
Listing 2-3: კოდის დამატება შემთხვევითი რიცხვის დასაგენერირებლად

ჯერ ვამატებთ სტრიქონს use rand::prelude::*;. prelude მოდული შეიცავს rand crate-ის ყველაზე ხშირად გამოყენებად ნაწილებს, ხოლო use ხდის ამ ელემენტებს ხელმისაწვდომს ჩვენი პროგრამის Scope-ში.

შემდეგ, შუაში ვამატებთ ორ სტრიქონს. პირველ სტრიქონში ვეძახით rand::rng ფუნქციას, რომელიც გვაძლევს კონკრეტულ შემთხვევითი რიცხვების გენერატორს, რომელსაც გამოვიყენებთ: ისეთს, რომელიც ლოკალურია მიმდინარე ნაკადისთვის (thread) და ინიციალიზებულია ოპერაციული სისტემის მიერ. შემდეგ, ვეძახით random_range მეთოდს შემთხვევითი რიცხვების გენერატორზე. ეს მეთოდი განსაზღვრულია RngExt Trait-ით, რომელიც არის rand::prelude მოდულის ნაწილი, რომელიც შემოვიტანეთ Scope-ში use rand::prelude::*; ინსტრუქციით. random_range მეთოდი არგუმენტად იღებს დიაპაზონის (range) გამოსახულებას და აგენერირებს შემთხვევით რიცხვს ამ დიაპაზონში. დიაპაზონის გამოსახულების ის სახეობა, რომელსაც აქ ვიყენებთ, იღებს ფორმას start..=end და მოიცავს როგორც ქვედა, ასევე ზედა ზღვარს, ასე რომ ჩვენ უნდა მივუთითოთ 1..=100, რათა მოვითხოვოთ რიცხვი 1-დან 100-მდე.

შენიშვნა: თქვენ უბრალოდ არ გეცოდინებათ, თუ რა შემოიტანოთ Scope-ში და რომელი მეთოდები და ფუნქციები გამოიძახოთ crate-იდან, ასე რომ თითოეულ crate-ს აქვს დოკუმენტაცია მისი გამოყენების ინსტრუქციებით. Cargo-ს კიდევ ერთი შესანიშნავი შესაძლებლობა ის არის, რომ cargo doc --open ბრძანების გაშვება ააგებს ყველა თქვენი დამოკიდებულების მიერ მოწოდებულ დოკუმენტაციას ლოკალურად და გახსნის მას თქვენს ბრაუზერში. მაგალითად, თუ დაინტერესებული ხართ rand crate-ის სხვა ფუნქციონალით, გაუშვით cargo doc --open და დააწკაპუნეთ rand-ზე მარცხენა გვერდითა ზოლში.

მეორე ახალი სტრიქონი ბეჭდავს საიდუმლო რიცხვს. ეს სასარგებლოა პროგრამის შემუშავებისას მისი ტესტირებისთვის, მაგრამ მას წავშლით საბოლოო ვერსიიდან. თამაში არ იქნება საინტერესო, თუ პროგრამა ბეჭდავს პასუხს დაწყებისთანავე!

სცადეთ პროგრამის გაშვება რამდენჯერმე:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
      Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
      Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

თქვენ უნდა მიიღოთ განსხვავებული შემთხვევითი რიცხვები, და ისინი ყველა უნდა იყოს 1-დან 100-მდე. თუ მიიღებთ გაფრთხილებებს, მათი იგნორირება უსაფრთხოა. თუ მიიღებთ შეცდომებს, გთხოვთ შეამოწმოთ, რომ გაქვთ rand = "0.10.1" თქვენს Cargo.toml-ში, რადგან rand-ის მომავალ ვერსიებს შეიძლება ჰქონდეს განსხვავებული API, მაგრამ 0.10 სერიის ნებისმიერი ვერსია იმუშავებს ამ თავის კოდთან.

ნავარაუდევი რიცხვის შედარება საიდუმლო რიცხვთან

ახლა, როდესაც გვაქვს მომხმარებლის შეყვანილი მონაცემი და შემთხვევითი რიცხვი, შეგვიძლია შევადაროთ ისინი. ეს ნაბიჯი ნაჩვენებია 2-4 ლისტინგში. გაითვალისწინეთ, რომ ეს კოდი ჯერ ვერ დაკომპილირდება, როგორც ავხსნით.

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    // --snip--
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}
Listing 2-4: ორი რიცხვის შედარების შესაძლო დაბრუნებული მნიშვნელობების დამუშავება

ჯერ ვამატებთ სხვა use ინსტრუქციას, რომელსაც შემოაქვს ტიპი სახელად std::cmp::Ordering Scope-ში სტანდარტული ბიბლიოთეკიდან. Ordering ტიპი არის კიდევ ერთი enum და აქვს ვარიანტები Less, Greater და Equal. ეს არის სამი შედეგი, რომლებიც შესაძლებელია ორი მნიშვნელობის შედარებისას.

შემდეგ, ბოლოში ვამატებთ ხუთ ახალ სტრიქონს, რომლებიც იყენებენ Ordering ტიპს. cmp მეთოდი ადარებს ორ მნიშვნელობას და მისი გამოძახება შესაძლებელია ნებისმიერ რამეზე, რისი შედარებაც შესაძლებელია. ის იღებს reference-ს იმასთან, რასთანაც გსურთ შედარება: აქ ის ადარებს guess-ს secret_number-თან. შემდეგ ის აბრუნებს Ordering enum-ის ვარიანტს, რომელიც შემოვიტანეთ Scope-ში use ინსტრუქციით. ვიყენებთ match გამოსახულებას იმის გადასაწყვეტად, თუ რა გავაკეთოთ შემდეგ იმის მიხედვით, თუ Ordering-ის რომელი ვარიანტი დაბრუნდა cmp-ის გამოძახებიდან guess-ისა და secret_number-ის მნიშვნელობებით.

match გამოსახულება შედგება arm-ებისგან (შტოებისგან). Arm-ი შედგება pattern-ისგან (შაბლონისგან), რომელსაც უნდა დაემთხვეს, და კოდისგან, რომელიც უნდა გაეშვას, თუ match-ისთვის მიცემული მნიშვნელობა ერგება ამ arm-ის pattern-ს. Rust-ი იღებს match-ისთვის მიცემულ მნიშვნელობას და თანმიმდევრობით ამოწმებს თითოეული arm-ის pattern-ს. Pattern-ები და match კონსტრუქცია Rust-ის მძლავრი შესაძლებლობებია: ისინი საშუალებას გაძლევთ გამოხატოთ სხვადასხვა სიტუაცია, რომელსაც თქვენი კოდი შეიძლება წააწყდეს, და უზრუნველყოფენ, რომ ყველა მათგანი დაამუშაოთ. ეს შესაძლებლობები დეტალურად იქნება დაფარული შესაბამისად მე-6 და მე-19 თავებში.

გავიაროთ მაგალითი match გამოსახულებით, რომელსაც აქ ვიყენებთ. ვთქვათ, მომხმარებელმა ივარაუდა 50 და ამჯერად შემთხვევით დაგენერირებული საიდუმლო რიცხვი არის 38.

როდესაც კოდი ადარებს 50-ს 38-თან, cmp მეთოდი დააბრუნებს Ordering::Greater-ს, რადგან 50 მეტია 38-ზე. match გამოსახულება იღებს Ordering::Greater მნიშვნელობას და იწყებს თითოეული arm-ის pattern-ის შემოწმებას. ის უყურებს პირველი arm-ის pattern-ს, Ordering::Less, და ხედავს, რომ Ordering::Greater მნიშვნელობა არ ემთხვევა Ordering::Less-ს, ასე რომ ის იგნორირებას უკეთებს კოდს იმ arm-ში და გადადის შემდგომ arm-ზე. შემდგომი arm-ის pattern არის Ordering::Greater, რომელიც ნამდვილად ემთხვევა Ordering::Greater-ს! ამ arm-ში ასოცირებული კოდი შესრულდება და ეკრანზე დაბეჭდავს Too big!-ს. match გამოსახულება სრულდება პირველი წარმატებული დამთხვევის შემდეგ, ასე რომ ის არ ჩახედავს ბოლო arm-ში ამ სცენარში.

თუმცა, კოდი 2-4 ლისტინგში ჯერ ვერ დაკომპილირდება. სცადეთ ის:

$ cargo build
   Compiling libc v0.2.186
   Compiling rand_core v0.10.1
   Compiling getrandom v0.4.3
   Compiling cfg-if v1.0.0
   Compiling chacha20 v0.10.1
   Compiling rand v0.10.1
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
   |                 |
   |                 arguments to this method are incorrect
   |
   = note: expected reference `&String`
              found reference `&{integer}`
note: method defined here
  --> /rustc/2d8144b7880597b6e6d3dfd63a9a9efae3f533d3/library/core/src/cmp.rs:1000:7

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error

შეცდომის არსი აცხადებს, რომ არის mismatched types (შეუსაბამო ტიპები). Rust-ს აქვს ძლიერი, სტატიკური ტიპების სისტემა. თუმცა მას ასევე აქვს ტიპების გამოყვანა (type inference). როდესაც დავწერეთ let mut guess = String::new(), Rust-მა შეძლო გამოეყვანა, რომ guess უნდა ყოფილიყო String და არ აიძულა ჩვენთვის ტიპის დაწერა. secret_number, მეორე მხრივ, არის რიცხვის ტიპი. Rust-ის რიცხვის რამდენიმე ტიპს შეიძლება ჰქონდეს მნიშვნელობა 1-დან 100-მდე: i32, 32-ბიტიანი რიცხვი; u32, უნიშნო (unsigned) 32-ბიტიანი რიცხვი; i64, 64-ბიტიანი რიცხვი; ასევე სხვა. თუ სხვაგვარად არ არის მითითებული, Rust-ი ნაგულისხმევად იყენებს i32-ს, რაც არის secret_number-ის ტიპი, თუ სხვაგან არ დაამატებთ ტიპის ინფორმაციას, რაც აიძულებს Rust-ს გამოიყვანოს განსხვავებული რიცხვითი ტიპი. შეცდომის მიზეზი ის არის, რომ Rust-ს არ შეუძლია შეადაროს სტრიქონი და რიცხვის ტიპი.

საბოლოო ჯამში, ჩვენ გვსურს გარდავქმნათ String, რომელსაც პროგრამა კითხულობს შეყვანილი მონაცემის სახით, რიცხვის ტიპად, რათა შევძლოთ მისი რიცხობრივად შედარება საიდუმლო რიცხვთან. ამას ვაკეთებთ ამ სტრიქონის დამატებით main ფუნქციის ტანში:

ფაილის სახელი: src/main.rs

use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    // --snip--

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

სტრიქონია:

let guess: u32 = guess.trim().parse().expect("Please type a number!");

ჩვენ ვქმნით ცვლადს სახელად guess. მაგრამ მოიცა, განა პროგრამას უკვე არ აქვს ცვლადი სახელად guess? აქვს, მაგრამ Rust-ი დამხმარედ გაძლევთ საშუალებას დაჩრდილოთ (shadow) guess-ის წინა მნიშვნელობა ახლით. Shadowing საშუალებას გვაძლევს ხელახლა გამოვიყენოთ guess ცვლადის სახელი, ნაცვლად იმისა, რომ გვაიძულოს ორი უნიკალური ცვლადის შექმნა, როგორიცაა მაგალითად guess_str და guess. ამას უფრო დეტალურად დავფარავთ მე-3 თავში, მაგრამ ჯერჯერობით იცოდეთ, რომ ეს შესაძლებლობა ხშირად გამოიყენება, როდესაც გსურთ მნიშვნელობის გარდაქმნა ერთი ტიპიდან მეორე ტიპში.

ჩვენ ვაკავშირებთ ამ ახალ ცვლადს guess.trim().parse() გამოსახულებასთან. guess გამოსახულებაში მიუთითებს თავდაპირველ guess ცვლადზე, რომელიც შეიცავდა შეყვანილ მონაცემს სტრიქონის სახით. trim მეთოდი String ინსტანციაზე წაშლის ნებისმიერ დაშორებას (whitespace) დასაწყისში და ბოლოში, რაც უნდა გავაკეთოთ სანამ შევძლებთ სტრიქონის გარდაქმნას u32-ად, რომელიც მხოლოდ რიცხობრივ მონაცემებს შეიცავს. მომხმარებელმა უნდა დააჭიროს enter-ს read_line-ის დასაკმაყოფილებლად და თავისი ნავარაუდევი რიცხვის შესაყვანად, რაც ამატებს ახალი სტრიქონის პერსონაჟს (newline character) სტრიქონს. მაგალითად, თუ მომხმარებელი აკრეფს 5-ს და დააჭერს enter-ს, guess ასე გამოიყურება: 5\n. \n წარმოადგენს “ახალ სტრიქონს”. (Windows-ზე enter-ზე დაჭერა იწვევს carriage return-ს და ახალ სტრიქონს, \r\n.) trim მეთოდი წაშლის \n-ს ან \r\n-ს, რაც იძლევა მხოლოდ 5-ს.

parse მეთოდი სტრიქონებზე გარდაქმნის სტრიქონს სხვა ტიპად. აქ ჩვენ მას ვიყენებთ სტრიქონიდან რიცხვად გარდასაქმნელად. Rust-ს უნდა ვუთხრათ ზუსტი რიცხვის ტიპი, რომელიც გვსურს, let guess: u32-ის გამოყენებით. ორწერტილი (:) guess-ის შემდეგ ეუბნება Rust-ს, რომ ჩვენ მივუთითებთ ცვლადის ტიპს. Rust-ს აქვს რამდენიმე ჩაშენებული რიცხვის ტიპი; აქ ნანახი u32 არის უნიშნო, 32-ბიტიანი მთელი რიცხვი (integer). ეს არის კარგი ნაგულისხმევი არჩევანი მცირე დადებითი რიცხვისთვის. სხვა რიცხვის ტიპების შესახებ ისწავლით მე-3 თავში.

გარდა ამისა, u32 მითითება ამ მაგალითის პროგრამაში და შედარება secret_number-თან ნიშნავს, რომ Rust-ი გამოიყვანს, რომ secret_number ასევე უნდა იყოს u32. ასე რომ, ახლა შედარება იქნება იმავე ტიპის ორ მნიშვნელობას შორის!

parse მეთოდი იმუშავებს მხოლოდ პერსონაჟებზე, რომლებიც ლოგიკურად შეიძლება გარდაიქმნას რიცხვებად და ასე რომ, მარტივად შეიძლება გამოიწვიოს შეცდომები. თუ, მაგალითად, სტრიქონი შეიცავდა A👍%-ს, ამის რიცხვად გარდაქმნის გზა არ იარსებებდა. რადგან ეს შეიძლება ჩაიშალოს, parse მეთოდი აბრუნებს Result ტიპს, ისევე როგორც read_line მეთოდი აკეთებს (განხილულია ადრე “შესაძლო შეცდომის დამუშავება Result-ით”-ში). ამ Result-ს დავამუშავებთ იმავე გზით expect მეთოდის ხელახლა გამოყენებით. თუ parse დააბრუნებს Err Result ვარიანტს, რადგან მან ვერ შეძლო რიცხვის შექმნა სტრიქონიდან, expect გამოძახება გათიშავს თამაშს და დაბეჭდავს შეტყობინებას, რომელსაც მას მივცემთ. თუ parse-ს შეუძლია სტრიქონის წარმატებით გარდაქმნა რიცხვად, ის დააბრუნებს Result-ის Ok ვარიანტს, ხოლო expect დააბრუნებს რიცხვს, რომელიც გვსურს Ok მნიშვნელობიდან.

ახლა გაუშვით პროგრამა:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
      Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

მშვენიერია! მიუხედავად იმისა, რომ დაშორებები დაემატა ნავარაუდევი რიცხვის წინ, პროგრამამ მაინც დაადგინა, რომ მომხმარებელმა ივარაუდა 76. გაუშვით პროგრამა რამდენჯერმე, რათა შეამოწმოთ განსხვავებული ქცევა სხვადასხვა სახის შეყვანილ მონაცემებთან: ივარაუდეთ რიცხვი სწორად, ივარაუდეთ რიცხვი, რომელიც ძალიან მაღალია, და ივარაუდეთ რიცხვი, რომელიც ძალიან დაბალია.

ახლა თამაშის უმეტესი ნაწილი მუშაობს, მაგრამ მომხმარებელს შეუძლია მხოლოდ ერთი ვარაუდის გაკეთება. შევცვალოთ ეს ციკლის (loop) დამატებით!

მრავალჯერადი ვარაუდის დაშვება ციკლით (Looping)

loop საკვანძო სიტყვა ქმნის უსასრულო ციკლს. ჩვენ დავამატებთ ციკლს, რათა მომხმარებლებს მივცეთ რიცხვის გამოცნობის მეტი შანსი:

ფაილის სახელი: src/main.rs

use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    // --snip--

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        // --snip--


        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

როგორც ხედავთ, ჩვენ გადავიტანეთ ყველაფერი შეყვანის მოთხოვნიდან მოყოლებული ციკლში. დარწმუნდით, რომ ციკლის შიგნით არსებული სტრიქონები შეიწიოთ კიდევ ოთხი დაშორებით თითოეული და ხელახლა გაუშვით პროგრამა. პროგრამა ახლა სამუდამოდ მოითხოვს სხვა ვარაუდს, რაც რეალურად ახალ პრობლემას წარმოშობს. არ ჩანს, რომ მომხმარებელს შეუძლია გამოსვლა!

მომხმარებელს ყოველთვის შეეძლო პროგრამის შეწყვეტა კლავიატურის მალსახმობის ctrl-C გამოყენებით. მაგრამ არის ამ მონსტრისგან თავის დაღწევის კიდევ ერთი გზა, როგორც აღნიშნულია parse განხილვაში “ნავარაუდევი რიცხვის შედარება საიდუმლო რიცხვთან”-ში: თუ მომხმარებელი შეიყვანს არა-რიცხვით პასუხს, პროგრამა გაითიშება. შეგვიძლია ისარგებლოთ ამით, რათა მომხმარებელს მივცეთ გამოსვლის საშუალება, როგორც აქ არის ნაჩვენები:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
      Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit

thread 'main' (6694925) panicked at src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

quit-ის აკრეფა თამაშს დაასრულებს, მაგრამ როგორც შეამჩნევდით, ასევე დასრულედება ნებისმიერი სხვა არა-რიცხვითი მონაცემის შეყვანისას. რბილად რომ ვთქვათ ეს არაოპტიმალურია; ჩვენ გვინდა, რომ თამაში შეწყდეს სწორი რიცხვის გამოცნობის შემდეგ.

გამოსვლა სწორი გამოცნობის შემდეგ

დავაპროგრამოთ თამაში ისე, რომ გამოვიდეს, როდესაც მომხმარებელი იგებს, break ინსტრუქციის დამატებით:

ფაილის სახელი: src/main.rs

use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

break სტრიქონის დამატება You win!-ის შემდეგ აიძულებს პროგრამას გამოვიდეს ციკლიდან, როდესაც მომხმარებელი სწორად გამოიცნობს საიდუმლო რიცხვის. ციკლიდან გამოსვლა ასევე ნიშნავს პროგრამიდან გამოსვლას, რადგან ციკლი არის main-ის ბოლო ნაწილი.

არავალიდური შეყვანილი მონაცემების დამუშავება

თამაშის ქცევის შემდგომი დახვეწისთვის, ნაცვლად პროგრამის გათიშვისა, როდესაც მომხმარებელი შეიყვანს არა-რიცხვს, გავაკეთოთ ისე, რომ თამაშმა იგნორირება გაუკეთოს არა-რიცხვს, რათა მომხმარებელმა შეძლოს გამოცნობის გაგრძელება. ამის გაკეთება შეგვიძლია იმ სტრიქონის შეცვლით, სადაც guess გარდაიქმნება String-იდან u32-ად, როგორც ნაჩვენებია 2-5 ლისტინგში.

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
Listing 2-5: არა-რიცხვითი ნავარაუდევი რიცხვის იგნორირება და სხვა ვარაუდის მოთხოვნა პროგრამის გათიშვის ნაცვლად

ჩვენ გადავდივართ expect გამოძახებიდან match გამოსახულებაზე, რათა გადავიდეთ შეცდომისას გათიშვიდან შეცდომის დამუშავებაზე. გახსოვდეთ, რომ parse აბრუნებს Result ტიპს და Result არის enum, რომელსაც აქვს ვარიანტები Ok და Err. ჩვენ ვიყენებთ match გამოსახულებას აქ, როგორც გავაკეთეთ cmp მეთოდის Ordering შედეგთან.

თუ parse-ს შეუძლია სტრიქონის წარმატებით გარდაქმნა რიცხვად, ის დააბრუნებს Ok მნიშვნელობას, რომელიც შეიცავს მიღებულ რიცხვს. ეს Ok მნიშვნელობა დაემთხვევა პირველი arm-ის pattern-ს, ხოლო match გამოსახულება უბრალოდ დააბრუნებს num მნიშვნელობას, რომელიც parse-მა აწარმოა და განათავსა Ok მნიშვნელობის შიგნით. ეს რიცხვი მოხვდება სწორედ იქ, სადაც გვსურს, ახალ guess ცვლადში, რომელსაც ვქმნით.

თუ parse ვერ შეძლებს სტრიქონის გარდაქმნას რიცხვად, ის დააბრუნებს Err მნიშვნელობას, რომელიც შეიცავს მეტ ინფორმაციას შეცდომის შესახებ. Err მნიშვნელობა არ ემთხვევა Ok(num) pattern-ს პირველ match arm-ში, მაგრამ ის ემთხვევა Err(_) pattern-ს მეორე arm-ში. ქვედა ტირე, _, არის catch-all (ყველაფრის დამჭერი) მნიშვნელობა; ამ მაგალითში ჩვენ ვამბობთ, რომ გვსურს დავემთხვეთ ყველა Err მნიშვნელობას, მიუხედავად იმისა, თუ რა ინფორმაცია აქვთ მათ შიგნით. ასე რომ, პროგრამა შეასრულებს მეორე arm-ის კოდს, continue, რაც ეუბნება პროგრამას გადავიდეს loop-ის შემდგომ იტერაციაზე და მოითხოვოს სხვა ვარაუდი. ასე რომ, ეფექტურად, პროგრამა იგნორირებას უკეთებს ყველა შეცდომას, რომელსაც parse შეიძლება წააწყდეს!

ახლა ყველაფერი პროგრამაში უნდა მუშაობდეს ისე, როგორც მოსალოდნელია. სცადეთ ის:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
      Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

შესანიშნავია! ერთი მცირე საბოლოო შესწორებით, ჩვენ დავასრულებთ რიცხვის გამოცნობის თამაშს. გახსოვდეთ, რომ პროგრამა კვლავ ბეჭდავს საიდუმლო რიცხვს. ეს კარგად მუშაობდა ტესტირებისთვის, მაგრამ ის აფუჭებს თამაშს. წავშალოთ println!, რომელიც გამოიტანს საიდუმლო რიცხვს. 2-6 ლისტინგი აჩვენებს საბოლოო კოდს.

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::prelude::*;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::rng().random_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
Listing 2-6: რიცხვის გამოცნობის თამაშის სრული კოდი

ამ ეტაპზე, თქვენ წარმატებით ააგეთ რიცხვის გამოცნობის თამაში. გილოცავთ!

შეჯამება

ეს პროექტი იყო პრაქტიკული გზა Rust-ის მრავალი ახალი კონცეფციის გასაცნობად: let, match, ფუნქციები, გარეგანი crate-ების გამოყენება და მეტი. მომდევნო რამდენიმე თავში თქვენ ისწავლით ამ კონცეფციების შესახებ უფრო დეტალურად. მე-3 თავი დაფარავს კონცეფციებს, რომლებიც პროგრამირების ენების უმეტესობას აქვს, როგორიცაა ცვლადები, მონაცემთა ტიპები და ფუნქციები, და აჩვენებს, თუ როგორ გამოიყენოთ ისინი Rust-ში. მე-4 თავი იკვლევს Ownership-ს — შესაძლებლობას, რომელიც Rust-ს განსხვავებულს ხდის სხვა ენებისგან. მე-5 თავი განიხილავს Struct-ებსა და მეთოდების სინტაქსს, ხოლო მე-6 თავი განმარტავს, თუ როგორ მუშაობს Enum-ები.