2.3-2.3.3.

This commit is contained in:
Alexander Zhirov 2023-01-22 18:27:20 +03:00
parent b64e6a47cb
commit ed90f97c42
2 changed files with 104 additions and 5 deletions

View File

@ -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. *Прим. науч. ред.*

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB