# Глава 7. Логический и целочисленные типы
Каждое значение в Rust имеет тип. Тип определяет способ хранения значения в памяти, а также допустимые операции над ним. Наиболее употребимые типы в Rust:
- скалярные типы: логический, целые и вещественные числа, символьный;
- примитивные составные: кортежи (_tuples_), массивы и срезы (_slices_);
- перечисления;
- структуры;
- типажи (_traits_);
- типы функций и замыканий (_closures_);
- типы указателей: ссылки, сырые указатели и указатели на функции.
Наиболее полный список типов можно посмотреть в справочнике языка в разделе [«Типы»](https://doc.rust-lang.org/reference/types.html). В этой главе будут рассмотрены логический и целочисленные типы.
# Логический тип
Логический тип `bool` имеет два возможных значения `true` и `false`. В одной из предыдущих глав [были рассмотрены](/courses/rust/chapters/rust_chapter_0030#block-logical-and-comparison-operations) операции над типом `bool`:
- логические: `&&`, `||`, `^`, `!`;
- сравнения: `==`, `!=`, `>`, `>=`, `<`, `<=`.
С помощью ключевого слова `as` можно преобразовать значения `true` и `false` в целые числа, в данном примере из `bool` в `usize`:
```rust {.example_for_playground .example_for_playground_001}
fn main() {
println!("true as integer: {}", true as usize);
println!("false as integer: {}", false as usize);
}
```
В консоль будет выведено:
```
true as integer: 1
false as integer: 0
```
Функция `print_logical_ops()` формирует таблицу логических операций. В таблице для каждой пары значений указываются результаты логических операций. Кромe стандартных операций `&&`, `||`, `^` в таблице используется импликация, логику которой нужно реализовать в функции `implication()`. {.task_text}
Импликация `⇒` — это логическая операция по смыслу равнозначная нестрогому условию «если…, то…», принятому в естественных языках. Импликация ложна только для комбинации `1 ⇒ 0`, что интерпретируется как «из истины нельзя получить ложь». В остальных случаях она истинна. {.task_text}
```rust {.task_source #rust_chapter_0070_task_0010}
fn implication(a: bool, b: bool) -> bool {
// добавьте реализацию функции
}
fn print_logical_ops() {
println!("| a b |and| or|xor|imp|");
for a in &[false, true] {
for b in &[false, true] {
print_line(*a, *b);
}
}
}
fn print_line(a: bool, b: bool) {
println!(
"|{:^3}{:^3}|{:^3}|{:^3}|{:^3}|{:^3}|",
a as u32,
b as u32,
(a && b) as u32,
(a || b) as u32,
(a ^ b) as u32,
implication(a, b) as u32
);
}
```
Если `a` ложно, то значение `b` можно игнорировать. Если `a` истинно, то на результат влияет только значение `b`. {.task_hint}
```rust {.task_answer}
fn implication(a: bool, b: bool) -> bool {
!a || b
}
fn print_logical_ops() {
println!("| a b |and| or|xor|imp|");
for a in &[false, true] {
for b in &[false, true] {
print_line(*a, *b);
}
}
}
fn print_line(a: bool, b: bool) {
println!(
"|{:^3}{:^3}|{:^3}|{:^3}|{:^3}|{:^3}|",
a as u32,
b as u32,
(a && b) as u32,
(a || b) as u32,
(a ^ b) as u32,
implication(a, b) as u32
);
}
```
# Целочисленные типы
Целые числа представлены в языке знаковыми и беззнаковыми типами с размерностью от 8 до 128 бит. Названия целочисленных типов в Rust начинаются с префикса `i` либо `u`, а далее следует размер в битах:
- знаковые: `i8`, `i16`, `i32`, `i64`, `i128`;
- беззнаковые: `u8`, `u16`, `u32`, `u64`, `u128`.
Зачем же в имени целого типа зашивать его размер? Например, в C++ есть тип `long`, который в зависимости от ОС и архитектуры процессора может занимать 4 или 8 байт. Такая привязка к целевой платформе усложняет разработку кроссплатформенного кода. Из-за нефиксированного размера тип `long` не подходит для некоторых задач: реализации бинарной сериализации и там, где требуется использовать полный диапазон 64-битных целых.
В тех случаях, когда неважен размер переменной можно воспользоваться типами `isize` и `usize`. Их размер зависит от архитектуры [целевой платформы](https://doc.rust-lang.org/nightly/rustc/platform-support.html). Так на 32-битной платформе `isize` и `usize` будут иметь размер 4 байта, а на 64-битной — 8 байт.
Во время исполнения программы размер типа и значения можно получить с помощью функций `size_of` и `size_of_val` из модуля `std::mem`:
```rust {.example_for_playground .example_for_playground_002}
use std::mem;
fn main() {
let usize_val: usize = 100;
let auto_val = 200;
println!("size of isize: {}", mem::size_of::<isize>());
println!("size of u128: {}", mem::size_of::<u128>());
println!("size of usize_val: {}", mem::size_of_val(&usize_val));
println!("size of -11: {}", mem::size_of_val(&-11));
println!("size of auto_val: {}", mem::size_of_val(&auto_val));
}
```
В консоль будут выведены размеры типов и переменных в байтах:
```
size of isize: 8
size of u128: 16
size of usize_val: 8
size of -11: 4
size of auto_val: 4
```
Каждый знаковый тип имеет диапазон значений от `-2 ** (n - 1)` до `2 ** (n - 1) - 1`, где `n` — количество битов в типе, `**` — знак возведения в степень . Так для `i32` диапазон значений соответствует интервалу от `-2147483648` до `2147483647`.
Каждый беззнаковый тип имеет диапазон значений от `0` до `2 ** n - 1`. Для `u32` диапазон значений будет от `0` до `4294967295`.
Для каждого целочисленного типа в стандартной библиотеке определены константы `MIN` и `MAX`. Пример:
```rust {.example_for_playground .example_for_playground_003}
fn main() {
println!("u16::MIN: {0:6}, {0:016b}", u16::MIN);
println!("u16::MAX: {0:6}, {0:016b}", u16::MAX);
println!("i16::MIN: {0:6}, {0:016b}", i16::MIN);
println!("i16::MAX: {0:6}, {0:016b}", i16::MAX);
}
```
В примере выводятся минимальные и максимальные значения для `u16` и `i16` в десятичной и двоичной системах счисления. Для вывода чисел в двоичном виде используется формат `0:016b`:
```
{0:016b}
│ ││ │
│ ││ └─────── b — двоичное представление числа
│ ││
│ │└───── 16 — минимальная длина печатаемого числа
│ │
│ └─── 0 — символ заполнения до 16 символов
│
└── 0 — индекс параметра в println!()
```
В консоли будет напечатано:
```
u16::MIN: 0, 0000000000000000
u16::MAX: 65535, 1111111111111111
i16::MIN: -32768, 1000000000000000
i16::MAX: 32767, 0111111111111111
```
## Целочисленные литералы
В определении литерала (безымянной константы) можно указать его тип с помощью суффикса, идентичного наименованию типа: `i8`, `u8`, `i16`, `u16`, `i32` и т.д. Если тип не указан, то по умолчанию целочисленный литерал будет иметь тип `i32`. В примере объявляются три целочисленных переменных с одинаковым значением, но разными типами:
```rust {.example_for_playground .example_for_playground_004}
fn main() {
use std::any::type_name_of_val as tn;
let v1 = 42i8;
let v2 = 42u128;
let v3 = 42;
println!("v1: {:4} = {}", tn(&v1), v1);
println!("v2: {:4} = {}", tn(&v2), v2);
println!("v3: {:4} = {}", tn(&v3), v3);
}
```
Программа печатает название переменной, её тип и значение. Имя типа определяется с помощью функции `type_name_of_val()` из модуля [any](https://doc.rust-lang.org/std/any/index.html) стандартной библиотеки. Конструкция `use <type> as <alias>` позволяет использовать функцию `std::any::type_name_of_val()` под коротким именем `tn()`. При запуске программы на экран будет выведено:
```
v1: i8 = 42
v2: u128 = 42
v3: i32 = 42
```
Целые числа можно задавать в двоичной, восьмеричной или шестнадцатеричной системах счисления с помощью префиксов `0b`, `0o` и `0x` соответственно. В примере переменные инициализируются литералами в разных системах счисления и для каждой переменной печатается значение в десятичной, двоичной, восьмеричной и шестнадцатеричной системах счисления:
```rust {.example_for_playground .example_for_playground_005}
fn main() {
let v1 = 0b1000_0001_u8;
let v2 = -0o10_i16;
let v3 = 0xABCD_EF01_u32;
println!("v1: {0:}, {0:b}, {0:o}, {0:x}", v1);
println!("v2: {0:}, {0:b}, {0:o}, {0:x}", v2);
println!("v3: {0:}, {0:b}, {0:o}, {0:x}", v3);
}
```
Символ подчеркивания `_` используется в литералах переменных `v1` и `v3` для улучшения читабельности и не имеет никакого значение. При компиляции символ `_` внутри числовых литералов игнорируется. То есть литерал `1_0_0_0` полностью эквивалентен `1000`. Программа выведет на экран:
```
v1: 129, 10000001, 201, 81
v2: -8, 1111111111111000, 177770, fff8
v3: 2882400001, 10101011110011011110111100000001, 25363367401, abcdef01
```
## Операции над целыми числами и переполнение
Над целочисленными типами возможны операции:
- арифметические: `+`, `-`, `*`, `/`, `%`;
- сравнения: `==`, `!=`, `>`, `>=`, `<`, `<=`;
- битовые: `!`, `&`, `|`, `^`, `>>`, `<<`.
Для арифметические и битовых операций существуют версии с одновременным присваиванием результата операции левому операнду, например: `i += 1`. Битовые операции будут рассмотрены позже в этой главе.
Rust — строготипизированный язык, поэтому выражения, участвующие в операциях, должны быть одного типа. В коде функции `clamp()` нарушается это требование:
```rust {.example_for_playground .example_for_playground_006}
fn clamp(val: i32, min: i16, max: i16) -> i32 {
// ошибка: сравнение i32 c i16
if val < min {
// ошибка: возврат i16 вместо i32
min
// ошибка: сравнение i32 c i16
} else if val > max {
// ошибка: возврат i16 вместо i32
max
} else {
val
}
}
```
Компиляция `clamp()` завершится с четырмя ошибками на несоответствие типов, для краткости показана только первая ошибка:
```
error[E0308]: mismatched types
--> src/main.rs:3:14
|
3 | if val < min {
| --- ^^^ expected `i32`, found `i16`
| |
| expected because this is `i32`
|
help: you can convert an `i16` to an `i32`
|
3 | if val < min.into() {
| +++++++
```
Исправить ошибки можно с помощью оператора приведения типа `as` или метода `into()`, как подсказывает компилятор:
```rust {.example_for_playground .example_for_playground_007}
fn clamp(val: i32, min: i16, max: i16) -> i32 {
let min = min as i32;
let max: i32 = max.into();
if val < min {
min
} else if val > max {
max
} else {
val
}
}
```
По возможности следует использовать расширяющее приведение, то есть от меньшего типа к большему, в данном случае от `i16` к `i32`. Сужающее приведение может оказаться небезопасным, а поведение программы неожиданным. Так если в условии переменную `val` привести к типу `i16`, то вызов функции `clamp(100000, -9999, 9999)` вернет `-9999` вместо ожидаемого `9999`.
### Переполнение
В отличии от языков C и C++ в Rust переполнение целых чисел не приводит в неопределенному поведению (_undefined behavior_). Поведение определено и зависит от типа сборки:
- в отладочной сборке переполнение, возникшее при вычислении арифметических операций, вызовет панику (_panic_);
- в релизе проверки на целочисленное переполнение отсутствуют, вместо этого при переполнении происходит возврат к началу диапазона. Так, первое выходящее за диапазон значение превращается в `0` для беззнакового целого, следующее за ним — это уже `1`, потом — `2`, и так далее.
Разное поведение при обработке целочисленного переполнения является компромиссом между безопасностью и скоростью. Отладочный режим позволяет обнаружить ошибки переполнения в операциях над целыми числами. В релизе авторы языка от таких проверок отказались ради сохранения высокой производительности.
Для демонстрации разного поведения при возникновении арифметического переполнения эта программа будет запущена в релизном и отладочном режимах сборки:
```rust {.example_for_playground .example_for_playground_008}
#![allow(arithmetic_overflow)]
fn main() {
println!("{} + 1 = {}", u8::MAX, u8::MAX + 1);
println!("{} - 1 = {}", u8::MIN, u8::MIN - 1);
println!("{} + 1 = {}", i16::MAX, i16::MAX + 1);
println!("{} - 1 = {}", i16::MIN, i16::MIN - 1);
println!("{}.abs() = {}", i8::MIN, i8::MIN.abs());
}
```
Атрибут `#![allow(arithmetic_overflow)]` отключает проверку на арифметическое переполнение во время компиляции. Без этого атрибута программа не соберется в отладочном режиме. Вычисление модуля минимального отрицательного числа `i8::MIN.abs()` также приведет к переполнению. Релизная сборка программы выполнится до конца, выведя на экран:
```
255 + 1 = 0
0 - 1 = 255
32767 + 1 = -32768
-32768 - 1 = 32767
-128.abs() = -128
```
Запуск программы в отладочной сборке завершится паникой на первой арифметической операции `u8::MAX + 1`. Вывод программы с включенной обратной трассировкой стека (_stack backtrace_):
```
thread 'main' panicked at src/main.rs:4:38:
attempt to add with overflow
stack backtrace:
0: rust_begin_unwind
at /rustc/25ef9e3d85d934b27d9dada2f9dd52b1dc63bb04/library/std/src/panicking.rs:647:5
1: core::panicking::panic_fmt
at /rustc/25ef9e3d85d934b27d9dada2f9dd52b1dc63bb04/library/core/src/panicking.rs:72:14
2: core::panicking::panic
at /rustc/25ef9e3d85d934b27d9dada2f9dd52b1dc63bb04/library/core/src/panicking.rs:144:5
3: arithmetic_overflow::main
at ./src/main.rs:4:38
4: core::ops::function::FnOnce::call_once
at /rustc/25ef9e3d85d934b27d9dada2f9dd52b1dc63bb04/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
```
Паника — это реакция программы на неустранимую ошибку, в данном случае на арифметическое переполнение в операции сложения `255 + 1`. По умолчанию, при возникновении паники происходит раскрутка стека. Во время раскрутки стека, освобождаются все ресурсы, которыми владеет текущий поток. После раскрутки стека поток останавливается. Если паника возникает в основном потоке, то программа аварийно завершает работу.
### Контроль переполнения
Паника и возврат к началу диапазона зачастую не являются желаемым поведением при переполнении. Это поведение можно изменить, используя четыре набора методов:
- `checked_*` — возвращает `Option`, который принимает значение `None` в случае переполнения;
- `saturating_*` — возвращает наибольшее/наименьшее значение при возникновении переполнения;
- `overflowing_*` — возвращает кортеж, содержащий при переполнении результат переноса и флаг переполнения.
- `wrapping_*` — при переполнении возвращает результат переноса.
Названия приведены со звездочкой, так как соответствующие функции есть для каждой операции: `checked_add()` — сложения, `checked_sub()` — вычитание, `checked_mul()` — умножение, `checked_div()` — деление и т.д. С [полным списком функций](https://doc.rust-lang.org/std/primitive.i32.html#implementations) можно ознакомиться в описании типа `i32` (или любого другого целого типа). Разница в поведении функций на примере сложения:
```rust {.example_for_playground .example_for_playground_009}
fn main() {
let max = u8::MAX;
println!("{}.checked_add(1) = {:?}", max, max.checked_add(1));
println!("{}.saturating_add(1) = {:?}", max, max.saturating_add(1));
println!("{}.overflowing_add(1) = {:?}", max, max.overflowing_add(1));
println!("{}.wrapping_add(1) = {:?}", max, max.wrapping_add(1));
}
```
На экран будет выведено:
```
255.checked_add(1) = None
255.saturating_add(1) = 255
255.overflowing_add(1) = (0, true)
255.wrapping_add(1) = 0
```
Например, если при переполнении требуется устанавливать определенное значение (в данном случае `u16::MIN`), то можно реализовать это следующим образом:
```rust {.example_for_playground .example_for_playground_010}
fn main() {
let (result, has_overflow) = u16::MAX.overflowing_mul(50000);
let fixed = if has_overflow { u16::MIN } else { result };
println!("result: {result}");
println!("fixed: {fixed}");
}
```
Функция `overflowing_mul()` вернет кортеж, содержащий результат умножения и флаг переполнения. Умножение `u16::MAX` на `50000` приведет к переполнению, поэтому вывод будет следующим:
```
result: 15536
fixed: 0
```
Сервис _executioner_ выполняет задания, которые получает из очереди сообщений. Каждое сообщение содержит: идентификатор, задание и его максимально допустимое время выполнения в секундах. Первоначально предполагалось, что ограничение по времени не будет превышать нескольких минут. На практике время исполнения задач достигаeт часа и более. {.task_text}
В сервисе допущена ошибка. Из-за нее он некорректно выполняет задания с длительностью более часа. Такие задания сервис прерывает раньше срока, а в отладочном режиме аварийно завершает работу. Опытные сисадмины вручную перезапускают проблемные задания, указывая максимальное время исполнения равное `0`. Ноль означает неограниченное время на исполнение задания. Последнее время проблемных заданий стало заметно больше. У сисадминов прибавилось рутинной работы. Необходимо найти и исправить эту ошибку. {.task_text}
_Примечание._ В сервисе используются два модуля `comm` и `exec`. В модуле `comm` определены структуры: `Queue` — очередь сообщений, метод `pop()` которой возвращает сообщение; `Message` — сообщение, содержащие id, задание и ограничение по времени; `PopResult` — результат извлечения из очереди, содержащий сообщение либо признак конца очереди. В модуле `exec` определены: `Config` — конфигурация сервиса; `Job` — задание; `RunnerPool` — пул раннеров, метод `execute()` пула передает задание свободному раннеру, метод `stop_all()` останавливает все раннеры и возвращает ошибку `RuntimeError`, если обнаружена проблема. Код этих модулей не содержит описанную проблему и опущен для простоты. {.task_text}
```rust {.task_source #rust_chapter_0070_task_0020}
fn process_queue(queue: &mut comm::Queue, runners: &mut exec::RunnerPool) {
loop {
match queue.pop() {
comm::PopResult::Value(msg) => process_message(&msg, runners),
comm::PopResult::Eof => break,
}
}
}
fn process_message(msg: &comm::Message, runners: &mut exec::RunnerPool) {
let id: u32 = msg.get_id();
let job: &exec::Job = msg.get_job();
let limit: u32 = to_microseconds(msg.get_time_limit());
runners.execute(id, job, limit);
}
fn to_microseconds(sec: u32) -> u32 {
sec * 1000000
}
fn main() -> Result<(), exec::RuntimeError> {
let cfg = exec::Config::read();
let mut queue = comm::Queue::new(&cfg);
let mut runners = exec::RunnerPool::new(&cfg);
process_queue(&mut queue, &mut runners);
runners.stop_all()
}
```
В функции `to_microseconds()` происходит переполнение при больших значениях аргумента `sec`. Необходимо воспользоваться функцией умножения, сигнализирующей о переполнении, например `overflowing_mul()`. При переполнении логично возвращать `0`, так как поместить корректное значение в `u32` не представляется возможным. {.task_hint}
```rust {.task_answer}
fn process_queue(queue: &mut comm::Queue, runners: &mut exec::RunnerPool) {
loop {
match queue.pop() {
comm::PopResult::Value(msg) => process_message(&msg, runners),
comm::PopResult::Eof => break,
}
}
}
fn process_message(msg: &comm::Message, runners: &mut exec::RunnerPool) {
let id: u32 = msg.get_id();
let job: &exec::Job = msg.get_job();
let limit: u32 = to_microseconds(msg.get_time_limit());
runners.execute(id, job, limit);
}
fn to_microseconds(sec: u32) -> u32 {
let (microseconds, flag) = sec.overflowing_mul(1000000);
if flag { 0 } else { microseconds }
}
fn main() -> Result<(), exec::RuntimeError> {
let cfg = exec::Config::read();
let mut queue = comm::Queue::new(&cfg);
let mut runners = exec::RunnerPool::new(&cfg);
process_queue(&mut queue, &mut runners);
runners.stop_all()
}
```
## Битовые операции
Битовыми операциям называются операции применяемые к каждому биту целого числа или паре чисел:
- `!` побитовое отрицание инвертирует бит: `!00000001 == 11111110`;
- `&` побитовое И возвращает 1 если оба бита равны 1, в других случаях возвращает 0: `0011 & 0101 == 0001`;
- `|` побитовое ИЛИ возвращает 1 если хотя бы один бит равен 1, и возвращает 0 если оба бита равны 0: `0011 | 0101 == 0111`;
- `^` побитовое исключающее ИЛИ возвращает 1 если один бит равен 1, а другой 0, и возвращает 0 если оба бита имеют одинаковые значения: `0011 ^ 0101 == 0110`;
- `<<` битовый сдвиг влево сдвигает биты влево на указанное количество позиций, заполняя освободившиеся биты 0: `11111101 << 4 ==> 11010000`;
- `>>` битовый сдвиг вправо сдвигает биты вправо на указанное количество позиций, заполняя освободившиеся биты 0 для положительных чисел и 1 для отрицательных чисел: `10111111(191) >> 4 ==> 00001011(11)`, `10111111(-65) >> 4 ==> 11111011(-5)`.
Для битовых операций существуют версии с одновременным присваиванием результата операции левому операнду: `!=`, `&=`, `|=`, `^=`, `<<=`, `>>=`.
## Порядок следования байтов
Процессоры оперируют машинными словами. Машинным словом называется единица данных, которая является естественной для данной архитектуры. Размер машинного слова в современных процессорах равен 64 битам, реже 32 битам. Минимальная адресуемая ячейка памяти, как правило, равна байту. Байт де-факто состоит из 8 бит. На заре вычислительной техники это было не так. Например, размер машинного слова в советской [ЭВМ М-2](https://www.computer-museum.ru/books/m1-m13/m2.htm) был равен 34 битам, а минимальная адресуемая ячейка — 10 бит. В [PDP-1](https://ru.wikipedia.org/wiki/PDP-1) компании [DEC](https://ru.wikipedia.org/wiki/Digital_Equipment_Corporation) минимальной адресуемой единицей было машинное слово размера 18 бит.
Так выглядит компьютер PDP-1 компании DEC:
![Компьютер PDP-1 компании DEC](https://raw.githubusercontent.com/senjun-team/senjun-courses/main/illustrations/rust/pdp_1_dec.jpg) {.illustration}
Порядок следования байтов (_endianness_) — это порядок расположения байт в машинном слове. Для разных процессоров, протоколов и форматов данных может использоваться один из двух порядков:
- от старшего к младшему (_big-endian_), наиболее значимый (самый старший) байт хранится первым, а за ним идут байты в порядке убывания значимости;
- от младшего к старшему (_little-endian_), наименее значимый (самый младший) байт хранится первым, а за ним следуют байты в порядке возрастания значимости.
Представление числа `0A0B0C0D` в big-endian и little-endian:
![Порядок следования байтов в big-endian и little-endian представлениях.](https://raw.githubusercontent.com/senjun-team/senjun-courses/main/illustrations/rust/endianness.jpg) {.illustration}
Вне зависимости от порядка следования байтов вывод значения целого числа идет слева направо от старшего байта к младшему, то есть `0A0B0C0D`. Если напечатать адрес каждого байта, то можно увидеть в каком порядке байты расположены. Для компьютера с _little-endian_ архитектурой вывод может быть таким:
```
0A0B0C0D at 0x7fff358a0024
| | | |
| | | 0D at 0x7fff358a0024
| | 0C at 0x7fff358a0025
| 0B at 0x7fff358a0026
0A at 0x7fff358a0027
```
В большинстве современных процессоров используется порядок байт _little-endian_. Например, в процессорных архитектрурах [x86-64](https://ru.wikipedia.org/wiki/X86-64) и [RISC-V](https://ru.wikipedia.org/wiki/RISC-V). Также _little-endian_ применяется в [USB](https://ru.wikipedia.org/wiki/USB) и [PCI](https://ru.wikipedia.org/wiki/PCI).
Порядок байт _big-endian_ является стандартным для протоколов [TCP/IP](https://ru.wikipedia.org/wiki/TCP/IP) и применяется во множестве форматов файлов — например, [EBML](https://ru.wikipedia.org/wiki/EBML), [JPEG](https://ru.wikipedia.org/wiki/.JPEG) и [PNG](https://ru.wikipedia.org/wiki/PNG).
Обычно разработчику не нужно заботиться о порядке следования байт. За исключением тех случаев, когда требуется читать «сырые» данные из сети или реализовать библиотеку, поддерживающую бинарный формат данных.
Реализуйте функцию `switch_endian()`, которая преобразует один порядок байт в другой. {.task_text}
```rust {.task_source #rust_chapter_0070_task_0030}
fn switch_endian(val: u32) -> u32 {
// добавьте реализацию функции
}
```
С помощью битовых операций нужно поменять местами соседние байты: `|0123| => |1032|`. После этого следует переставить пары байт: `|1032| ==> |3210|`. Пример перестановки двух байт: `(val & 0x00FF) << 8 | (val & 0xFF00) >> 8`. {.task_hint}
```rust {.task_answer}
fn switch_endian(val: u32) -> u32 {
// меняем местами соседние байты
let tmp = (val & 0x00FF00FF) << 8 | (val & 0xFF00FF00) >> 8;
// меняем местами первую пару байт со второй
(tmp & 0x0000FFFF) << 16 | (tmp & 0xFFFF0000) >> 16
}
```
В стандартной библиотеке у целочисленных типов есть методы для преобразования порядка следования байтов:
- `to_be()` преобразует текущее значение в _big-endian_,
- `to_le()` преобразует текущее значение в _little-endian_.
Пример использования:
```rust {.example_for_playground .example_for_playground_011}
fn main() {
let val1 = 0xDD00CC00_BB00AA00_u64;
println!("val1 => be: {:016X}", val1.to_be());
println!("val1 => le: {:016X}", val1.to_le());
let val2 = 0b01111110_10000001_i16;
println!("val2 => be: {:016b}", val2.to_be());
println!("val2 => le: {:016b}", val2.to_le());
}
```
Вывод на экран:
```
val1 => be: 00AA00BB00CC00DD
val1 => le: DD00CC00BB00AA00
val2 => be: 1000000101111110
val2 => le: 0111111010000001
```
## Дополнительный код
Для представления чисел со знаком требуется признак, указывающий на то, является число положительным или отрицательным. Для знаковых типов таким признаком служит старший бит старшего байта (знаковый бит). У всех отрицательных чисел знаковый бит равен `1`, у положительных — всегда `0`.
Наиболее очевидным представлением отрицательных чисел является включение знакового бита с сохранением состояния остальных бит:
```
77 => 01001101
-77 => 11001101
```
Такое представление называется прямым кодом, однако у него есть [недостатки](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%BE%D0%B9_%D0%BA%D0%BE%D0%B4#%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5_%D0%BF%D1%80%D1%8F%D0%BC%D0%BE%D0%B3%D0%BE_%D0%BA%D0%BE%D0%B4%D0%B0). Основной недостаток в усложнении логики арифметических операций для отрицательных чисел. Нужно поддержать арифметику отрицательных чисел с прямым кодом в процессоре либо в компиляторах. Первое усложняет архитектуру процессора, второе уменьшает эффективность арифметических операций для чисел меньше `0`. Поэтому в современных компьютерах прямой код используется только для представления неотрицательных целых чисел.
В качестве альтернативы прямому коду на ранних ЭВМ (например, [PDP-1](https://ru.wikipedia.org/wiki/PDP-1)) использовался [обратный код](https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%80%D0%B0%D1%82%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D0%B4). Обратный код отрицательного числа получается инвертированием его модуля:
```
77 => 01001101
-77 => 10110010
```
В англоязычной литературе обратный код называют «первым дополнением» (_"ones' complement"_). У обратного кода также есть недостатки, основной — наличие двух нолей:
```
0 => 00000000
-0 => 11111111
```
Наличие двух нулей в обратном коде приводит к усложнению операции суммирования. Поэтому в современных компьютерах для представления отрицательных целых чисел используется [дополнительный код](https://ru.wikipedia.org/wiki/%D0%94%D0%BE%D0%BF%D0%BE%D0%BB%D0%BD%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D0%B4). Дополнительный код отрицательного числа можно вычислить инвертированием его модуля (получается обратный код) и прибавлением к инверсии `1`:
![Вычисление дополнительного кода.](https://raw.githubusercontent.com/senjun-team/senjun-courses/main/illustrations/rust/twos_complement.jpg) {.illustration}
Вычисление дополнительного кода можно реализовать так:
```rust {.example_for_playground .example_for_playground_012}
fn main() {
let val: u8 = 77;
println!("{0:3} => {0:08b}", val);
println!("{0:3} => {0:08b}", (!val + 1) as i8);
}
```
В консоль будет выведено:
```
77 => 01001101
-77 => 10110011
```
В англоязычной литературе дополнительный код называют «вторым дополнением» (_"two’s complement"_). Его применение упрощает архитектуру процессора, так как можно использовать одинаковые операции сложения и вычитания для знаковых и беззнаковых целых чисел. При этом представление положительных чисел не отличается для знаковых и беззнаковых типов. Основным недостатком дополнительного кода является плохая читабельность отрицательных значений в двоичной, восьмеричной и шестнадцатеричной системах счисления.
Реализуйте функции преобразования целого числа в прямой код и обратно. Функция `to_direct_code()` конвертирует целое из дополнительного кода в прямой. Она возвращает кортеж, содержащий преобразованное число и признак переполнения. Функция `from_direct_code()` преобразует целое число из прямого кода в дополнительный. {.task_text}
_Примечание._ Пример распаковки кортежа, возвращенного из метода: `let (value, flag) = acode.overflowing_sub(1);`. Пример возвращения кортежа из функции: `(value, false)` или `return (value, false);`. {.task_text}
```rust {.task_source #rust_chapter_0070_task_0040}
fn to_direct_code(acode: i32) -> (i32, bool) {
// добавьте реализацию функции
}
fn from_direct_code(dcode: i32) -> i32 {
// добавьте реализацию функции
}
```
Прямой код отрицательного числа можно вычислить так: вычесть `1` от начального значения, инвертировать модуль полученной разности. При вычитании `1` из `i32::MIN` произойдет переполнение, поэтому следует использовать метод `overflowing_sub()`. Алгоритм вычисления дополнительного кода был описан ранее. При преобразовании из прямого кода в дополнительный нужно учитывать наличие `-0`. Положительные числа всегда закодированы в прямом коде и преобразованию не подлежат. {.task_hint}
```rust {.task_answer}
fn to_direct_code(acode: i32) -> (i32, bool) {
if acode >= 0 {
(acode, false)
} else {
let (tmp, flag) = acode.overflowing_sub(1);
(!tmp | i32::MIN, flag)
}
}
fn from_direct_code(dcode: i32) -> i32 {
if dcode >= 0 {
dcode
} else {
!(dcode & i32::MAX) + 1
}
}
```
## Заключение
- Логический тип `bool` представлен двумя возможными значениями: `true` и `false`. Для типа `bool` доступны операции: логические (`&&`, `||`, `!`) и сравнения (`==`, `!=`, `>`, `>=`, `<`, `<=`).
- Целочисленные типы бывают знаковыми и беззнаковыми. Их названия начинаются с префикса `i` либо `u` и заканчиваются размером в битах: `8`, `16`, `32`, `64`, `128`. Также есть платформозависимые типы `isize` и `usize`, размер которых зависит от архитектуры целевой платформы.
- Целочисленные литералы также могут быть заданы в двоичной, восьмеричной или шестнадцатеричной системах счисления с помощью префиксов `0b`, `0o` и `0x` соответственно. В определении литерала можно указать его тип с помощью суффикса, идентичного наименованию типа. Для улучшения читабельности можно использовать знак подчеркивания: `0x_00FF_00FF_u32`.
- Над целочисленными типами возможны операции: арифметические, сравнения и битовые.
- При выполнении арифметических операций возможно переполнение. Для обработки таких исключительных ситуаций у целочисленных типов имеются методы, начинающиеся с `checked_*`, `saturating_*`, `overflowing_*`, `wrapping_*`.
- В некоторых случаях следует учитывать порядок следования байтов: _big-endian_, _little-endian_. Для преобразования порядка байтов следует использовать методы: `to_be()` и `to_le()`.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!