ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1138
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Переменные и понятие изменяемости
Как упоминалось в разделе
“Сохранение значений в переменных”
, по умолчанию переменные являются неизменяемыми. Это одна из многих подсказок, которые Rust даёт вам для написания кода таким образом, чтобы использовать преимущества безопасности и простого параллелизма, которые предлагает Rust. Однако у вас есть возможность сделать ваши переменные изменяемыми. Давайте рассмотрим, как и почему Rust поощряет неизменность, и почему иногда вы можете отказаться от этого.
Когда переменная неизменяемая, то её значение нельзя менять, как только значение привязано к её имени. Приведём пример использования этого типа переменной. Для этого создадим новый проект variables в каталоге projects при помощи команды: cargo new variables
Потом в созданной папке проекта variables откройте исходный файл src/main.rs и замените содержимое следующим кодом, который пока не будет компилироваться:
Файл: src/main.rs
Сохраните код программы и выполните команду cargo run
. В командной строке вы увидите сообщение об ошибке:
Данный пример показывает, как компилятор помогает вам находить ошибки в ваших программах. Ошибки компилятора могут вызывать разочарование, но на самом деле они лишь означают, что ваша программа ещё не делает то, что вы от неё хотите. Они не
fn main
() { let x =
5
; println!
(
"The value of x is: {x}"
); x =
6
; println!
(
"The value of x is: {x}"
);
}
$
cargo run
Compiling variables v0.1.0 (file:///projects/variables) error[E0384]: cannot assign twice to immutable variable `x`
-->
src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`. error: could not compile `variables` due to previous error
означают, что вы не являетесь хорошим программистом! Опытные разработчики Rust также получают ошибки компиляции.
Сообщение об ошибке указывает, что причиной ошибки является то, что вы cannot assign twice to immutable variable `x`
(не можете присвоить неизменяемой переменной новое значение), потому что вы пытались присвоить второе значение неизменяемой переменной x
Важно, что мы получаем ошибку времени компиляции, при попытке изменить значение,
обозначенное как неизменяемое, потому что такая ситуация может привести к ошибкам.
Если одна часть нашего кода исходит из предположения, что значение никогда не изменится, а другая часть кода изменяет это значение, вполне возможно, что первая часть кода не будет делать то, для чего она была предназначена. Причину такого рода ошибок может быть трудно отследить постфактум, особенно когда второй фрагмент кода изменяет значение только иногда. Компилятор Rust гарантирует, что если вы заявите,
что значение не изменится, оно действительно не изменится, поэтому вам не нужно следить за ним самостоятельно. Таким образом, ваш код легче понять.
Но изменяемость может быть очень полезной и может сделать код более удобным для написания. Хотя переменные неизменяемы по умолчанию, вы можете сделать их изменяемыми, добавив mut перед именем переменной, как вы делали это в Главе 2.
Добавление mut также передаёт будущим читателям кода информацию о том, что другие части кода будут изменять значение этой переменной.
Например, изменим src/main.rs на следующий код:
Файл: src/main.rs
Запустив программу, мы получим результат:
Когда используется mut
, мы можем изменять значение, привязанное к x
, с
5
до
6
. В
конечном счёте, решение о том, использовать изменяемость или нет, зависит от вас и от того, что вы считаете наиболее приемлемым в данной конкретной ситуации.
fn main
() { let mut x =
5
; println!
(
"The value of x is: {x}"
); x =
6
; println!
(
"The value of x is: {x}"
);
}
$
cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
Константы
Подобно неизменяемым переменным, константы — это значения, которые связаны с именем и не могут изменяться, но между константами и переменными есть несколько различий.
Во-первых, не разрешается использовать mut с константами. Константы не просто неизменны по умолчанию — они неизменны всегда. Вы объявляете константы,
используя ключевое слово const вместо ключевого слова let
, и тип должен быть явно указан. Мы собираемся рассмотреть типы и аннотации типов в следующем разделе
“Типы данных”
, так что не беспокойтесь о деталях сейчас. Просто знайте, что вы всегда должны явно указывать тип.
Константы можно объявить в любой области видимости, включая глобальную. Это делает их удобными для значений, про которые должны знать многие другие части кода.
Последней разницей является то, что константы можно установить только в константное выражение, а не в результат значения, которое можно посчитать только во время выполнения.
Вот пример объявления константы:
Имя константы
THREE_HOURS_IN_SECONDS
и её значение устанавливается в результате умножения числа 60 (количество секунд в минуте) на 60 (количество минут в часе) на 3
(количество часов, которое мы хотим подсчитать в этой программе). Соглашение об именах констант в Rust состоит в том, чтобы использовать все символы в верхнем регистре с символами подчёркивания между словами. Компилятор способен вычислить ограниченный набор операций во время компиляции, что позволяет нам записать это значение так, чтобы его было легче понять и проверить, вместо того, чтобы устанавливать для этой константы значение 10800. См раздел справочника Rust,
посвящённый вычислению констант для получения дополнительной информации о том,
какие операции можно использовать при объявлении констант.
Константы являются корректными для всего времени выполнения программы внутри той области видимости, где они были объявлены. Это свойство делает константы полезными для значений приложения, о которых может потребоваться знать нескольким частям программы. Например, максимальное количество очков, которое может заработать любой игрок в игре, или скорость света.
Именование жёстко заданных значений, используемых в вашей программе, в качестве констант является удобным способом передать их смысл тем, кто будет сопровождать этот код в будущем. Это также помогает иметь только одно место в коде, которое вам нужно будет изменить, если заданное значение потребует обновления.
const
THREE_HOURS_IN_SECONDS: u32
=
60
*
60
*
3
;
Затенение (переменных)
Как вы видели в руководстве по игре Угадайка в
Главе 2
, вы можете объявить новую переменную с тем же именем, что и предыдущая переменная. Rustaceans говорят, что первая переменная затенена второй, а это значит, что компилятор увидит вторую переменную, когда вы воспользуетесь именем переменной. По сути, вторая переменная затеняет первую, присваивая себе любое использование имени переменной до тех пор,
пока либо она сама не будет затенена, либо область действия не закончится. Мы можем затенить переменную, используя то же имя переменной и повторив использование ключевого слова let следующим образом:
Файл: src/main.rs
Эта программа сначала привязывает x
к значению
5
. Затем она создаёт новую переменную x
, повторяя let x =
, беря исходное значение и добавляя
1
, чтобы значение x
стало равным
6
. Затем во внутренней области видимости, созданной с помощью фигурных скобок, третий оператор let также затеняет x
и создаёт новую переменную, умножая предыдущее значение на
2
, чтобы дать x
значение
12
. Когда эта область заканчивается, внутреннее затенение заканчивается, и x
возвращается к значению
6
. Запустив эту программу, она выведет следующее:
Затенение отличается от объявления переменной с помощью mut
, так как мы получим ошибку компиляции, если случайно попробуем переназначить значение без использования ключевого слова let
. Используя let
, можно выполнить несколько превращений над значением, при этом оставляя переменную неизменяемой, после того как все эти превращения завершены.
Другой разницей между mut и затенением является то, что мы создаём совершенно новую переменную, когда снова используем слово let
(ещё одну). Мы можем даже fn main
() { let x =
5
; let x = x +
1
;
{ let x = x *
2
; println!
(
"The value of x in the inner scope is: {x}"
);
} println!
(
"The value of x is: {x}"
);
}
$
cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
изменить тип значения, но снова использовать предыдущее имя. К примеру, наша программа спрашивает пользователя, сколько пробелов он хочет разместить между некоторым текстом, запрашивая символы пробела, но мы на самом деле хотим сохранить данный ввод как число:
Первая переменная spaces
— является строковым типом, а вторая переменная spaces
— числовым типом. Таким образом, затенение избавляет нас от необходимости придумывать разные имена, такие как spaces_str и spaces_num
; вместо этого мы можем повторно использовать более простое имя spaces
. Однако, если мы попытаемся использовать для этого mut
, как здесь показано, то мы получим ошибку времени компиляции:
Ошибка говорит, что не разрешается менять тип переменной:
Теперь, когда вы имеете представление о работе с переменными, посмотрим на большее количество типов данных, которые они могут иметь.
let spaces =
" "
; let spaces = spaces.len(); let mut spaces =
" "
; spaces = spaces.len();
$
cargo run
Compiling variables v0.1.0 (file:///projects/variables) error[E0308]: mismatched types
-->
src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`. error: could not compile `variables` due to previous error
Первая переменная spaces
— является строковым типом, а вторая переменная spaces
— числовым типом. Таким образом, затенение избавляет нас от необходимости придумывать разные имена, такие как spaces_str и spaces_num
; вместо этого мы можем повторно использовать более простое имя spaces
. Однако, если мы попытаемся использовать для этого mut
, как здесь показано, то мы получим ошибку времени компиляции:
Ошибка говорит, что не разрешается менять тип переменной:
Теперь, когда вы имеете представление о работе с переменными, посмотрим на большее количество типов данных, которые они могут иметь.
let spaces =
" "
; let spaces = spaces.len(); let mut spaces =
" "
; spaces = spaces.len();
$
cargo run
Compiling variables v0.1.0 (file:///projects/variables) error[E0308]: mismatched types
-->
src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`. error: could not compile `variables` due to previous error
Типы данных
Каждое значение в Rust имеет определённый тип данных, сообщающий Rust, какие данные используются и как с ними работать. Мы рассмотрим два подмножества типов данных: скалярные и сложные.
Не забывайте, что Rust является статически типизированным (statically typed) языком.
Это означает, что он должен знать типы всех переменных во время компиляции. Обычно компилятор может предположить, какой тип используется (вывести его), основываясь на значении и на том, как мы с ним работаем. В случаях, когда может быть выведено несколько типов, необходимо добавлять аннотацию типа вручную. Например, когда мы конвертировали
String в число с помощью вызова parse в разделе
«Сравнение предположения с загаданным номером»
главы 2, мы должны добавить такую аннотацию:
Если мы не добавим аннотацию типа
: u32
выше, Rust отобразит ошибку, потому что компилятору нужно больше информации от нас, чтобы узнать, какой тип мы хотим использовать:
В будущем вы увидите различные аннотации для разных типов данных.
Скалярные типы данных
Скалярный тип представляет единственное значение. В Rust есть четыре скалярных типа:
целые и вещественные числа, логический тип и символы. Вы можете узнать эти типы по другим языкам программирования. Посмотрим, как они работают в Rust.
Целочисленные типы
Целое число (integer) — это число без дробной части. В главе 2 мы использовали один целочисленный тип — u32
. Это объявление типа указывает, что значение, с которым оно связано, должно быть целым числом без знака (типы целых чисел со знаком начинаются с i
вместо u
), которое занимает 32 бита памяти. В таблице 3-1 показаны let guess: u32
=
"42"
.parse().expect(
"Not a number!"
);
$
cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations) error[E0282]: type annotations needed
-->
src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ consider giving `guess` a type
For more information about this error, try `rustc --explain E0282`. error: could not compile `no_type_annotations` due to previous error
встроенные целочисленные типы в Rust. Мы можем использовать любой из этих вариантов для объявления типа целочисленного значения.
Таблица 3-1: целочисленные типы в Rust
Длина
Со знаком
Без знака
8 бит i8
u8 16-bit i16
u16 32 бит i32
u32 64 бит i64
u64 128-bit i128
u128
arch isize usize
Каждый вариант может быть как со знаком, так и без знака и имеет явный размер. Такая характеристика типа как знаковый и беззнаковый определяет возможность числа быть отрицательным. Другими словами, должно ли число иметь знак (знаковое) или оно всегда будет только положительным и, следовательно, может быть представлено без знака (беззнаковое). Это похоже на написание чисел на бумаге: когда знак имеет значение, число отображается со знаком плюс или со знаком минус; однако, когда можно с уверенностью предположить, что число положительное, оно отображается без знака.
Числа со знаком хранятся с использованием дополнительного кода
Каждый вариант со знаком может хранить числа от -(2 n - 1
) до 2 n - 1
- 1 включительно,
где n — количество битов, которые использует этот вариант. Таким образом, i8
может хранить числа от -(2 7
) до 2 7
- 1, что равно значениям от -128 до 127. Варианты без знака могут хранить числа от 0 до 2 n
- 1, поэтому u8
может хранить числа от 0 до 2 8
- 1, что равно значениям от 0 до 255.
Кроме того, isize и usize зависят от архитектуры компьютера, на котором запускается ваша программа, что отражено в таблице как «arch»: 64-битный, если вы используете 64- битную архитектуру, и 32-битный, если вы на 32-битной архитектуре.
Целочисленные литералы можно записывать в любой из форм, показанных в таблице 3-
2. Обратите внимание, что числовые литералы, которые могут быть несколькими числовыми типами, позволяют использовать суффикс типа, например
57u8
, для обозначения типа. Числовые литералы также могут использовать
_
в качестве визуального разделителя, чтобы число было легче читать, например
1_000
, которое будет иметь то же значение, как если бы вы указали
1000
Таблица 3-2: Целочисленные литералы в Rust
Числовые литералы
Пример
Десятичный
98_222
Шестнадцатеричный
0xff
Таблица 3-1: целочисленные типы в Rust
Длина
Со знаком
Без знака
8 бит i8
u8 16-bit i16
u16 32 бит i32
u32 64 бит i64
u64 128-bit i128
u128
arch isize usize
Каждый вариант может быть как со знаком, так и без знака и имеет явный размер. Такая характеристика типа как знаковый и беззнаковый определяет возможность числа быть отрицательным. Другими словами, должно ли число иметь знак (знаковое) или оно всегда будет только положительным и, следовательно, может быть представлено без знака (беззнаковое). Это похоже на написание чисел на бумаге: когда знак имеет значение, число отображается со знаком плюс или со знаком минус; однако, когда можно с уверенностью предположить, что число положительное, оно отображается без знака.
Числа со знаком хранятся с использованием дополнительного кода
Каждый вариант со знаком может хранить числа от -(2 n - 1
) до 2 n - 1
- 1 включительно,
где n — количество битов, которые использует этот вариант. Таким образом, i8
может хранить числа от -(2 7
) до 2 7
- 1, что равно значениям от -128 до 127. Варианты без знака могут хранить числа от 0 до 2 n
- 1, поэтому u8
может хранить числа от 0 до 2 8
- 1, что равно значениям от 0 до 255.
Кроме того, isize и usize зависят от архитектуры компьютера, на котором запускается ваша программа, что отражено в таблице как «arch»: 64-битный, если вы используете 64- битную архитектуру, и 32-битный, если вы на 32-битной архитектуре.
Целочисленные литералы можно записывать в любой из форм, показанных в таблице 3-
2. Обратите внимание, что числовые литералы, которые могут быть несколькими числовыми типами, позволяют использовать суффикс типа, например
57u8
, для обозначения типа. Числовые литералы также могут использовать
_
в качестве визуального разделителя, чтобы число было легче читать, например
1_000
, которое будет иметь то же значение, как если бы вы указали
1000
Таблица 3-2: Целочисленные литералы в Rust
Числовые литералы
Пример
Десятичный
98_222
Шестнадцатеричный
0xff
Числовые литералы
Пример
Восьмеричный
0o77
Двоичный
0b1111_0000
Байт (только u8
)
b'A'
Как же узнать, какой тип целого числа использовать? Если вы не уверены, значения по умолчанию в Rust, как правило, подходят для начала: целочисленные типы по умолчанию i32
. Основной случай, в котором вы должны использовать isize или usize
, — это индексация какой-либо коллекции.
Переполнение целых чисел
Допустим, у вас есть переменная типа u8
, которая может хранить значения от 0 до
255. Если вы попытаетесь присвоить переменной значение вне этого диапазона,
например 256, произойдёт переполнение целого числа, что может привести к одному из двух вариантов поведения. Когда вы выполняете компиляцию в режиме отладки,
Rust содержит проверки на переполнение целых чисел, которые заставят вашу программу паниковать во время выполнения, если такое произойдёт. Rust использует термин паника, когда программа завершается с ошибкой; мы обсудим панику более подробно в разделе " Непоправимые ошибки в случае panic!
"
в главе
9.
Когда вы выполняете компиляцию в режиме release с флагом
--release
, Rust не
включает проверки на переполнение целых чисел, которые вызывают панику.
Вместо этого, если происходит переполнение, Rust выполняет свёртку с двойным
дополнением. Короче говоря, значения, превышающие максимальное значение,
которое может хранить тип, "сворачиваются" к минимальному из значений,
которые может хранить тип. В случае u8
, значение 256 становится 0, значение 257
становится 1 и так далее. Программа не запаникует, но переменная будет иметь значение, которое, вероятно, не соответствует вашим ожиданиям. Полагаться на поведение свёртки при переполнении целых чисел считается ошибкой.
Чтобы явно обработать возможность переполнения, вы можете использовать следующие группы методов, предоставляемые стандартной библиотекой для примитивных числовых типов:
Обёртывание во всех режимах с помощью методов wrapping_*
, например wrapping_add
Верните значение
None
, если произошло переполнение при использовании методов checked_*
Верните число и логическое значение, указывающее, имело ли место переполнение с помощью методов overflowing_*
Считать корректным установку минимального или максимального значения,
используя метод saturating_*
Числа с плавающей запятой
В Rust также есть два примитивных типа для чисел с плавающей запятой, которые представляют собой числа с десятичными точками. Типы чисел с плавающей запятой в
Rust — это f32
и f64
, имеющие размер 32 и 64 бита соответственно. Тип по умолчанию
— f64
, потому что на современных процессорах он примерно такой же скорости, как f32
, но обеспечивает большую точность. Все типы с плавающей запятой обладают знаком.
Вот пример, демонстрирующий числа с плавающей запятой в действии:
Файл : src/main.rs
Числа с плавающей точкой представлены согласно стандарту IEEE-754. Тип f32
является числом с плавающей точкой одинарной точности, а f64
имеет двойную точность.
Числовые операции
Rust поддерживает основные математические операции, которые ожидаются для всех типов чисел: сложение, вычитание, умножение, деление и остаток. Целочисленное деление округляется до ближайшего целого числа. В следующем коде показано, как использовать каждую числовую операцию в выражении let
:
Файл : src/main.rs fn main
() { let x =
2.0
;
// f64
let y: f32
=
3.0
;
// f32
} fn main
() {
// addition let sum =
5
+
10
;
// subtraction let difference =
95.5
-
4.3
;
// multiplication let product =
4
*
30
;
// division let quotient =
56.7
/
32.2
; let floored =
2
/
3
;
// Results in 0
// remainder let remainder =
43
%
5
;
}