Основи Rust – Голова 2. Використання змінних і типів



попередньому главі було багато косяків з перекладом. Спасибі всім за зазначення недоліків, текст був повністю перероблений. Сподіваюся ця глава вийшла більш «осудною» з мого боку, старався як міг.
У цій главі будуть розглянуті основні будівельні матеріали Rust програм: змінні та їх типи. Ми обговоримо такі питання, як змінні з базовими типами, вказівка типу і область видимості змінних. Так само, ми обговоримо один з наріжних каменів в стратегії безпеки Rust — незмінюваність.

Коментарі
В ідеалі програма повинні бути самодокументируемой, використовуючи описові імена змінних і легкий для читання код, але завжди є випадки, в яких необхідно вказати коментарі з описом роботи програми або алгоритму. Rust має наступні правила написання коментарів:
  • Рядкові коментарі (//): Абсолютно все, що йде після //, є коментарем і не буде компілюватися
  • Блок або багаторядкові коментарі (/* */): Все, що знаходиться між початковим /* і кінцевим */ символами не буде компілюватися
Однак, у Rust бажано використовувати тільки однорядкові коментарі, навіть для кількох рядків:
fn main() {
// тут відбувається виконання програми.
// Тут ми вказуємо відобразити повідомлення з привітанням:
println!("Ласкаво просимо в гру!");
}

Використовуйте блокові коментарі тільки потрібно закоментувати коду.

Rust також має коментарі документації (///), їх корисно використовувати у великих проектах, де потрібна офіційна документація для розробників. Ці коментарі встановлюються перед елементом на окремому рядку і підтримують мову розмітки Markdown:
/// Початок виконання гри
fn main() {
}

За збірку коментарів до документації відповідає інструмент rustdoc.

Глобальні константи
Часто додатку потрібно кілька незмінних значень (констант). Вони не змінюються під час роботи програми. Припустимо, ми хочемо написати гру, під назвою «Атака монстрів», в якій буде параметр рівня здоров'я, ім'я ігри і максимальний рівень здоров'я (100) – це константи. Ми хочемо мати можливість звертатися до цих констант з будь-якої ділянки коду, для цього ми визначаємо їх на початку файлу, іншими словами, вказуємо в глобальній області видимості. Константи оголошуються ключовим словом static:
static MAX_HEALTH: i32 = 100;
static GAME_NAME: &'static str = "Атака Монстрів";

fn main() {
}

Імена констант необхідно вказувати верхньому регістрі, а для розділення слів використовувати символ підкреслення. Також потрібно вказати тип констант. MAX_HEALTH є 32-бітовим цілим числом (i32), GAME_NAME – рядком (str). За таким же принципом оголошується тип змінної у, різниця лише в тому, що у змінній його можна не вказувати.

Не забивайте поки голову щодо &'static. Оскільки Rust є низькорівневим мовою, багато речей в ньому вимагають уточнення. Символ & служить посиланням на щось (в ній міститься на адресу значення в пам'яті), в нашому випадку він містить посилання на рядок. Однак, якщо ми напишемо тільки &str і скомпилируем, то ми отримаємо помилку:
static GAME_NAME: &str = "Атака Монстрів";

2:22 error: missing lifetime specifier [E0106]

2:22 означає, що у нас помилка у рядку 2 і 22 символі. Також ми повинні додати спецификатор часу життя 'static до анотації типу, в результаті ми маємо &'static str. У Rust час життя об'єкта дуже важливий момент, оскільки від нього залежить на скільки довго об'єкт затримається в пам'яті. Коли час життя підходить до кінця, компілятор Rust позбавляється від об'єкта та звільняє пам'ять, яку об'єкт займав. Час життя у 'static саме довгий, такий об'єкт залишається жити в програмі на протязі всієї її роботи і доступний у всіх місцях коду.

Але не дивлячись на те, що ми додали і спецификатор і посилання, компілятор все одно видає нам попередження:
static item is never used: `MAX_HEALTH`, #[warn(dead_code)]

Аналогічне попередження і у GAME_NAME. Ці попередження не перешкоджають компіляції програма збереться. Тим не менш, компілятор прав, на протяг всього коду до цих об'єктів ніхто не звертався. Якщо у вас буде справжній проект, то скористайтеся об'єктами або видаліть їх.

Підказка

Пройде якийсь час, перш ніж початківець розробник почне вважати Rust компілятор своїм другом, а не дратівної машиною, яка постійно випльовує помилки та попередження. Наприклад, якщо вийде подібна помилка, програма не запуститься:
error: aborting due to previous errors

Але пам'ятайте: виправлення помилок усуває проблеми з запуском, таким чином, це економить купу часу, який може піти даремно на пошуки глюків. Часто в повідомленнях про помилку також вказується і корисна інформація про те, як усунути цю проблему. Rust так само попереджає нас про невикористовуваних об'єктах в коді, наприклад змінних, функцій, імпортованих модулів та інше. Якщо у вас визначена змінна змінна (тобто, мінлива яку можна змінювати) і на протязі всього коду вона не змінювала своє значення, Rust також видасть попередження при компіляції. Компілятор настільки добре робить свою роботу, що якщо вам вдасться виправити всі зауваження, швидше за все ваша програма буде працювати коректно!

Крім статичних значень ми також можемо використовувати прості незмінні значення. Константи завжди потрібно оголошувати із зазначенням типу, наприклад: const PI: f32 = 3.14.


Друк за допомогою інтерполяції рядків
Очевидний спосіб використання змінних — це виводити їх значення:
static MAX_HEALTH: i32 = 100;
static GAME_NAME: &'static str = "Атака Монстрів";

fn main() {
const PI: f32 = 3.14;
println!("Гра, в яку ви граєте, називається {}.", GAME_NAME);
println!("У вас {} одиниць життів", MAX_HEALTH);
}

Після запуску програма видасть наступне:
Гра, в яку ви граєте, називається Атака Монстрів.
У вас 100 одиниць життів

Константа PI є у стандартній бібліотеці, щоб їй скористатися встановіть цей оператор на початку коду:
use std::f32::consts;

А працювати з нею можна ось так:
println!("{}", consts::PI);

Перший аргумент у println! – це літеральна рядок, що містить плейсхолдер (placeholder) {}. Значення константи після коми перетворюється в рядок і вставляється замість {}. Можна вказати кілька плейсхолдеров, якщо пронумерувати їх по порядку:
println!("У грі '{0}', у вас буде {1} % очок життя, так, ви прочитали правильно: {1} очок!", GAME_NAME, MAX_HEALTH);

Програма видасть наступне:
В грі 'Атака Монстрів', у вас буде 100 % очок життя, так, ви прочитали правильно: 100 очок!

Плейсхолдер може також містити один або кілька параметрів, що передаються по імені:
println!(" {points} % життя", points=70);

Програма видасть:
У вас 70 % життя

Всередині {} можна вказати тип форматування:
println!("Значення MAX_HEALTH - {x}, шістнадцятковий формат", MAX_HEALTH); // 64
println!("Значення MAX_HEALTH - {:b}, це бінарний формат", MAX_HEALTH); // 1100100
println!("Значення pi - {e}, це формат з плаваючою комою", consts::PI); // 3.141593e0

Наступні типи форматування використовуються для об'єктів з певним типом:
  • o для восьмиричного
  • x для шістнадцяткового числа в нижньому регістрі
  • X для шістнадцяткового числа у верхньому регістрі
  • p для покажчиків
  • b для бінарних
  • e для експонентному форматі в нижньому регістрі
  • E для експонентному форматі у верхньому регістрі
  • ? для налагодження
Макрос format! має ті ж параметри і працює також, як println!, тільки він повертає рядок, а не виводить текст.

Для більш докладного вивчення форматування можете відвідати розділ офіційній документації.

Значення і примітивні типи
У наших констант є значення. Значення буває різних типів: 70 – ціле число, 3.14 – число з плаваючою комою, Z та q – символьний тип (вони є символами) у форматі unicode, кожен символ займає 4 байти в пам'яті. "Godzilla" має тип рядок $str (за замовчуванням кодування UTF-8), true та false – булевий тип, вони є булевими операторами значеннями. Цілі числа можна написати в різних форматах:
  • У шістнадцятковому форматі з 0x (число 70 0x46)
  • У вісімковому форматі з 0o (число 70 0o106)
  • В бінарному форматі з 0b (0b1000110)
Символи підкреслення можна використовувати для читабельності, наприклад 1_000_000. Іноді компілятор буде вимагати вказати тип числа з суфіксом. Наприклад, після u або i вказується кількість біт пам'яті: 8, 16, 32 або 64:
  • 10usize означає беззнакове ціле число, розмір машинного коду usize, яке може бути будь-яким з типів u8, u16,u32 або u64
  • 10isize означає знакове ціле число, розмір машинного коду isize, яке може бути будь-яким з типів i8, i16, i32 або i64
  • 3.14f32 означає 32-бітне число з плаваючою точкою
  • 3.14f64 означає 64-бітове число з плаваючою точкою
Числові типи i32 та f64 є значеннями за промовчанням, щоб розрізнити їх вам потрібно додати в типі f64 .0, наприклад так: let e = 7.0;.

Якщо компілятор повідомляє вам, що він не може визначити тип змінної, то необхідно вказати його явно.

Пріоритети операторів у Rust схожі на ті, що використовуються в інших Сі-подібних мовах. Однак, Rust не має операторів инкремента (++) і декремента (). Для порівняння двох значень на рівність використовується ==, а для перевірки їх відмінності- !=.

Існує навіть порожнє значення () нульового розміру, так званий тип unit. Він використовується для вказівки на значення, що повертається, коли вираз або функція нічого не повертають (немає значення), як і у випадку з функцією, яка тільки друкує текст в консолі. Значення () не дорівнює значенню null в інших мовах, дужки () означають, що немає значення, тоді як null є значенням.

Документація по Rust

Для більш глибокого вивчення інформації по темі, рассказываемой в цій главі, можете скористатися офіційною документацією по стандартній бібліотеці. Зліва розташовано список всіх контейнерів. Однак, найбільш корисною функцією там є поле пошуку. Введіть у ньому кілька літер або слово і ви отримаєте ряд посилань на корисні матеріали.

Прив'язка значення до змінної
Зберігання всіх значень у масиві не самий кращий варіант, так як нам може знадобитися змінити яке-небудь з цих значень. У Rust ми можемо прив'язати значення змінної з допомогою прив'язки let:
fn main() {
let energy = 5; // значення 5 прив'язується до змінної energy
}

На відміну від таких мов, як Python або Go, нам необхідно вказати в кінці крапку з комою, щоб закінчити оголошення. У огидно випадку компілятор видасть помилку:
error: expected one of `.`, `;`, or an operator, found `}`

Прив'язку ми теж створюємо поки без її застосування, тому не звертайте увагу на попередження:
values.rs:2:6: 2:7 warning: unused variable: `energy`, #[warn(unused_variables)] on by default

Підказка

Щоб відключити попередження про невикористаний змінних використовуйте префікс нижнього підкреслення перед ім'ям змінної:
let _energy = 5;

Зверніть увагу на те, що в попередньому прикладі ми не зазначали тип. Rust припустить, що тип змінної energy буде ціле число. Якщо тип змінної неочевидний, компілятор спробує знайти в коді місця, де ця змінна використана. Однак, можна вказувати тип значення і таким способом:
let energy = 5u16;

Це трохи допоможе компілятору з зазначенням типу у energy, в нашому випадку ми вказали 2-бітне беззнакове ціле число.

Ми можемо скористатися змінної energy, використовуючи його вираження. Наприклад, привласнити іншої змінної або просто роздрукувати його:
let copy_energy = energy;
println!("Кількість енергії: {}", energy););

Ось кілька інших оголошень:
let level_title = "Рівень 1";
let dead = false;
let magic_number = 3.14f32;
let empty = (); // значення модульного типу ()

Значення змінної magic_number також можна записати у форматі 3.14_f32. Нижнє підкреслення відокремлює цифри від типу для кращої читабельності.

Якщо нової змінної вказати вже існуюче ім'я, то вона замінить стару змінну. Наприклад, якщо ми додамо:
let energy = "Дуже багато";

Старої змінної вже не можна буде скористатися, а її пам'ять буде звільнена.

Змінювані і незмінні змінні
Припустимо, що ми використовуємо аптечку і наша енергія піднімається до значення 25. При цьому, якщо ми напишемо:
energy = 25;

То ми отримаємо помилку:
error re-assignment of immutable variable `energy`

Що тут не так? Rust використовує програмну мудрість: багато помилок походить від випадкового або неправильного зміни змінних, так що не дозволяйте кодом змінювати значення, якщо ви свідомо це не вказали.
Зверніть увагу:
Змінні в Rust за замовчуванням незмінні, теж саме відбувається і в функціональних мовах. У чисто функціональних мовах мінливість навіть не допускається.

Якщо вам потрібна змінна, яка буде змінюватися під час виконання коду, вам треба оголосити її разом з mut:
let mut fuel = 34;
fuel = 60;
Оголосити просту змінну теж не вийде:
let n;

Подібне призведе до помилки:
error: unable to infer enough type information about `_`; type annotations required.

Компілятор необхідно вказати тип цієї змінної. Ми передаємо інформацію за типом коли присвоюємо значення:
n = -2;

Але, як говориться в повідомлення, ми також можемо вказати тип наступним чином:
let n: i32;

Крім цього, ми можемо написати відразу всі разом:
let n: i32 = -2;

Для примітивних типів подібне робиться за допомогою вказівки суфікса:
let x = 42u8;
let magic_number = 3.14f64;

Спроба використовувати неинициализированную змінну призведе до помилки:
error: use of possibly uninitialized variable

Щоб уникнути невизначеного поведінки, локальні змінні потрібно ініціалізувати перш, ніж вони будуть використані.

Область дії змінної і затінення
Поглянемо ще раз на приклад, який розглядали вище:
fn main() {
let energy = 5; // значення 5 прив'язується до змінної energy
}

Тут змінна розташовується в локальній області функції між символами {}. Після символу } мінлива виходить з області видимості і її виділена пам'ять звільняється.

Ми можемо зробити більш обмежену область видимості всередині функції за допомогою визначення блоку коду, який буде міститися всередині пари фігурних дужок:
fn main() {
let outer = 42;
{ // початок блоку коду
let inner = 3.14;
println!("Змінна inner: {}", inner);
let outer = 99; // показати першу змінну outer
println!("Змінна outer: {}", outer);
} // кінець блоку коду
println!("Змінна outer: {}", outer);
}

Ми одержимо такий висновок:
Мінлива inner: 3.14
Змінна outer: 99
Змінна outer: 42

Змінна, визначена в блоці (inner) видно тільки всередині блоку. Змінна всередині блоку може мати таке ж ім'я, як і змінна зовні (outer), яка замінюється (затінюється) змінної всередині блоку до тих пір, поки виконання внутрішнього блоку не завершиться.

Отже, навіщо нам може знадобитися використовувати блок коду? В розділі «Вираження» ми побачимо, що блок коду може повертати значення, яке можливо прив'язати до змінної з let. Блок коду також може бути порожнім – {}.

Перевірка і перетворення типу
Rust повинен знати тип у кожної змінної, у зв'язку з цим він проводить перевірку (під час компіляції) на правильне використання змінних. З точки зору типів, програми безпечні і можна уникнути цілого ряду помилок.

З цієї ж причини із-за статичної типізації ми не можемо змінювати тип змінної протягом всього її життя. Наприклад, змінна scope у цьому прикладі не може змінити тип з числового на рядок:
fn main() {
let score: i32 = 100;
score = "ВИ ПЕРЕМОГЛИ!"
}

Ми отримаємо помилку компілятора:
error: mismatched types: expected `i32`, found `&'static str`(expected i32, found &-ptr)

Однак, нам дозволено писати ось так:
let score = "YOU WON!";

Rust дозволяє нам змінити змінні. Кожна прив'язка let створює нову змінну score, приховуючи попередню, яка звільняє пам'ять. Насправді, це дуже корисно, тому як змінні за замовчуванням є незмінними.

Додавання рядка з допомогою + у Rust не буде працювати:
let player1 = "Ваня";
let player2 = "Петя";
let player3 = player1 + player2;

Ми отримаємо помилку:
error: binary operation `+` cannot be applied to type `&str`.

Ви можете скористатися функцією to_string(), щоб перетворити тип значення String:
let player3 = player1.to_string() + player2;

Або ж скористайтесь макросом format!:
let player3 = format!("{}{}", player1, player2);

В обох випадках змінна player3 буде мати значення “ВаняПетя“.

Давайте з'ясуємо, що відбудеться, якщо присвоїти значення змінної одного типу значення іншого типу:
fn main() {
let points = 10i32;
let mut saved_points: u32 = 0;
saved_points = points; // помилка!
}

Знову не вийшло. Ми отримали помилку:
error: mismatched types: expected `u32`, found `i32` (expected u32, found i32)

Для максимальної перевірки типу Rust не дозволяє автоматичне (чи неявний) перетворення одного типу в інший, як це зроблено в C++. Наприклад, коли значення f32 перетворено у ani32, числа після десяткової коми втрачаються, якщо робити це автоматично, то можуть статися помилки. Однак, ми можемо зробити явне перетворення (кастинг) за допомогою ключового слова as:
saved_points = points as u32;

Коли points містить від'ємне значення, знак буде втрачено після перетворення. Точно так само велике значення, як float, перетворюється в ціле число, десятковий частина відсікається:
let f2 = 3.14;
saved_points = f2 as u32; // буде обрізано до значення 3

Крім того, значення повинно бути конвертованим у новий тип, так як рядок не можна буде перетворити в ціле число:
let mag = "Gandalf";
saved_points = mag as u32; // error: non-scalar cast:`&str'as'u32`

Згладжування

Іноді може бути корисно дати нове, більш або коротке описове ім'я існуючого типу. Це можна зробити за допомогою type:
type MagicPower = u16;

fn main() {
let run: MagicPower= 7800; 
}

Ім'я у type починається з великої літери, як і кожне слово, яке є частиною імені. Що станеться, якщо ми поміняємо значення з 7800 78000? Компілятор видасть нам наступне попередження:
warning: literal out of range for its type.

Виразу
Rust – вираз-орієнтована мова, це означає, що більшість фрагментів коду є виразами, тобто, вони обчислюють значення і повертають значення (у цьому сенсі, що значення теж є виразами). Однак, вираження самі по собі не утворюють осмислений код, вони повинні використовуватися спільно з операторами.

Прив'язки let є операторами оголошення. Вони не є виразами:
let a = 2; // a прив'язується до 2
let b = 5; // b прив'язується до 5
let n = a + b; // n прив'язується до 7

a + b; є оператором вираження, він повертає null значення (). Якщо нам треба повернути результат додавання, то потрібно прибрати крапку з комою. Rust'необхідно знати коли оператори закінчують свою дію, за це причини, практично всі рядки у Rust закінчуються крапкою з комою.

Як ви думаєте, що тут присвоюється?
m = 42;

Це не прив'язка, оскільки відсутній let. Це – вираз, який повертає значення null (). Складова прив'язка, як тут:
let p = q = 3;

у Rust взагалі заборонена. Це поверне помилку:
error: unresolved name q

Однак, ви можете скористатися прив'язкою let:
let mut n = 0;
let mut m = 1;
let t = m; m = n; n = t;
println!("{} {} {}", n, m, t); // буде надруковано 1 0 1

Блок коду також є виразом, який буде повертати значення свого останнього виразу, якщо ми опустимо крапку з комою. Наприклад, в наступному фрагменті коду, n1 отримує значення 7, а n2 не отримує значення, тому що значення, що повертається у другому блоці було придушене:
let n1 = {
let a = 2;
let b = 5;
a + b // <-- немає крапки з комою!
};
println!("n1: {}", n1); // виведе - "n1: 7"

let n2 = {
let a = 2;
let b = 5;
a + b;
};
println!("n2: {:?}", n2); // виведе - "n2: ()"

Тут змінні a та b оголошені в блоці коду і живі поки живе сам блок, так як локальні змінні для блоку. Зверніть увагу на те, що необхідна крапка з комою після закриває фігурної дужки блоку } ;. Для друку порожнього значення (), нам потрібно вказати {:?}, як спецификатор формату.

Стек і купа
Оскільки виділення пам'яті є дуже важливою темою у Rust, нам треба мати чітку картину всього, що відбувається. Пам'ять програми поділяється на стек і купу. stackoverflow більш детально описали ці поняття. Примітивні значення, такі як цифри, символи, значення true/false, зберігаються в стеку, в той час, як значення більш складних об'єктів, які можуть рости, розміщуються в купі пам'яті. В стеку містяться адреси пам'яті на об'єкти, які розташовуються в купі:

В той час, як стек має обмежений розмір, купи може зростати до необхідних йому розмірів.

Давайте виконаємо наступний приклад і спробуємо візуалізувати пам'ять програми:
let health = 32;
let mut game = "Космічні загарбники";

Значення зберігаються в пам'яті і мають адреси. Змінна health містить ціле число із значенням 32, яке зберігається в стеку з адресою 0x23fba4, в той час, як мінлива ігри містить рядок, що зберігається в купі, починаючи своє становище з адреси 0x23fb90. Це були адреси на момент запуску програми, коли ви запустите програму вони будуть іншими.

Змінні, до яких прив'язані значення, є покажчиками або посиланнями на значення. ігри є посиланням на “Космічні загарбники“. Адреса задається оператором &. Таким чином, &health буде вказувати на місце розташування значення 32, &game на місце, де лежить “Космічні загарбники“.

Ми можемо надрукувати ці адреси за допомогою рядка, використовуючи формат {:p} для покажчиків:
println!("Адресу значення health: {:p}", &health); // 0x23fba4
println!("Адресу значення game: {:p}", &game); // 0x23fb90
println!("Значення змінної game: {}", game); // надрукує "Космічні загарбники"

Отже, ми маємо наступну ситуацію в пам'яті (адреси пам'яті будуть відрізнятися при кожному виконанні):

Ми можемо створити псевдонім, який є інший посиланням, вказує на те ж місце в пам'яті:
let game2 = &game;
println!("{:p}", game2); // 0x23fb90

Щоб отримати значення об'єкта, а не його посилання, додайте до імені зірочку:
println!("{}", *game2); // надрукує "Космічні загарбники"

Ця рядок еквівалентна:
println!("game: {}", &game);

Наведений приклад трохи спрощений, так як Rust буде ще виділяти значення в стеку, який не буде змінюватися в розмірі. Це все робилося з метою показати вам, як працюють посилання на значення.

Ми вже знаємо, що прив'язка let є незмінною, так що це значення можна змінити:
health = 33; // error: re-assignment of immutable variable `health`.

Якщо y оголосити як:
let y = &health;

Тоді *y буде мати значення 32. Посилальних змінним можна також дати тип:
let x: &i64;

Після прив'язки let, x ще не вказує на значення, і вона не містить адреса пам'яті. У Rust немає способу створити нульовий покажчик, як це робиться в інших мовах. При спробі привласнити змінної x значення nil, null або пусте значення (), призведе до помилки. Одна тільки ця особливість рятує програмістів Rust від незліченних помилок. Крім того, намагаючись використовувати x вираженні, наприклад:
println!("{:?}", x);

Ми отримаємо помилку:
error: use of possibly uninitialized variable: `error'

Заборонено розміщувати змінювану посилання на незмінний об'єкт, в іншому випадку незмінну змінну можна буде редагувати через змінну посилання:
let tricks = 10;
let reftricks = &mut tricks;

Буде видана помилка:
error: cannot borrow immutable local variable `tricks` as mutable

Посилання на змінну перемененную score може бути фіксованої або змінної відповідно:
let mut score = 0;
let score2 = &score;
// error: cannot assign to immutable borrowed content *score2
// *score2 = 5; 

let mut score = 0;
let score3 = &mut score;
*score3 = 5;

Значення у score можна змінити тільки з допомогою змінною посилання score3.

З деяких причин, які ми розглянемо пізніше, ви можете зробити тільки одну змінну посилання на змінну змінну:
let score4 = &mut score;

Буде видана помилка:
error: cannot borrow `score` as mutable more than once at a time 

Тут ми торкаємося серце системи безпеки пам'яті Rust, де запозичення змінної є одним з його ключових понять.

Купа займає набагато більше місця в пам'яті, ніж стек, тому важливо, щоб комірки пам'яті звільнялися по закінченню роботи. Rust бачить коли у змінної повинно закінчиться час життя (іншими словами, виходить за межі області видимості), під час компіляції він вставляє код на цьому місці, який буде звільняти пам'ять під час роботи програми. В інших мовах програмування такої поведінки немає.

Значення стека можна упакувати, тобто, розмістити в купі, для цього використовується Box:
let x = Box::new(5i32);

Box є об'єктом, який посилається на значення в купі.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.