diff --git a/04-массивы-ассоциативные-массивы-и-строки/README.md b/04-массивы-ассоциативные-массивы-и-строки/README.md index efcda15..77f0e44 100644 --- a/04-массивы-ассоциативные-массивы-и-строки/README.md +++ b/04-массивы-ассоциативные-массивы-и-строки/README.md @@ -28,14 +28,14 @@ - [4.4.5. Удаление элементов](#4-4-5-удаление-элементов) - [4.4.6. Перебор элементов](#4-4-6-перебор-элементов) - [4.4.7. Пользовательские типы](#4-4-7-пользовательские-типы) -- [4.5. Строки]() - - [4.5.1. Кодовые точки]() - - [4.5.2. Кодировки]() - - [4.5.3. Знаковые типы]() - - [4.5.4. Массивы знаков + бонусы = строки]() - - [4.5.4.1. Цикл foreach применительно к строкам]() -- [4.6. Опасный собрат массива – указатель]() -- [4.7. Справочник]() +- [4.5. Строки](#4-5-строки) + - [4.5.1. Кодовые точки](#4-5-1-кодовые-точки) + - [4.5.2. Кодировки](#4-5-2-кодировки) + - [4.5.3. Знаковые типы](#4-5-3-знаковые-типы) + - [4.5.4. Массивы знаков + бонусы = строки](#4-5-4-массивы-знаков--бонусы--строки) + - [4.5.4.1. Цикл foreach применительно к строкам](#4-5-4-1-цикл-foreach-применительно-к-строкам) +- [4.6. Опасный собрат массива – указатель](#4-6-опасный-собрат-массива-–-указатель) +- [4.7. Итоги и справочник](#4-7-итоги-и-справочник) Предыдущие главы лишь косвенно знакомили нас с массивами, ассоциативными массивами и строками (тут выражение, там литерал), пора уже познакомиться с ними по-настоящему. Оперируя только этими тремя типами данных, можно написать много хорошего кода, так что теперь, когда в нашем арсенале уже есть выражения и инструкции, самое время побольше узнать о массивах, ассоциативных массивах и строках. @@ -979,104 +979,31 @@ foreach (k; gammaFunc.byKey()) ## 4.5. Строки -К строкам в D особое отношение. Два решения, принятые на ранней ста -дии развития языка (еще при его определении), оказались выигрышны -ми. Во-первых, в качестве своего стандартного набора знаков D принял -Юникод. (А Юникод сегодня – самый популярный и всеобъемлющий -стандарт определения и представления текстовых данных.) Во-вторых, -D использует кодировки UTF-8, UTF-16 и UTF-32, не отдавая предпоч -тения ни одной из них и не препятствуя использованию в вашем коде -любой другой кодировки. +К строкам в D особое отношение. Два решения, принятые на ранней стадии развития языка (еще при его определении), оказались выигрышными. Во-первых, в качестве своего стандартного набора знаков D принял Юникод. (А Юникод сегодня – самый популярный и всеобъемлющий стандарт определения и представления текстовых данных.) Во-вторых, D использует кодировки UTF-8, UTF-16 и UTF-32, не отдавая предпочтения ни одной из них и не препятствуя использованию в вашем коде любой другой кодировки. -Чтобы понять, как D работает с текстом, нужно кое-что знать о Юнико -де и UTF. Если хотите изучить эти предметы в полном объеме, книга -«Unicode Explained» послужит вам полезным источником инфор -мации, а документированный стандарт «Консорциума Юникода» – -сейчас в пятом издании, что соответствует версии 5.1 стандарта Юни -код, – самая полная и точная справка по стандарту. +Чтобы понять, как D работает с текстом, нужно кое-что знать о Юникоде и UTF. Если хотите изучить эти предметы в полном объеме, книга «Unicode Explained» послужит вам полезным источником информации, а документированный стандарт «Консорциума Юникода» – сейчас в пятом издании, что соответствует версии 5.1 стандарта Юникод, – самая полная и точная справка по стандарту. + +[В начало ⮍](#4-5-строки) [Наверх ⮍](#4-массивы-ассоциативные-массивы-и-строки) ### 4.5.1. Кодовые точки -Нужно пояснить: Юникод различает «абстрактный знак», или *кодовую точку* (*code point*), и его представление, или *кодировку* (*encoding*). Об -этой тонкости мало кто знает, отчасти потому, что в стандарте ASCII нет -такого отдельного представления. Старый добрый стандарт ASCII каж- -дому знаку, часто встречающемуся в англоязычных текстах, (и каждо- -му из немногих «управляющих кодов») ставит в соответствие число -в диапазоне от 0 до 127, то есть 7 бит. Когда был предложен стандарт -ASCII, большинство компьютеров уже использовали 8-битный байт (ок- -тет) в качестве адресуемой единицы, и вопрос о «кодировании» ASCII- -текста не стоял. (Оставшийся бит оставлял простор для творчества, что -закончилось «Кембрийским взрывом»[^5] взаимно несовместимых расши- -рений.) +Нужно пояснить: Юникод различает «абстрактный знак», или *кодовую точку* (*code point*), и его представление, или *кодировку* (*encoding*). Об этой тонкости мало кто знает, отчасти потому, что в стандарте ASCII нет такого отдельного представления. Старый добрый стандарт ASCII каждому знаку, часто встречающемуся в англоязычных текстах, (и каждому из немногих «управляющих кодов») ставит в соответствие число в диапазоне от 0 до 127, то есть 7 бит. Когда был предложен стандарт ASCII, большинство компьютеров уже использовали 8-битный байт (октет) в качестве адресуемой единицы, и вопрос о «кодировании» ASCII-текста не стоял. (Оставшийся бит оставлял простор для творчества, что закончилось «Кембрийским взрывом»[^5] взаимно несовместимых расширений.) -Юникод же, напротив, сначала определяет кодовые точки, то есть, по -просту говоря, числа, поставленные в соответствие абстрактным зна -кам. Абстрактный знак «A» получает номер 65, абстрактный знак ¤ – -номер 8364 и т. д. Принятие решений о том, какие знаки заслуживают -быть включенными в таблицу знаков Юникода и как присваивать им -номера, – одно из важных дел, которыми занимается организация «Кон -сорциум Юникода». И это здорово, потому что все могут использовать -установленное ею соответствие между абстрактными знаками и чис -лами, не беспокоясь о таких мелочах, как его определение и докумен -тирование. +Юникод же, напротив, сначала определяет кодовые точки, то есть, попросту говоря, числа, поставленные в соответствие абстрактным знакам. Абстрактный знак A получает номер 65, абстрактный знак € – номер 8364 и т. д. Принятие решений о том, какие знаки заслуживают быть включенными в таблицу знаков Юникода и как присваивать им номера, – одно из важных дел, которыми занимается организация «Консорциум Юникода». И это здорово, потому что все могут использовать установленное ею соответствие между абстрактными знаками и числами, не беспокоясь о таких мелочах, как его определение и документирование. -По версии стандарта Юникод 5.1 кодовые точки Юникод находятся -в диапазоне от 0 до 1 114 111 (верхний предел гораздо чаще приводят -в шестнадцатеричном представлении: `0x10FFFF`, или `U+10FFFF` – в особой -юникодовской форме записи). Возможно, обычное заблуждение о том, -что двумя байтами можно представить любой из знаков таблицы Юни -кода, столь распространено из-за того, что некоторые языки приняли -в качестве стандарта двухбайтное представление знаков (что, в свою -очередь, стало следствием именно такого представления в более ранних -версиях стандарта Юникод). На самом деле, число знаков Юникод ров -но в 17 раз превышает 65 536 (максимальное число, доступное для двух -байтного представления). (По правде говоря, кодовые точки с большими значениями практически не используются, а многие из них вообще по -ка не имеют представления.) +По версии стандарта Юникод 5.1 кодовые точки Юникод находятся в диапазоне от 0 до 1 114 111 (верхний предел гораздо чаще приводят в шестнадцатеричном представлении: `0x10FFFF`, или `U+10FFFF` – в особой юникодовской форме записи). Возможно, обычное заблуждение о том, что двумя байтами можно представить любой из знаков таблицы Юникода, столь распространено из-за того, что некоторые языки приняли в качестве стандарта двухбайтное представление знаков (что, в свою очередь, стало следствием именно такого представления в более ранних версиях стандарта Юникод). На самом деле, число знаков Юникод ровно в 17 раз превышает 65 536 (максимальное число, доступное для двух байтного представления). (По правде говоря, кодовые точки с большими значениями практически не используются, а многие из них вообще пока не имеют представления.) -В любом случае, когда дело касается кодовых точек, можно не думать -об их представлении. Отвлеченно можно считать кодовые точки огром -ной таблицей значений функции, ставящей в соответствие целым чис -лам от 0 до 1 114 111 абстрактные знаки. Порядок назначения номеров -из этого диапазона имеет множество нюансов, но это не умаляет пра -вильности нашего высокоуровневого описания. О конкретном пред -ставлении кодовой точки из таблицы Юникод в виде последовательно -сти байтов позаботится *кодировка*. +В любом случае, когда дело касается кодовых точек, можно не думать об их представлении. Отвлеченно можно считать кодовые точки огромной таблицей значений функции, ставящей в соответствие целым числам от 0 до 1 114 111 абстрактные знаки. Порядок назначения номеров из этого диапазона имеет множество нюансов, но это не умаляет правильности нашего высокоуровневого описания. О конкретном представлении кодовой точки из таблицы Юникод в виде последовательности байтов позаботится *кодировка*. + +[В начало ⮍](#4-5-1-кодовые-точки) [Наверх ⮍](#4-массивы-ассоциативные-массивы-и-строки) ### 4.5.2. Кодировки -Если бы Юникод не задумываясь последовал общему подходу ASCII, он -бы просто расширил верхнюю границу `0x10FFFF` до следующего байта, -чтобы каждая кодовая точка представлялась бы тремя байтами. Но -у такого решения есть определенный недостаток. В большинстве тек -стов на английском или другом языке с основанным на латинице алфа -витом была бы задействована статистически очень малая часть от обще -го количества кодовых точек (чисел), то есть память тратилась бы пона -прасну. Размер обычных текстов на латинице просто-напросто вырос -бы втрое. Алфавиты с большим количеством знаков (такие как азиат -ские системы письменности) нашли бы трем байтам лучшее примене -ние, и это нормально, ведь в целом в тексте было бы меньше знаков (но -каждый знак был бы более информативен). +Если бы Юникод не задумываясь последовал общему подходу ASCII, он бы просто расширил верхнюю границу `0x10FFFF` до следующего байта, чтобы каждая кодовая точка представлялась бы тремя байтами. Но у такого решения есть определенный недостаток. В большинстве текстов на английском или другом языке с основанным на латинице алфавитом была бы задействована статистически очень малая часть от общего количества кодовых точек (чисел), то есть память тратилась бы понапрасну. Размер обычных текстов на латинице просто-напросто вырос бы втрое. Алфавиты с большим количеством знаков (такие как азиатские системы письменности) нашли бы трем байтам лучшее применение, и это нормально, ведь в целом в тексте было бы меньше знаков (но каждый знак был бы более информативен). -Чтобы не занимать лишнее место, Юникод принял несколько схем ко -дирования с *переменной длиной* представления знаков. Такие схемы ис -пользуют один или несколько «более узких» кодов для представления -всего диапазона кодовых точек Юникода. Узкие коды (обычно 8- или -16-битные) называются *кодовыми единицами* (*code units*). Каждая ко -довая точка представляется одной или несколькими кодовыми едини -цами. +Чтобы не занимать лишнее место, Юникод принял несколько схем кодирования с *переменной длиной* представления знаков. Такие схемы используют один или несколько «более узких» кодов для представления всего диапазона кодовых точек Юникода. Узкие коды (обычно 8- или 16-битные) называются *кодовыми единицами* (*code units*). Каждая кодовая точка представляется одной или несколькими кодовыми единицами. -Первой стандартизированной кодировкой, работающей по этому прин -ципу, стала кодировка UTF-8. UTF-8, которую Кен Томпсон придумал -однажды вечером в небольшом ресторанчике в Нью-Джерси, – поч -ти образцовый пример оригинального и надежного решения. Основная -идея UTF-8: использовать для кодирования любого заданного знака от -1 до 6 байт; добавлять управляющие биты, по которым можно будет -различать представления знаков разной длины. Представления пер -вых 127 кодовых точек в кодировке UTF-8 идентичны представлениям -в ASCII. То есть все ASCII-тексты автоматически становятся коррект -ными с точки зрения UTF-8, что само по себе блестящий ход. Для кодо -вых точек, не входящих в диапазон ASCII, UTF-8 использует представ -ления разной длины (табл. 4.2). +Первой стандартизированной кодировкой, работающей по этому принципу, стала кодировка UTF-8. UTF-8, которую Кен Томпсон придумал однажды вечером в небольшом ресторанчике в Нью-Джерси, – почти образцовый пример оригинального и надежного решения. Основная идея UTF-8: использовать для кодирования любого заданного знака от 1 до 6 байт; добавлять управляющие биты, по которым можно будет различать представления знаков разной длины. Представления первых 127 кодовых точек в кодировке UTF-8 идентичны представлениям в ASCII. То есть все ASCII-тексты автоматически становятся корректными с точки зрения UTF-8, что само по себе блестящий ход. Для кодовых точек, не входящих в диапазон ASCII, UTF-8 использует представления разной длины (табл. 4.2). *Таблица 4.2. Битовые представления UTF-8. Длина представления определяется по контрольным битам, что позволяет выполнять синхронизацию посреди потока, восстановление после ошибок и просмотр строки в обратном направлении* @@ -1089,119 +1016,39 @@ ASCII, большинство компьютеров уже использова |`00200000–03FFFFFF`|`111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx`| |`04000000–7FFFFFFF`|`1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx`| -Поскольку на сегодня верхней границей диапазона кодовых точек Юни -код является число `0x10FFFF`, две последние последовательности зарезер -вированы для использования в будущем; в настоящее время корректны -только четырехбайтные представления. +Поскольку на сегодня верхней границей диапазона кодовых точек Юникод является число `0x10FFFF`, две последние последовательности зарезервированы для использования в будущем; в настоящее время корректны только четырехбайтные представления. -Выбранные последовательности управляющих битов обладают двумя -любопытными свойствами: -1. Первый байт представления всегда отличается от остальных его бай -тов. +Выбранные последовательности управляющих битов обладают двумя любопытными свойствами: +1. Первый байт представления всегда отличается от остальных его байтов. 2. Первый байт однозначно определяет длину представления. -Первое свойство является ключевым, так как находит два важных при -менения. Первое – простая синхронизация: начав принимать переда -ваемую информацию в кодировке UTF-8, прямо посреди потока легко -можно выяснить, где начинается представление следующей кодовой -точки (просто найдите следующий байт, начинающийся не с 10). Вто -рое применение этого свойства – просмотр в обратном направлении: по -строке UTF-8 можно легко перемещаться от конца к началу, не сбива -ясь. Возможность просматривать строки UTF-8 в обратном направле -нии позволяет организовать множество алгоритмов (например, эффек -тивный поиск последнего вхождения одной строки в другую). Второе -свойство последовательностей управляющих битов не столь значимо, -но оно упрощает и ускоряет обработку строк. +Первое свойство является ключевым, так как находит два важных применения. Первое – простая синхронизация: начав принимать передаваемую информацию в кодировке UTF-8, прямо посреди потока легко можно выяснить, где начинается представление следующей кодовой точки (просто найдите следующий байт, начинающийся не с 10). Второе применение этого свойства – просмотр в обратном направлении: по строке UTF-8 можно легко перемещаться от конца к началу, не сбиваясь. Возможность просматривать строки UTF-8 в обратном направлении позволяет организовать множество алгоритмов (например, эффективный поиск последнего вхождения одной строки в другую). Второе свойство последовательностей управляющих битов не столь значимо, но оно упрощает и ускоряет обработку строк. -В идеале часто встречающиеся кодовые точки должны иметь малые -представления, а редко встречающиеся – большие. При таких услови -ях кодировка UTF-8 работает как хороший статистический кодиров -щик, обозначая более часто встречающиеся знаки меньшим количест -вом битов. Это удобно для языков с алфавитом на основе латиницы, где -для большинства букв достаточно одного байта, а для редких букв с ди -акритическими знаками – двух. +В идеале часто встречающиеся кодовые точки должны иметь малые представления, а редко встречающиеся – большие. При таких условиях кодировка UTF-8 работает как хороший статистический кодировщик, обозначая более часто встречающиеся знаки меньшим количеством битов. Это удобно для языков с алфавитом на основе латиницы, где для большинства букв достаточно одного байта, а для редких букв с диакритическими знаками – двух. -UTF-16 – тоже кодировка с переменной длиной, но в ней применяется -другой (пожалуй, менее элегантный) подход к кодированию. Кодовые -точки со значениями в диапазоне от `0` до `0xFFFF` кодируются одной 16-бит -ной кодовой единицей, а кодовые точки со значениями в диапазоне от -`0x10000` до `0x10FFFF` представляются *суррогатными парами*, то есть дву -мя кодовыми единицами, первая из которых находится в диапазоне от -`0xD800` до `0xDBFF`, а вторая – в диапазоне от `0xDC00` до `0xDFFF`. Ради этой ко -дировки Юникод отказался от отображения кодовых точек на значения -в диапазоне `0xD800–0xDBFF`. Диапазоны значений первой и второй кодо -вых единиц называются *верхней суррогатной зоной* (*high surrogate area*) -и *нижней суррогатной зоной* (*low surrogate area*) соответственно. +UTF-16 – тоже кодировка с переменной длиной, но в ней применяется другой (пожалуй, менее элегантный) подход к кодированию. Кодовые точки со значениями в диапазоне от `0` до `0xFFFF` кодируются одной 16-битной кодовой единицей, а кодовые точки со значениями в диапазоне от `0x10000` до `0x10FFFF` представляются *суррогатными парами*, то есть двумя кодовыми единицами, первая из которых находится в диапазоне от `0xD800` до `0xDBFF`, а вторая – в диапазоне от `0xDC00` до `0xDFFF`. Ради этой кодировки Юникод отказался от отображения кодовых точек на значения в диапазоне `0xD800–0xDBFF`. Диапазоны значений первой и второй кодовых единиц называются *верхней суррогатной зоной* (*high surrogate area*) и *нижней суррогатной зоной* (*low surrogate area*) соответственно. -Обычно UTF-16 критикуют за то, что в этой кодировке статистически -редкие случаи также становятся наиболее сложными в обработке и тре -буют самого тщательного рассмотрения. К сожалению, не все, но боль -шинство знаков Юникода – так называемая базовая многоязыковая -плоскость (Basic Multilingual Plane, BMP) – действительно могут быть -закодированы единственной кодовой единицей кодировки UTF-16, по -этому множество программ, работающих с UTF-16, автоматически при -нимают одну кодовую единицу за представление одного знака, отказы -ваясь от проверок на наличие суррогатных пар в пользу эффективности. -Еще больше усугубляет путаницу то, что некоторые языки изначально -выбрали поддержку предшественницы UTF-16 – кодировки UCS-2 (в ко -торой одной кодовой точке соответствуют ровно 16 бит), а позже реши -ли добавить поддержку UTF-16, что осложнило использование старого -кода, полагающегося на соответствие между знаками и их кодовыми -единицами вида один-к-одному. +Обычно UTF-16 критикуют за то, что в этой кодировке статистически редкие случаи также становятся наиболее сложными в обработке и требуют самого тщательного рассмотрения. К сожалению, не все, но большинство знаков Юникода – так называемая базовая многоязыковая плоскость (Basic Multilingual Plane, BMP) – действительно могут быть закодированы единственной кодовой единицей кодировки UTF-16, поэтому множество программ, работающих с UTF-16, автоматически принимают одну кодовую единицу за представление одного знака, отказываясь от проверок на наличие суррогатных пар в пользу эффективности. Еще больше усугубляет путаницу то, что некоторые языки изначально выбрали поддержку предшественницы UTF-16 – кодировки UCS-2 (в которой одной кодовой точке соответствуют ровно 16 бит), а позже решили добавить поддержку UTF-16, что осложнило использование старого кода, полагающегося на соответствие между знаками и их кодовыми единицами вида один-к-одному. -Наконец, кодировка UTF-32 использует 32 бита для одной кодовой еди -ницы. Это означает, что в кодировке UTF-32 принято самое простое -и легкое в использовании, но в то же время самое «прожорливое» пред -ставление кодовых точек. Обычно рекомендуют придерживаться сле -дующей политики: кодировку UTF-8 использовать для хранения, а к ко -дировке UTF-32 обращаться лишь временно, во время обработки, и толь -ко при необходимости. +Наконец, кодировка UTF-32 использует 32 бита для одной кодовой единицы. Это означает, что в кодировке UTF-32 принято самое простое и легкое в использовании, но в то же время самое «прожорливое» представление кодовых точек. Обычно рекомендуют придерживаться следующей политики: кодировку UTF-8 использовать для хранения, а к кодировке UTF-32 обращаться лишь временно, во время обработки, и только при необходимости. + +[В начало ⮍](#4-5-2-кодировки) [Наверх ⮍](#4-массивы-ассоциативные-массивы-и-строки) ### 4.5.3. Знаковые типы -В языке D определено три знаковых типа: `char`, `wchar` и `dchar`, обозначаю -щие кодовые единицы кодировок UTF-8, UTF-16 и UTF-32 соответствен -но. В качестве значений свойства `.init` этих типов намеренно выбраны -некорректные значения: `char.init` равно `0xFF`, `wchar.init` – `0xFFFF`, а `dchar.init` – `0x0000FFFF`. +В языке D определено три знаковых типа: `char`, `wchar` и `dchar`, обозначающие кодовые единицы кодировок UTF-8, UTF-16 и UTF-32 соответственно. В качестве значений свойства `.init` этих типов намеренно выбраны некорректные значения: `char.init` равно `0xFF`, `wchar.init` – `0xFFFF`, а `dchar.init` – `0x0000FFFF`. -Из табл. 4.2 видно, что константа `0xFF` не может быть частью корректно -го битового представления знака в кодировке UTF-8, а значению `0xFFFF` -Юникод намеренно не ставит в соответствие никакую кодовую точку. +Из табл. 4.2 видно, что константа `0xFF` не может быть частью корректного битового представления знака в кодировке UTF-8, а значению `0xFFFF` Юникод намеренно не ставит в соответствие никакую кодовую точку. -Используемые по отдельности, значения этих трех знаковых типов ве -дут себя в основном как целые числа без знака и иногда могут использо -ваться для хранения некорректных UTF-представлений кодовых точек -(компилятор не заботится о том, чтобы везде использовались коррект -ные представления кодовых точек), но изначально задуманное назначе -ние типов `char`, `wchar` и `dchar` – служить UTF-представлениями кодовых -точек. А для работы с 8-, 16- и 32-битными целыми числами без знака -и для представления кодировок, не входящих в группу UTF, лучше все -го использовать типы `ubyte`, `ushort` и `uint` соответственно. Например, -для работы с применявшимися до появления Юникода 8-битными ко -довыми страницами вы можете взять за основу значения типа `ubyte`, -а не `char`. +Используемые по отдельности, значения этих трех знаковых типов ведут себя в основном как целые числа без знака и иногда могут использоваться для хранения некорректных UTF-представлений кодовых точек (компилятор не заботится о том, чтобы везде использовались корректные представления кодовых точек), но изначально задуманное назначение типов `char`, `wchar` и `dchar` – служить UTF-представлениями кодовых точек. А для работы с 8-, 16- и 32-битными целыми числами без знака и для представления кодировок, не входящих в группу UTF, лучше всего использовать типы `ubyte`, `ushort` и `uint` соответственно. Например, для работы с применявшимися до появления Юникода 8-битными кодовыми страницами вы можете взять за основу значения типа `ubyte`, а не `char`. + +[В начало ⮍](#4-5-3-знаковые-типы) [Наверх ⮍](#4-массивы-ассоциативные-массивы-и-строки) ### 4.5.4. Массивы знаков + бонусы = строки -Сформированный массив любого знакового типа – такого как `char[]`, -`wchar[]` или `dchar[]` – компилятор и библиотека средств поддержки вре -мени исполнения считают строками Юникода в одной из UTF-кодиро -вок. Следовательно, массивы знаков сочетают в себе мощь и гибкость, -свойственные массивам, и некоторые дополнительные преимущества, -предоставляемые Юникодом. +Сформированный массив любого знакового типа – такого как `char[]`, `wchar[]` или `dchar[]` – компилятор и библиотека средств поддержки времени исполнения считают строками Юникода в одной из UTF-кодировок. Следовательно, массивы знаков сочетают в себе мощь и гибкость, свойственные массивам, и некоторые дополнительные преимущества, предоставляемые Юникодом. -На самом деле, в D уже определены три типа строк, соответствующие -трем размерам представления знаков: `string`, `wstring` и `dstring`. Это не -особые типы, а всего лишь псевдонимы массивов знаковых типов с од -ним отличием: знаковый тип снабжен квалификатором `immutable`, за -прещающим произвольное изменение отдельных знаков в строке. На -пример, тип `string` – более короткий синоним для типа `immutable(char)[]`. -Подробное обсуждение квалификаторов типов (в том числе `immutable`) -мы отложим до главы 8, но для строк в любой кодировке действие `immutable` объясняется очень просто: свойства значения типа `string`, также -известного как `immutable(char)[]`, идентичны свойствам значения типа -`char[]` (а свойства значения типа `immutable(wchar)[]` – свойствам значения -типа `wchar[]`), за исключением маленького отличия: нельзя присвоить -новое значение отдельному знаку строки: +На самом деле, в D уже определены три типа строк, соответствующие трем размерам представления знаков: `string`, `wstring` и `dstring`. Это не особые типы, а всего лишь псевдонимы массивов знаковых типов с одним отличием: знаковый тип снабжен квалификатором `immutable`, запрещающим произвольное изменение отдельных знаков в строке. Например, тип `string` – более короткий синоним для типа `immutable(char)[]`. Подробное обсуждение квалификаторов типов (в том числе `immutable`) мы отложим до главы 8, но для строк в любой кодировке действие `immutable` объясняется очень просто: свойства значения типа `string`, также известного как `immutable(char)[]`, идентичны свойствам значения типа `char[]` (а свойства значения типа `immutable(wchar)[]` – свойствам значения типа `wchar[]`), за исключением маленького отличия: нельзя присвоить новое значение отдельному знаку строки: ```d string a = "hello"; @@ -1209,23 +1056,14 @@ char h = a[0]; // Все в порядке a[0] = 'H'; // Ошибка! Присваивать типу immutable(char) запрещено! ``` -Чтобы изменить в строке какой-то конкретный знак, требуется создать -новое значение типа `string`, применив конкатенацию: +Чтобы изменить в строке какой-то конкретный знак, требуется создать новое значение типа `string`, применив конкатенацию: ```d string a = "hello"; a = 'H' ~ a[1 .. $]; // Все в порядке, делает выражение a == "Hello" истинным ``` -Почему было принято такое решение? В конце концов в приведенном -выше примере совершенно бессмысленно выделять новую область памя -ти под целую строку (вспомните, в разделе 4.1.6 говорилось, что опера -тор `~` всегда требует выделения новой области памяти под новый массив), -вместо того чтобы просто изменить уже имеющуюся строку. В пользу -квалификатора `immutable` говорит то, что его наличие упрощает ситуа -ции, когда объект типа `string`, `wstring` или `dstring` копируется, а потом -изменяется. Квалификатор гарантирует отсутствие лишних ссылок на -одну и ту же строку. Например: +Почему было принято такое решение? В конце концов в приведенном выше примере совершенно бессмысленно выделять новую область памяти под целую строку (вспомните, в разделе 4.1.6 говорилось, что оператор `~` всегда требует выделения новой области памяти под новый массив), вместо того чтобы просто изменить уже имеющуюся строку. В пользу квалификатора `immutable` говорит то, что его наличие упрощает ситуации, когда объект типа `string`, `wstring` или `dstring` копируется, а потом изменяется. Квалификатор гарантирует отсутствие лишних ссылок на одну и ту же строку. Например: ```d string a = "hello"; @@ -1237,51 +1075,23 @@ a = 'H' ~ a[1 .. $]; assert(a == "Hello" && b == "hello" && c == "hell"); ``` -Неизменяемость отдельных знаков позволяет работать с несколькими -переменными, ссылающимися на одну и ту же строку, не боясь, что из -менение одной из них отразится и на других. Копировать строковые -объекты очень дешево, поскольку не реализуется никакая особая стра -тегия копирования (например, раннее копирование или копирование -при записи). +Неизменяемость отдельных знаков позволяет работать с несколькими переменными, ссылающимися на одну и ту же строку, не боясь, что изменение одной из них отразится и на других. Копировать строковые объекты очень дешево, поскольку не реализуется никакая особая стратегия копирования (например, раннее копирование или копирование при записи). -Не менее весомая причина запретить изменения в строках на уровне ко -довых единиц – такие изменения все равно лишены смысла. Элементы -`string` имеют разную длину, а в большинстве случаев требуется заменить -логические знаки (кодовые точки), а не физические (кодовые единицы), -поэтому желание проводить хирургические операции над отдельными -знаками возникает редко. Гораздо легче записать правильный UTF-код, -отказавшись от присваивания отдельным знакам, но уделив больше -внимания работе с целыми строками и их фрагментами. Стандартная -библиотека D задает тон, поддерживая работу со строками как с едины -ми сущностями (а не с индексами и отдельными знаками). Тем не менее -писать UTF-код не так легко; например, в предыдущем примере в кон -катенации `'H' ~ a[1 .. $]` допущена ошибка: эта запись предполагает, что -первая кодовая точка занимает ровно один байт. Правильное решение -выглядит так: +Не менее весомая причина запретить изменения в строках на уровне кодовых единиц – такие изменения все равно лишены смысла. Элементы `string` имеют разную длину, а в большинстве случаев требуется заменить логические знаки (кодовые точки), а не физические (кодовые единицы), поэтому желание проводить хирургические операции над отдельными знаками возникает редко. Гораздо легче записать правильный UTF-код, отказавшись от присваивания отдельным знакам, но уделив больше внимания работе с целыми строками и их фрагментами. Стандартная библиотека D задает тон, поддерживая работу со строками как с едиными сущностями (а не с индексами и отдельными знаками). Тем не менее писать UTF-код не так легко; например, в предыдущем примере в конкатенации `'H' ~ a[1 .. $]` допущена ошибка: эта запись предполагает, что первая кодовая точка занимает ровно один байт. Правильное решение выглядит так: ```d a = 'H' ~ a[stride(a, 0) .. $]; ``` -Функция `stride` из модуля `std.utf` стандартной библиотеки возвращает -длину кода знака в указанной позиции строки. Для доступа к функции -`stride` и другому полезному содержимому библиотеки вставьте где-ни -будь ближе к началу программы строку: +Функция `stride` из модуля `std.utf` стандартной библиотеки возвращает длину кода знака в указанной позиции строки. Для доступа к функции `stride` и другому полезному содержимому библиотеки вставьте где-нибудь ближе к началу программы строку: ```d import std.utf; ``` -В нашем случае вызов `stride(a, 0)` возвращает количество байт двоич -ного представления первого знака (кодовой точки) в строке `a`. Именно -это число мы используем при получении среза, помечая начало второго -знака. +В нашем случае вызов `stride(a, 0)` возвращает количество байт двоичного представления первого знака (кодовой точки) в строке `a`. Именно это число мы используем при получении среза, помечая начало второго знака. -Наглядный пример поддержки Юникода языком можно обнаружить -в строковых литералах, с которыми мы уже успели познакомиться (см. -раздел 2.2.5). Строковые литералы D «понимают» кодовые точки из -таблицы Юникод и автоматически кодируют их в соответствии с любой -выбранной вами кодировкой. Например: +Наглядный пример поддержки Юникода языком можно обнаружить в строковых литералах, с которыми мы уже успели познакомиться (см. раздел 2.2.5). Строковые литералы D «понимают» кодовые точки из таблицы Юникод и автоматически кодируют их в соответствии с любой выбранной вами кодировкой. Например: ```d import std.stdio; @@ -1295,25 +1105,15 @@ void main() } ``` -Несмотря на то что внутренние представления строк `a`, `b` и `c` сильно от -личаются друг от друга, вам не нужно об этом беспокоиться, потому что -вы задаете литерал в абстрактном виде, используя кодовые точки. Ком -пилятор заботится обо всех тонкостях кодирования, так что в итоге -программа печатает три строки с одним и тем же текстом: +Несмотря на то что внутренние представления строк `a`, `b` и `c` сильно отличаются друг от друга, вам не нужно об этом беспокоиться, потому что вы задаете литерал в абстрактном виде, используя кодовые точки. Компилятор заботится обо всех тонкостях кодирования, так что в итоге программа печатает три строки с одним и тем же текстом: ```d Независимо от представления λ стоит €=20. ``` -Кодировка литерала определяется контекстом, в котором этот литерал -используется. В предыдущем примере компилятор преобразует строко -вый литерал, без какой-либо обработки во время исполнения програм -мы, из кодировки UTF-8 в кодировку UTF-16, а потом в кодировку -UTF-32 (соответствующие типам `string`, `wstring` и `dstring`), хотя написа -ние литералов во всех трех случаях одинаково. Если требуемая кодиров -ка литерала не может быть однозначно определена, добавьте к нему суф -фикс `c`, `w` или `d` (например, `"как_здесь"d`): строка будет преобразована в ко -дировку UTF-8, UTF-16 или UTF-32 соответственно (см. раздел 2.2.5.2). +Кодировка литерала определяется контекстом, в котором этот литерал используется. В предыдущем примере компилятор преобразует строковый литерал, без какой-либо обработки во время исполнения программы, из кодировки UTF-8 в кодировку UTF-16, а потом в кодировку UTF-32 (соответствующие типам `string`, `wstring` и `dstring`), хотя написание литералов во всех трех случаях одинаково. Если требуемая кодировка литерала не может быть однозначно определена, добавьте к нему суффикс `c`, `w` или `d` (например, `"как_здесь"d`): строка будет преобразована в кодировку UTF-8, UTF-16 или UTF-32 соответственно (см. раздел 2.2.5.2). + +[В начало ⮍](#4-5-4-массивы-знаков--бонусы--строки) [Наверх ⮍](#4-массивы-ассоциативные-массивы-и-строки) #### 4.5.4.1. Цикл foreach применительно к строкам @@ -1326,12 +1126,7 @@ foreach (c; str) } ``` -то переменная `c` поочередно примет значение каждой из *кодовых единиц* строки `str`. Например, если `str` – массив элементов типа `char` (с ква -лификатором `immutable` или без), то переменной c присваивается тип -`char`. Это ожидаемо, если вспомнить, как ведет себя цикл просмотра -с массивами, но иногда для строк такое поведение нежелательно. На -пример, напечатаем знаки строки типа `string`, заключив каждый из -них в квадратные скобки. +то переменная `c` поочередно примет значение каждой из *кодовых единиц* строки `str`. Например, если `str` – массив элементов типа `char` (с квалификатором `immutable` или без), то переменной c присваивается тип `char`. Это ожидаемо, если вспомнить, как ведет себя цикл просмотра с массивами, но иногда для строк такое поведение нежелательно. Например, напечатаем знаки строки типа `string`, заключив каждый из них в квадратные скобки. ```d void main() @@ -1351,11 +1146,9 @@ void main() [H][a][l][l][�][�][,][ ][V][�][�][r][l][d][!] ``` -Негатив знака `�` (может отличаться в зависимости от операционной сис -темы и используемого шрифта) – это немой протест консоли против отображения некорректного UTF-кода. Разумеется, попытка напечатать отдельный элемент типа `char`, обретающий смысл только в сочетании с другими элементами типа `char`, обречена на провал. +Негатив знака `�` (может отличаться в зависимости от операционной системы и используемого шрифта) – это немой протест консоли против отображения некорректного UTF-кода. Разумеется, попытка напечатать отдельный элемент типа `char`, обретающий смысл только в сочетании с другими элементами типа `char`, обречена на провал. -Но самое интересное начинается, если вы укажете для `c` другой знако -вый тип. Например, назначим переменной `c` тип `dchar`: +Но самое интересное начинается, если вы укажете для `c` другой знаковый тип. Например, назначим переменной `c` тип `dchar`: ```d ...тот же самый код, добавлен только тип "dchar"... @@ -1365,9 +1158,7 @@ foreach (dchar c; str) } ``` -В этом случае компилятор автоматически вставляет код для перекоди -ровки «на лету» каждой кодовой единицы в `str` в представление, дик -туемое типом переменной `c`. Наш цикл напечатает: +В этом случае компилятор автоматически вставляет код для перекодировки «на лету» каждой кодовой единицы в `str` в представление, диктуемое типом переменной `c`. Наш цикл напечатает: ```sh [H][a][l][l][å][,][ ][V][ä][r][l][d][!] @@ -1377,21 +1168,13 @@ foreach (dchar c; str) В рассмотренном примере в инструкции `foreach` выполнялось перекодирование в направлении от «узкого» к более «широкому» представлению, но обратное преобразование также возможно. Например, можно начать со значения типа `dstring`, а затем просмотреть его по одному (закодированному) знаку типа `char`. +[В начало ⮍](#4-5-4-1-цикл-foreach-применительно-к-строкам) [Наверх ⮍](#4-массивы-ассоциативные-массивы-и-строки) + ## 4.6. Опасный собрат массива – указатель -Объект массива отслеживает в памяти группу типизированных объек -тов, сохраняя адреса ее верхней и нижней границ. Указатель – «наполо -вину массив»: он позволяет отслеживать только один объект. Поэтому -указатель не знает, где начинается и заканчивается группа объектов. -Если вы получите эту информацию откуда-то извне, то сможете исполь -зовать ее для организации перемещения указателя, заставляя его ука -зывать на соседние элементы. +Объект массива отслеживает в памяти группу типизированных объектов, сохраняя адреса ее верхней и нижней границ. Указатель – «наполовину массив»: он позволяет отслеживать только один объект. Поэтому указатель не знает, где начинается и заканчивается группа объектов. Если вы получите эту информацию откуда-то извне, то сможете использовать ее для организации перемещения указателя, заставляя его указывать на соседние элементы. -Указатель на объект типа `T` обозначается как тип `T*` и по умолчанию -имеет значение `null` (то есть указывает «в никуда»). Направить указа -тель на объект можно с помощью оператора получения адреса `&`, а ис -пользовать этот объект – с помощью оператора разыменования `*` (см. -раздел 2.3.6.2). Например: +Указатель на объект типа `T` обозначается как тип `T*` и по умолчанию имеет значение `null` (то есть указывает «в никуда»). Направить указатель на объект можно с помощью оператора получения адреса `&`, а использовать этот объект – с помощью оператора разыменования `*` (см. раздел 2.3.6.2). Например: ```d int x = 42; @@ -1401,22 +1184,9 @@ int* p = &x; // Получить адрес x assert(x == 11); // Переменная x была изменена с помощью указателя p ``` -Указатели могут участвовать в арифметических операциях, что делает -чрезвычайно заманчивым их применение в качестве курсоров внутри -массивов. Если увеличить указатель на единицу, он будет указывать на -следующий элемент массива, если уменьшить на единицу – на предыду -щий элемент. Прибавив к указателю целое число `n`, получим указатель -на объект, отстоящий от элемента, на который указывал исходный ука -затель, на `n` позиций вправо, если `n` больше нуля, и влево, если `n` меньше -нуля. Ради упрощения операции индексирования выражение `p[n]` экви -валентно выражению `*(p + n)`. Наконец, разница между двумя указате -лями `p2 - p1` соответствует такому целому числу `n`, что `p1 + n == p2`. +Указатели могут участвовать в арифметических операциях, что делает чрезвычайно заманчивым их применение в качестве курсоров внутри массивов. Если увеличить указатель на единицу, он будет указывать на следующий элемент массива, если уменьшить на единицу – на предыдущий элемент. Прибавив к указателю целое число `n`, получим указатель на объект, отстоящий от элемента, на который указывал исходный указатель, на `n` позиций вправо, если `n` больше нуля, и влево, если `n` меньше нуля. Ради упрощения операции индексирования выражение `p[n]` эквивалентно выражению `*(p + n)`. Наконец, разница между двумя указателями `p2 - p1` соответствует такому целому числу `n`, что `p1 + n == p2`. -Можно получить адрес первого элемента массива `arr` с помощью вы -ражения вида `arr.ptr`. Следовательно указатель на последний элемент -непустого массива arr можно получить с помощью выражения `arr.ptr + arr.length - 1`, а указатель на область памяти сразу за последним эле -ментом массива – с помощью выражения `arr.ptr + arr.length`. Проиллю -стрируем все сказанное примером: +Можно получить адрес первого элемента массива `arr` с помощью выражения вида `arr.ptr`. Следовательно указатель на последний элемент непустого массива arr можно получить с помощью выражения `arr.ptr + arr.length - 1`, а указатель на область памяти сразу за последним элементом массива – с помощью выражения `arr.ptr + arr.length`. Проиллюстрируем все сказанное примером: ```d auto arr = [ 5, 10, 20, 30 ]; @@ -1431,16 +1201,7 @@ assert(*p == 30); assert(p - arr.ptr == 3); ``` -Однако будьте осторожны: если вы не обладаете информацией об адре -сах границ массива (указатель вам об этом не сообщит, а значит, это -должно быть известно откуда-то еще), ситуация вскоре может стать не -предсказуемой. Никакие операции с участием указателей не проверя -ются: указатель – это всего лишь адрес памяти длиной в слово[^6], и ариф -метические операции, которые вы к нему применяете, просто слепо ис -полняют то, о чем вы просите. Это делает указатели невероятно быст -рыми и при этом ужасно неосведомленными. Указатель недостаточно -умен даже для того, чтобы понять, что он указывает на отдельный объ -ект (в отличие от указания на элемент массива): +Однако будьте осторожны: если вы не обладаете информацией об адресах границ массива (указатель вам об этом не сообщит, а значит, это должно быть известно откуда-то еще), ситуация вскоре может стать непредсказуемой. Никакие операции с участием указателей не проверяются: указатель – это всего лишь адрес памяти длиной в слово[^6], и арифметические операции, которые вы к нему применяете, просто слепо исполняют то, о чем вы просите. Это делает указатели невероятно быстрыми и при этом ужасно неосведомленными. Указатель недостаточно умен даже для того, чтобы понять, что он указывает на отдельный объект (в отличие от указания на элемент массива): ```d auto x = 10; @@ -1457,56 +1218,23 @@ y += 100; // Хм... *y = 0xdeadbeef; // Русская рулетка ``` -Присваивать значение с помощью указателя, который не указывает на -корректные данные, – значит играть в русскую рулетку с целостностью -своей программы: записи могут «приземлиться» где угодно, растоптав -самые тщательно оберегаемые данные, а то и код. Все это делает указа -тели *небезопасным для памяти* (*memory-unsafe*) средством. +Присваивать значение с помощью указателя, который не указывает на корректные данные, – значит играть в русскую рулетку с целостностью своей программы: записи могут «приземлиться» где угодно, растоптав самые тщательно оберегаемые данные, а то и код. Все это делает указатели *небезопасным для памяти* (*memory-unsafe*) средством. -Поэтому старательно избегайте указателей, отдавая предпочтение мас -сивам, ссылкам на классы (см. главу 6), аргументам функций, передан -ным с ключевым словом `ref` (см. раздел 5.2.1), и автоматическому управ -лению памятью. Все эти средства безопасны, могут эффективно прове -ряться и почти не снижают быстродействие. +Поэтому старательно избегайте указателей, отдавая предпочтение массивам, ссылкам на классы (см. главу 6), аргументам функций, переданным с ключевым словом `ref` (см. раздел 5.2.1), и автоматическому управлению памятью. Все эти средства безопасны, могут эффективно проверяться и почти не снижают быстродействие. -В действительности, массивы – весьма полезная абстракция, тщатель -но спроектированная с единственною целью: *создать самое быстрое после указателей средство с учетом ограничений безопасности для памяти*. Очевидно, что сам по себе указатель не имеет доступа к доста -точному количеству информации, чтобы выяснить что-то самостоя -тельно; массив, напротив, знает свой размер, поэтому может легко про -верять, что все операции над ними совершаются в пределах границ рас -положения данных. +В действительности, массивы – весьма полезная абстракция, тщательно спроектированная с единственною целью: *создать самое быстрое после указателей средство с учетом ограничений безопасности для памяти*. Очевидно, что сам по себе указатель не имеет доступа к достаточному количеству информации, чтобы выяснить что-то самостоятельно; массив, напротив, знает свой размер, поэтому может легко проверять, что все операции над ними совершаются в пределах границ расположения данных. -С точки зрения высокого уровня можно отметить, что массивы слиш -ком низкоуровневые и что их реализация недотягивает до абстрактно -го типа данных. С другой стороны, если мыслить низкоуровневыми ка -тегориями, может показаться, что в массивах нет необходимости, так -как они могут быть реализованы с помощью указателей. Ответ на оба -эти аргумента против массивов все тот же: «Я все объясню». +С точки зрения высокого уровня можно отметить, что массивы слишком низкоуровневые и что их реализация недотягивает до абстрактного типа данных. С другой стороны, если мыслить низкоуровневыми категориями, может показаться, что в массивах нет необходимости, так как они могут быть реализованы с помощью указателей. Ответ на оба эти аргумента против массивов все тот же: «Я все объясню». -Ценность массивов в том, что из всех абстракций эта абстракция – са -мая низкоуровневая, но при этом она уже безопасна. Если бы язык пре -доставлял только указатели, то был бы не способен обеспечить безопас -ность различных пользовательских конструкций более высокого уров -ня, построенных на базе указателей. Массивы также не должны быть -слишком высокоуровневыми, потому что являются встроенными, а зна -чит, все остальное будет создаваться, используя их как основу. Хорошее -встроенное средство должно быть низкоуровневым и быстрым, чтобы -можно было строить на его базе абстракции более высокого уровня, не -обязательно столь же быстрые. Именно так и развиваются абстракции. +Ценность массивов в том, что из всех абстракций эта абстракция – самая низкоуровневая, но при этом она уже безопасна. Если бы язык предоставлял только указатели, то был бы не способен обеспечить безопасность различных пользовательских конструкций более высокого уровня, построенных на базе указателей. Массивы также не должны быть слишком высокоуровневыми, потому что являются встроенными, а значит, все остальное будет создаваться, используя их как основу. Хорошее встроенное средство должно быть низкоуровневым и быстрым, чтобы можно было строить на его базе абстракции более высокого уровня, необязательно столь же быстрые. Именно так и развиваются абстракции. -Существует «урезанная» безопасная версия D, известная как SafeD (см. -главу 11), также есть флаг компилятора, установка которого включает -проверку принадлежности используемых в программе инструкций и ти -пов данных этому безопасному подмножеству средств языка. Естест -венно, в безопасном D (SafeD) запрещено большинство операций с ука -зателями. Встроенные массивы – это важное средство, позволяющее -создавать мощные, выразительные программы на SafeD. +Существует «урезанная» безопасная версия D, известная как SafeD (см. главу 11), также есть флаг компилятора, установка которого включает проверку принадлежности используемых в программе инструкций и типов данных этому безопасному подмножеству средств языка. Естественно, в безопасном D (SafeD) запрещено большинство операций с указателями. Встроенные массивы – это важное средство, позволяющее создавать мощные, выразительные программы на SafeD. + +[В начало ⮍](#4-6-опасный-собрат-массива-–-указатель) [Наверх ⮍](#4-массивы-ассоциативные-массивы-и-строки) ## 4.7. Итоги и справочник -В табл. 4.3 собрана информация об операциях над динамическими мас -сивами, в табл. 4.4. – об операциях над массивами фиксированной дли -ны, а в табл. 4.5 – об операциях над ассоциативными массивами. +В табл. 4.3 собрана информация об операциях над динамическими массивами, в табл. 4.4. – об операциях над массивами фиксированной длины, а в табл. 4.5 – об операциях над ассоциативными массивами. *Таблица 4.3. Операции над динамическими массивами (`a` и `b` – два значения типа `T[]`; `t`, `t1`, ..., `tk` – значения типа `T`; `n` – значение, приводимое к типу `размер_t`)* @@ -1573,6 +1301,8 @@ y += 100; // Хм... |`a.byKey()`|`int delegate(int delegate(ref K))`|Возвращает делегат, пригодный для использования в цикле `foreach` для итерации по ключам| |`a.byValue()`|`int delegate(int delegate(ref V))`|Возвращает делегат, пригодный для использования в цикле `foreach` для итерации по значениям| +[В начало ⮍](#4-7-итоги-и-справочник) [Наверх ⮍](#4-массивы-ассоциативные-массивы-и-строки) + [^1]: quadrupeds (англ.) – четвероногие. – *Прим. пер.* [^2]: Заметим также, что переход по нужному индексу статического многомерного массива происходит за один раз, а сам массив хранится в непрерывной области памяти. Например, для хранения массива `arr` типа `int[5][5]` выделяется область размером `5 * 5 * int.sizeof` байт, а переход по адресу `arr[2][2]` выглядит как `&arr + 2 * 5 + 2`. Если же статический массив размещается в сегменте данных (как глобальная переменная или как локальная с атрибутом `static`), а индексы известны на этапе компиляции, то переход по указателю вообще не потребуется. – *Прим. науч. ред.* [^3]: При этом для массива типа `V[K]` передаваемые ключи должны иметь тип `immutable(K)` или неявно приводимый к нему. Это требование введено для того, чтобы в процессе работы программы значение ключа не могло быть изменено косвенным образом, что повлекло бы нарушение структуры ассоциативного массива. – *Прим. науч. ред.*