diff --git a/02-основные-типы-данных-выражения/README.md b/02-основные-типы-данных-выражения/README.md index 27b4812..98c34d7 100644 --- a/02-основные-типы-данных-выражения/README.md +++ b/02-основные-типы-данных-выражения/README.md @@ -11,11 +11,11 @@ - [2.2.5.1. Строковые литералы: WYSIWYG, с разделителями, строки токенов и импортированные](#2-2-5-1-строковые-литералы-wysiwyg-с-разделителями-строки-токенов-шестнадцатеричные-и-импортированные) - [2.2.5.2. Тип строкового литерала](#2-2-5-2-тип-строкового-литерала) - [2.2.6. Литералы массивов и ассоциативных массивов](#2-2-6-литералы-массивов-и-ассоциативных-массивов) - - [2.2.7. Функциональные литералы (лямбда-функция)]() -- [2.3. Операции]() - - [2.3.1. l-значения и r-значения]() - - [2.3.2. Неявные преобразования чисел]() - - [2.3.2.1. Распространение интервала значений]() + - [2.2.7. Функциональные литералы (лямбда-функция)](#2-2-7-функциональные-литералы) +- [2.3. Операции](#2-3-операции) + - [2.3.1. l-значения и r-значения](#2-3-1-l-значения-и-r-значения) + - [2.3.2. Неявные преобразования чисел](#2-3-2-неявные-преобразования-чисел) + - [2.3.2.1. Распространение интервала значений](#2-3-2-1-распространение-интервала-значений) - [2.3.3. Типы числовых операций]() - [2.3.4. Первичные выражения]() - [2.3.4.1. Выражение assert]() @@ -482,6 +482,104 @@ assert(b == 1.5); [В начало ⮍](#2-2-7-функциональные-литералы) [Наверх ⮍](#2-основные-типы-данных-выражения) +## 2.3. Операции + +В следующих главах подробно описаны все операторы D в порядке убывания приоритета. Это естественный порядок, в котором вы бы группировали и вычисляли небольшие подвыражения в группах все большего размера. + +С операторами тесно связаны две независимые темы: l- и r-значения и правила преобразования чисел. Необходимые определения приведены в следующих двух разделах. + +[В начало ⮍](#2-3-операции) [Наверх ⮍](#2-основные-типы-данных-выражения) + +### 2.3.1. l-значения и r-значения + +Множество операторов срабатывает только тогда, когда l-значения удовлетворяют ряду условий. Например, не нужно быть гением, чтобы понять: присваивание `5 = 10` не соответствует правилам. Для успеха присваивания необходимо, чтобы левый операнд был *l-значением*. Пора дать точное определение l-значения (а заодно и сопутствующего ему *r-значения*). Названия терминов происходят от реального положения этих значений относительно оператора присваивания. Например, в инструкции `a = b` значение `a` расположено слева от оператора присваивания, поэтому оно называется l-значением; соответственно значение `b`, расположенное справа, – это r-значение[^17]. + +К l-значениям относятся: +- все переменные, включая параметры функций, даже те, которые запрещено изменять (то есть определенные с квалификатором `immutable`); +- элементы массивов и ассоциативных массивов; +- поля структур и классов (о них мы поговорим позже); +- возвращаемые функциями значения, помеченные ключевым словом `ref`; +- разыменованные указатели. + +Любое l-значение может выступить в роли r-значения. К r-значениям также относится все, что не вошло в этот список: литералы, перечисляемые значения (которые вводятся с помощью ключевого слова `enum`; см. раздел 7.3) и результаты таких выражений, как `x + 5`. Обратите внимание: для присваивания быть l-значением необходимо, но не достаточно – нужно успешно пройти еще несколько семантических проверок, таких как проверка прав на доступ (см. главу 6) и проверка прав на изменение (см. главу 8). + +[В начало ⮍](#2-3-1-l-значения-и-r-значения) [Наверх ⮍](#2-основные-типы-данных-выражения) + +### 2.3.2. Неявные преобразования чисел + +Мы только что коснулись темы преобразований; теперь пора рассмотреть ее подробнее. Здесь достаточно запомнить всего несколько простых правил: + +1. Если числовое выражение компилируется в C и *также* компилируется в D, то его тип будет одинаковым в обоих языках (обратите внимание: D не обязан принимать все выражения на C). +2. Никакое целое значение не преобразуется к типу меньшего размера. +3. Никакое значение с плавающей запятой не преобразуется неявно в целое значение. +4. Любое числовое значение (целое или с плавающей запятой) неявно преобразуется к любому значению с плавающей запятой. + +Правило 1 лишь незначительно усложняет работу компилятора, и это обоснованное усложнение. Поскольку D достаточно сильно «пересекается» с C и C++, это вдохновляет людей на бездумное копирование целых функций на этих языках в программы на D. Так пусть уж лучше D из соображений безопасности и переносимости отказывается время от времени от некоторых конструкций, чем если бы компилятор «проглотил» модуль из 2000 строк, а полученная программа заработала бы не так, как ожидалось, что определенно осложнило бы жизнь незадачливому программисту. Однако с помощью правила 2 язык D закручивает гайки посильнее, чем C и C++. Так что при переносе кода из этих языков на D диагностирующие сообщения время от времени будут указывать вам на «сырые» куски кода, рекомендуя вставить подходящие проверки и явные преобразования типов. + +Рисунок 2.3 иллюстрирует правила преобразования для всех числовых типов. Для преобразования выбирается кратчайший путь; для двух путей одинаковой длины результаты преобразований совпадают. Независимо от количества шагов преобразование считается одношаговым процессом, преобразования неупорядочены и им не назначены приоритеты – тип или преобразуется к другому типу, или нет. + +[В начало ⮍](#2-3-2-неявные-преобразования-чисел) [Наверх ⮍](#2-основные-типы-данных-выражения) + +#### 2.3.2.1. Распространение интервала значений + +В соответствии с приведенными выше правилами обыкновенное число, такое как 42, будет недвусмысленно оценено как число типа `int`. А теперь взгляните на столь же заурядную инициализацию: + +```d +ubyte x = 42; +``` + +По неумолимым законам проверки типов вначале 42 распознается как `int`. Затем это число типа `int` будет присвоено переменной `x`, а это уже влечет насильственное преобразование типов. Разрешать такое грубое преобразование опасно (ведь многие значения типа `int` на самом деле не поместятся в `ubyte`). С другой стороны, требовать преобразования типов для очевидно безошибочного кода было бы очень неприятно. + +![image-2-3](images/image-2-3.png) + +***Рис. 2.3.*** *Неявные преобразования чисел. Значение одного типа может быть автоматически преобразовано в значение другого типа тогда и только тогда, когда существует направленный путь от исходного типа до желаемого. Выбирается кратчайший путь, и преобразование считается одношаговым независимо от действительной длины пути. Преобразование в обратном направлении возможно, если оно осуществимо на основе метода распространения интервала значений (см. раздел 2.3.2.1)* + +Язык D элегантно разрешает эту проблему с помощью способа, прообразом которого послужила техника оптимизации компиляторов, известная как *распространение интервала значений (value range propagation)*: каждому значению в выражении ставится в соответствие интервал с границами в виде наименьшего и наибольшего возможных значений. Эти границы отслеживаются во время компиляции. Компилятор разрешает присвоить значение некоторого типа значению более «узкого» типа тогда и только тогда, когда интервальная оценка присваиваемого значения покрывается «целевым» типом. Очевидно, что для такой константы, как 42, как наибольшим, так и наименьшим значением будет 42, поэтому для присваивания нет преград. + +Конечно же, в такой типовой ситуации можно было бы использовать гораздо более простой алгоритм, однако в общем случае логично применять метод распространения интервала значений, так как он прекрасно справляется и со сложными ситуациями. Рассмотрим функцию, которая извлекает из значения типа `int` младший и старший байты: + +```d +void fun(int val) +{ + ubyte lsByte = val & 0xFF; + ubyte hsByte = val >>> 24; + ... +} +``` + +Этот код корректен независимо от того, каким будет введенное значение `val`. В первом выражении на значение накладывается маска, сбрасывающая все биты его старшего байта, а во втором делается сдвиг, в результате которого старший байт `val` перемещается на место младшего, а оставшиеся биты обнуляются. + +И в самом деле, компилятор правильно типизирует функцию `fun`, так как сначала он вычисляет интервал `val & 0xFF` и получает [0; 255] независимо от `val`, затем вычисляет интервал для `val >>> 24` и получает то же самое. Если бы вместо этих операций вы поставили операции, результат которых необязательно вместится в `ubyte` (например, `val & 0x1FF` или `val >>> 23`), компилятор не принял бы такой код. + +Метод распространения интервала значений применим для всех арифметических и логических операций; например, значение типа `uint`, разделенное на 100 000, всегда вместится в `ushort`. Кроме того, этот метод правильно работает и со сложными выражениями, такими как маскирование, после которого следует деление. Например: + +```d +void fun(int val) +{ + ubyte x = (val & 0xF0F0) / 300; + ... +} +``` + +В приведенном примере оператор `&` устанавливает границы интервала в 0 и `0хF0F0` (то есть 61 680 в десятичной системе счисления). Затем операция деления устанавливает границы в 0 и 205. Любое число из этого диапазона вмещается в `ubyte`. + +Определение корректности преобразований к меньшему типу по методу распространения интервала значений – несовершенный и консервативный подход. Одна из причин в том, что интервалы значений отслеживаются близоруко, внутри одного выражения, а не в нескольких смежных выражениях. Например: + +```d +void fun(int x) +{ + if (x >= 0 && x < 42) + { + ubyte y = x; // Ошибка! Нельзя втиснуть int в ubyte! + ... + } +} +``` + +Совершенно ясно, что инициализация не содержит ошибок, но компилятор не поймет этого. Он бы мог, но это серьезно усложнило бы реализацию и замедлило процесс компиляции. Выбор был сделан в пользу менее чувствительного распространения интервала значений в рамках одного выражения. Проведенный нами опыт показал, что такой умеренный анализ помогает программе избежать самых грубых ошибок, возникающих из-за ненадлежащего преобразования типов. Для оставшихся ошибок первого рода вы можете использовать выражения `cast` (см. раздел 2.3.6.7). + +[В начало ⮍](#2-3-2-1-распространение-интервала-значений) [Наверх ⮍](#2-основные-типы-данных-выражения) + [^1]: Впрочем, использование нелатинских букв является дурным тоном. – *Прим. науч. ред.* [^2]: С99 – обновленная спецификация C, в том числе добавляющая поддержку знаков Юникода. – *Прим. пер.* [^3]: Сам язык не поддерживает восьмеричные литералы, но поскольку они присутствуют в некоторых C-подобных языках, в стандартную библиотеку был добавлен соответствующий шаблон. Теперь запись `std.conv.octal!777` аналогична записи `0777` в C. – *Прим. науч. ред.* @@ -498,3 +596,4 @@ assert(b == 1.5); [^14]: В литерале массива допустима запятая, после которой нет элемента, например [1, 2,] – длина этого массива равна 2, а последняя запятая попросту игнорируется. Это сделано для удобства автоматических генераторов кода: при генерации текста литерала массива они конкатенируют строки вида `"очередной_элемент"`, не обрабатывая отдельно последний элемент, запятая после которого была бы не нужна. – *Прим. науч. ред.* [^15]: Заключенное в 1989 году соглашение между коммунистами и демократами, ознаменовавшее собой достижение компромисса между двумя партиями. В данном случае также ищется «компромиссный» тип. – *Прим. пер.* [^16]: In situ (лат.) – на месте. – *Прим. пер.* +[^17]: От англ. left-value и right-value. – *Прим. науч. ред.* diff --git a/02-основные-типы-данных-выражения/images/image-2-3.png b/02-основные-типы-данных-выражения/images/image-2-3.png new file mode 100644 index 0000000..2eefd9f Binary files /dev/null and b/02-основные-типы-данных-выражения/images/image-2-3.png differ