Това е най-изчерпателното онлайн ръководство за езика за програмиране Rust.

В тази статия програмно ще ви запозная с Rust Programming Language, бих посочил, че Rust Language не е за начинаещи програмисти.

Трудно е.

Твърди се, че 37% от потребителите му се чувстват продуктивни едва след месец, а 70% след година.

Ако нямате достатъчно опит в програмирането, тази статия ще ви послужи по-скоро като отблъскване, отколкото като ръководство.

С това казано, нека се потопим в:

Ще създадем прост любовен калкулатор:

Ще научим за низове, условия, функции, оператори, разпространение на грешки, модули (импортиране), съвпадение (изразителна версия на if), структури, енуми, методи и характеристики и куп други неща

Преди да се задълбочим, ако сте бекенд разработчик или възнамерявате да се задълбочите в тази кариера, присъединете се към други разработчици, за да получавате ежедневни статии за бекенд разработка, които ще повишат продуктивността ви.

Какво е Rust?

Rust е език за системно програмиране с фокус върху производителността, безопасността на паметта и безопасната едновременност. До голяма степен се разглежда от разработчиците като заместител на зрелите C и C++, езици, колкото стари, толкова и мощни.

Създаден от Graydon Hoare в Mozilla Research с участието на Dave Herman и създателя на Javascript, Brendan Eich.

С петгодишен рекорд на най-обичания език за програмиране в света на индекса на препълване на стека 2020:

Не е чудно, че езикът Rust е изминал дълъг път предвид възрастта си.

Защо Rust?

Езикът за програмиране Rust предлага на масата нови и упорити начини за правене на нещата.

Концепции като Borrow Checker звучат ново за повечето разработчици, идващи от Garbage-Collected езици като Go, Javascript и Python.

Вместо Garbage-Collection, езикът за програмиране Rust позволява на разработчика да прави избор за управление на паметта с концепции като собственост и заемане.

Borrow Checker, срещу който повечето разработчици на Rust биха се борили от време на време, гарантира безопасността на паметта, чужда концепция за основните C и C++.

Инсталация

Инсталирането на Rust е доста лесно. Отидете на официалния уебсайт https://www.rust-lang.org/tools/install и следвайте стъпките.

От вашия CLI потвърдете инсталацията си с rustc --version и cargo --version

Първият е стандартният инструмент на Rust за компилиране и изпълнение на вашия код.

Последното е абстракция, която предоставя общи инструменти, необходими на разработчиците за организиране на техния код и други външни библиотеки.

Освен това получавате линтер: clippy и хубав начин да изпълнявате тестове за вашия код.

Изграждане на прост любовен калкулатор в Rust

Всички ние имахме свои собствени любовни пиеси, докато бяхме млади. Може би си спомняте популярния метод „пламъци“.

Тук ще използваме основна формула за извличане на проценти на съвместимост:

(length_of_shorter_name/length_of_longer_name) × 100

Псевдокод:

Начинът на правене на нещата в Rust се различава от този на другите езици, оттук и необходимостта от псевдокод, който да очертава стъпките в нашия код.

Тази програма изчислява любовната съвместимост на мъж и жена с техните имена като вход:

function handle_input(arg: memory_address_to_store_input) { 
 * accepts `memory address` *
 * returns nothing * 
 request input from user 
 trim whitespace from user input 
 store cleaned input into `arg` 
 returns the length of the input 
} 
function calculate_compatibility(name1: String, name2: String) { 
 * accepts Strings for both input * 
 * returns number as percentage * 
 if length of name1 > length of name2: 
     percentage = (length of name2 ⁄ length of name1) x 100 
 else if length of name2 > length of name1: 
     percentage = (length of name1 ⁄ length of name2) x 100 
 else: * lengths are equal * 
     percentage = 50 
 } 
function main() { 
 initialize `name1` as string 
 initialize `name2` as string 
 name1_count = handle_input(memory address of name1) 
 name2_count = handle_input(memory address of name2) 
 compatibility = calculate_compatibility(name1_count, name2_count) 
 print compatibility 
}

Код

Първо, ще използваме cargo за инициализиране на нашия проект. Cargo ще ви помогне да управлявате компилацията и други дребни неща, свързани с изчерпването на кода.

За да инициализирате, отидете до терминала и въведете:

cargo new love_calculator

Основната функция

use std::io; 
fn main() { 
  let mut name1 = String::new(); 
  let mut name2 = String::new(); 
}

Първият ред на нашата програма ще включва въвеждане на функционалност от стандартната библиотека в обхват.

Модулът io, както е обяснено в неговата документация '... съдържа редица общи неща, от които ще се нуждаете, когато правите въвеждане и извеждане'.

Както при много C-подобни езици, входната точка на всяка програма на Rust е функцията main. Декларирането на функция използва ключовата дума fn и името на функцията.

Има такъв синтаксис:

fn function_name(arg1: type, arg2: type, ...argn: type) → return_type { 
   function body; //statements end in a semicolon return value	
//return values do not require a semicolon or the 'return' keyword. If you need a semicolon, perhaps as part of a logic, you can then use the return keyword 
}

При липса на тип на връщане, Rust имплицитно връща това, което се нарича тип единица, записано като Ok().

Концепцията за Null не съществува в Rust, вместо това Rust я представя с тип, наречен None, като показва липсата на стойност. Неговият аналог е Some, който може да се използва за обвиване на всеки тип данни.

Във функцията main ние декларираме две променливи, name1 и name2, като използваме ключовата дума let като:

  1. Бидейки променлив, с ключовата дума mut;
  2. Като низ: Има два вида низове, които ще срещнете в Rust, другият е низов литерал.

Както при много други езици, методът new се използва за инициализиране на екземпляр на обект или Struct в този случай.

Rust използва Structs и Enum за разлика от обекти и класове за абстрахиране на данни.

По-късно ще разгледаме по-подробно тези две концепции.

Точно сега, нека премахнем действителния ни код на любовния калкулатор.

По-долу е кодът, който ще създаде нашия любовен калкулатор в Rust въз основа на името, въведено от потребителя.

use std::cmp::Ordering; 
use std::convert::TryInto; 
use std::io; 
fn get_input(var: &mut String) -> i32 { 
  io::stdin() .read_line(var) .expect("Failed to read line");
  var.chars() .count() .try_into() .expect("Length of name is too large!") 
}
fn calculate_compatibility(name1_count: i32, name2_count: i32) -> f32 { 
  match name1_count.cmp(&name2_count) { 
    Ordering::Less => {   
     ((name1_count as f32 / name2_count as f32) as f32 * 100.0) as f32 
},
Ordering::Greater => { 
  ((name2_count as f32 / name1_count as f32) as f32 * 100.0) as f32 },
Ordering::Equal => { 50.0 } 
}
}
fn main() { 
 let mut name1 = String::new(); 
 let mut name2 = String::new(); 
 println!("Enter first person name: "); 
 let name1_count = get_input(&mut name1); 
 println!("Enter second person name: "); 
 let name2_count = get_input(&mut name2); 
 let compat_value = calculate_compatibility(name1_count, name2_count).floor(); 
 println!("Love compatibility between {} and {} is {}%", name1.trim(), name2.trim(), compat_value); 
}

Сега нека преминем към обяснение на всяка от концепциите, използвани в кода по-долу, за да разберем по-ясно как Rust обработва определени концепции за програмиране.

Ресурси Ако търсите да научите повече RUST, разгледайте тези невероятни курсове. Ultimate Rust Crash Course е един от най-добрите курсове за езика за програмиране Rust. Инструкторът обяснява сложни термини с лекота и помага за разбирането на езика. Научете Rust чрез изграждане на реални приложения ви отвежда направо до създаване на реални приложения с помощта на езика за програмиране Rust. Ако търсите да се занимавате с проекти, този курс определено е за вас.

Обратно към статията:

Собственост и заеми

След това бихме искали да извикаме функцията, която обработва въвеждането от потребителя в съответствие с предварително инициализираните променливи.

За момент ще оставим функцията main и ще се насочим към функцията handle_input:

Преди да направим това, нека да разгледаме концепцията за Собственост и заемане в Rust

Собственост

Разгледайте този кратък фрагмент:

fn main() { 
   //for brevity sake, we won't specify this main function in subsequent snippets 
   let hello = "hello"; 
} 
// at this point, hello no longer exists.

По подразбиране променливите са с блоков обхват. Hello е пример за това, което наричаме низов литерал: твърдо кодирани низови стойности.

let hello = String::from("hello"); 
let bye = hello; 
println!("{}", hello); 
//the remaining arguments to the println macro are variables that would replace '{}' in the string.

Изпълнението на този код би извело грешка (наречена паника в Rust):

error[E0382]: borrow of moved value: `hello` --> main.rs:4:20 | 2 | let hello = String::from("hello"); | ----- move occurs because `hello` has type `std::string::String`, which does not implement the `Copy` trait 3 | let bye = hello; | ----- value moved here 4 | println!("{}", hello); | ^^^^^ value borrowed here after move error: aborting due to previous error

За повече информация относно тази грешка опитайте rustc --explain E0382.

Това означава концепция, наречена Преместване.

Това означава, че в Rust опитът за освобождаване на неизползвана памет, тъй като и hello, и bye сочат към един и същ адрес на паметта, ще обезсили променливата hello.

Всеки опит за извикване на променливата ще доведе до грешка. Като език за системно програмиране с акцент върху безопасността на паметта, това е полезна функция, която предотвратява достъпа до адрес на паметта, след като вече не е необходим.

Rust извършва форма на копиране, известна като плитко копиране. По този начин и двете променливи препращат към едно и също нещо.

В друг пример, показан по-долу:

let hello = String::from("hello"); 
let bye = " and bye"; 
let greeting = hello + bye; 
println!("{}. Again, {}? {}", greeting, hello, bye);

Отново, както се очаква, този код няма да се компилира. bye престава да съществува в момента, в който се използва някъде другаде.

Новата променлива поема собствеността на bye и bye престава да съществува. hello от друга страна, няма проблеми с това. Това е така, защото поради използването на String::new(), размерът на hello е известен по време на компилиране, следователно тук няма дълбоко или плитко копиране.

Използвайки кода по-горе, можем да разрешим този проблем, като извършим явно дълбоко копиране по следния начин:

let hello = String::from("hello"); 
let bye = hello.copy(); 
println!("{}", hello);

Този път нямаше да имаме грешки, тъй като основните данни се копират в bye и собствеността върху bye не се извършва.

Заемане

Какво ще кажете за функциите?

Как да предадем аргументи във функции и да използваме отново променливите? Rust има някаква магия, подобна на указател, наречена Заемане.

С него можем да преминаваме към функции, препратки към променливи и достъп до базови данни и дори да ги променяме (както ще видим във функцията handle_input).

Нека да създадем функцията handle_input и да научим заемане от нея:

fn handle_input(var: &mut String) → i32 { 
 io::stdin() .read_line(var) .expect("Failed to read line"); var.chars() .count() .try_into() .expect("Length of name is too large!") 
}

Функцията приема забавен тип аргумент, &mut String. Вероятно ще го разпознаете като очакване на променлив низ като аргумент, но отива по-далеч от това:

Знакът & показва, че функцията очаква препратка към този тип данни, тъй като бихме искали да променим стойността й, вместо да поемем собствеността върху нея, както Rust обикновено очаква.

Тази концепция се нарича Заем.

В края на изпълнението на функцията контролът върху адреса на паметта се връща обратно към първоначалната променлива.

И така, ние извикваме метода stdin на модула io за функционалности, свързани с входа, read_line прави магията на предаване на потребителския вход в указаната променлива.

Методът expect се използва за разпространение на грешка:

Има случаи, в които бихме искали нашата функция да върне тип грешка на извикващия код, за да се справи, ако възникне грешка при извършване на операция.

Функцията expect (подобна на unwrap ) ни позволява да посочим персонализирано съобщение за паниката (както се извикват изключенията в Rust).

Докато unwrap вместо това ще върне грешката в кода за повикване без персонализирано съобщение.

Върната стойност (типове данни)

Функцията handle_input има тип връщане i32, което е 32-битово цяло число със знак.

Други типове цели числа включват i8, i16, i64, i128 за цели числа със знак съответно 8, 16, 64 и 128.

Целочислените типове без знак са с префикс u с варианти като u8, u16, u32, u64 и u128 с техните очевидни нива на точност.

За стойности с плаваща запетая са налични само два типа: f32 и f64 съответно за 32 и 64-битова точност.

Разглеждане на фрагмента с върната стойност:

var.chars() 
.count() 
.try_into() 
.expect("Length of name is too large!")

Методът chars първо разбива текста, даден от потребителя, в масив от отделни знаци, count дава своя брой с тип usize, целочислен тип за размера на данните в Rust.

Функцията try_into от друга страна се опитва да преобразува тип usize в i32.

Ще забележите, че try_into автоматично знае типа, от който се нуждаем, поради типа на връщането на функцията - друга магия на Rust.

И накрая, обвиваме всичко с expect за възможни грешки, които могат да възникнат.

Назад към основната функция

В този момент можем да опитаме да извикаме функцията и да инициализираме нашите променливи по следния начин:

println!("Enter first person name: "); 
let name1_count = get_input(&mut name1); 
println!("Enter second person name: "); 
let name2_count = get_input(&mut name2);

Съвпадения

След това ще ни трябва функция за изчисляване на съвместимостта:

fn calculate_compatibility(name1_count: i32, name2_count: i32) → f32 { 
  match name1_count.cmp(&name2_count) { 
    Ordering::Less => { ((name1_count as f32 / name2_count as f32) as f32 * 100.0) as f32 }, 
   Ordering::Greater => { ((name2_count as f32 / name1_count as f32) as f32 * 100.0) as f32 }, 
    Ordering::Equal => { 50.0 } 
 } 
}

Тъй като ще правим малко аритметика, функцията би трябвало да връща тип float.

След това сравняваме двете стойности, за да видим коя е по-висока. За да направите това, вместо оператора if със синтаксис на:

if <condition> { 
  block of code 
} 
else if <condition> { 
  block of code 
} 
else { 
  block of code 
}

Ще използваме match, тъй като сравнението на две числа в Rust дава тип данни, а не булево:

match name1_count.cmp(&name2_count) { 
   Ordering::Less => { }, 
   Ordering::Greater => { }, 
   Ordering::Equal => { } 
}

Инструкцията за съвпадение се състои от match, последвано от тип или операция, която връща тип. В блока от код по-долу е arms с пътеки на код, който да се изпълнява в зависимост от очаквания тип.

С тази функционалност на типове, върнати за сравнение, трябва да включим в обхват типовете, които се очаква да бъдат върнати с израза:

use std::cmp::Ordering;

Структури, енуми и методи

На този етап ще видите, че има два начина, по които наричаме методи:

  1. type.method(): Това са например методи - методи, обвързани с конкретен екземпляр от типа.
  2. type::method(): Това е за статични методи - методи, които са обвързани със самия тип и не се нуждаят от себе си (препратката към екземпляра) като параметър.

Както споменахме по-рано, Rust използва Structs и Enum за абстракции на данни. Структурата е представена синтактично така:

struct User { username: String, first_name: String, }

И тогава методите за тази структура се правят с ключовата дума impl:

impl User { 
  fn new(username: String, first_name: String) → User { 
    User { username, first_name } 
  } 
  fn say_name(&self) → { 
     println!("{} name is {}", self.username, self.first_name); 
  } 
}

Както можете да се досетите, new е статичен метод, докато say_name е метод на екземпляр.

Енумите от друга страна се използват за групиране на подобни структури под абстракция.

Например, ако искаме да разширим структурата User, за да съдържа поле за пол, бихме могли да внедрим структура за пол по следния начин:

struct Male { symbol: u8, } 
struct Female { symbol: u8, }

и след това ги групирайте под полов списък по следния начин:

enum Gender { Female, Male, }

След това нашата потребителска структура може да използва тази абстракция така:

struct User { username: String, first_name: String, gender: Gender, 
}

Достъпът до структури, свързани с enum, се извършва с оператора ::.

Връщайки се към кода за подреждане по-горе, можем да видим, че подреждането е преброяване с типове структури: Less, Greater и Equal.

Когато сравнението е направено с метода cmp, методът дава един от тези типове, тогава можем да се възползваме от елегантния синтаксис за съвпадение, за да извършим нашите изчисления въз основа на тези типове:

Ordering::Less => { ((name1_count as f32 / name2_count as f32) as f32 * 100.0) as f32 }, 
Ordering::Greater => { ((name2_count as f32 / name1_count as f32) as f32 * 100.0) as f32 }, 
Ordering::Equal => { 50.0 }

Тъй като тук ще работим с дроби, е важно да превключите типовете данни към плаващи стойности, за да поддържате прецизност.

Това става с помощта на ключовата дума as. Трябва да прехвърлим всички стойности в типа f32, преди кодът ни да работи успешно.

За равното рамо също трябва да направим 50 като стойност с плаваща запетая, за да съответства на връщания тип. Лесно е да пропуснете.

Върната стойност (съвпадение)

Тъй като имаме едноредови операции като разрешена стойност, нямаме нужда от ключовата дума return. Стойностите ще бъдат върнати имплицитно поради липсата на точка и запетая като разделител.

Обратно към основното (края?)

Можем да се върнем към основната функция и да извикаме функцията calculate_compatibility и да покажем резултатите:

let compat_value = calculate_compatibility(name1_count, name2_count).floor(); 
println!("Love compatibility between {} and {} is {}%", name1.trim(), name2.trim(), compat_value);

Методът trim премахва празните знаци в началото и края на низа.

Черти

Ние сме готови! Сега ще компилираме и стартираме нашата програма с cargo run в кодовата директория.

Уви! Имаме грешка:

error[E0599]: no method named `try_into` found for type `usize` in the current scope --> src/main.rs:10:25 | 10 | var.chars().count().try_into().expect("Length of name is too large!") | ^^^^^^^^ method not found in `usize` | = help: items from traits can only be used if the trait is in scope help: the following trait is implemented but not in scope; perhaps add a `use` for it: | 1 | use std::convert::TryInto; | error: aborting due to previous error For more information about this error, try `rustc --explain E0599`. error: could not compile `love_calculator`.

Нещо за чертите.

Не много полезен пример

И така, какви са чертите? Е, чертите са начин, по който Руст казва: Наследство.

Тъй като кодовата база се увеличава, забелязваме, че някои структури изглежда имат подобни характеристики и е важно бъдещите структури с такива характеристики да могат да изпълняват действия, които тези структури могат да извършват.

Ще бъде използван пример, избран от документацията на Rust. Да кажем, че прилагаме туитове по следния начин:

struct Tweet { username: String, content: String, location: String, retweet: bool, 
}

Може да искаме да приложим метод summarize, който показва кратка форма на туита:

impl Tweet { 
 fn summarize(&self) → String { 
  if self.retweet { format!("{} retweeted: {} while in {}", self.username, self.content, self.location) 
  else { 
     format!("{} tweeted: {} while in {}", self.username, self.content, self.location) 
  } 
 }
 
}

Помислете за друг код, в който внедрихме новинарска статия по следния начин:

struct Article { title: String, author: String, content: String, }

и трябва да се приложи метод на обобщаване. Докато бихме могли да го приложим и като отделен метод:

impl Article { 
 fn summarize(&self) → String { 
  format!("{} posted an article title: {}", self.author, self.title)
 }
}

бихме могли да стандартизираме нашия код, като създадем характеристика, която и двете структури могат да изпълняват. По този начин можем да осигурим последователност при работа с различни структури с подобна функционалност.

Така че нашата черта се прилага така:

trait Summary { fn summarize(&self) → String }

След това прикрепяме чертата към нашите структури и прилагаме методите, тъй като тя така или иначе ще бъде приложена:

// for Tweet 
impl Summary for Tweet { 
  fn summarize(&self) → String { 
    if self.retweet { 
      format!("{} retweeted: {} while in {}", self.username, self.content, self.location) 
    else { 
      format!("{} tweeted: {} while in {}", self.username, self.content, self.location) 
    } 
  } 
} 
// for Article 
impl Summary for Article { 
  fn summarize(&self) → String { 
    format!("{} posted an article title: {}", self.author, self.title) 
  } 
}

Тогава можем да извикаме нашите методи:

fn main() { 
 let tweet = Tweet { 
    String::from("Lord_Sarcastic"), String::from("Hello, world! Bye, world!"), String::from("Lagos"), false 
 } 
 let article = Article { String::from("Python and Rust?"),  String::from("Lord_Sarcastic"), String::from("Python and Rust make a good match") 
} 
 println!("Tweet summary: '{}'", tweet.summarize()); 
 println!("Article summary: '{}'", article.summarize()); 
}

Много полезен пример

С черти бихме могли също да имаме имплементация по подразбиране за методи. Като пример, ако искахме метод за показване, който използва метода summarize под капака, бихме могли да имаме такава черта:

trait Summary { 
  fn summarize(&self) → String; fn display(&self) → String { format!("Summary: {}", self.summarize()) 
  } 
}

В този момент и двете структури могат да извикат display, без да са имплементирани методи за това, но summarize трябва да бъде имплементиран, за да работи.

За да не се отклоним от пътя...

Защо това? Работата е да използваме функции, които включват черти, чертата трябва да е в обхват.

Можем да видим, че компилаторът на Rust настоява да включим черта в обхват, преди да позволи преобразуване на типа. Това може да се коригира с прост ред:

use std::convert:TryInto;

Краен списък с кодове

Крайният списък с кодове изглежда така:

use std::cmp::Ordering; 
use std::convert::TryInto; 
use std::io; 
fn get_input(var: &mut String) -> i32 { 
  io::stdin() .read_line(var) .expect("Failed to read line");
  var.chars() .count() .try_into() .expect("Length of name is too large!") 
} 
fn calculate_compatibility(name1_count: i32, name2_count: i32) -> f32 { 
  match name1_count.cmp(&name2_count) { 
    Ordering::Less => {   
     ((name1_count as f32 / name2_count as f32) as f32 * 100.0) as f32 
}, 
Ordering::Greater => { 
  ((name2_count as f32 / name1_count as f32) as f32 * 100.0) as f32 }, 
Ordering::Equal => { 50.0 } 
} 
} 
fn main() { 
 let mut name1 = String::new(); 
 let mut name2 = String::new(); 
 println!("Enter first person name: "); 
 let name1_count = get_input(&mut name1); 
 println!("Enter second person name: "); 
 let name2_count = get_input(&mut name2); 
 let compat_value = calculate_compatibility(name1_count, name2_count).floor(); 
 println!("Love compatibility between {} and {} is {}%", name1.trim(), name2.trim(), compat_value); 
}

Окончателният код може да бъде получен тук, мое репо, посветено на изучаването на Rust и неговите парадигми.

Още ресурси

  1. Официална документация
  2. „Въведение в Rust“
  3. Ръководство за съвети и трикове за начинаещи на RUST 2020
  4. „Learing Rust“
  5. Ultimate Rust Crash Course е един от най-добрите курсове за езика за програмиране Rust. Инструкторът обяснява сложни термини с лекота и помага за разбирането на езика.
  6. „Научете Rust чрез изграждане на реални приложения“ ви отвежда направо към изграждането на реални приложения с помощта на езика за програмиране Rust. Ако искате да се заемете с проекти, този курс определено е за вас.

Заключение

Въпреки славата му като системен език, ние успяхме да използваме Rust за тривиална задача като изграждането на калкулатор.

Езикът за програмиране Rust се използва в други области, освен в любовните калкулатори, като сървъри, компилатори, бек-енд на уеб приложения, WASM (уеб асемблиране) и общо взето всичко, което бихте написали с език за програмиране.

Ако искате да се потопите в езика за програмиране Rust, тъй като не докоснахме толкова много важни концепции, бихте искали да проверите официалната „документация“.

Първоначално публикувано в https://masteringbackend.com на 11 януари 2021 г.