3. Арифметика, сравнения и преобразования.
Изучите порядок вычислений, целочисленное деление, побитовые операции и cast.
Wave Foundation
Совместный просмотр результатов расчетов и типов расчетов
Выражение — это код, который вычисляет значение. Имена переменных, литералы, вызовы функций и выражения, связывающие несколько значений с помощью операторов, — все это выражения. В математике, даже если это выглядит как одно и то же уравнение, результат будет разным в зависимости от того, является ли это числом целым или действительным, и сколько бит оно содержит.
В этой главе, начиная с простых вычислений, рассматриваются круглые скобки, деление, логические операции, побитовые операции и приведения. Каждый пример представляет собой полный файл main.wave, который можно запустить с помощью wavec run main.wave.
Диапазон заключен в круглые скобки
fun main() {
var first: i32 = 2 + 3 * 4;
var second: i32 = (2 + 3) * 4;
println("{} {}", first, second);
}
Результат выполнения:
14 20
Умножение вычисляется перед сложением, поэтому первое выражение равно 2+12. Во втором уравнении мы берем сумму в скобках, которая равна 5, а затем умножаем на 4. Цель состоит в том, чтобы не использовать меньше круглых скобок. Рекомендуется использовать его, чтобы читатель мог легко понять суть расчета.
Даже при многократном использовании одного и того же оператора направление цепочки важно. 20 - 5 - 3 — это (20 - 5) - 3, что равно 12. 20 - (5 - 3) — это 18. Точная полная последовательность находится в справочник оператора.
Целочисленное деление и остаток
Деление двух целых чисел не дает дробного результата с плавающей запятой. Используйте деление для частного и оператор остатка для остатка.
fun main() {
var items: i32 = 17;
var box_size: i32 = 5;
var full_boxes: i32 = items / box_size;
var remaining: i32 = items % box_size;
println("boxes={} remaining={}", full_boxes, remaining);
println("negative quotient={}", -17 / 5);
}
Результат выполнения:
boxes=3 remaining=2
negative quotient=-3
Разделив 17 предметов на группы по 5, получим 3 полные группы и 2 оставшихся предмета. Деление со знаком сокращается до нуля, поэтому -17/5 равно -3. Это отличается от округления в сторону отрицательной бесконечности.
Делить на 0 нельзя. signed Значение, полученное путем деления минимального значения на -1, не относится к одному типу. Функции, которые принимают эти входные данные, должны проверять перед делением или использовать математические вычисления checked API.
Точка преобразования меняет результат.
Следующие два выражения хранятся в переменной f64, но процесс вычисления отличается.
fun main() {
var already_divided: f64 = (7 / 2) as f64;
var floating_division: f64 = (7 as f64) / (2 as f64);
if (already_divided == 3.0) {
println("integer division lost the fraction");
}
if (floating_division == 3.5) {
println("floating division kept the fraction");
}
}
Результат выполнения:
integer division lost the fraction
floating division kept the fraction
Первое выражение выполняет целочисленное деление для получения числа 3, а затем преобразует его в f64. Второй преобразует операнды в f64 перед выполнением деления с плавающей запятой. Выбор более широкого типа для последней переменной не может восстановить информацию, потерянную ранее.
Значения с плавающей запятой являются приблизительными. Два результата, которые выглядят как одно и то же десятичное значение, могут оказаться непригодными для точного сравнения на равенство. Выберите допуск, соответствующий единицам измерения и масштабу проблемы; один фиксированный эпсилон подходит не для каждого расчета.
Сравнение выполнено bool
<, <=, >, >=, ==, != проверьте взаимосвязь. Один знак равенства = является присваиванием, а два знака равенства == — сравнением равенства.
fun main() {
var age: i32 = 20;
var minimum: i32 = 18;
var eligible: bool = age >= minimum;
if (eligible) {
println("eligible");
}
if (age != minimum) {
println("not exactly the boundary");
}
}
Результат выполнения:
eligible
not exactly the boundary
«Не менее 18» включает 18; «больше 18» исключает это. Проверьте эту границу с помощью тестов 17, 18 и 19. Сравнения смешанного типа зависят от знака и ширины, поэтому преобразование обоих операндов в предполагаемый тип может сделать сравнение более понятным.
Логические операции и краткая оценка
&& проверяет, являются ли оба значения истинными, || проверяет, являются ли более одного истинными, а ! меняет истинное/ложное значение. Эта операция не всегда выполняет выражение в правой части.
fun report() -> bool {
println("right side evaluated");
return true;
}
fun main() {
var enabled: bool = false;
if (enabled && report()) {
println("both true");
}
if (!enabled || report()) {
println("at least one true");
}
}
Результат выполнения:
at least one true
В первом условии enabled является ложным, поэтому нет необходимости смотреть вправо. Во втором случае !enabled истинно, поэтому правая часть также не нужна. Поэтому вывод report никогда не появляется.
Вы можете использовать это, чтобы проверить знаменатель перед делением. Фрагмент тела функции if (divisor != 0 && value / divisor > 2) { ... } не выполняет деление, когда знаменатель равен 0. Однако сам по себе этот тест не разрешает другие границы, такие как минимальное значение signed/-1.
&& имеет приоритет над ||. В сложных политиках укажите свое намерение в скобках, например (member && active) || admin.
Сужение или расширение целых чисел
as — явное приведение типов. Старшие биты, которые отбрасываются при сужении целого числа, не могут быть восстановлены путем его повторного расширения.
fun main() {
var original: i32 = 300;
var small: u8 = original as u8;
var widened: i32 = small as i32;
println("{} -> {} -> {}", original, small, widened);
var negative: i8 = -1;
var signed_value: i32 = negative as i32;
var same_bits: u8 = negative as u8;
println("{} {}", signed_value, same_bits);
}
Результат выполнения:
300 -> 44 -> 44
-1 255
Младшие 8 бит из 300 равны 44. Если вы расширите -1 до типа signed, знак будет расширен для сохранения -1. Если интерпретировать те же 8 бит, что и unsigned, это будет 255.
Преобразования, которые стремятся сохранить значения в диапазоне, и преобразования, которые пытаются манипулировать битами памяти, имеют разные цели. Если у вас есть пользовательский ввод, сначала проверьте, является ли это диапазоном назначения, и преобразуйте его. Наличие cast не гарантирует, что значение находилось в безопасном диапазоне.
Замените на bool.
Преобразование целого числа в bool дает false для нуля и true в противном случае. Это не усекает до младшего бита: 2 также преобразуется в true. Для значений с плавающей запятой только +0,0 и -0,0 преобразуются в false; все остальные значения, включая NaN и бесконечности, преобразуются в true.
fun main() {
var zero: i32 = 0;
var two: i32 = 2;
if (!(zero as bool)) {
println("zero is false");
}
if (two as bool) {
println("two is true");
}
}
Результат выполнения:
zero is false
two is true
Преобразование указателей в bool не поддерживается. Сравните указатель с null явно, например с pointer != null. Безопасно ли читать адрес, отличный от null, это отдельный вопрос.
битовые операции
&, |, ^, ~ охватывают каждый бит целого числа. Его можно использовать для выражения разрешений или функций в битах. Ниже 1 — разрешение на чтение, 2 — разрешение на запись.
const READ: u32 = 1;
const WRITE: u32 = 2;
fun main() {
var permissions: u32 = READ | WRITE;
if ((permissions & WRITE) != 0) {
println("write enabled");
}
permissions = permissions & ~WRITE;
println("remaining={}", permissions);
}
Результат выполнения:
write enabled
remaining=1
Добавьте бит с помощью OR и проверьте наличие определенного бита с помощью AND. Создайте маску, в которой только соответствующие биты равны 0, с помощью ~WRITE и сотрите ее. Битовые операции &·| отличаются от операторов сокращенной оценки &&·|| из bool.
Ширина и количество смен
Сдвиг влево перемещает биты влево и отбрасывает старшие биты за пределами ширины операнда. Сдвиг вправо использует расширение знака для значений со знаком и нулевое расширение для значений без знака. Результат всегда имеет тип левого операнда.
fun main() {
var bits: u8 = 129;
var shifted: u8 = bits << 1;
var negative: i32 = -8;
var positive: u32 = 8;
println("left={}", shifted);
println("right={} {}", negative >> 1, positive >> 1);
}
Результат выполнения:
left=2
right=-4 4
Сдвиг значения u8 129 влево на единицу отбрасывает его старший бит и оставляет 2. Исходное значение счетчика сдвига должно быть неотрицательным и меньше разрядности левого операнда. Для u8 допустимые значения от 0 до 7. Недопустимое количество констант — это ошибка времени компиляции; неверный счетчик времени выполнения вызывает ловушку.
Преобразование значений с плавающей запятой в целые числа
Преобразование чисел с плавающей запятой в целое число сначала отбрасывает дробную часть в направлении нуля, а затем проверяет целевой диапазон целых чисел. Результаты NaN, бесконечность и выход за пределы диапазона недействительны. Недопустимые преобразования констант приводят к ошибке времени компиляции; недопустимые преобразования во время выполнения вызывают ловушку.
Ловушка не возвращает значение ошибки из функции. Для устранения сбоев преобразования разработайте интерфейс, который проверяет диапазон перед преобразованием. Чтобы получить сохраненные биты значения с плавающей запятой, используйте функции преобразования битов в std::math::float вместо числового приведения.
Упражнение и решение
Напишите функцию, которая делит 137 вон сдачи на 50 вон и остаток и изменяет только целые числа в диапазоне от 0 до 255 на u8. В этом примере демонстрируется этап проверки как отдельная функция, которая указывает выход за пределы диапазона как -1.
fun checked_byte(value: i32) -> i32 {
if (value < 0 || value > 255) {
return -1;
}
var small: u8 = value as u8;
return small as i32;
}
fun main() {
var money: i32 = 137;
println("coins={} remainder={}", money / 50, money % 50);
println("{} {} {}", checked_byte(0), checked_byte(255), checked_byte(256));
}
Результат выполнения:
coins=2 remainder=37
0 255 -1
Причина, по которой -1 можно использовать в качестве индикатора неудачи, заключается в том, что диапазон успеха составляет от 0 до 255. Если любое целое число может быть значением успеха, необходимо другое представление результата. Мы продолжим эту разработку в главе об обработке ошибок позже.
приоритет
Приоритет операторов следующий, начиная с самого высокого:
- Основные выражения и постфиксный доступ: вызовы функций, доступ к полям, индексирование, постфикс
++·-- - Унарные операции:
!,~,&,deref, префикс++·--, унарные+·- asпреобразование типа*,/,%+,-<<,>><,<=,>,>===,!=- Бит
& - Бит
^ - Бит
| &&||- Присваивание и составное присвоение
Цепочка присвоений объединяется справа. При смешивании различных типов операторов используйте круглые скобки, чтобы четко обозначить порядок вычислений.
Предметы, которые можно заменить
Присвоение, ++ и -- требует выражения, обозначающего место хранения, например переменную, поле, элемент массива или разыменованный указатель. Запись в const не разрешена.
сдвиг
Результат сдвига всегда имеет тип левого операнда; правильный тип операнда не расширяет вычисления. Сдвиг влево отбрасывает старшие биты за пределами этой ширины. Сдвиг вправо расширяет знаковые значения и расширяет ноль беззнаковым значениям.
Число сдвигов должно быть целым числом, исходное значение которого удовлетворяет условиям 0 <= n < LHS bit width. Он проверяется перед любым усечением до меньшего типа. Недопустимое количество констант является ошибкой времени компиляции; неверный счетчик времени выполнения вызывает ловушку.
Преобразования с использованием логических значений и значений с плавающей запятой
Преобразование целого числа в bool дает false для нуля и true для любого другого значения. Преобразование чисел с плавающей запятой в bool дает false только для +0,0 и -0,0; NaN и положительная или отрицательная бесконечность дают true. Преобразования указателей в bool не поддерживаются: сравните явно с pointer != null.
Преобразование чисел с плавающей запятой в целое число отбрасывает дробную часть в направлении нуля, а затем проверяется диапазон назначения. Результаты NaN, бесконечность и выход за пределы диапазона недействительны. Недопустимые преобразования констант являются ошибками времени компиляции; недопустимые преобразования во время выполнения вызывают ловушку. Ловушка не является возвратом устранимой ошибки.
&& и || являются оценками короткого замыкания. Побочные эффекты невыполненного правого операнда не возникают. Вы можете проверить результаты с небольшими значениями в арифметический класс.
смена, которая провалилась намеренно
Число сдвигов для 8-битного значения должно быть от 0 до 7. Приведенные ниже программы следует отклонить перед выполнением.
fun main() {
var value: u8 = 1;
var result: u8 = value << 8;
}