Главная / Курсы / Rust / Логический и целочисленные типы
# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!