Добавлена глава 1

This commit is contained in:
Alexander Zhirov 2025-04-14 01:43:30 +03:00
parent a448f86621
commit 9d5342275a
3 changed files with 829 additions and 0 deletions

View File

@ -5,3 +5,4 @@
## Содержание ## Содержание
- [Предисловие](book/00-предисловие) - [Предисловие](book/00-предисловие)
- [1. Основные задачи](book/01-основные-задачи)

View File

@ -0,0 +1,827 @@
# 1. Основные задачи
В этой главе мы начнём знакомство с D и рассмотрим некоторые из его ключевых возможностей. Вы изучите следующие рецепты:
- Установка компилятора и написание программы "Hello World"
- Добавление дополнительных модулей (файлов) в вашу программу
- Использование внешних библиотек
- Создание и обработка массивов
- Использование ассоциативных массивов для преобразования ввода
- Создание пользовательского типа вектора
- Использование пользовательского типа исключений
- Понимание неизменяемости
- Нарезка строки для получения подстроки
- Создание дерева классов
## Введение
Язык D заимствует элементы из нескольких других языков программирования, включая статически типизированные языки, такие как C, C++ и Java, а также динамические языки, такие как Ruby, Python и JavaScript. Общий синтаксис очень похож на C: использование фигурных скобок для обозначения блоков, объявления в виде инициализатора имени типа и многое другое. Фактически, большая часть, но не весь, код на C может быть скомпилирован и в D.
D также ориентирован на удобство, продуктивность и мощь моделирования. Эти принципы можно проиллюстрировать с помощью функции вывода типов в D. Вывод типов позволяет писать код, не задумываясь явно о типе переменной и не повторяя его. Это обеспечивает удобство, характерное для динамических языков, при сохранении проверок статической типизации на этапе компиляции. Вы будете использовать вывод типов на протяжении всех ваших программ. Любая переменная, объявленная без указания типа (обычно для этого используется ключевое слово `auto`), получает выведенный тип, который автоматически определяется по правой части присваивания. D — один из самых быстро компилируемых языков, что обеспечивает быстрые циклы редактирования и запуска, способствующие стремительной разработке, как в динамических языках. Мощь моделирования проявляется в богатых возможностях генерации кода, интроспекции и пользовательских типов в D, которые вы начнёте изучать в этой главе при рассмотрении структур и классов.
## Установка компилятора и написание программы "Hello World"
Вы собираетесь создать свою первую программу на D — простую программу "Hello World".
### Как это сделать...
Отлично! Давайте выполним шаги для создания вашей первой программы на D:
1. Загрузите компилятор DMD с сайта https://dlang.org/download.html.
2. Если вы используете установщик для вашей платформы, он выполнит установку. Если вы используете ZIP-архив, просто распакуйте его и используйте вместо установщика. Исполняемые файлы для каждой операционной системы находятся в папке `dmd2/названиеашей_ос/bin`. Вы можете добавить эту папку в переменную окружения `PATH`, чтобы не указывать полный путь при каждом запуске компилятора.
3. Создайте файл в вашем любимом текстовом редакторе со следующим содержимым и назовите его `hello.d`:
```d
import std.stdio : writeln;
void main() {
writeln("Hello, world!");
}
```
4. Скомпилируйте программу. В командной строке выполните следующее:
```sh
dmd hello.d
```
5. Запустите программу следующим образом:
```sh
./hello
```
Вы должны увидеть следующее сообщение:
```
Hello, world!
```
### Как это работает...
Компилятор DMD является ключевым инструментом для работы с языком D. Хотя существуют IDE, в этой книге они использоваться не будут. Поэтому вы научитесь использовать компилятор и будете лучше подготовлены к решению проблем, возникающих в процессе сборки.
Исходные файлы D представляют собой текст в формате Unicode, который компилируется в исполняемые программы. По умолчанию компилятор DMD создаёт исполняемый файл с тем же базовым именем, что и первый переданный ему файл. Например, при вызове `dmd hello.d` создаётся файл `hello.exe` в Windows или `hello` в Unix-системах. Вы можете изменить имя выходного файла с помощью опции `dmd of`, например, `dmd oftest hello.d` создаст файл `test.exe`. Подробнее об опциях `dmd` вы узнаете по мере необходимости.
Теперь рассмотрим строки файла `hello.d`, начиная с оператора `import`:
```d
import std.stdio;
```
Программа на D состоит из модулей. Каждый модуль — это файл, но в отличие от C или C++, где используются текстовые директивы `#include`, в D применяется символический `import`. При импорте модуля его публичные члены становятся доступны для использования. Вы можете импортировать один и тот же модуль несколько раз без негативных последствий, и порядок импортов на верхнем уровне не важен.
В данном случае вы импортируете модуль `std.stdio`, который является частью стандартной библиотеки и предоставляет функции ввода-вывода, включая функцию `writeln`, которую вы используете позже в коде. Теперь обсудим функцию `main()`:
```d
void main()
```
Программы на D обычно начинают выполнение с функции `main()`. Функция `main()` в D может опционально принимать аргументы командной строки типа `string[]` и возвращать либо `void`, либо целочисленное значение. Все варианты допустимы.
> Можно писать программы на D, которые начинаются не с `main()`, что позволяет обойти инициализацию runtime D. Об этом вы узнаете в главе 11, "D для программирования ядра".
Здесь вы возвращаете void, так как не возвращаете конкретного значения. Runtime автоматически вернёт ноль операционной системе при нормальном завершении программы, а в случае завершения из-за исключения — код ошибки. Теперь рассмотрим следующую функцию вывода:
```d
writeln("Hello, world!");
```
Наконец, вы вызываете функцию `writeln` из модуля `std.stdio`, чтобы вывести **Hello, World!**. Функция `writeln` принимает любое количество аргументов любого типа и автоматически преобразует их в строки для вывода. Она также автоматически добавляет символ новой строки в конец вывода.
### Кое-что еще...
Здесь вы использовали компилятор DMD. Есть и другие компиляторы для D, помимо DMD: GDC и LDC. Узнать больше о них можно на сайтах https://gdcproject.org/ и https://github.com/ldc-developers/ldc соответственно.
## Добавление дополнительных модулей (файлов) в вашу программу
По мере роста программы вам захочется разделить её на несколько файлов. D предлагает способ сделать это, который похож, но не идентичен другим популярным языкам программирования. Файлы исходного кода D также называют модулями D.
### Как это сделать...
Чтобы добавить модули в вашу программу, выполните следующие шаги:
1. Создайте дополнительный файл.
2. Добавьте в этот файл объявление модуля с именем пакета и модуля, которые должны быть значимыми для вашего проекта и этого файла соответственно:
```d
module yourpackage.yourmodulename;
```
3. Импортируйте модуль в основной файл для использования. Импорт в D гибкий — его можно размещать не только в начале файла, и он может встречаться несколько раз. Укажите полное имя модуля, как в его объявлении:
```d
import yourpackage.yourmodulename;
```
4. Используйте функции из модуля, при необходимости указывая полное имя модуля для избежания неоднозначностей.
5. Скомпилируйте программу, указав все файлы в командной строке с помощью следующего синтаксиса:
```sh
dmd file1.d file2.d
```
6. Это создаст один исполняемый файл из переданных файлов. Имя исполняемого файла по умолчанию совпадает с именем первого указанного файла. В данном случае исполняемый файл будет называться `file1`.
### Как это работает...
В D код организован в модули. Существует взаимно однозначное соответствие между файлами и модулями — каждый исходный файл D является модулем, и каждый модуль D — это файл. Для использования модуля нужно импортировать его в текущую область видимости, обращаться к его членам в коде и добавить его в команду сборки при компиляции.
Модули в D концептуально схожи с статическими классами с единственным экземпляром: они могут содержать конструкторы, деструкторы, поля и т.д. Каждое объявление в модуле может иметь атрибуты и квалификаторы защиты. В D модули, а не классы, являются единицей инкапсуляции, поэтому любой код внутри одного модуля имеет доступ ко всем его сущностям, независимо от квалификаторов защиты.
Модули имеют логические имена, которые не обязаны совпадать с именем файла. Имя задаётся с помощью оператора `module` в начале файла. Этот оператор необязателен, и если его опустить, имя модуля по умолчанию будет соответствовать имени файла. Однако настоятельно рекомендуется указывать `module` с именем пакета (первая часть имени, разделённого точками) для всех модулей, которые могут быть импортированы, чтобы избежать проблем при организации кода в директории. Ошибка "модуль `foo.bar` должен быть импортирован как модуль `foo`" возникает из-за отсутствия оператора `module` в импортируемом модуле. Обычно структура пакетов соответствует структуре директорий исходных файлов. Хотя имена пакетов и модулей не обязаны совпадать с именами директорий и файлов, их соответствие помогает другим разработчикам и инструментам сборки понять структуру проекта.
Оператор `import` может появляться в любом месте кода. В области видимости модуля он может быть размещён где угодно и использоваться многократно без изменений. В локальных областях видимости `import` должен быть указан до использования сущности, и его видимость ограничена этой областью.
Имена членов модуля не обязаны быть уникальными среди всех модулей программы. При необходимости их можно разрешать в точке использования или в операторе импорта с помощью идентификатора модуля. Пример:
```d
import project.foo; // Разрешение через project.foo
import bar; // Разрешение через bar
project.foo.func(); // Вызов project.foo.func
bar.func(); // Вызов bar.func
```
Компилятор всегда выдаст ошибку, если ссылка неоднозначна, чтобы вы могли явно указать свои намерения.
> Вы можете переименовывать модули при импорте. Оператор `import foo = project.foo;` позволяет использовать `foo` для разрешения имён вместо написания полного имени `project.foo` каждый раз.
### Кое-что еще...
В дистрибутиве `dmd` есть программа `rdmd`, которая рекурсивно находит зависимые модули и компилирует их автоматически. С `rdmd` достаточно передать только модуль, содержащий `main`.
### Дополнительно
Документация по модулям на https://dlang.org/spec/module.html описывает работу модульной системы D, включая все формы оператора `import` и информацию о защите символов.
## Использование внешних библиотек
D может использовать внешние библиотеки, написанные на D или других языках, с прямым доступом к функциям на C, таким как функции операционной системы или многочисленные библиотеки на C для различных задач. Здесь вы узнаете, как использовать внешнюю библиотеку в программе на D.
### Как это сделать...
Чтобы использовать внешние библиотеки в D, выполните следующие шаги:
1. Создайте или загрузите привязки (bindings) — список прототипов функций и определений структур данных библиотеки.
2. Для 32-битной Windows с dmd: получите или создайте импортируемую библиотеку (`.lib` файл).
3. Если есть `.lib` файл, используйте `coffimplib`.
4. Если есть только DLL, используйте `implib`.
5. Импортируйте привязку с помощью:
```d
import package.module.name;
```
6. Компилируйте с библиотекой: На Linux передайте `-L-l<libname>` в `dmd`. На Windows передайте `.lib` файл в `dmd` при компиляции. Это свяжет библиотеку с исполняемым файлом, создав рабочую программу.
### Как это работает...
D бинарно совместим с C, но не совместим на уровне исходного кода. Это значит, что вы можете напрямую линковать C-библиотеки, включая библиотеки ОС, без обёрток или вызывающего кода. Однако требуется портировать заголовочные файлы, прототипы функций и объявления переменных в D. Этот процесс называется привязкой (binding).
Можно использовать библиотеку, указав только прототипы нужных функций с минимальной типобезопасностью, но рекомендуется максимально точно портировать заголовочный файл C. Это минимизирует ошибки и упрощает использование для программистов, знакомых с документацией и использованием C.
В коде библиотека используется как любой другой модуль: импортируйте модуль, вызывайте функции и разрешайте имена, используя полные имена пакетов и модулей.
При компиляции флаг `-L` в dmd передаёт аргументы напрямую компоновщику. На 32-битной Windows использование существующих библиотек может быть сложным, так как dmd использует старый формат OMF, несовместимый с более распространённым COFF. Здесь помогают `implib` и `coffimplib` — они генерируют формат, ожидаемый компоновщиком `optlink`, из доступных форматов. Команда `implib` создаёт `.lib` файл из `.dll` для прямого использования в D. Формат вызова `implib` следующий:
```
implib /s myfile.lib myfile.dll
```
Команда `coffimplib` преобразует распространённый формат COFF `.lib` в формат, требуемый D. Формат вызова:
```
coffimplib myfile.lib
```
Эти программы можно загрузить отдельно с сайта Digital Mars, компании, создавшей язык D и компилятор DMD. Они не нужны для сборки 64-битных программ на Windows или программ на других ОС.
### Кое-что еще...
Компилятор DMD поддерживает `pragma(lib, "name");`, который автоматически добавляет флаг компоновщика при сборке, если модуль передан в командную строку dmd. В GDC этот прагма не полностью поддерживается, но и не вредит — выдаётся предупреждение о неподдерживаемом прагма.
Также можно создавать интерфейсные файлы для библиотек на D с расширением `.di`. Они традиционно используются и могут генерироваться автоматически с опцией `dmd -H`. Файлы `.di` похожи на заголовочные файлы в C/C++: содержат определения интерфейсов, но без тел функций. Использование `.di` необязательно.
### Дополнительно
- Иногда использование библиотек не сводится к простому вызову их функций, или вы хотите улучшить API. Подробности — в главе 4, "Интеграция".
- Deimos (https://github.com/d-programming-deimos) — официальный репозиторий с привязками для популярных C-библиотек. Он не меняет API, а лишь предоставляет портированные заголовочные файлы для D, избавляя от необходимости писать прототипы вручную.
- Dub (https://code.dlang.org/) — полуофициальный менеджер пакетов D. На code.dlang.org доступны библиотеки сообщества, включая привязки к C и чистые D-библиотеки.
- Для разработки под 32-битную Windows пакет Basic Utilities от Digital Mars (https://digitalmars.com/download/freecompiler.html) включает `implib` и другие инструменты для создания сложных `.exe` файлов.
- В директории dmd2/src/druntime/import в архиве dmd находятся интерфейсные файлы .di для runtime-библиотеки D и стандартной библиотеки C.
## Создание и обработка массивов
D имеет три встроенных типа массивов: статические массивы с фиксированной длиной, известной на этапе компиляции; динамические массивы с переменной длиной; и ассоциативные массивы, похожие на хэш-таблицы или словари в других языках. Массивы и срезы в D — очень гибкий и простой в использовании инструмент, применяемый почти во всех программах на D. Здесь вы рассмотрите некоторые их возможности, создав список целых чисел и найдя сумму его содержимого.
### Как это сделать...
Шаги для создания и обработки массивов:
1. Объявите переменную массива следующей командой:
```d
int[] arr;
```
2. Добавьте данные в массив, как показано:
```d
arr ~= 1;
arr ~= [2, 3];
```
3. Создайте функцию, которая принимает срез и выполняет обработку, как показано в следующем коде:
```d
int sum(in int[] data) {
int total = 0;
foreach(item; data)
total += item;
return total;
}
```
4. Передайте срез массива в функцию, как показано в следующем коде:
```d
// Динамические массивы можно передавать напрямую. Статические
// массивы можно преобразовать в срез с помощью оператора [].
writeln("Сумма элементов ", arr, " равна ", sum(arr));
```
### Как это работает...
Типы в D всегда читаются справа налево. Массив `int[]` — это массив целых чисел. Указатель `string* []` — это указатель на массив указателей на строки. Массив `int[][]` — это массив массивов целых чисел, т.е. ступенчатый массив.
В D есть два вида массивов: статические и динамические. Статический массив — это тип-значение, представляющий непрерывный блок памяти фиксированного размера (аналог массива в C). Динамический массив — это, по сути, структура с двумя полями: указателем на данные и длиной данных. В отличие от статического массива, динамические массивы и срезы имеют семантику ссылок. Доступ к указателю и длине осуществляется через свойства `.ptr` и `.length` соответственно. В данном примере использовался динамический массив, синтаксис которого совпадает со срезом.
Основные операции с массивами: добавление, индексация и создание срезов.
- **Добавление:** Выполняется с помощью оператора `~=`. Также существует бинарный оператор `~` (не путать с унарным `~`, который инвертирует биты), который объединяет два массива в новый. Можно добавить отдельный элемент или другой статический/динамический массив совместимого типа.
- **Индексация:** Выполняется с помощью оператора `[expr]`, например, `arr[0]`. Это похоже на C, но в D массивы знают свою длину, что позволяет автоматически проверять границы. Попытка доступа к индексу за пределами массива вызовет RangeError.
- **Создание среза:** Выполняется с помощью оператора `[]`, например, `arr[]` или `arr[0 .. 2]`. Срез предоставляет представление части массива, начиная с левого индекса (включительно) и заканчивая правым (исключительно, поэтому длина массива может быть конечной границей; есть сокращение `$`). `[]` возвращает срез всего массива и полезен для передачи статических массивов или пользовательских типов массивов в функции, ожидающие срезы или динамические массивы. Создание среза — быстрая операция с постоянным временем выполнения.
Для перебора массива или среза используется цикл `foreach`. Указывается переменная итерации, затем точка с запятой и переменная, которую нужно перебрать. Тип переменной можно не указывать явно, например, `foreach(item; array)` или `foreach(int item; array)`.
В примере кода параметр функции определён как `in`. Ключевое слово `in` — это сокращение для комбинации классов хранения `const` и `scope`. Это означает, что массив и его содержимое нельзя изменять (это вызовет ошибку компиляции), а также нельзя сохранять копию или возвращать ссылку на переданный массив.
### Кое-что еще...
D также поддерживает векторные операции с массивами, например:
```d
arr = arr + 5;
```
Этот код добавляет 5 к каждому элементу массива. Также можно копировать массивы следующим образом: `arr2 = arr`. Это скопирует `arr` в `arr2`. Для этого длина обоих массивов должна совпадать. Чтобы скопировать массив без совпадения длин, используйте `array.dup`.
### Дополнительно
- https://dlang.org/articles/d-array-article.html — подробности управления памятью массивов и различия между динамическим массивом и срезом.
- https://dlang.org/spec/arrays.html — более полный список возможностей массивов в D.
- Рецепт "Создание замены массива" в главе 5 "Управление ресурсами". Показывает, как создать новый тип массива с такими же возможностями, как у встроенных массивов, но с кастомным поведением или стратегиями выделения памяти.
- Рецепт "Избежание сборщика мусора" в главе 5 "Управление ресурсами". Обсуждает особенности выделения памяти встроенными массивами и как избежать использования сборщика мусора.
## Использование ассоциативных массивов для преобразования ввода
D также поддерживает ассоциативные массивы, иногда называемые картами или словарями. Ассоциативный массив связывает произвольные ключи со значениями. В отличие от обычного массива, ключи не обязаны быть последовательными и не должны быть целыми числами. Здесь вы изучите их функциональность, создав программу, которая переводит входные строки в другие строки.
### Как это сделать...
Давайте переведём ввод, используя следующие шаги:
1. Объявите ассоциативный массив с ключами и значениями строкового типа.
2. Инициализируйте его начальными данными.
3. Перебирайте входные строки. Если строка есть в массиве, выведите значение и удалите её. Если нет, добавьте эту строку с заменой.
4. По завершении переберите массив, чтобы показать изменения.
Код:
```d
void main() {
import std.stdio, std.string;
string[string] replacements = ["test": "passed", "text": "replaced"];
replacements["foo"] = "bar";
assert(replacements["test"] == "passed");
foreach(line; stdin.byLine()) {
line = line.strip(); // обрезаем пробелы
// проверяем, есть ли строка в массиве...
if (auto replacement = line in replacements) {
// если да, выводим замену и удаляем её
writeln(line, " => ", *replacement);
replacements.remove(line.idup);
} else {
// если нет, добавляем в массив
writeln(line);
replacements[line.idup] = "previously inserted!";
}
}
foreach(line, replacement; replacements)
writeln("Mapping ", line, " => ", replacement);
}
```
Когда программа исчерпает строки для обработки, она выведет текущее содержимое массива, показывая, что было добавлено и удалено по мере ввода данных.
### Как это работает...
Сначала вы объявили главную функцию `main` и импортировали модули `std.stdio` и `std.string`, которые содержат функции ввода-вывода и удаления пробелов, использованные позже.
Затем вы объявили ассоциативный массив, который сопоставляет строки другим строкам. Синтаксис: `ValueType[KeyType]`, где оба типа могут быть любыми типами D. Вы также инициализировали массив `replacements` с помощью литерала ассоциативного массива.
> Ключи ассоциативного массива могут быть пользовательскими типами. Для этого пользовательский тип должен реализовать методы `opHash`, `opCmp` и `opEquals`.
Синтаксис литерала ассоциативного массива: `[Key:Value, Key:Value, ...]`. Литералы ассоциативного массива могут содержать как константы времени компиляции, так и данные времени выполнения; `["foo":x]` также допустим.
Далее вы установили значение вне литерала и проверили значение ключа для демонстрации. Ассоциативные массивы используют синтаксис, похожий на обычные массивы: те же скобки для получения и установки элементов.
Затем вы вошли в цикл замены, читая стандартный ввод построчно, удаляя пробелы и ища строку в массиве `replacements`. Рассмотрим эту строку подробнее:
```d
if (auto replacement = line in replacements) {
```
Справа используется оператор `in` для поиска ключа. Он возвращает указатель на элемент, если ключ найден, и `null`, если нет.
Использование указателя, возвращённого оператором `in`, необязательно. Конструкция `if (line in replacements)` работает так же. Также существует обратный оператор `!in`. Условие `if (line !in replacements)` истинно, если строки нет в массиве `replacements`.
Слева переменная объявляется и присваивается прямо в `if`-условии. Это ограничивает область видимости переменной. Если переменная `replacement` доступна, она гарантированно не `null`, иначе `if` не выполнится!
В следующем примере вы переходите в ветку `true` условия `if`. Эта ветка использует оператор разыменования `replacement` для вывода значения. Оператор `*` необходим, так как `in` возвращает указатель на элемент, а не сам элемент. Затем вы удаляете ключ из массива с помощью встроенного метода `remove`. При следующем вводе этой строки она не будет заменена.
В ветке `false` переменная `replacement` недоступна, и попытка её использовать вызовет ошибку компиляции. Вместо этого вы добавляете новую строку в массив замен. Свойство `.idup` требуется, так как ключи ассоциативного массива должны быть неизменяемыми, а `stdin.byLine` возвращает изменяемый буфер. `Array.idup` создаёт новую неизменяемую копию данных.
Наконец, когда ввод исчерпан, вы перебираете ассоциативный массив с помощью цикла `foreach`. Синтаксис: `foreach(index, value; array)`. Вы выводите текущее состояние. Параметр `index` необязателен, если нужны только значения.
### Кое-что еще...
Вы также можете получить список всех ключей и значений с помощью свойств `.keys` и `.values` ассоциативного массива. Переменные `std.traits.KeyType` и `std.traits.ValueType` позволяют выполнять отражение типов ассоциативных массивов на этапе компиляции.
## Создание пользовательского типа вектора
Пользовательские типы в D широко применяются для группировки данных, моделирования объектов, обеспечения проверок на этапе компиляции и многого другого. Здесь вы создадите простой тип вектора с длиной и направлением, чтобы рассмотреть основные возможности.
### Подготовка
При создании пользовательской коллекции в D первым шагом является выбор типа: класс, структура, шаблон mixin или объединение (union). Шаблоны mixin удобны для повторного использования кода: они определяют код, который может быть скопирован в другой тип с параметризацией. Объединения используются, когда один блок памяти должен поддерживать несколько типов, и они наименее распространены в типичном коде D. Классы и структуры — основа пользовательских типов в D, и у них много общего. Ключевое различие — полиморфное наследование: если оно нужно, выбирайте класс. В противном случае структуры легче, обеспечивают максимальную гибкость, позволяют точно задавать расположение каждого байта без скрытых данных, перегружать все операторы, использовать детерминированное уничтожение (идиома RAII из C++) и поддерживать как ссылочную, так и значимую семантику в зависимости от ваших задач. Структуры в D также поддерживают форму подтипизации, но не виртуальные функции, что будет рассмотрено в главе 6, "Обёрнутые типы".
Кратко:
|Структура|Класс|
|-|-|
|Точный контроль над расположением памяти|Поддержка виртуальных функций и наследования|
|Идеально для легковесных обёрток других типов|Всегда ссылочный тип|
|Детерминированное уничтожение|Обычно управляется сборщиком мусора|
Поскольку вашему типу вектора не нужны виртуальные функции, он будет структурой.
### Как это сделать...
Создадим тип вектора, следуя этим шагам:
1. Объявите структуру с именем. Объявление может быть где угодно, но для общей доступности поместите его в область видимости верхнего уровня модуля. В D, в отличие от C++, точка с запятой в конце определения структуры не нужна:
```d
struct Vector {}
```
2. Определите необходимые данные и добавьте их в структуру. Здесь нужны величина (magnitude) и направление (direction) — оба типа `float`:
```d
struct Vector {
float magnitude;
float direction;
}
```
3. Добавьте методы для работы с данными. Нужно реализовать сложение векторов и преобразование из координат `(x, y)`. Полный код:
```d
struct Vector {
// данные
float magnitude;
float direction;
// методы
/// создание вектора из точки (x, y)
static Vector fromPoint(float[2] point) {
import std.math;
Vector v;
float x = point[0];
float y = point[1];
v.magnitude = sqrt(x ^^ 2 + y ^^ 2);
v.direction = atan2(y, x);
return v;
}
/// преобразование в точку (x, y), возвращает массив
float[2] toPoint() const {
import std.math;
float x = cos(direction) * magnitude;
float y = sin(direction) * magnitude;
return [x, y];
}
/// оператор сложения
Vector opBinary(string op : "+")(Vector rhs) const {
auto point = toPoint(), point2 = rhs.toPoint();
point[0] += point2[0];
point[1] += point2[1];
return Vector.fromPoint(point);
}
}
```
4. Используйте новый тип:
```d
auto origin = Vector(0, 0);
import std.math;
auto result = origin + Vector(1.0, PI);
import std.stdio;
writeln("Vector result: ", result);
writeln(" Point result: ", result.toPoint());
```
Выводит `Vector(1.0, 3.14)` и `[-1, 0]`, показывая сумму векторов в виде величины и направления, затем в координатах `(x, y)`. Результаты могут слегка отличаться из-за округления чисел с плавающей точкой на вашем компьютере.
### Как это работает...
Структуры в D — это агрегатные типы, содержащие данные и методы. Все члены и методы определяются внутри структуры, между открывающей и закрывающей фигурными скобками. Члены данных объявляются как переменные: тип (может быть выведен, если есть инициализатор), имя и, опционально, инициализатор, который должен быть вычислен на этапе компиляции. Без явного инициализатора члены структуры устанавливаются в значения, указанные в определении структуры.
Методы имеют синтаксис, аналогичный функциям на уровне модуля, с двумя отличиями: они могут быть `static`, и к ним можно добавить `const`, `immutable` или `inout`, что применяется к переменной `this`. Переменная `this` автоматически объявляется и представляет текущий экземпляр объекта в методе. Подробности об этих модификаторах будут рассмотрены в рецепте об иммутабельности.
Перегрузка операторов в D реализуется через методы с особыми именами. В примере определён `opBinary` для перегрузки бинарных операторов, таких как сложение, причём только для `+`. Также можно перегрузить приведение типов, присваивание, проверку равенства и др.
При использовании вы объявили вектор с помощью `auto`, применяя автоматически определённый конструктор.
При выводе результата использовалось автоматическое форматирование строки, отображающее имя и значения, аналогично автоматическому конструктору. Для управления этим можно реализовать собственный метод `toString`.
### Дополнительно
- Глава 6, "Обёрнутые типы", раскроет продвинутые возможности, включая создание ссылочного типа с помощью структур, использование конструкторов, деструкторов, постблит-копирования и др.
- Раздел "Наследование и динамическое приведение классов" покажет, как максимально использовать классы.
- Документацию по перегрузке операторов можно найти на https://dlang.org/spec/operatoroverloading.html. Там описаны все операторы, доступные для перегрузки, и их реализация.
## Использование пользовательского типа исключений
D использует исключения для обработки ошибок, как и многие другие языки. Исключения в D — это классы, наследующиеся от `Throwable`, и различаются по типу. Рекомендуется создавать новый подкласс исключений для каждого типа ошибок в вашем коде, чтобы пользователи получали максимум информации и контроля.
### Как это сделать...
Создадим пользовательский тип исключения по шагам:
1. Объявите класс, наследующийся от `Exception`.
2. Создайте конструктор с минимальными параметрами: `string file` и `size_t line`, с значениями по умолчанию `__FILE__` и `__LINE__`.
3. Передайте аргументы конструктору `Exception`.
4. Используйте исключение.
Код:
```d
class MyException : Exception {
this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
super(message, file, line, next);
}
}
void main() {
import std.stdio;
try {
throw new MyException("message here");
} catch(MyException e) {
writeln("caught ", e);
}
}
```
Этот код создаёт и обрабатывает пользовательское исключение `MyException`, выводя информацию об ошибке.
### Как это работает...
D использует исключения для обработки ошибок. Все выбрасываемые объекты наследуются от `Exception` (для восстанавливаемых ошибок) или `Error` (для невосстанавливаемых, которые обычно не следует перехватывать). Общий базовый класс — `Throwable`.
Обычно пользовательское исключение наследуется от `Exception` и определяет, как минимум, конструктор, передающий данные в `super()`. Можно добавить дополнительные данные для конкретного случая.
Конструктор `Exception` принимает четыре аргумента: сообщение, имя файла, номер строки и, опционально, ссылку на другое исключение. Сообщение, файл и строка формируют текст ошибки, который выводится, если исключение не перехвачено.
Указывать файл и строку при выбросе необязательно: параметры `__FILE__` и `__LINE__` автоматически подставляются в месте вызова, что помогает точно указать источник ошибки. Параметр `Throwable` `next` используется, если обработчик исключения выбрасывает новое исключение; он ссылается на предыдущее исключение.
> Для полной трассировки стека при выводе исключения компилируйте с включённой отладочной информацией: `dmd -g`.
### Кое-что еще...
При использовании функций C проверяйте коды ошибок и преобразуйте их в исключения. Если устанавливается errno, используйте `ErrnoException` из модуля `std.exception`:
```d
import core.sys.posix.unistd; // низкоуровневые функции POSIX
import core.sys.posix.fcntl; // дополнительные функции POSIX
import std.exception; // для ErrnoException
auto fd = open("myfile.txt", O_RDONLY);
// open() возвращает -1 при ошибке и устанавливает errno
if (fd == -1)
throw new ErrnoException("Couldn't open myfile.txt");
// автоматическое закрытие файла в конце области видимости
scope(exit) close(fd);
/* чтение файла */
```
### Дополнительно
Охранные конструкции `scope` (описаны в главе 5, "Управление ресурсами") удобны при работе с исключениями. Они позволяют размещать код очистки или восстановления рядом с точкой создания ресурса, обеспечивая безопасность при выбросе исключений. В примере `scope(exit)` гарантирует закрытие файла даже при исключении.
## Понимание неизменяемости
Здесь вы рассмотрите, как использовать иммутабельность в функциях и типах данных. Иммутабельность помогает писать код, который легче понимать и поддерживать, поскольку ограничивает места, где данные могут изменяться.
### Подготовка
Сначала напишите функцию. Затем проанализируйте её и определите, что она должна делать. Только просматривает переданные данные? Хранит или возвращает ссылку на переданные данные? Эти факты о том, как функция использует свои аргументы, помогут выбрать подходящие квалификаторы.
### Как это сделать...
Применение `const` и `immutable` немного отличается для свободных функций и методов объектов.
#### Написание функций
Если вы принимаете тип значения, `const` и `immutable` не так важны. Рассмотрим случаи:
Если вы только просматриваете значение (не храните и не изменяете), используйте ключевое слово `in`. Для строк используйте `char[]` вместо `string` (`string` — это псевдоним для `immutable(char)[]`):
```d
void foo(in char[] lookAtThis) { /* просмотр lookAtThis */ }
```
Если вы храните ссылку, предпочтительно принимать `immutable` данные, если возможно:
```d
void foo(immutable(ubyte)[] data) { stored = data; }
```
Если вы изменяете данные, но не храните их, используйте `scope`, но не `const` (`in` — это сокращение для `scope const`):
```d
void foo(scope char[] changeTheContents) { /* изменение содержимого */ }
```
Если вы не изменяете и не храните данные, но возвращаете ссылку, используйте `inout`:
```d
inout(char)[] substring(inout(char)[] haystack, size_t start, size_t end) {
return haystack[start .. end];
}
```
Если вы изменяете само значение (не только содержимое, на которое оно ссылается), используйте `ref`:
```d
void foo(ref string foo) { /* изменение самого foo */ }
```
#### Написание методов объектов
При написании методов объектов все упомянутые ранее правила для функций применимы, но добавляется квалификатор для параметра `this`. Квалификатор для `this` указывается либо перед, либо после функции:
```d
int foo() const { return this.member; } // this квалифицируется как const
const int foo() { return this.member; } // то же самое
```
Вторая форма может быть перепутана с возвратом `const` значения (правильный синтаксис: `const(int) foo() { ... }`), поэтому предпочтительна первая форма. Квалификаторы для `this` лучше размещать в конце функции.
### Как это работает...
Квалификаторы `const` в D отличаются от C++ двумя ключевыми способами: в D есть квалификатор `immutable`, который означает, что данные никогда не изменятся, квалификаторы `const` и `immutable` в D транзитивны, то есть всё, доступное через `const`/`immutable` ссылку, также является `const`/`immutable`. В D нет исключений, подобных ключевому слову `mutable` в C++.
Эти различия обеспечивают более строгие гарантии, что особенно полезно при хранении данных.
При хранении данных предпочтительны либо `immutable`, либо изменяемые `(mutable)` данные - `const` обычно не очень полезен для переменных-членов класса, так как он лишь запрещает классу изменять их, но не предотвращает изменения другими функциями. `immutable` гарантирует, что никто не сможет изменить данные. Их можно хранить, не опасаясь неожиданных изменений. Изменяемые данные всегда полезны для хранения приватного состояния объекта.
Гарантия неизменности — главная сила `immutable` данных. Вы получаете все преимущества приватной копии, зная, что никто другой не изменит данные, без необходимости создавать фактическую копию. Квалификаторы `const` и `immutable` наиболее полезны для ссылочных типов, таких как указатели, массивы и классы. Они малоэффективны для типов значений, таких как скаляры (`int`, `float` и т.д.) или структуры, поскольку эти типы и так копируются при передаче в функции.
При просмотре данных строгая гарантия неизменности не требуется. Здесь полезен `const`. Квалификатор `const` означает, что вы не будете изменять данные, но не исключает их изменения другими. Ключевое слово `in` — это сокращение для `scope const`. Параметры `scope` пока не полностью реализованы, но концепция полезна. `scope`-параметр обещает, что ссылка на данные не будет сохранена. Вы просматриваете данные, но не храните на них ссылку. В сочетании с `const` это идеально для входных данных, которые вы только изучаете. Также есть удобное сокращение `in`.
При возврате ссылки на const-данные важно сохранить константность. Для этого в D используется `inout`. Рассмотрим функцию C `strstr`:
```d
char *strstr(const char *haystack, const char *needle);
```
Она возвращает указатель на место в `haystack`, где найдена `needle`, или `null`, если `needle` не найдена. Проблема в том, что константность `haystack` теряется в возвращаемом значении, позволяя изменять константные данные через указатель, что нарушает систему типов.
В C++ часто дублируют функцию: одна с `const`, другая без. D решает проблему, сохраняя гарантию константности, теряемую в C, и избегая дублирования, как в C++. Правильное определение функции в стиле `strstr` в D:
```d
inout(char)* strstr(inout(char)* haystack, in char* needle);
```
Метод `inout` используется для возвращаемого значения вместо `const`, а также применяется к одному или нескольким параметрам или ссылке `this`. Внутри функции `inout(T)` эквивалентно `const(T)`. В сигнатуре `inout` — это шаблон, зависящий от входных данных: изменяемый `haystack` даёт изменяемый указатель, `const haystack``const` указатель, `immutable haystack``immutable` указатель. Одна функция для трёх случаев.
В D также есть параметры `ref`, которые дают ссылку на саму переменную, как показано в коде:
```d
void foo(int a) { a = 10; }
void bar(ref int a) { a = 10; }
int test = 0;
foo(test);
assert(test == 0);
bar(test);
assert(test == 10);
```
В этом примере переменная `test` передаётся в `foo` по значению. Изменения `a` внутри функции не видны снаружи.
> Если бы `a` был указателем, изменения самого `a` не были бы видны, но изменения `*a` — были бы. Поэтому `const` и `immutable` полезны для указателей.
В функции `bar` параметр передаётся по ссылке (`ref`). Изменения a внутри функции видны снаружи: `test` становится равен 10.
> Некоторые гайды советуют передавать структуры по `ref` для оптимизации, даже если изменения не нужны. Я не рекомендую это без профилирования, подтверждающего проблему копирования структур. Также `ref` не позволяет передать литерал структуры, так как нет внешней переменной для обновления. Это ограничивает возможности.
## Нарезка строки для получения подстроки
Строки в D — это просто массив символов. Любые операции с массивами применимы к строкам. Однако, поскольку строка — это массив UTF-8, есть некоторые особенности, которые могут удивить. Здесь мы разберём получение подстроки через срезы и обсудим возможные подводные камни.
### Как это сделать...
Давай попробуем получить подстроку из строки, следуя этим шагам:
1. Объявим строку следующим образом:
```d
string s = "月明かり is some Japanese text.";
```
2. Найдём правильные индексы начала и конца. Извлечём японский текст, найдя первый пробел в строке и сделав срез до этого места с помощью кода:
```d
import std.string;
string japaneseText = s[0 .. s.indexOf(" ")];
```
3. Пройдёмся по строке, рассматривая кодовые единицы UTF-8 и кодовые точки Unicode. Чтобы увидеть разницу в строке, используем следующий код:
```d
import std.stdio;
foreach(idx, char c; japaneseText)
writefln("UTF-8 Code unit at index %d is %d", idx, c);
foreach(dchar c; japaneseText)
writefln("UTF-32 code unit with value %d is %c", c, c);
```
Программа выведет больше кодовых единиц в UTF-8, чем в `dchar`, потому что японский текст состоит из многобайтовых символов, в отличие от английского текста.
### Как это работает...
Строки в D используют Unicode. Это сложный стандарт, достойный отдельной книги, но в D достаточно знать основы. Строки и исходный код в D используют кодировку UTF-8, что позволяет вставлять текст на любом языке в исходный файл и обрабатывать его в коде.
Однако UTF-8 имеет особенность: длина одной кодовой точки переменная. Часто одна кодовая точка — это один символ, но из-за сложности Unicode графемы (видимый символ) могут состоять из нескольких кодовых точек. Для английского текста UTF-8 идеально совпадает с ASCII: одна кодовая единица — один символ. Но в других языках, например японском, символов слишком много для одного байта, и все они в UTF-8 многобайтовые.
Хотя в вашей программе всего четыре символа, срез `s[0 .. 4]` не даст все четыре символа. Оператор среза в D работает с кодовыми единицами (байтами). В результате вы получите частичный результат, который может быть непригоден для использования.
Вместо этого вы нашли правильный индекс с помощью функции `indexOf` из стандартной библиотеки. Она ищет подстроку и возвращает её индекс или -1, если ничего не найдено. Срез `[start .. end]` включает `start`, но исключает `end`. Таким образом, `[0 .. indexOf(...)]` берёт строку от начала до пробела (не включая его). Этот срез безопасен, даже если содержит многобайтовые символы.
Наконец, вы прошли по японскому тексту, чтобы изучить кодировку. Цикл `foreach` понимает UTF-кодировку. Первый вариант запрашивает `char` (кодовые единицы UTF-8) и выдаёт их без декодирования. Второй вариант запрашивает `dchar` — кодовые единицы UTF-32, численно эквивалентные кодовым точкам Unicode. Итерация по `dchar` медленнее, чем по `char`, но упрощает работу с многобайтовыми символами. Второй цикл выводит только одну запись на японский символ или любой другой символ, не кодируемый одной кодовой единицей UTF-8.
### Кое-что еще...
D также поддерживает строки UTF-16 и UTF-32, представленные типами `wstring` и `dstring` соответственно. Рассмотрим их:
- `wstring`: Очень полезен в Windows, так как эта ОС нативно работает с UTF-16.
- `dstring`: Потребляет много памяти — примерно в 4 раза больше, чем `string` для английского текста, — но обходит некоторые упомянутые проблемы. Каждый индекс массива соответствует одной кодовой точке Unicode.
## Создание дерева классов
Классы в D используются для объектно-ориентированного программирования. Чтобы разобраться, как они работают, вы создадите небольшую иерархию наследования для вычисления операций сложения и вычитания.
### Подготовка
Перед написанием класса подумайте, подходит ли он для задачи. Будете ли вы использовать наследование для создания объектов, которые можно подставить вместо родительского? Если нет, лучше использовать struct. Если наследование нужно только для повторного использования кода без подстановки, подойдёт шаблон mixin. Здесь вы используете классы для подстановки и шаблон mixin для частичного повторного использования кода.
### Как это сделать...
Создадим иерархию классов, выполнив следующие шаги:
1. Создайте класс с необходимыми данными и методами. Для вашего вычислителя выражений создайте два класса: `AddExpression` и `SubtractExpression`. Им понадобятся переменные для левой и правой части выражения и метод для вычисления результата.
2. Выделите общие методы из подставляемых классов в интерфейс и укажите, что классы наследуются от него, добавив двоеточие после имени класса и имя интерфейса. В `AddExpression` и `SubtractExpression` есть метод `evaluate`. Вы перенесёте его сигнатуру (без тела) в интерфейс `Expression`.
3. Если остаётся много дублирующего кода, вынесите одинаковый код в шаблон mixin и внедрите его в точке использования.
> Если нужно использовать только часть mixin, можно переопределить отдельные объявления, указав свои ниже оператора `mixin`.
4. Функции должны, по возможности, использовать параметры интерфейса вместо классов для максимальной повторной используемости.
Вот ваш текущий код:
```d
interface Expression {
// Общий метод из созданных классов
int evaluate();
}
mixin template BinaryExpression() {
// Общий код реализации из классов
private int a, b;
this(int left, int right) { this.a = left; this.b = right; }
}
// printResult может вычислять и выводить результат для любого класса выражения
// благодаря использованию общего интерфейса
void printResult(Expression expression) {
import std.stdio;
writeln(expression.evaluate());
}
class AddExpression : Expression { // наследуется от интерфейса
mixin BinaryExpression!(); // добавляет общий код
int evaluate() { return a + b; } // реализация метода
}
class SubtractExpression : Expression {
mixin BinaryExpression!();
int evaluate() { return a - b; }
}
```
5. Добавим класс `BrokenAddExpression`, который использует наследование для переопределения метода `evaluate` из `AddExpression`:
```d
class BrokenAddExpression : AddExpression {
this(int left, int right) {
super(left, right);
}
// Изменяет evaluate так, чтобы вычитать вместо сложения!
// Обратите внимание на ключевое слово override
override int evaluate() { return a - b; }
}
```
6. Наконец, создадим экземпляры и используем их следующим образом:
```d
auto add = new AddExpression(1, 2);
printResult(add);
auto subtract = new SubtractExpression(2, 1);
printResult(subtract); // та же функция, что выше!
```
Использование выведет `3` и `1`, показывая разные операции. Также можно создать экземпляр `BrokenAddExpression` и присвоить его переменной `add`:
```d
add = new BrokenAddExpression(1, 2);
printResult(add); // выведет -1
```
### Как это работает...
Классы в D похожи на классы в Java. Они всегда являются ссылочными типами, поддерживают модель одиночного наследования с корневым объектом и могут реализовывать любое количество интерфейсов.
Конструкторы классов определяются с помощью ключевого слова `this`. При создании нового класса вызывается один из конструкторов. Вы можете определить столько конструкторов, сколько хотите, если каждый имеет уникальный набор параметров.
> Классы могут иметь деструкторы, но их использование обычно не рекомендуется. Когда объект класса собирается сборщиком мусора, его дочерние члены могут уже быть собраны, что делает их недоступными для деструктора. Попытка доступа к ним, скорее всего, приведёт к сбою программы. Кроме того, поскольку сборщик мусора работает в непредсказуемое время (с точки зрения класса), сложно предсказать, будет ли деструктор вызван и когда. Если требуется детерминированное уничтожение, лучше использовать структуру (struct) или обернуть класс в структуру и самостоятельно вызывать деструктор с помощью функции `destroy()`.
Экземпляры объектов автоматически приводятся к базовому типу (upcast). Поэтому вы смогли присвоить `BrokenAddExpression` переменной `add`, которая статически типизирована как `AddExpression`. Это также причина, по которой любой из этих классов можно передать в функцию `printResult`, так как они автоматически приводятся к интерфейсу при необходимости. Однако в обратном направлении, при приведении от интерфейса или базового класса к производному классу, требуется явное приведение. Если приведение не удалось, возвращается `null`. Используйте следующий код для лучшего понимания:
```d
if (auto bae = cast(BrokenAddExpression) expression) {
/* нам передали экземпляр BrokenAddExpression,
теперь можно использовать переменную bae для доступа
к её специфичным членам */
} else {
/* нам передали другой класс */
}
```
В классах D все методы по умолчанию виртуальные. Невиртуальные методы создаются с ключевым словом `final`, которое предотвращает их переопределение в подклассе. Абстрактные функции, обозначенные ключевым словом `abstract`, не обязаны иметь реализацию, но должны быть реализованы в дочернем классе, если объект нужно создать. Все методы в интерфейсе, не помеченные как `final` или `static`, являются абстрактными и должны быть реализованы в неабстрактном классе.
При переопределении виртуальной или абстрактной функции родительского класса необходимо использовать ключевое слово `override`. Если метод с `override` не соответствует ни одному методу родителя, компилятор выдаст ошибку. Это гарантирует совместимость метода дочернего класса с определением родителя, обеспечивая возможность замены родительского класса. (За поведенческую совместимость, конечно, отвечает программист!)
Шаблон mixin — это особенность D, отсутствующая в C++ и Java. Шаблон mixin — это список объявлений, переменных, методов и/или конструкторов. В месте использования применяется следующий код:
```d
mixin BinaryExpression!();
```
Этот код фактически копирует содержимое шаблона в место вызова `mixin`. Шаблон может принимать аргументы, указанные в скобках. В данном случае параметризация не требовалась, поэтому скобки пусты. Шаблоны в D, включая mixin, могут принимать различные аргументы: значения, типы и символы. Подробно шаблоны будут рассмотрены позже в книге.
### Кое-что еще...
Использование интерфейсов и шаблонов mixin, как в данном примере, позволяет достичь результата, схожего с множественным наследованием в C++, но без наследования состояния и без проблемы ромбовидного наследования, характерной для C++.
### Дополнительно
- Рецепт "Симуляция наследования со структурами" в главе 6, "Обёрнутые типы", показывает, как можно реализовать подтипизацию и расширение данных, подобное наследованию с данными, используя структуры.
- Официальная документация находится по адресу https://dlang.org/spec/class.html и содержит дополнительные детали о возможностях классов.

View File

@ -5,3 +5,4 @@
## Содержание ## Содержание
- [Предисловие](00-предисловие) - [Предисловие](00-предисловие)
- [1. Основные задачи](01-основные-задачи)