Вы находитесь на странице: 1из 523

Станислав Чернышев

ОСНОВЫ
DART
2-е издание, переработанное и
дополненное

Использование данного файла означает, что вы согласны с условиями


лицензии, расположенной на следующей странице

2024
ПУБЛИЧНАЯ ЛИЦЕНЗИЯ
Учебник Чернышева Станислава Андреевича «Основы Dart»,
называемое далее «Произведением», защищено действующим
российским и международным авторско-правовым
законодательством. Все права на Произведение, предусмотренные
законом, как имущественные, так и неимущественные, принадлежат
его автору. Настоящая Лицензия устанавливает способы
использования электронной версии Произведения, право на которые
предоставлено автором и правообладателем неограниченному кругу
лиц, при условии безоговорочного принятия этими лицами всех
условий данной Лицензии. Любое использование Произведения, не
соответствующее условиям данной Лиценции, а равно и
использование Произведения лицами, не согласными с условиями
Лицензии, возможно только при наличии письменного разрешения
автора и правообладателя, а при отсутствии такого разрешения
является противозаконным и преследуется в рамках гражданского,
административного и уголовного права. Автор и правообладатель
настоящим разрешает следующие виды использования данного
файла, являющегося электронным представлением Произведения,
без уведомления правообладателя и без выплаты авторского
вознаграждения:
1) воспроизведение Произведения (полностью или частично) на
бумаге путем распечатки с помощью принтера в одном экземпляре
для удовлетворения личных бытовых или учебных потребностей, без
права передачи воспроизведенного экземпляра другим лицам;
2) копирование и распространение данного файла в
электронном виде, в том числе путем записи на физические носители
и путем передачи по компьютерным сетям, с соблюдением
следующих условий:
➢ все воспроизведенные и передаваемые любым лицам
экземпляры файла являются точными копиями оригинального
файла в формате PDF или EPUB, при копировании не производится
никаких изъятий, сокращений, дополнений, искажений и любых
других изменений, включая изменение формата представления
файла;
➢ распространение и передача копий другим лицам
производится исключительно бесплатно, то есть при передаче
не взимается никакое вознаграждение ни в какой форме, в том
числе в форме просмотра рекламы, в форме платы за носитель или за
сам акт копирования и передачи, даже если такая плата оказывается
значительно меньше фактической стоимости или себестоимости
носителя, акта копирования и т. п.
Любые другие способы распространения данного файла при
отсутствии письменного разрешения автора запрещены. В
частности, запрещается:
➢ внесение каких-либо изменений в данный файл, создание и
распространение искаженных экземпляров, в том числе
экземпляров, содержащих какую-либо часть произведения;
➢ распространение данного файла в Сети Интернет через веб-
сайты, оказывающие платные услуги, через сайты коммерческих
компаний и индивидуальных предпринимателей (включая
файлообменные и любые другие сервисы, организованные в Сети
Интернет коммерческими компаниями, в том числе бесплатные), а
также через сайты, содержащие рекламу любого рода;
➢ продажа и обмен физических носителей, содержащих данный
файл, даже если вознаграждение значительно меньше себестоимости
носителя;
➢ включение данного файла в состав каких-либо
информационных и иных продуктов;
➢ распространение данного файла в составе какой-либо
платной услуги или в дополнение к такой услуге.
С другой стороны, разрешается дарение (бесплатная передача)
носителей, содержащих данный файл, бесплатная запись данного
файла на носители, принадлежащие другим пользователям,
распространение данного файла через бесплатные
децентрализованные файлообменные P2P-сети и т. п. Ссылки на
экземпляр файла, расположенные на официальной странице Boosty,
телеграм-канале или группы VK автора, разрешены без ограничений.
С. А. Чернышев запрещает Российскому авторскому
обществу и любым другим организациям производить любого
рода лицензирование этого произведения и осуществлять в
интересах автора какую бы то ни было иную связанную с
авторскими правами деятельность без его письменного
разрешения.
ОСНОВЫ
DART
2 издание, переработанное и дополненное

2024
Чернышев, Станислав Андреевич
Основы Dart – 2-е изд., перераб. и доп. / С.А. Чернышев. ‒ 2024. ‒ 521 с.,
ил.

Учебник «Основы Dart» (актуальная версия на момент написания – 3.2)


предназначен для самостоятельного изучения и использования
преподавателями. В нем рассмотрена история создания языка
программирования, его синтаксис и основные нюансы, встроенные типы
данных и т.д.
Каждая глава заканчивается набором лабораторных работ, что
позволяет использовать учебник в образовательных процессах ВУЗов, СУЗов
или школах, а также даст возможность в большем объеме прокачать свои
скиллы тем людям, кто самостоятельно обучается по нему.
Исходные коды рассматриваемых в книге примеров, начиная с 2-й
главы, можно скачать с github-репозитория автора:
https://github.com/MADTeacher/dart_basics
Учебник может использоваться школьниками, студентами и их
преподавателями, и всеми, кому интересен язык программирования Dart.
Если хотите отблагодарить автора, то информация по тому, как это
можно сделать, находится в предисловии и конце книги.

© Чернышев С. А., 2021


© Чернышев С. А., 2024,
с изменениями
Выражаю огромную благодарность моей жене и
дочери, ученикам кооперации «Пафос и
превозмогание!!!», а также всем тем, кто
поддержал актуализацию предыдущей книги по
основам Dart, как финансово, так и морально
Содержание

Предисловие ............................................................................................. 11
Глава 1. Краткая история и встроенные типы данных ....................... 14
1.1. Краткая история и основные особенности Dart ............................... 14
1.2. Установка и настройка рабочего окружения .................................... 16
1.3. Правила именования .......................................................................... 23
1.4. Встроенные типы данных Dart .......................................................... 23
1.4.1. Числа (int, double) ....................................................................... 24
1.4.2. Строки (String) ............................................................................ 27
1.4.3. Логические значения (bool) ....................................................... 30
1.4.4. Списки (List) ................................................................................ 30
1.4.5. Записи (Record) ........................................................................... 40
1.4.6. Множества (Set) .......................................................................... 43
1.4.7. Таблицы (Map) ............................................................................ 47
1.4.8. Runes и Symbols .......................................................................... 50
1.5. Модификаторы final, const и late ....................................................... 51
1.6. Null-безопасность (Null-safety) .......................................................... 52
1.7. Тип данных dynamic vs Object ........................................................... 56
1.8. Чтение данных с клавиатуры ............................................................. 58
Резюме по главе ......................................................................................... 61
Вопросы для самопроверки ...................................................................... 61
Лабораторная работа № 1. Встроенные типы данных Dart .................... 62
Часть 1. Задания по строкам ............................................................... 63
Часть 2. Задания по спискам ............................................................... 64
Часть 3. Задания по множествам ........................................................ 67
Часть 4. Задания по мэпам (таблицам / картам) ................................ 69
Глава 2. Операторы, pattern matching и управляющие конструкции 71
2.1. Основные операторы Dart .................................................................. 71
2.2. Что такое Pattern Matching и Destructuring? ..................................... 76
2.2.1. Деструктурирование списка ...................................................... 77
2.2.2. Деструктурирование записи ...................................................... 79
2.2.3. Деструктурирование мэпы (таблицы) ...................................... 80
2.2.4. Деструктурирование экземпляра класса .................................. 81
2.3. Управление потоком выполнения кода ............................................ 83
2.3.1. Условный оператор if ............................................................... 83
2.3.2. Оператор if-case ....................................................................... 86
2.3.3. Тернарный оператор ?: ............................................................. 90
2.3.4. Оператор ?? ................................................................................ 91
2.3.5 Операторы циклов (for, for-in, while и do-while) .................. 92
2.3.6 Операторы потока выполнения (break, continue,
return) .................................................................................................. 96

6
2.3.7. Оператор выбора потока выполнения (switch-case) ............. 97
Резюме по разделу ................................................................................... 114
Вопросы для самопроверки .................................................................... 114
Лабораторная работа № 2. Управляющие конструкции, арифметические
операции и шаблоны ............................................................................... 115
Часть 1. Задания на арифметические операции .............................. 115
Часть 2. Задания на шаблоны ............................................................ 117
Часть 3. Задания на управляющие конструкции ............................. 122
Лабораторная работа № 3. Циклы и битовые операции ....................... 124
Часть 1. Задания на циклы ................................................................ 125
Часть 2. Задания на побитовые операции ........................................ 127
Глава 3. Функции, библиотеки, пакеты и их тестирование 130
3.1. Что такое абстракция? ...................................................................... 131
3.2. Функция как способ написания художественного произведения 133
3.2.1. Простой текст – это первый уровень абстракции.................. 134
3.2.2. Раздел - новый уровень абстракции ....................................... 134
3.2.3. Очередной уровень абстракции – главы и части ................... 137
3.3. Функции в Dart .................................................................................. 140
3.3.1. Объявление входных аргументов функции ........................... 142
3.3.2. Необязательные аргументы функции по умолчанию ........... 147
3.3.3. Область видимости переменных............................................ 149
3.3.4. Обращение к функции через переменную ............................. 150
3.3.5. Функция как входной аргумент другой функции .................. 151
3.3.6. Type Alias ................................................................................... 152
3.3.7. Анонимные и стрелочные функции ....................................... 155
3.3.8. Замыкания ................................................................................ 157
3.3.9. Рекурсия .................................................................................... 159
3.3.10. Генераторные функции .......................................................... 161
3.4. Создание и импортирование библиотек ......................................... 164
3.4.1. Импортирование кода из файла с расширением «.dart» ....... 166
3.4.2. Импортирование части функциональности........................... 169
3.4.3. Отложенная загрузка импортируемого файла или
библиотеки ......................................................................................... 170
3.4.4. Создание и использование библиотеки ................................. 171
3.5. Тестирование функций .................................................................... 174
3.5.1. Установка пакета test в проект ............................................. 175
3.5.2. Написание тестов ..................................................................... 175
3.5.3. Запуск тестов ............................................................................ 179
3.5.4. Конфигурация тестов ............................................................... 181
3.4.5. Тестирование пользовательского ввода ................................. 185
3.6. Создание пакета и его подключение к проекту .............................. 188
3.6.1. Создание пакета ....................................................................... 189

7
3.6.2. Локальное подключение пакета .............................................. 191
3.6.3. Удаленное подключение пакета ............................................. 192
Резюме по разделу ................................................................................... 192
Вопросы для самопроверки .................................................................... 193
Лабораторная работа № 4. Функции ...................................................... 194
Лабораторная работа № 5. Рекурсия ...................................................... 197
Лабораторная работа № 6. Замыкание .................................................. 200
Глава 4. Объектно-ориентированное программирование .................203
4.1. Абстракция в объектно-ориентированном программировании .. 203
4.1.1. Основа абстракции в ООП – класс .......................................... 204
4.1.2. Сложносоставной класс: от деталей к обобщению на примере
автомобиля ......................................................................................... 204
4.1.3. Наследование – новый уровень абстракции? ........................ 205
4.1.4. Три столпа абстракции в ООП: интерфейс, полиморфизм и
приведение ......................................................................................... 206
4.1.5. Что такое абстрактный класс и интерфейс в рамках языка
программирования? .......................................................................... 211
4.1.6 Инкапсуляция и сокрытие ........................................................ 212
4.2. Объявление класса ............................................................................ 213
4.3. Конструктор класса ........................................................................... 219
4.3.1. Именованный конструктор ..................................................... 223
4.3.2. Константный конструктор ....................................................... 227
4.3.3. Фабричный конструктор .......................................................... 228
4.4. Статические переменные и методы класса .................................... 231
4.5. Перегрузка операторов..................................................................... 232
4.5.1. Перегрузка арифметических операторов .............................. 232
4.5.2. Перегрузка операторов сравнения ......................................... 235
4.5.3. Перегрузка битовых операторов ............................................ 237
4.5.4. Перегрузка индексных операторов ........................................ 238
4.6. Методы расширения (Extension methods) ....................................... 240
4.7. Наследование и переопределение методов .................................... 243
4.8. Абстрактный класс и интерфейс...................................................... 254
4.9. Продвижение приватных полей (Private field promotion) .............. 262
4.10. Модификаторы класса .................................................................... 263
4.10.1. Отсутствие модификатора ..................................................... 264
4.10.2. Модификатор abstract .......................................................... 267
4.10.3. Модификатор base ................................................................. 268
4.10.4. Модификатор interface и abstract interface .................. 273
4.10.5. Модификатор final ............................................................... 281
4.10.6. Модификатор sealed.............................................................. 281
4.10.7. Миксины и модификатор класса mixin ................................ 295
4.11. Generics (Обобщения) ..................................................................... 302

8
4.12. Enum (Перечисления) ..................................................................... 305
4.13. Exceptions (Исключения) ................................................................ 315
4.13.1. Конструкция try…catch…finally .......................................... 316
4.13.2. Генерация исключений и ошибок ......................................... 318
4.13.3. Пользовательские исключения и ошибки ............................ 321
4.13.4. Трассировка стека .................................................................. 322
4.13.5 Assert (Утверждение) ............................................................... 324
4.14. Тестирование классов ..................................................................... 325
Резюме по разделу ................................................................................... 327
Вопросы для самопроверки .................................................................... 328
Лабораторная работа № 7. Объектно-ориентированное
программирование .................................................................................. 329
Лабораторная работа № 8. Перегрузка операторов .............................. 333
Глава 5. Сборка приложения. Работа с файлами и директориями ...337
5.1. Сборка приложения .......................................................................... 337
5.1.1. Флаг exe ..................................................................................... 339
5.1.2. Флаг aot-snapshot.................................................................... 340
5.1.3. Флаг jit-snapshot.................................................................... 340
5.1.4. Флаг kernel ............................................................................... 341
5.2. Конфигурация запускаемого приложения ...................................... 342
5.3. Работа с файлами .............................................................................. 345
5.4. Работа с директориями .................................................................... 351
5.5. База данных на основе файла и однонаправленного списка ....... 355
5.6. Работа с JSON-файлами .................................................................... 373
5.6.1. Зачем нужен null? ..................................................................... 380
5.6.2. Валидация данных ................................................................... 381
5.6.3. Пакет json_serializable для десериализации и сериализации
объектов в JSON-формат ................................................................... 385
5.7. Простая БД по типу «ключ:значение» в формате JSON .................. 390
Резюме по разделу ................................................................................... 397
Вопросы для самопроверки .................................................................... 397
Лабораторная работа № 9. Работа с текстовыми файлами .................. 398
Лабораторная работа № 10. Работа с JSON-файлами............................ 400
Глава 6. Асинхронное и сетевое программирование. Isolate.............411
6.1. Event Loop архитектура в Dart ......................................................... 412
6.1.1. Базовая концепция цикла и очереди событий ....................... 412
6.1.2. Очереди и цикл событий в Dart ............................................... 413
6.2. Асинхронное программирование .................................................... 415
6.2.1. Future API, async и await ......................................................... 416
6.2.2. Stream (Поток)........................................................................... 432
6.3. Isolate (Изоляты) ............................................................................... 443
6.4 Async или Isolate? ............................................................................... 463

9
6.5. Зоны (Zones) ...................................................................................... 463
6.6. Сетевое программирование ............................................................. 471
6.6.1. Разработка пакета «protocol» ................................................. 474
6.6.2. Клиент-серверное приложение на основе TCP ...................... 482
6.6.3. Клиент-серверное приложение на основе UDP ...................... 491
6.6.4. Структура HTTP-сообщений и REST URLs .............................. 497
6.6.4. HTTP-сервер и клиент .............................................................. 500
Резюме по разделу ................................................................................... 510
Вопросы для самопроверки .................................................................... 510
Лабораторная работа № 11. Асинхронное программирование и
изоляты .................................................................................................... 511
Лабораторная работа № 12. Клиент-серверное приложение ............... 513
Планы на следующую книгу по Dart .....................................................515
Курсы автора на Stepik ...........................................................................516
Паттерны проектирования GoF на Dart ................................................. 516
Грокаем Python через разработку проекта ............................................ 516
Python в мультиагентных системах ....................................................... 516
Как отблагодарить автора и зачем это делать? ...................................517
Где посмотреть актуальную версию книги? ........................................519
Список используемых источников .......................................................520

10
Предисловие

Добрый день! Меня зовут


Станислав Чернышев, я автор
данного учебника и канала
«MADTeacher» на YouTube.
Основная моя деятельность – наука,
преподавание и выполнение на
заказ различных проектов с моими
учениками.
За спиной более 10 лет в IT: работа в сфере ВПК,
разработка различных проектов на заказ, управление
командой разработчиков, пару выгораний и, само собой,
преподавание. Мне нравится обучать, и, чего уж там скрывать
– я просто в бешенстве от сложившейся ситуации в сфере
образования…
Этот учебник – один из маленьких шагов, в попытке
изменить чашу весов таким образом, чтобы мои ученики и
студенты выходили на рынок труда с актуальным набором
компетенций. Если вы не относитесь к их числу, то скорей
всего, уже успели начать свою карьеру в IT или только
планируете «Захват мира» ^_^ Очень надеюсь, что она
поможет вам в этом и предоставит необходимый объем
знаний для изучения новой технологии.
Огромное спасибо всем тем, кто морально, а особенно
денежно поддержал второе переиздание учебника, сделав его
возможным! Ниже приводится список донэйторов (чьи имена
и никнеймы удалось выявить при переводах и не пожелали
оставаться анонимами), в порядке убывания по внесенному
вкладу:
a.alistrat, Starletovod, PackRuble, ReinRaus, Олег О.,
Александр Остапенко, Павел М., Дмитрий М., Ruslan Vafin,
Если у вас имеется желание поддержать мои начинания,
то это можно сделать через Тинькофф:

11
https://www.tinkoff.ru/rm/chernyshev.stanislav20/FUUfY1048

Или ЮMoney: https://yoomoney.ru/to/410011696202148

Адрес EVM кошелька Bybit Wallet:


0x3ff35d9325f8c4cbabd6f14ba5e170459420faa8

Адрес SUI кошелька Bybit Wallet:


0x9300ecb7a65ab4564a4c81ef045f0ef8d175a13fe3cfc7acdd2
5b8afa0b00225

12
Все деньги, полученные таким образом, идут на
поддержку моей образовательной деятельности, покупку
различного оборудования для докторской диссертации,
оплату публикаций статей в научных журналах и приближают
момент, когда еще раз возьмусь за «перо» для написания
новой книги или актуализации текущей.
Что касается исходных кодов рассматриваемых в книге
примеров, их можно скачать с моего github-репозитория:
https://github.com/MADTeacher/dart_basics

По поводу найденных опечаток, ошибок и т.д., просьба


писать на почту (см. ниже), либо через группу в VK

madteacher@bk.ru MADTeacher

MADTeacher

https://vk.com/madteacher

13
Глава 1. Краткая история и встроенные
типы данных

1.1. Краткая история и основные особенности Dart


Dart – объектно-ориентированный язык программирования с сильной
статической типизацией и поддержкой обобщенного программирования
(шаблоны/дженерики) [1]. Он не поддерживает множественное
наследование, то есть родителем производного класса может выступать
только один базовый класс. В тоже самое время, как и в языке
программирования Java или C#, класс может реализовывать множество
интерфейсов. По своему синтаксису Dart очень похож на семейство языков
C (Си) – (C++, C#, Java, Kotlin и т. д.).
Dart – молодой язык программирования, который был впервые
анонсирован корпорацией Google 10 октября 2011 года. Первая версия
языка увидела свет в ноябре 2013 года, далее отметилась версия 2.12 (март
2021), начиная с которой Dart поддерживает null-safety (нулевую
безопасность, null-безопасность). В ее основе лежат следующие принципы
проектирования [2]:
− По умолчанию не допускает значения NULL. Если не указывается явно,
что переменная может иметь значение NULL. Dart будет выдавать
ошибки на этапе компиляции при присваивании такой переменной
значения NULL.
− Использование null-safety позволяет оптимизировать компилятор.
Уменьшается не только количество ошибок, связанных с
присваиванием NULL, а также объем скомпилированного приложения
и повышается скорость его выполнения.
10 мая 2023 года свет увидела третья версия этого языка
программирования (в книге используется Dart 3.2) и вызвала бурю
негодований, т.к. его разработчики пошли по пути усложнения и помимо
прикольных фич, типа: сопоставления шаблонов (pattern matching), switch-
выражений, записей (Record) и т.д., расширили количество модификаторов
класса. Тем, кто раньше писал на C++, Java, Kotlin это не доставило проблем,
но для разработчиков, которые знакомы только с Dart, такая ситуация не
сулила ничего хорошего. В итоге, у кого бы не спрашивал из знакомых, все
продолжили писать код, как будто этих новых модификаторов и не
существует, лишь изредка пользуясь некоторыми из них. А саму ситуацию
назвали: «Ад для джуна», так как у более опытных it-шников появился
инструмент, посредством которого они могут для стажеров или junior-
разработчиков устроить то еще незабываемое собеседование. К тому же, с

14
момента анонса Dart 3, прекратилась поддержка всех версий младше Dart
2.12, то есть Dart стал полностью null-safety.
В момент своего появления на свет, Dart позиционировался Google как
язык программирования для замены JavaScript, что и сыграло с ним
довольно злую шутку. Несмотря на изначальный интерес сообщества
программистов, его не стали повсеместно использовать и чаще всего
упоминание об этом языке программирования можно было встретить на
форумах при описании pet-проекта. Единственное, что вернуло Dart из того
колодца забытия, в который он погружался месяц от месяца – выход первой
версии Flutter SDK [3] в конце 2018 года, где Dart занял место основного
языка программирования, на котором посредством Flutter ведется
разработка. Сам Flutter представляет собой набор инструментов для
разработки приложений с графическим пользовательским интерфейсом
различной сложности как для мобильных устройств, так для Интернета и
настольных компьютеров. Это связано с тем, что разработка ведется в
рамках одной кодовой базы для следующих платформ: iOS, Android, macOS,
Windows, Linux и Web. К тому же ООО «Открытая мобильная платформа»
анонсировала (https://habr.com/ru/articles/761176/) порт Flutter под
отечественную мобильную операционную систему «ОС Аврора».
Таким образом, сейчас Dart – оптимизированный для клиентской
части язык программирования, позволяющий вести разработку быстрых
приложений на любой платформе. При этом он дает возможность
использовать динамический тип в сочетании с проверками во время
выполнения. Это особенно полезно при быстром прототипировании.
В основе технологии, которая используется в компиляторе Dart и
позволяет запускать пользовательский код, лежит понятие платформа.
Всего выделяется 2 вида платформ (см. рис. 1.1):
1. Native platform. Данная платформа используется для приложений,
разрабатываемых под мобильные устройства и персональные компьютеры
с различными типами операционных систем. Сюда входит как виртуальная
машина Dart с JIT-компиляцией, так и опережающий компилятор (AOT) для
создания машинного кода. Виртуальная машина Dart с JIT-компилятором
используется в процессе разработки приложений и предоставляет
разработчику возможность горячей перезагрузки приложения (нет
необходимости компилировать приложение снова и запускать его), сбор
различных метрик в реальном времени и т.д. Когда же приложение готово
к развертыванию на целевой платформе или его загрузке в магазин для
последующего скачивания пользователем, компилятор Dart AOT
обеспечивает опережающую компиляцию в машинный код ARM или x64.
Скомпилированный AOT код выполняется внутри среды выполнения Dart,
где также присутствует сборщик мусора, в котором применяется подход на
основе поколений.

15
2. Web platform. Данная платформа используется для приложений,
ориентированных на Интернет. В этом случае также используется 2 вида
компиляторов Dart. Первый (dartdevc) используется только в процессе
разработки, а второй (dart2js) для окончательной сборки приложения перед
его развертыванием. Оба этих компилятора переводят пользовательский
код, написанный на Dart в JavaScript.

Рисунок 1.1 – Структура платформы языка программирования Dart

Dart однопоточный язык программирования, что накладывает ряд


ограничений. Да, имеется возможность писать асинхронный код, но
«привычного» по другим языкам класса Thread здесь нет. Вместо него
используется понятие изолят (Isolate). В отличие от обычного потока
изоляты не разделяют общую память, а взаимодействовать между друг
другом могут посредством сообщений.
У Dart имеется свой менеджер пакетов – pub, который позволяет
устанавливать существующие в хранилище пакеты. В большинстве случаев
нет надобности взаимодействовать с ним напрямую. Достаточно просто
прописать в виде зависимости проекта пакет, который необходимо
установить, в файл «pubspec.yaml».
Обязательным требованием к запускаемому приложению является
наличие функции верхнего уровня «main», выступающей в роли точки входа
(запуска) для разрабатываемого приложения. При этом, аналогично таким
языкам программирования, как: C++, С#, Java т.д., каждая команда в коде
завершается символом «;».

1.2. Установка и настройка рабочего окружения


Для разработки на языке программирования Dart могут
использоваться как различные IDE (IntelliJ IDEA, Eclipse), так и редакторы
кода (Visual Studio Code, Vim, Emacs) после установки в них

16
соответствующих плагинов/расширений. Также существует
ознакомительный веб-сервис для написания и выполнения Dart кода,
располагающийся по адресу: https://dartpad.dev.
В книге написание кода будет вестись в Visual Studio Code, который
можно скачать по следующей ссылке:
https://code.visualstudio.com/download
Далее необходимо скачать Dart SDK под ту операционную систему,
которую обычно используете. В моем случае это Windows 10 (рис. 1.2):
https://dart.dev/get-dart/archive

Рисунок 1.2 – Скачиваем архив с текущей стабильной версией Dart SDK

После того, как архив с Dart SDK загрузился, распакуйте его в удобную
для вас директорию. Обычно этой директорией выступает корневой
каталог диска (C, D, F и т. д.). Теперь необходимо прописать путь до
распакованного Dart SDK в переменных средах в переменной «Path», как
показано на рисунке ниже:

17
Рисунок 1.3 – Указываем путь до Flutter SDK в переменной «Path»

Запускаем Visual Studio Code, устанавливаем расширение «Dart» и


перезапускаем приложение:

Рисунок 1.4 – Установка расширения «Dart» для VS Code

18
После перезапуска VS Code создадим новый проект для Dart. Для этого
используем английскую связку клавиш «Ctrl+Shift+P» и введем в
появившейся командной строке «Dart: New Project». Данная команда может
появиться в списке команд до того, как введете ее полностью. В этом случае
просто выбираем ее из списка:

Рисунок 1.5 – Создание нового проекта

На этом шаге обращаем внимание на правый нижний угол


приложения. Возможно оно предложит установить какие-то новые
зависимости расширения «Dart», что не даст с первой попытки создать
новый проект. Если такое произошло, то снова повторяем предыдущий шаг
и в появившемся новом списке выбираем «Console Application»:

Рисунок 1.6 – Создание консольного приложения

На следующих шагах необходимо выбрать директорию, в которой


будет располагаться консольный проект и его имя. После успешно
проделанных шагов VS Code может спросить вас: «Доверяете ли вы сами
себе?»

19
Рисунок 1.7 – Добавление создаваемого проекта в Workspace Trust

Если у вас нет доверия даже к себе, то лучше отложите книгу и


забросьте программирование, вам прямой путь в кибербезопасность
В итоге внешний вид VS Code должен выглядеть примерно следующим
образом:

Рисунок 1.8 – Созданный проект

По нажатию на клавишу «F5» можно запустить код в режиме отладки


(debug), а воспользовавшись следующим сочетанием клавиш «Ctrl +F5» код
запустится без данного режима. Попробуйте один из этих вариантов. В
результате должна появиться консоль с текстом «Hello world: 42!».
На данный момент структура директории проекта должна выглядеть
следующим образом:

20
Рисунок 1.9 – Структура директории проекта

В каталоге «bin» должен находиться находится файл с точкой входа в


приложение, в котором объявлена функция верхнего уровня main. В
директории «lib» принято хранить основной код проекта в виде
подключаемого пакета. А в папке «test» находятся файлы с тестовым
окружением проекта.
В данный момент не будем заострять внимание на директориях «lib» и
«test». Откройте файл в каталоге «bin» и измените в нем код с:
import 'package:hello_world/hello_world.dart' as hello_world;

void main(List<String> arguments) {


print('Hello world: ${hello_world.calculate()}!');
}

на
void main(List<String> arguments) {
print('Hello world!');
}

При повторном запуске проекта у вас в терминал должно вывестись


«Hello world!».
Перед тем, как пустимся во все тяжкие, нам нужно подстелить соломы.
По умолчанию Google собирает некоторую "анонимную" статистику по
метрикам и отчетах о сбоях, что может привести к невозможности

21
пользоваться автодополнением кода, когда у вас нет соединения с
интернетом. Чаще всего, такая ситуация, будет сопровождаться следующей
плашкой уведомлений:

Рисунок 1.10 – Нет подключения к серверу Google

Чтобы не быть застигнутыми врасплох, давайте отключим этот


инструментарий благих намерений «Большого Брата». Для этого откройте
терминал и введите команду
dart --disable-analytics
После чего нажмите Enter. Если все выполнили правильно, то в
терминале выведется следующее уведомление:

Рисунок 1.11 – Успешное отключение сбора метрик

Весь последующий код текущей главы написан в функции верхнего


уровня main.

22
1.3. Правила именования
При написании кода на Dart лучше придерживаться следующих
рекомендаций при объявлении переменных, функций, классов и их
методов:
1. При объявлении переменных, функций и методов классов
используется верблюжий стиль, а само название начинается с
маленькой буквы (lowerCamelCase). Для логического разделения слов
в объявляемой переменной необходимо использовать символ в
верхнем регистре: myCatName. Имя же объявляемого класса начинается
с большой буквы (UpperCamelCase): DailySchedule;
2. Нельзя использовать в начале объявляемого имени числовые
значения;
3. Регистр символов имеет значение. Так, например, var CHECK = 10; и
var check = 10; две совершенно разные переменные;
4. Не используйте в качестве имен переменных ключевые слова Dart;
5. Если имя переменной, функции и т.д. начинается с символа «_», то она
является приватной (для импортирующего код модуля).

1.4. Встроенные типы данных Dart


Перед знакомством с встроенными типами данных давайте обратимся
к документации. В ней говорится, что все помещаемые в переменную
значения являются объектами, которые в свою очередь представляют собой
экземпляр класса. Такая концепция очень похожа на ту, что применяется в
языке программирования Python, в котором все является объектом. Таким
образом, даже числа, строки, функции и null – это объекты.
Существуют следующие встроенные типы данных:
− Числа (int, double);
− Строки (String);
− Логические значения (bool);
− Списки (List);
− Записи (Record);
− Множества (Set);
− Таблицы (Map);
− Руны (Rune);
− Символы (Symbol);
− Значение null (Null).
Но, прежде чем начнем знакомиться поближе с встроенными типами
данных и их объявлением, нам необходимо поговорить про комментарии.
Они в Dart делятся на 2 типа: однострочные и многострочные. В первом

23
случае используется «//», после чего идет комментарий, который не
переносится на следующую строку:
// комментарий
var a = 10; // еще один комментарий

Если комментарий будет занимать более 2-х строк, то его каждую


строку необходимо начинать либо с «//», либо использовать многострочный
формат записи комментария:
/*
Сверх
длинный комментарий
*/
var a = 10;

Комментарии можно использовать не только для того, чтобы


комментировать происходящее в коде. Например, посредством
комментариев можно исключить выполнение определенной строки или
блока кода (то есть закомментировать их). Но сильно этим увлекаться не
стоит, так как такой подход засоряет чистоту вашей кодовой базы, из-за
чего в последующем обязательно возникнут трудности у новых людей в
команде.
Строки же документации, которые позволяют использовать
инструмент dartdoc для автоматической генерации документации вашего
проекта начинаются с «///». Считается плохим тоном в тех частях, где
пояснения должны попасть в документацию использовать простые
комментарии, так как в этом случае они будут пропущены. Более подробно
с тем, как принято документировать код в проектах, разрабатываемых с
использованием Dart, можно ознакомиться в руководстве по
документированию проектов, расположенному на официальном сайте [4].
В данной книге комментарии используются для демонстрации того,
какой результат будет на выходе программы, что вернет та или иная строка
кода или дополнительного пояснения.

1.4.1. Числа (int, double)


В Dart всего два числовых типа данных: целочисленные ( int) и
вещественные, т. е. с плавающей точкой (double).
Целочисленные значения типа int, в зависимости от платформы,
могут занимать в памяти не более 64 бит. В виртуальной машине Dart числа
типа int могут принимать значения в диапазоне от -263 до 263 - 1, а при
переводе кода в JavaScript используется диапазон значений, который
характерен для этого языка программирования: от -253 до 253 - 1

24
Числа с плавающей точкой типа double занимают в памяти 64 бита и
реализованы в соответствии со стандартом IEEE 754.
Теперь давайте рассмотрим, как можно объявлять переменные
числовых типов данных:
int a = 5;
int hex = 0xDEAFF; // 912127
var b = 10; // int
double c = 30.5;
var d = 1.1;
var exponents = 1.42e5; // 142000.0

Ключевое слово var перед именем переменной означает, что


компилятор Dart сам выведет тип объявляемой переменной в зависимости
от того, что разработчик напишет в правой части объявления после символа
«=».
Как и в других языках программирования со статической типизацией,
если мы объявили целочисленную переменную, то компилятор не даст нам
записать в эту переменную значение вещественного типа:
int a = 5;
a = 3.5; // error: A value of type 'double' can't be
//assigned to a variable of type 'int'.
var b = 2;
b = 3.5; // error

При этом вещественным переменным мы можем присваивать


целочисленные значения:
double a = 3.5;
a = 5;

var b = 2.2;
b = 3;

Типы int и double являются подтипами типа num, в котором в свою


очередь определены такие операции с числами, как: *, /, + и -. Если мы
объявим переменную типа num и сначала присвоим ей целочисленное
значение, а после вещественное, это не будет считаться ошибкой, так как
работа с этими числами осуществляется через экземпляр базового класса
типа num:
num a = 3;
a = 5.3;

Особо внимательно нужно подходить к сравнению чисел, т.к., не ровен


час, наткнуться на неприятные моменты при сравнении двух

25
вещественных значений из-за округления. Поэтому их, рекомендуют
сравнивать посредством >=, <= или метода compareTo, который возвращает:
− отрицательное число, если значение, с которым происходит
сравнение – больше;
− ноль, если они равны;
− положительное число, если меньше.
print(4.compareTo(5)); // -1
print(5.compareTo(4)); // 1
print(4.compareTo(4)); // 0

Ниже приведен ряд операций как над целочисленными переменными,


так и вещественными:
// Берем значение по модулю
print(-5.abs()); // 5

// Округление до большего и меньшего ближайшего целого


print(5.5.round()); // 6
print(5.5.floor()); // 5

// Число четное или нет?


print(5.isEven); // false
print(6.isEven); // true

// Число нечетное или нет?


print(5.isOdd); // true
print(6.isOdd); // false

// Число отрицательное или нет?


print((-5).isNegative); // true
print(6.isNegative); // false

// Представление числа в заданной системе счисления


print(15.toRadixString(2)); // 1111 - двоичная
print(15.toRadixString(8)); // 17 - восьмеричная
print(15.toRadixString(16)); // f - шестнадцатеричная

// Минимальное количество битов, необходимое


// для хранения целого числа
print(5.bitLength); // 3
print(22.bitLength); // 5

// Расчет наибольшего общего делителя


print(30.gcd(12)); // 6
print(4.gcd(2)); // 2

26
1.4.2. Строки (String)
Строки в Dart представляют собой последовательность символов в
кодировке UTF-16. Для их объявления (создания) могут использоваться как
одинарные, так и двойные кавычки:
String s1 = 'Мама мыла раму';
var s2 = "Мама мыла две рамы";
var s3 = '''Многострочная
строка''';

Для обращения к конкретному элементу строки по его индексу можно


использовать квадратные скобки:
print(s2[0]); // М, т.к. индексация начинается с нуля

Так как строки – неизменяемый тип данных (Immutable), то запись


вида: s2[0] = 'П' приведет к ошибке. В виду этого на основе одного
объекта должен быть создан другой, где в процессе создания производятся
необходимые изменения:
var s4 = 'П' + s2.substring(1); // Пама мыла две рамы

В этом случае использовалась операция конкатенация (операция


сложения между двумя строками). Из строки s2 были взяты все символы,
кроме первого, посредством метода substring. Он используется, когда
необходимо вырезать подстроку определенной длины. Для этого в метод
substring необходимо передать индекс первого и последнего элемента, на
основе которых сформируется новая строка. Например:
var s3 = 'П' + s2.substring(1, 9); // Пама мыла

Узнать длину строки можно, обратившись к атрибуту переменной


length:
print(s2.length); // 18

Для перевода всех символов в верхний или нижний регистр


используются следующие методы:
print(s2.toUpperCase()); // МАМА МЫЛА ДВЕ РАМЫ
print(s2.toLowerCase()); // мама мыла две рамы

Когда вызываете такие методы у строк, необходимо помнить, что они


не влияют на оригинальный объект, а возвращают преобразованное
значение, которое необходимо присвоить другой переменной для
последующей работы с ним. Более наглядно это объяснит следующий
пример:
var s2 = "Мама мыла две рамы";
s2.toUpperCase();

27
print(s2); // Мама мыла две рамы
var s3 = s2.toUpperCase();
print(s3); // МАМА МЫЛА ДВЕ РАМЫ

Теперь рассмотрим ситуацию, которая будет встречаться довольно


часто при написании кода – перевод числа в строку и наоборот:
// String -> int
var myInt = int.parse('34'); // строка в число

// String -> double


var myDouble = double.parse('11.45');

// int -> String


String s1 = 14.toString();
String s2 = myInt.toString();

// double -> String


String s3 = 3.14159.toStringAsFixed(2); // 2 числа после точки
String s4 = myDouble.toString();
Помимо конкатенации существует операция объединения, создающая
новую строку на основе заданной, где она дублируется указываемое
количество раз:
var s2 = "Oo";
print(s2*4); // OoOoOoOo

Для посимвольного сравнения строк используйте следующий подход:


var s1 = 'Oo', s2 = 'Oo';
print(s2 == s1); // true - строки равны
print(s2 == 'oO'); // false

Когда же стоит задача лексикографически (в алфавитном порядке)


сравнить одну строку с другой, тут на помощь придет метод compareTo,
который вернет:
− отрицательное целое число, если текущая строка меньше строки, с
которой она сравнивается;
− положительное целое число, если текущая строка больше строки, с
которой она сравнивается;
− ноль, если строки равны.
var s1 = 'Мама', s2 = 'Папа';
print(s1.compareTo(s2)); // -1
print(s2.compareTo(s1)); // 1
print(s1.compareTo('Мама')); // 0

Для проверки того, входит ли символ или подстрока в строку, тип


String предоставляет метод contains:

28
var s1 = 'Вот те на!';
print(s1.contains('е')); // true
print(s1.contains('на')); // true
print(s1.contains('-_-')); // false

Еще этот метод предоставляет возможность задать позицию, от


которой в основной строке будет осуществляться поиск (по умолчанию –
ноль):
print(s1.contains('В')); // true
print(s1.contains('В',0)); // true
print(s1.contains('В',1)); // false

Стоит только задать отрицательную позицию или больше длины


строки и программа экстренно завершится со следующей ошибкой:
print(s1.contains('В', 11)); // RangeError: Invalid value:
// Not in inclusive range 0..10

Иногда бывает необходимо найти номер индекса с первым или


последним вхождением символа или подстроки в строку:
var s1 = 'Мама мыла две рамы';
print(s1.indexOf('м')); // 2
print(s1.lastIndexOf('м')); // 16
print(s1.indexOf('м', 6)); // 16
print(s1.indexOf ('М', 6));
// -1, т.к. символ не найден (6 -> длина строки)

print(s1.lastIndexOf ('М', 6));


// 0, т.к. поиск начинается с конца (6 -> 0)

// Для обоих методов можно задавать индекс,


// с которого начнется поиск в строке

Строки могут содержать лишние пробелы (в начале, конце или сразу в


обоих местах), чтобы от них избавиться воспользуйтесь следующими
методами:
var s1 =' Ma ma ';
print(s1.trimLeft()); //Ma ma
print(s1.trim()); //Ma ma
print(s1.trimRight()); // Ma ma

Еще одна частая задача – замена одного символа или подстроки в


строке на другие:
var s1 = "Мама мыла рамы";
print(s1.replaceAll('мы', 'ру')); // Мама рула рару
print(s1.replaceAll('м', 'н')); // Мана ныла раны
print(s1.replaceFirst('м', 'М')); // МаМа мыла рамы

29
Чтобы разбить строку на несколько частей, воспользуйтесь методом
split:
var s1 = "Мама мыла рамы";
print(s1.split(' ')); // [Мама, мыла, рамы]
print(s1.split('л')); // [Мама мы, а рамы]
print(s1.split('мыла')); // [Мама , рамы]

В результате работы метода split вернется список подстрок,


количество которых будет зависеть от выбранного разделителя.
Для проверки же, осуществляется работа с пустой строкой или нет,
воспользуйтесь следующими свойствами экземпляра типа данных:
var myStr = 'Hello, world!';

print(myStr.isEmpty); // false
print(myStr.isNotEmpty); // true

myStr = '';
print(myStr.isEmpty); // true
print(myStr.isNotEmpty); // false

Более подробно с методами, которые предоставляет класс String


можно ознакомиться в официальной документации Dart [5].

1.4.3. Логические значения (bool)


Переменные типа bool могут принимать только 2 значения: true и
false. Их объявление производится следующим образом:
bool a = false;
var b = true;

1.4.4. Списки (List)


Списки – позиционно упорядоченные коллекции объектов. В отличие
от строк, списки можно модифицировать на месте путем присваивания по
индексу или вызовом некоторых списковых методов. Это позволяет
использовать списки как довольно гибкий инструмент для представления
коллекций объектов, таких как: перечня продуктов в чеке, текущие дела на
день и т.д.
Так как в Dart нет такого типа переменных, как массивы, вместо них
используются списки. В виду этого они делятся на два типа:
− с фиксированным количеством элементов;
− с произвольным количеством элементов.
По умолчанию создается список с произвольным количеством
элементов. Аналогично числам и строкам списки можно объявлять

30
несколькими способами. Отдав вывод типа объектов, с которыми работает
список на откуп Dart, либо задать явно:
var myList1 = [ 1, 2, 3];
List<int> myList2 = [1, 2, 3];
var myList1 = <int>[]; // пустой список

Изменение значения элемента списка производится следующим


образом:
myList1[0] = 20;
print(myList1); // [20, 2, 3]

Так как списки по умолчанию могут хранить только объекты одного


типа данных, то если добавить или изменить значение элемента списка
значением другого типа данных, это приведет к ошибке:
myList1[0] = 20.4; // error: A value of type 'double' can't
// be assigned to a variable of type 'int'.
myList1.add('Oo'); // error: A value of type 'String' can't
// be assigned to a variable of type 'int'.

Для создания списка из неизменяемых (константных) элементов, в


который невозможно добавить новые или удалить уже существующие
элементы, необходимо использовать ключевое слово const, либо
именованный конструктор - unmodifiable:
var constList = const [4, 2, 1];
// или var constList = List.unmodifiable([4, 2, 1]);
constList[0] = 5;//exception: Cannot modify an unmodifiable list
constList.remove(2); // Unsupported operation: Cannot remove
// from an unmodifiable list

Далее приведем некоторые операции добавления и удаления


элементов:
var myList = <int>[]; // пустой список для элементов типа int

// добавление в список

myList.add(4); // в конец списка


myList.addAll([1, 3, 5]); // расширяем список элементами другого
print(myList); // [4, 1, 3, 5]
print(myList.length); // 4 – размер списка

// создаем новый список, добавляя в него элементы существующего


var myList2 = <int>[1, ...myList];
print(myList2); // [1, 4, 1, 3, 5]

// Расширение списка с помощью оператора +=


myList2 += [4, 5, 6];

31
print(myList2); // [1, 4, 1, 3, 5, 4, 5, 6]

// Вставка элемента на указанную позицию с помощью метода insert


myList2.insert(0, 100);
print(myList2); // [100, 1, 4, 1, 3, 5, 4, 5, 6]

// Вставка элементов списка на указанную позицию


// с помощью метода insertAll
myList2 = <int>[1, ...myList];
myList2.insertAll(2, [4, 5, 6]);
print(myList2); // [1, 4, 4, 5, 6, 1, 3, 5]

// удаление из списка

myList = <int>[1, 2, 2, 5, 2, 10];

// удаляем элемент из списка, хранящийся по индексу 0


myList.removeAt(0);
print(myList); // [2, 2, 5, 2, 10]

// удаляем первый элемент с заданным значением


myList.remove(2);
print(myList); // [2, 5, 2, 10]

// удаляем последний элемент


myList.removeLast();
print(myList); // [2, 5, 2]

// удаляем диапазон элементов с k по n-1


myList = <int>[1, 2, 2, 5, 2, 10];
myList.removeRange(1, 4);
print(myList); // [1, 2, 10]

// удаляем все элементы


myList.clear();
print(myList); // []

// удаление элемента по условию


myList = <int>[1, 2, 2, 5, 2, 10];
// удаляем все элементы, кратные 2
myList.removeWhere((element) => element % 2 == 0);
print(myList); // [1, 5]

В последнем случае удаления, на вход метода removeWhere подается


лямбда-функция (анонимная функция), в которой задаем, что
перебираемые элементы списка будут присваиваться переменной element
и проверяться на условие кратности двойки. В том случае, когда значение

32
элемента списка кратно двум, этот элемент удаляется из списка. Более
подробно данный вид функций мы разберем в 3-й главе.
Теперь рассмотрим случай, когда список уже сформирован и нам
необходимо перезаписать значения его элементов:
var myList = <int>[1, 2, 2, 5, 2, 10];

// Использование оператора []=


myList[0] = 10;
print(myList); // [10, 2, 2, 5, 2, 10]

// Использование метода setAll


myList.setAll(0, [100, 200, 300]); // 0 - стартовый индекс
print(myList); // [100, 200, 300, 5, 2, 10]

// Метод replaceRange удаляет все элементы в заданном диапазоне


// заменяя на элементы заданной последовательности
myList.replaceRange(1, 3, [4, 4, 4]);
print(myList); // [100, 4, 4, 4, 5, 2, 10]

myList = <int>[1, 2, 2, 5, 2, 10];


myList.replaceRange(1, 5, [4, 4]);
print(myList); // [1, 4, 4, 10]

// Использование метода fillRange


// перезаписывает диапазон значений элементов
// задаваемым значением
myList.fillRange(1, 3, 0);
print(myList); // [1, 0, 0, 10]

Во всех случаях, когда используются диапазоны или стартовые


индексы надо проявлять осторожность, т.к. неправильно заданный
диапазон с учетом изменяемых значений элементов списка, способен
привести к падению приложения:
var myList = <int>[1, 2, 2, 5, 2, 10];

// Использование метода setAll


myList.setAll(4, [100, 200, 300]); // 4 - стартовый индекс
print(myList); // RangeError (RangeError (end): Invalid value:
// Not in inclusive range 4..6: 7)

Следующим рассмотрим довольно интересный метод – join, на вход


которого подается строка разделитель (сепаратор, по умолчанию – пустая
строка). Он позволяет преобразовать каждый элемент списка в строку и
объединить их:
var myList = <int>[1, 2, 2, 5, 2, 10];

33
print(myList.join('-')); // 1-2-2-5-2-10
print(myList.join()); // 1225210
print(myList.join(', ')); // 1, 2, 2, 5, 2, 10

Такое поведение можно использовать, если в строку необходимо


внести изменения и не хочется прибегать к основным методам работы с
ней:
var myStr = 'Hello, world!';

var myList = myStr.split('');


myList[5] = 'O';
myList.first = 'P'; // обращение к первому элементу списка
myList.last = '?'; // обращение к последнему элементу списка
print(myList); // [P, e, l, l, o, O, , w, o, r, l, d, ?]

myStr = myList.join();
print(myStr); // PelloO world?

Свойства first и last списка позволяют как получить находящиеся в


заданных местах значения элементов списка, так и установить их. Другие
пару свойств позволяют определить, содержит список элементы или нет:
var myList = <int>[1, 2, 2, 5, 2, 10];

print(myList.isEmpty); // false
print(myList.isNotEmpty); // true

myList = [];
print(myList.isEmpty); // true
print(myList.isNotEmpty); // false

А благодаря свойству reversed можно инвертировать


последовательность элементов в списке:
var myList = <int>[1, 2, 2, 5, 2, 10];

// создает список из итерируемой последовательности


var myList2 = List.from(myList.reversed);
print(myList2); // [10, 2, 5, 2, 2, 1]

Теперь разберем, как из списка сформировать новый, скопировав в


него значения элементов из заданного диапазона или по определенному
условию:
var myList = <int>[1, 2, 2, 5, 2, 10];

// параметр end задавать не обязательно


var myList2 = myList.sublist(3);
print(myList2); // [5, 2, 10]

34
print(myList.sublist(2, 4)); // [2, 5]
print(myList.sublist(2, 2)); // [ ], если start == end

// Формируем новый список по условию,


// чтобы в него попали элементы со значениями, кратные 2
myList = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
myList2 = myList.where((element) => element % 2 == 0).toList();
print(myList2); // [2, 4, 6, 8, 10]

Далее рассмотрим, как можно проверить входит ли элемент в список


и осуществить поиск индекса или самого элемента с заданным значением:
var myList = <int>[1, 2, 3, 5, 2, 4];

// проверяем наличие 3-ки в списке


print(myList.contains(3)); // true

// поиск первого вхождения искомого значения


print(myList.indexOf(3)); // 2
// можно задать позицию с какого индекса начнется поиск
print(myList.indexOf(3, 3)); // -1, т.к. элемент с
// таким значением не найден

// поиск последнего вхождения искомого значения


print(myList.lastIndexOf(2)); // 4
print(myList.lastIndexOf(2, 3)); // 1, т.к. поиск идет
// с конца в начало
print(myList.lastIndexOf(2, 0)); // -1

// поиск первого вхождения значения элемента массива


// удовлетворяющего условию, что оно > 3
print(myList.indexWhere((element) => element > 3)); // 3
// 3 – номер индекса

print(myList.indexWhere((element) => element > 3, 4)); // 5

// поиск последнего вхождения значения элемента массива


// удовлетворяющего условию, что оно > 3
print(myList.lastIndexWhere((element) => element > 3)); // 5
print(myList.lastIndexWhere((element) => element > 3, 2));// -1

myList = <int>[5, 9, 4, 5, 6, 8];


// поиск значения первого элемента, удовлетворяющего условию
// что оно кратно 3
print(myList.firstWhere((element) => element % 3 == 0)); // 9

// поиск значения последнего элемента, удовлетворяющего условию


// что оно кратно 3

35
print(myList.lastWhere((element) => element % 3 == 0)); // 6

Иногда бывает необходимо проверить, есть вообще элемент с таким


значением, а не искать его индекс. Для этого воспользуйтесь методом any:
var myList = <int>[5, 9, 4, 5, 6, 8];

// есть ли в списке элемент со значением >= 7?


print(myList.any((element) => element >= 7)); // true
print(myList.any((element) => element >= 10)); // false

Далее посчитаем сколько раз в списке хранится задаваемое значение.


Этот же подход можно использовать для подсчета количества вхождений
символа в строку:
var myList = <int>[1, 2, 2, 5, 2, 10];
print(myList.where((element) => element == 2).length); // 3

String s = 'Мама мыла раму';


List<String> letters = s.split('');
print(letters.where((element) => element == 'а').length); // 4

Порой встречается задача перемешать элементы в списке, чтобы они


расположились в «случайном порядке»:
var myList = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
myList.shuffle();
print(myList); // [7, 3, 6, 9, 4, 5, 10, 1, 8, 2]

String s = 'Мама мыла раму';


List<String> letters = s.split('');
letters.shuffle();
print(letters.join()); // ауы мааммраМл

А теперь рассмотрим, как отсортировать элементы списка. По


умолчанию для чисел осуществляется сортировка по возрастанию, а для
строк применяется лексикографическая сортировка:
var myList = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
myList.shuffle();
print(myList); // [6, 9, 8, 2, 10, 7, 1, 5, 3, 4]

myList.sort(); // сортировка по возрастанию


print(myList); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

myList.shuffle();
// сортировка по убыванию
myList.sort(((a, b) => b.compareTo(a)));
print(myList); // [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

36
String s = 'Мама мыла раму';
List<String> letters = s.split('');
letters.shuffle();
letters.sort();
print(letters); // [ , , М, а, а, а, а, л, м, м, м, р, у, ы]

Передаваемая на вход метода sort анонимная функция должна


возвращать:
− отрицательное число, если значение, с которым происходит
сравнение – больше;
− ноль, если они равны;
− положительное число, если меньше.
Чтобы создать список с фиксированным количеством элементов
(массив), можно воспользоваться следующим способом его объявления:
var myList = List<int>.filled(5, 0);
print(myList); // [0, 0, 0, 0, 0]
myList[0] = 200;
print(myList); // [200, 0, 0, 0, 0]
myList.add(10); // exception: Cannot add to a fixed-length list

Когда вам нужно на основе текущего списка создать новый, с


внесением некоторых изменений в хранящиеся значения, используйте
метод map (поддерживается List, Set и Map). Для примера, давайте
увеличим значения целочисленного списка на 1, а также в 2 раза и
преобразуем числа в строки:
var myList = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

var newList1 = myList.map((element) => element + 1).toList();


print(newList1); // [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

var newList2 = myList.map((element) => element * 2).toList();


print(newList2); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

var strList = myList.map(


(element) => element.toString()
).toList();
print(strList); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Так как метод map возвращает набор элементов (значений)


Iterable<T>, где T – текущий тип элементов списка, то его необходимо
явным образом привести к списку, что мы и делаем, вызывая метод
toList().
Еще один метод, который можно использовать со списками и
множествами – reduce. Он сокращает коллекцию до одного значения путем
итеративного объединения ее элементов. На его вход подается анонимная

37
(лямбда) функция, состоящая из двух аргументов: аккумулирующее
значение, хранящее результат предыдущих вычислений и текущий
итерируемый элемент коллекции. Для работы этого метода необходимо
наличие хотя бы одного элемента в коллекции:
List<int> numbers = [1, 2, 3, 4, 5];

// Вычисляем сумму элементов списка


int sum = numbers.reduce((value, element) => value + element);
print('Сумма элементов списка: $sum'); // 15

// Вычисляем произведение элементов списка


int mul = numbers.reduce((value, element) => value * element);
print('Произведение элементов списка: $mul'); // 120

Как и в Python, в Dart поддерживаются довольно гибкие способы


формирования списков:
var check = false;
var myList = [
'Привет!',
'Это список!',
'Из 3-х эл-ов!',
if (check) 'Ой! Уже из 4-х!'];
print(myList);
// [Привет!, Это список!, Из 3-х эл-ов!] при check = false
// [Привет!, Это список!, Из 3-х эл-ов!, Ой! Уже из 4-х!]
// при check = true

Либо списки могут быть сформированы так:


var intList = [1, 2, 3, 4, 5, 6, 7, 8];
var stringList = [
for (var i in intList) '#$i'
];
print(stringList); // [#1, #2, #3, #4, #5, #6, #7, #8]
// формируем новый список только из тех элементов,
// что делятся на 2 без остатка
var newList = [
for (var i in intList) if (i % 2 == 0) i
];
print(newList); // [2, 4, 6, 8]

К работе с такими типами данных, как List, Set, Map,


пользовательские классы и т. д., необходимо подходить с осторожностью.
Это связано с тем, что при объявлении новой переменной и
инициализацией ее посредством присваивания существующей переменной
в системе, копируется не значение, а ссылка на этот объект:
void main(List<String> arguments) {

38
var myList = [10, 20];
var newList = myList;
newList.add(40);
add(myList, 20);
print(myList); // [10, 20, 40, 20]
print(newList); // [10, 20, 40, 20]
}

Из приведенного примера видно, что переменной newList


присвоилась ссылка на список, с которым была связана переменная myList.
В итоге получилось, что через две переменные мы работаем с одним и тем
же объектом. Для того, чтобы работать с копией списка, то есть с новым
объектом, который будет проинициализирован значениями
существующего объекта, Dart предоставляет следующие механизмы:
void main(List<String> arguments) {
var myList = [10, 3, 4, 1];
var newList = List.from(myList);
newList.add(2);
print('Элементы myList: $myList');
// Элементы myList: [10, 3, 4, 1]

print('Элементы newList: $newList');


// Элементы newList: [10, 3, 4, 1, 2]

var newList1 = [...myList];


newList1.add(77);
print('Элементы myList: $myList');
// Элементы myList: [10, 3, 4, 1]
print('Элементы newList1: $newList1');
// Элементы newList: [10, 3, 4, 1, 77]

var newList2 = []..addAll(myList);


newList2.add(5);
print('Элементы myList: $myList');
// Элементы myList: [10, 3, 4, 1]

print('Элементы newList2: $newList2');


// Элементы newList: [10, 3, 4, 1, 5]
}

Но если элементы списка относятся к типу данных, на которые


копируются ссылки, то следует предельно осторожно копировать такие
объекты, так как изменение элементов создаваемого объекта будут
сказываться на существующем объекте:
void main(List<String> arguments) {
var myList = [[10, 3, 4, 1]];

39
var newList = List.from(myList);
newList[0].add(77);
print('Элементы myList: $myList');
// Элементы myList: [[10, 3, 4, 1, 77]]

print('Элементы newList: $newList');


// Элементы newList: [[10, 3, 4, 1, 77]]
}

Подобные механизмы копирования имеются у множеств и таблиц


(класс Map). Более подробно с методами, которые предоставляет тип данных
List можно ознакомиться в официальной документации Dart [6].

1.4.5. Записи (Record)


Этот тип данных добавился в третьей версии Dart и вместе с
модификатором класса sealed, дал воздохнуть разработчикам полной
грудью, т.к. раньше для поддержки нужной функциональности
приходилось в проект тащить пакет freezed в виде внешней зависимости.
Конечно, он предоставляет куда больший функционал, чем тип Record, но
мало кто его использует на все 100%.
Так что же такого уникального в записях? Они анонимны,
неизменяемы и позволяют группировать разные типы данных. Если вы
знакомы с другими языками программирования, то в голове, наверное,
мелькнуло слово: «кортеж» (tuple / product). Но, нет! Кортеж представляет
собой упорядоченную коллекцию позиционных элементов (полей)
различного типа, тогда как в записи они не упорядочены и могут быть
именованными. То есть к элементам записи нельзя обратиться по индексу,
только через так называемый геттер (getter – метод для получения данных).
По сути, объявляя запись мы объявляем новый тип, состоящий из
нескольких.
Поскольку записи анонимны, то при их объявлении не используется
упоминание типа Record:
var myRecord = (10, '-_-');
// или (int, String) myRecord = (10, '-_-');
print(myRecord); // (10, -_-)

// вывод типа в рантайме


print(myRecord.runtimeType); // (int, String)

// Обращение к элементам записи


print(myRecord.$1); // 10
print(myRecord.$2); // -_-

40
В данном случае мы объявили запись с неименованными
позиционными полями, поэтому обращение к ним производится через
геттер $1 и $2. Такой подход не всегда удобен, т.к. приходится держать в
голове через какой геттер обращаться к необходимым данным, что
обходится использованием именованных полей:
var myRecord = (cost: 10, smile: '-_-');
// или ({int cost, String smile}) myRecord = (cost: 10,
// smile: '-_-');
print(myRecord); // (cost: 10, smile: -_-)

// Вывод типа в рантайме


print(myRecord.runtimeType); // ({int cost, String smile})

// Обращение к элементам записи


print(myRecord.cost); // 10
print(myRecord.smile); // -_-

Именованные поля при объявлении записи задаются посредством


фигурных скобок и могут чередоваться с неименованными (но только в
правой части выражения):
var myRecord = (3.14, cost: 10, smile: '-_-', 22);
// или
// (double, int, {int cost, String smile}) myRecord = (3.14,
// cost: 10, smile: '-_-', 22);
print(myRecord); // (3.14, 22, cost: 10, smile: -_-)

// Вывод типа в рантайме


print(myRecord.runtimeType);
// (double, int, {int cost, String smile})

// Обращение к элементам записи


print(myRecord.cost); // 10
print(myRecord.smile); // -_-
print(myRecord.$1); // 3.14
print(myRecord.$2); // 22

При явном объявлении типа записи, сначала должны идти типы


неименованных позиционных полей, а потом в фигурных скобках
указываются именованные поля с их типом:
// Good
(double, int, {int cost, String smile}) myRecord = (3.14,
cost: 10, smile: '-_-', 22);

// Bad
(double, {int cost, String smile}, int) myRecord = (3.14,
cost: 10, smile: '-_-', 22);

41
// Error: A value of type '(double, int, {int cost,
// String smile})' can't be assigned
// to a variable of type '(double, {int cost, String smile})'

({int cost}, int, {String smile}) myRecord = (5, cost: 10,


smile: '-_-',);
// Error: A value of type '(int, {int cost, String smile})'
// can't be assigned to a variable of type '({int cost})'.

Последовательность позиционных полей при инициализации записи


данными должна сохраняться:
// Good
(int, String) myRecord = (10, '-_-');

// Bad
(int, String) myRecord2 = ('-_-', 10);
// Error: A value of type '(String, int)' can't be
// assigned to a variable of type '(int, String)'.

Теперь же давайте добавим щепотку магии в наш скучный рассказ!


Хорошенько подумайте какой тип данных в следующем коде
автоматически выведется Dart:
var myRecord = ('-_-');

Если вы подумали, что у переменной myRecord будет тип записи


(String), то спешу вас разочаровать:
print(myRecord.runtimeType); // String
print(myRecord); // -_-

В итоге получилась обычная строка! Чтобы не допускать такие ошибки,


лучше всегда за последним элементом объявляемой записи ставьте
запятую:
var myRecord = ('-_-',);

print(myRecord.runtimeType); // (String)
print(myRecord); // (-_-)

Либо используйте при объявлении именованные поля:


var myRecord = (smile: '-_-');

print(myRecord.runtimeType); // ({String smile})


print(myRecord); // (smile: -_-)

42
Еще одним свойством записей является то, что их можно проверять на
равенство. Единственное, что нужно учитывать – тип проверяемых записей
должен совпадать, иначе неровен час допустить ошибку:
var myRecord1 = (10, '-_-');
var myRecord2 = (10, '-_-');
print(myRecord1 == myRecord2); // true

var myRecord3 = (cost: 10, smile: '-_-');


var myRecord4 = (cost: 10, smile: '-_-');
print(myRecord3 == myRecord4); // true

print(myRecord1 == myRecord3); // false

Распаковка записи с позиционными и/или именованными полями


осуществляется следующим образом:
var myRecord1 = (10, '-_-');

var (fist, second) = myRecord1;


print('$fist $second'); // 10 -_-

var myRecord2 = (3.14, cost: 10, smile: '-_-', 22);


var (fistPos, secondPos,
cost: costPos, smile: SmilePos) = myRecord2;
// Сначала распаковываем позиционные,
// именованные можно распаковывать в любом порядке
print(fistPos); // 3.14
print(costPos); // 10
print(SmilePos); // -_-
print(secondPos); // 22

В целом, записи в Dart – довольно мощный инструмент, позволяющий


возвращать из функции сразу несколько значений (объединять несколько
объектов в один), создавать неизменяемые наборы данных и достаточно
изящным образом объявлять и реализовывать Data Transfer Object (DTO –
объект передачи данных).

1.4.6. Множества (Set)


Множество – неупорядоченная совокупность объектов одного типа
данных, в которой не может быть дубликатов. Их часто используются для
двух целей: удаление дубликатов и проверка принадлежности. Как и другие
типы, множество можно объявить явным и неявным образом:
var mySet = <int>{1, 2, 5, 5, 5, 6, 7, 8};
// Set<int> mySet = {1, 2, 5, 5, 5, 6, 7, 8};
// Set<int> mySet = {}; // пустое множество
// var mySet = <int>{}; // пустое множество

43
print(mySet); // {1, 2, 5, 6, 7, 8}

// Создание неизменяемого множества


var str = 'Hello World';
var unmodifiableSet = Set.unmodifiable(str.split(''));
// или
// var unmodifiableSet = const {'a', 'b', 'c'};
print(unmodifiableSet); // {H, e, l, o, , W, r, d}
unmodifiableSet.remove('a'); // Unsupported operation

Для примера давайте рассмотрим ситуацию, в которой у нас имеется


список из различных чисел и нам необходимо отбросить все существующие
в нем дубликаты для формирования нового списка:
var myList = <int>[1, 1, 1, 2, 2, 5, 5, 5, 6, 7, 8];
print(myList); // [1, 1, 1, 2, 2, 5, 5, 5, 6, 7, 8]
var newList = Set<int>.from(myList).toList();
print(newList); // [1, 2, 5, 6, 7, 8]

Аналогичный подход можно применить, если хотим на основе строки


получить не дублируемый список символов, из которых она состоит:
var str = 'Мама мыла раму';
print(str); // Мама мыла раму
var newList = Set<String>.from(
str.toLowerCase().split('')
).toList();
print(newList); // [м, а, , ы, л, р, у]

Как и в случае со строками, значение элементов множества не может


быть изменено:
var mySet = <int>{1, 2, 5, 5, 5, 6, 7, 8};
mySet[0] = 3; // The operator '[]=' isn't
// defined for the type 'Set<int>'.

Добавляются и удаляются элементы в множество следующим


способом:
var mySet = <int>{1, 2, 5, 5, 5, 6, 7, 8};

// Добавление
mySet.add(10);
print(mySet); // {1, 2, 5, 6, 7, 8, 10}

mySet.addAll([11, 12, 13]);


print(mySet); // {1, 2, 5, 6, 7, 8, 10, 11, 12, 13}

// Удаление

44
mySet.remove(1); // удаляем один элемент
print(mySet); // {2, 5, 6, 7, 8, 10, 11, 12, 13}

mySet.removeAll([2, 6]); // удаляем последовательность элементов


print(mySet); // {5, 7, 8, 10, 11, 12, 13}

mySet.removeWhere((element) => element > 10);


// удаление по условию
print(mySet); // {5, 7, 8, 10}

mySet.clear();
print(mySet); // {}

Эти методы мы использовали ранее для добавления и удаления


элементов из списка. При этом, аналогично списку, можно довольно гибко
создавать новые множества:
var myList = <int>[1, 2, 5, 5, 5, 6, 7, 8];
var newSet= <int>{
for (var i in myList) if (i % 2 == 0) i
};
print(newSet); // {2, 6, 8}

А таже множество имеет ряд свойств и методов, которые


рассматривали при знакомстве типом List:
var mySet = <int>{1, 2, 5, 6, 7, 8, 9, 10};

print(mySet.first); // 1
print(mySet.last); // 10
// Внимание, множество не упорядочено!
// Это синтетический пример!!!

print(mySet.length); // 8
print(mySet.isEmpty); // false
print(mySet.isNotEmpty); // true

print(mySet.firstWhere((element) => element > 5)); // 6


print(mySet.firstWhere(
(element) => element > 30, orElse: () => 0
)
); // 0
// аргумент orElse: () => 0 - возвращает значение по умолчанию

print(mySet.lastWhere((element) => element > 5)); // 10


print(
mySet.lastWhere((element) => element > 15, orElse: () => 0)
); // 0

45
print(mySet.where((element) => element % 3 == 0).toSet());
// {6, 9}

Dart поддерживает всего три математические операции над


множествами:
− Объединение (union);
− Разница (difference);
− Пересечение (intersection).
Давайте рассмотрим, как эти операции реализуются в коде:
var mySetA = <int>{1, 2, 5, 6, 7, 8};
var mySetB = <int>{20, 22, 5, 6, 73, 88, 25};
print(mySetA.union(mySetB));
// {1, 2, 5, 6, 7, 8, 20, 22, 73, 88, 25} объединение
print(mySetA.difference(mySetB)); // {1, 2, 7, 8} А-В
print(mySetB.difference(mySetA)); // {20, 22, 73, 88, 25} В-А
print(mySetA.intersection(mySetB)); //{5, 6} пересечение A и В

В каждом приведенном случае, в процессе вызова метода у множества,


создается новый экземпляр класса множества, в который и записывается
результат операции, после чего он возвращается вызываемым методом. То
есть, исходное множество остается без изменений:
var mySetA = <int>{1, 2, 5, 6, 7, 8};
var mySetB = <int>{20, 22, 5, 6, 73, 88, 25};
var newSet = mySetA.union(mySetB);
print(newSet); // {1, 2, 5, 6, 7, 8, 20, 22, 73, 88, 25}
print(mySetA); // {1, 2, 5, 6, 7, 8}
print(mySetB); // {20, 22, 5, 6, 73, 88, 25}

Для проверки вхождения элемента(ов) в множество следует


использовать метод contains и containsAll:
var mySetA = <int>{1, 2, 5, 6, 7, 8};
print(mySetA.contains(2)); // true
print(mySetA.contains(10)); // false

// проверка на то, является ли множество


// подмножеством множества А
print(mySetA.containsAll({5, 6})); // true
print(mySetA.containsAll({5, 9})); // false

Более подробно с методами, которые предоставляет класс Set можно


ознакомиться в официальной документации Dart [7].

46
1.4.7. Таблицы (Map)
Так как не принято переводить название этого типа данных, то будем
придерживаться этой традиции. Map представляет из себя объект, который
связывает ключи и значения. Как ключи, так и значения могут быть
объектами любого типа данных. Ключ по своей сути уникален и не может
встречаться несколько раз, в то время как значение может использоваться
сколько угодно раз и связываться с различными ключами. Говоря простыми
словами, Map представляет собой серию пар «ключ:значение» (MapEntry <K,
V>).
Ниже приведен пример объявления этого типа данных:
var myMap = <String, String>{
//ключ //значение
'first': 'Мама',
'second': 'мыла',
'fifth': 'раму'
};
print(myMap); // {first: Мама, second: мыла, fifth: раму}

var myMap2 = <int, String>{


1: 'Мама',
2: 'мыла',
3: 'раму'
};
print(myMap2); // {1: Мама, 2: мыла, 3: раму}

var myMap3 = Map<String, int>(); // пустой объект


var myMap4 = <String, int>{};

// Создание мэпы из двух списков


List<int> keys = [1, 2, 3, 4, 5];
List<String> values = ['one', 'two', 'three', 'four', 'five'];
var myMap = Map<int, String>.fromIterables(keys, values);
print(myMap); // {1: one, 2: two, 3: three, 4: four, 5: five}

Изменение значения, которое хранится по ключу или добавление


новой пары «ключ:значение» производится практически идентично:
var myMap = <int, String>{
1: 'Мама',
2: 'мыла',
3: 'раму'
};

myMap[1] = 'Бабушка';
// добавляем новую пару «ключ:значение»

myMap [10] = 'по утрам!';

47
print(myMap);
// {1: Бабушка, 2: мыла, 3: раму, 10: по утрам!}

Для добавления сразу нескольких пар «ключ:значение» используйте


метод addAll:
var myMap = <int, String>{
1: 'Мама',
3: 'раму',
2: 'мыла',
};

myMap.addAll({4: 'имя', 5: 'яблоко'});


print(myMap); // {1: Мама, 3: раму, 2: мыла, 4: имя, 5: яблоко}

Попытка извлечь данные по несуществующему ключу не приведет ни


к чему хорошему, так как вернется null:
var myMap = <int, String>{
1: 'Мама',
};

var a = myMap[2];
print(a); // null

Более подробно с null разберемся несколько позже. Сейчас же нас


интересует, как избежать подобной ситуации. А именно – добавить
значение по умолчанию, если при обращении по ключу такого нет в Map,
либо получить значение по уже имеющемуся. Для этих целей существует
метод putIfAbsent. Он принимает на вход значение ключа, и анонимную
функцию, возвращающую значение, которое надо записать, если такого
ключа еще нет и возвращает хранящееся по ключу значение:
var myMap = <int, String>{
1: 'Оо',
};

print(myMap.putIfAbsent(1, () => '!!!')); // Оо


print(myMap); // {1: Оо}

print(myMap.putIfAbsent(3, () => '!!!')); // !!!


print(myMap); // {1: Оо, 3: !!!}

Далее приведены некоторые примеры работы со свойствами


экземпляра Map:
var myMap = <int, String>{
1: 'a', 2: 'b', 3: 'c',
4: 'd', 5: 'e', 6: 'f',
};

48
// Количество элементов
print(myMap.length); // 6

// Список ключей
print(myMap.keys.toList()); // [1, 2, 3, 4, 5, 6]

// Список значений
print(myMap.values.toList()); // [a, b, c, d, e, f]

// Хранит элементы или пустой


print(myMap.isEmpty); // false
print(myMap.isNotEmpty); // true

Для удаления элементов существуют следующие методы:


var myMap = <int, String>{
1: 'a', 2: 'b', 3: 'c',
4: 'd', 5: 'e', 6: 'f',
};

// Удалить пару «ключ:значение»


myMap.remove(1); // указываем ключ
print(myMap); // {2: b, 3: c, 4: d, 5: e, 6: f}

// удалить все связки пар, которые подходят


// под условие, что значение ключа не делится
// на 2 без остатка
myMap.removeWhere((key, value) => (key % 2 != 0));
print(myMap); // {2: b, 4: d, 6: f}

// очистка мапы
myMap.clear();
print(myMap); // {}

Проверить наличие ключа или значения можно следующим способом:


var myMap = <int, String>{
1: 'a', 2: 'b', 3: 'c',
4: 'd', 5: 'e', 6: 'f',
};

print(myMap.containsKey(2)); // true
print(myMap.containsKey(34)); // false

print(myMap.containsValue('b')); // true
print(myMap.containsValue('g')); // false

49
И под конец знакомства с типом Map, давайте рассмотрим некоторые
методы для обновления хранимого по ключу значения:
var myMap = <int, String>{ 1: 'a', 3: 'c', 2: 'f', };

myMap.update(
3, // ключ
(value) => 'k', // новое значение
);
print(myMap); // {1: a, 3: k, 2: f}

myMap.update(
2, // ключ
(value) => '$value!', // новое значение
);
print(myMap); // {1: a, 3: k, 2: f!}

myMap.update(
7, // ключ
(value) => '$value!', // новое значение
ifAbsent: () => 'l', // если ключа нет, то добавить
);
print(myMap); // {1: a, 3: k, 2: f!, 7: l}

myMap.updateAll( // Применяется ко всем значениям


(key, value) => value.toUpperCase(),
);
print(myMap); // {1: A, 3: K, 2: F!, 7: L}

Более подробно с методами, которые предоставляет класс Map можно


ознакомиться в официальной документации Dart [8].

1.4.8. Runes и Symbols


Runes аналогичны строкам с тем отличием, что они представляют
собой последовательность символов в кодировке UTF-32, а не UTF-16. В
этом случае каждый символ представляет собой запись вида '\uXXX', где
ХХХХ четырех числовое значение в шестнадцатеричной системе
счисления. Например, букве «П» соответствует представление '\u041F'.
Объекты типа Symbol представляют собой некоторые идентификаторы
для ссылки на различные элементы API, такие как библиотеки или классы.
Они применяются не так часто и возможно вам никогда не придется их
лицезреть. Для того, чтобы объявить Symbol необходимо использовать «#»:
var mySymbol = #myAPI;
print(mySymbol); // Symbol("myAPI")

50
1.5. Модификаторы final, const и late
Модификаторы final и const по своей сути очень похожи.
Переменные, впереди типа которых ставятся эти модификаторы не могут
изменяться в процессе выполнения программы. Ключевое их отличие
заключается в том, что константные переменные должны быть
инициализированы в момент объявления, а переменные с модификатором
final можно инициализировать позже, но только один раз:
final int a;
const int b = 10;
a = 4; // ok
b = 5; // error: Constant variables can't be assigned a value.
a = 3; // error: The final variable 'a' can only be set once.

Тип, в случае использования данных модификаторов, можно явно не


указывать. Он будет выведен Dart автоматически:
final a;
const b = 10;
const name = 'Петр';

Ключевое слово const можно использовать не только для объявления


неизменяемых переменных. Его еще используют для создания постоянных
значений и объявления конструкторов, посредством которых создаются
неизменяемые значения. Благодаря этому любая переменная может иметь
неизменяемое значение:
// нельзя добавлять элементы средством метода add
var first = const [];
final second = const [];
const third = [];

first = const [10, 3, 23]; // ok


second = const [10, 3, 23]; // error
third = const [10, 3, 23]; // error
first[0] = 30; // Unsupported operation: Cannot modify an
unmodifiable list

Модификатор late был добавлен в версии Dart 2.12 и имеет два


варианта использования:
1. Для объявления переменной, не хранящей значение null,
инициализация которой происходит уже после ее объявления;
2. Для ленивой инициализации переменной.
Отличие final от late заключается в том, что переменные с
модификатором final не могут быть объявлены на верхнем уровне кода.
При этом, переменные, объявленные с одним из этих модификаторов,

51
должны быть проинициализированы до их использования. Иначе в
процессе выполнения приложение выбросит исключение:
late String name;
// final int variable; // ошибка

void main(List<String> arguments) {


final int variable; // ok
// print(name);
// LateInitializationError: Field 'name'
// has not been initialized
name = 'Михаил';
print(name); // ok
}

1.6. Null-безопасность (Null-safety)


Чтобы рассмотреть тему null-безопасности нам придется забежать
немного вперед, но это позволит более подробно объяснить те моменты,
где она используется и почему была введена в версии Dart 2.12.
Основная проблема, когда у нас объект может хранить значение null
связана с тем, что это может вызвать падение программы и увеличение
кодовой базы проекта за счет введения дополнительных проверок на null.
Переменная экземпляра класса имеет некоторое состояние и реализует
поведение. В тоже самое время, если переменная хранит ссылку на null мы
не можем реализовать необходимое поведение в рамках приложения. Null
ничего не знает о поведении объекта, в связи с чем, когда мы пытаемся
вызвать какой-либо метод у переменной, происходит падение приложения.
Начиная с версии Dart 2.12 все объявляемы переменные создаются как
null-safety, то есть переменной объявляемого типа данных нельзя
присвоить значение null. Также, если мы не проинициализировали
переменную до ее использования, компилятор выведет ошибку:
class Cat {
void helloMaster(){
print("Мяу-у-у-у!!!");
}
}

void main(List<String> arguments) {


Cat myCat;
myCat.helloMaster(); // The non-nullable local variable 'myCat'
// must be assigned before it can be used.
}

Давайте рассмотрим следующую ситуацию. Вы живете в квартире с


кошкой. Каждый раз, когда открываете холодильник, она начинает

52
неистово мяукать, пытаясь надавить на жалость, чтобы ее покормили
колбасой. Закодировать эту ситуацию можно написать следующим
образом:
class Cat {
void helloMaster(){
print("Мяу-у-у-у!!!");
}
}

void openFridge(Cat cat){


cat.helloMaster(); // Мяу-у-у-у!!!
}

void main(List<String> arguments) {


var myCat = Cat();
openFridge(myCat);
}

В данном случае в коде подразумевается, что кошка всегда есть в


квартире. А вдруг ее забрал кто-то из родственников к ветеринару или она
настолько любит гулять, что раз в день ваша вторая половинка выходит с
ней на улицу… и как раз в этот момент вы решили открыть холодильник?
Так вот, открывая холодильник вы все равно услышите это протяжное
«Мяу-у-у-у!!!». Как так? Кошки же не должно быть в квартире, а значит
переменная не должна хранить ссылку на экземпляр класса. Она должна
указывать на null.
С учетом null-safety нельзя экземпляру класса кошки, который
объявлен в примере выше, присвоить значение null:
void main(List<String> arguments) {
var myCat = Cat();
myCat = null; // error: A value of type 'Null' can't be assigned
// to a variable of type 'Cat'.
}

В этом случае необходимо явно указать компилятору, что объявляемая


переменная не является null-safety, то есть она может ссылаться на null.
Для этого используется символ «?» сразу после объявления типа
переменной:
void main(List<String> arguments) {
Cat firstCat = null; // error: A value of type 'Null' can't
//be assigned to a variable of type 'Cat'.
Cat? myCat = null; // ок
}

53
Так как теперь у нас кошка может то присутствовать, то отсутствовать
в квартире, то в метод openFridge необходимо передавать аргумент типа
«Cat?» и учитывать, ссылается передаваемый аргумент на null или на
экземпляр класса. Для этого Dart предоставляет несколько возможностей в
виде операторов: «?.», «??» и «!.». Оператор «?.» вызовет метод
экземпляра класса, если переменная не ссылается на null, иначе никакой
метод вызываться не будет:
void openFridge(Cat? cat){
cat?.helloMaster();
}

void main(List<String> arguments) {


Cat? myCat;
Cat? newCat = Cat();
openFridge(myCat); // ничего не выведется
openFridge(newCat); // Мяу-у-у-у!!!
}

Оператор «??» позволит нам организовать заглушку. Если переменная


ссылается на null, то работа будет производиться с экземпляром класса,
переданным в функцию (более подробно этот оператор рассмотрим во
второй главе):
void openFridge(Cat? cat){
final someCat = cat ?? Cat();
someCat.helloMaster();
}

void main(List<String> arguments) {


Cat? myCat;
Cat? newCat = Cat();
openFridge(myCat); // Мяу-у-у-у!!!
openFridge(newCat); // Мяу-у-у-у!!!
}

То есть при использовании этого оператора, даже если кошки нет в


квартире, по комнате все равно пронесется протяжное «Мяу-у-у-у!!!».
Используя последний оператор «!.», вы как бы говорите компилятору,
что хоть переменная и может ссылаться на null, вы более чем уверены, что
она ссылается на экземпляр класса:
void openFridge(Cat? cat){
cat!.helloMaster();
}

void main(List<String> arguments) {


Cat? newCat = Cat();

54
openFridge(newCat); // Мяу-у-у-у!!!
}

В тоже самое время, при передаче в функцию null, приложение


выбросит исключение:
void openFridge(Cat? cat){
cat!.helloMaster(); // _CastError (Null check operator
// used on a null value)
}

void main(List<String> arguments) {


openFridge(null);
}
Таким образом, использование не null-safety переменных оправдано
только в том случае, если по другому логику работы программы не
реализовать.
Дополнительно обращайте внимание на то, что не во всех случаях
значения не null-safety переменных могут присваиваться null-safety
переменным:
void main(List<String> arguments) {
Cat? cat;
Cat newCat = cat; // error: A value of type 'Cat?' can't
// be assigned to a variable of type 'Cat'.
}

void main(List<String> arguments) {


Cat? cat = Cat();
Cat newCat = cat; // ok
}

Аналогичным образом можно объявлять и не null-safety переменные


встроенных типов данных:
int? a;
String? name = null;
// и т.д.

Все не null-safety типы данных наследуются от Object?, а null-safety от


Object, который является базовым классом для всех объектов Dart,
кроме null.
Если хотите поближе познакомиться с null-safety и как выстроен
подход к организации типов данных в Dart, советую ознакомиться со
следующей статьей: https://dart.dev/null-safety/understanding-null-safety

55
1.7. Тип данных dynamic vs Object
Так как Dart позиционировался в качестве замены JavaScript, то без
наличия такого типа данных как dynamic было бы даже нереально
попытаться сказать об этом вслух. Dynamic позволяет разработчику, в
случае необходимости, одной переменной присваивать значения
совершенно различных типов данных:
dynamic myValue = 3;
myValue = 4.10;
print(myValue); // 4.10
myValue = 'oO';
print(myValue); // oO
myValue = [3, 4, 'w'];
print(myValue); // [3, 4, w]
myValue = null;
print(myValue); // null

Но и Object позволяет делать тоже самое:


Object myValue = 3;
myValue = 4.10;
print(myValue); // 4.10
myValue = 'oO';
print(myValue); // oO
myValue = [3, 4, 'w'];
print(myValue); // [3, 4, w]
// значение null возможно только при использовании Object?

Так в чем между ними разница? Согласно спецификации Dart 3


(https://dart.dev/guides/language/spec) dynamic – статический тип, который
является базовым для всех других типов, как и Object, но отличается от них
тем, что разрешает все операции. Это значит, что следующий код
запустится без предупреждений, но в процессе выполнения, приложение
завершится сбоем:
void main(List<String> arguments) {
dynamic myValue = 3;
myValue.run(); // NoSuchMethodError: Class 'int'
// has no instance method 'run'
}

То есть при сборке приложения не осуществляется проверка есть ли


вообще этот метод у объекта, и когда осуществляется безуспешная попытка
его вызвать в процессе работы приложения, оно экстренно завершает
работу. Но что будет, если такой метод у объекта есть?
class Cat {
void helloMaster(){

56
print("Мяу-у-у-у!!!");
}
}

void main(List<String> arguments) {


dynamic myValue = Cat();
myValue.helloMaster(); // Мяу-у-у-у!!!
}

Как видите, программа корректно завершилась. А вот при


использовании Object (или Object?), Dart выдаст ошибку еще на этапе
компиляции:
void main(List<String> arguments) {
Object myValue = 3;
myValue.run();
}
/* Error: The method 'run' isn't defined for the class 'Object'.
- 'Object' is from 'dart:core'.
Try correcting the name to the name of an existing method, or
defining a method named 'run'. */

Если у переменной dynamic типа можно просто вызвать


существующий метод, хранящегося в ней объекта, то тип Object такое
запрещает. При его использовании нужно явно проверить объект какого
типа там сейчас храниться, чтобы иметь возможность вызвать
необходимый метод:
class Cat {
void helloMaster(){ print("Мяу-у-у-у!!!"); }
}

void main(List<String> arguments) {


Object myValue = Cat();
if (myValue is Cat){
myValue.helloMaster(); // Мяу-у-у-у!!!
}
}

Давайте подведем некоторые итоги:


− только два типа Dart могут хранить все значения: Object? и dynamic;
− если хотите указать, что разрешаете для передачи или хранения все
объекты, то используйте Object?;
− если хотите разрешить хранить или передавать все объекты, кроме
null, используйте Object;

57
− при использовании Object? или Object используйте is проверки для
определения типа. Это позволит убедиться, что у объекта имеется
необходимый метод, прежде чем к нему обращаться;
− тип dynamic разрешает все операции (они не отслеживаются на этапе
компиляции), но такое поведение может привести к экстренному
завершению приложения;
Несмотря на наличие такого гибкого механизма, как dynamic, его не
рекомендуется использовать повсеместно, так как это может повлечь за
собой трудно отлавливаемые ошибки не только в самом коде, но и в логике
разрабатываемого приложения. Поэтому вместо dynamic лучше
предпочитать работу с Object. Основное исключением из этого правила
является работа с существующими API, которые используют dynamic.
Например, Map<String, dynamic> используется для представления JSON-
объекта.

1.8. Чтение данных с клавиатуры


В завершении главы нам остается разобраться с вводом данных с
клавиатуры. Их приведению к необходимому типу данных и дальнейшему
использованию. Это необходимо для успешного выполнения заданий
лабораторной работы, располагающейся после вопросов для самопроверки.
То, как мы ранее настроили рабочее окружение – недостаточно, т.к. по
умолчанию в VS Code для Dart используется консоль, из которой
невозможно переопределить поток ввода. Чтобы исправить эту ситуацию,
пройдите по следующим вкладкам меню:
File -> Preferences -> Settings
Далее в графе поиска введите dart cli и поменяйте в параметре Dart:
Cli Console с debugConsole на terminal:

Рисунок 1.12 – Выбор Dart: Cli Console

58
Несмотря на то, что при инициализации строковой переменной или в
функции print мы можем использовать кириллицу, ее ввод с терминала
при работающем приложении не поддерживается. Поэтому вооружаемся
словариком и постигаем дзен английского языка
Для ввода данных с клавиатуры через терминал нам потребуется
импортировать библиотеку dart:io, которая позволяет работать с
операциями ввода-вывода, файлами, сокетами, HTTP и т.д. Мы немного
упростим код и будем считать, что пользователь всегда вводит корректное
значение:
import 'dart:io';

void main() {
print('Введите целочисленное значение');
String? input = stdin.readLineSync(); // синхронный ввод данных

print(input?.runtimeType); // String

var inputInt = int.tryParse(input!); // перевод строки в число


print(inputInt.runtimeType); // int

print('Введенное значение: $inputInt');


}

Теперь нажмите F5 и в открывшимся терминале введите значение 5,


после чего завершите ввод данных используя клавишу Enter. В итоге, ваша
программа должна отработать следующим образом:

Рисунок 1.13 – Пример работы приложения

Теперь давайте введем последовательность чисел через пробел и на их


основе сформируем список:
import 'dart:io';

59
void main() {
print('Введите числа через пробел: ');
String? input = stdin.readLineSync();
List<String> inputValues = input!.split(' ');
List<int> numbers = inputValues.map(int.parse).toList();
print('Введенный список: $numbers');
print('Размер списка: ${numbers.length}');
}

Рисунок 1.14 – Пример ввода списка

Для ввода Map нам понадобятся два списка:


import 'dart:io';

void main() {
print('Введите список ключей: ');
String? keysInput = stdin.readLineSync();

print('Введите список значений: ');


String? valuesInput = stdin.readLineSync();

List<String> keys = keysInput!.split(' ');

List<String> valuesStr = valuesInput!.split(' ');


List<int> values = valuesStr.map(int.parse).toList();

var myMap = Map.fromIterables(keys, values);


print('Введенная мэпа: $myMap');
print('Размер мэпы: ${myMap.length}');
}

Рисунок 1.15 – Пример ввода данных для Map


Ввод множества ничем серьезным не будет отличаться от ввода
списка:
import 'dart:io';

void main() {
print('Введите числа через пробел: ');
String? input = stdin.readLineSync();

60
List<String> inputValues = input!.split(' ');

var setString = Set<String>.from(inputValues);


var setInt = Set<int>.from(inputValues.map(int.parse));
print('Числовое множество: $setString');
print('Строковое множество: $setInt');
print('Размер множества: ${setString.length}');
}

Рисунок 1.16 – Пример ввода множества

Резюме по главе
В первой главе помимо краткой истории языка программирования
Dart, были рассмотрены его основные особенности, встроенные типы
данных и способы работы с ними. Приведенные способы работы со
списками, множествами и т. д., постоянно будут встречаться в ходе работы
над реальными проектами, так что лучше их изучить в самом начале пути
и не заглядывать каждый раз в справочник для уточнения по методам и
способам работы с ними.
Отдельно стоит отметить концепцию null-safety, что позволяет не
беспокоиться о наличии значения null в переменных и предоставляет
разработчикам механизм введения в код разрабатываемого приложения не
null-safety типов данных и операторы для работы с ними.
Также нами были рассмотрены правила наименования переменных.
Что касается того, с заглавной или строчной буквы будет начинаться имя
переменной, функции, класса и т.д., эти правила носят рекомендательный
характер. Это совершенно не значит, что вы обязаны их придерживаться,
но само следование этим правилам является «хорошим тоном» при
написании приложений. Такое положение дел связано с тем, что его
придерживается огромное количество программистов, в соответствии с
чем, код становится более читаемый. Согласитесь, куда приятнее вникать в
то, что написано в коде, когда все следуют одному и тому же соглашению
по наименованию.

Вопросы для самопроверки


1. Для замены какого языка программирования разрабатывался Dart?
2. Какие 2 платформы используются в компиляторе Dart? Для чего они
используются и какие между ними различия?

61
3. Какие ключевые особенности у языка программирования Dart?
4. Какие встроенные типы данных предоставляет Dart?
5. Какому числовому типу данных можно присваивать как
целочисленные, так и вещественные значения?
6. В чем отличие типа String от Rune?
7. Какие ограничения имеются при работе со строками?
8. Что такое список? Как использовать список в Dart в качестве массива?
Какие методы списка вы знаете? Расскажите за что они отвечают.
9. Что такое запись? Чем запись отличается от кортежа и какие типы
полей у нее существуют? Как обращаться к полям записи?
10. Что такое множество? Приведите его ключевые особенности. Какие
методы множества вы знаете? Расскажите за что они отвечают.
11. Что такое мэпа (таблица, карта, Map)? Приведите ее ключевые
особенности. Какие методы Map вы знаете? Расскажите за что они
отвечают.
12. Какой тип данных необходимо использовать, если необходимо
объявляемой переменной присваивать значения различных типов
данных?
13. В чем сходство и отличие dynamic и Object? Когда и что из них лучше
использовать?
14. В чем схожи, а чем отличаются модификаторы final и const?
15. В чем схожи, а чем отличаются модификаторы final и late?
16. Перечислите ключевые моменты концепции null-безопасности (null-
safety)?
17. На сколько типов делятся комментарии в Dart? Приведите их
примеры.
18. Какие правила при наименовании переменных существуют?

Лабораторная работа № 1. Встроенные типы данных


Dart
Цель работы: познакомиться с основными способами работы с
встроенными типами данных средствами языка программирования Dart.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Для ввода текстовых данных не использовать кириллицу;
• Использование условных конструкций и циклов
ЗАПРЕЩЕНО.

62
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

Часть 1. Задания по строкам

1. Напишите приложение, где пользователь вводит строку и букву,


наличие которой предстоит проверить в введенной строке. Выведите в
терминал полученный результат в терминал, а также индекс последнего
вхождения буквы в строку.
2. Напишите приложение, где пользователь вводит строку и букву.
Выведите в терминал длину строки, также индекс первого и последнего
вхождения буквы в строку.
3. Напишите приложение, где пользователь вводит строку и букву.
Посчитайте сколько раз заданная буква входит в строку и выведите
полученный результат, а также индекс первого вхождения буквы в строку,
в терминал.
4. Напишите приложение, где пользователь вводит строку и два символа
(например a и b). Замените в строке все символы «a» на «b» и выведите
полученный результат в терминал.
5. Напишите приложение, где пользователь вводит слово и на его основе
создается новая переменная, сформированная из первого, среднего и
последнего символов введенного слова. Полученный результат выведите в
терминал. Например: «Привет!» -> «Пв!»
6. Напишите приложение, где пользователь вводит слово и на его основе
создается новая переменная, сформированная из трех средних символов.
Полученный результат выведите в терминал. Например: «МамаМылаРаму»
-> «ыла».
7. Напишите приложение, где пользователь вводит две строки str1 и str2.
Программа должна создать новую строку str3 путем добавления str2 в
середину str1. Полученный результат выведите в терминал. Например: str1
= «Мама», str2 = «Раму» -> «МаРамума».
8. Напишите приложение, где пользователь вводит слово и на его основе
формирует новая переменная путем удаления символов из первой строки
(с нулевого элемента по 3-й). Полученный результат выведите в терминал.
Например: «МамаМылаРаму» -> «МылаРаму».
9. Напишите приложение, где пользователь вводит две строки str1 и str2.
Программа должна создать новую строку str3 состоящую из первого,
среднего и последнего символов строк str1 и str2. Полученный результат
выведите в терминал. Например: str1 = «Мама», str2 = «Утром» -> «МУмрам».

63
10. Напишите приложение, где пользователь вводит строку.
Программа должна ее инвертировать и вывести в терминал. Например:
«Йо-хо-хо!» -> «!ох-ох-оЙ».
11. Напишите приложение, где пользователь вводит строку. Используя
пробелы, разбейте ее на части, сформировав список. Полученный список и
его размер выведите в терминал.
Таблица 1.1
Варианты работ
№ варианта Номера заданий к варианту
1. 1, 2, 3
2. 1, 3, 5
3. 4, 5, 10
4. 5, 6, 9
5. 1, 3, 7
6. 6, 10, 11
7. 7, 9, 11
8. 3, 4, 5
9. 3, 4, 9
10. 9, 10, 11
11. 2, 7, 8
12. 7, 8, 11
13. 5, 7, 8
14. 4, 5, 10
15. 1, 6, 9
16. 5, 6, 7
17. 6, 8, 10
18. 2, 6, 11
19. 3, 4, 7
20. 3, 5, 8

Часть 2. Задания по спискам

1. Напишите приложение, позволяющее пользователю вводить список


вещественных значений. Выведите в терминал его размер, значения
первого и последнего элемента.
2. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений и число A, на которое необходимо увеличить
значения элементов списка, после чего добавить A в конец списка.
Выведите в терминал полученный результат.

64
3. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений и число A, на которое необходимо уменьшить
значения элементов списка, после чего добавить A добавить в начало
списка. Выведите в терминал полученный результат.
4. Напишите приложение, позволяющее пользователю вводить список
вещественных значений и два числа (например a и b). Программа должна
вставить число «a» на позицию «b». Выведите в терминал полученный
результат.
5. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений и число. Удалите из списка все элементы равные
введенному числу. Выведите в терминал полученный результат.
6. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений и число. Посчитайте сколько в списке находится
элементов, равных введенному значению и выведите в терминал
полученный результат.
7. Напишите приложение, позволяющее пользователю вводить список
вещественных значений. Найдите элемент с максимальным значением и
выведите в терминал полученный результат. Разрешается использовать
тернарный оператор ?:.
8. Напишите приложение, позволяющее пользователю вводить список
вещественных значений. Найдите элемент с минимальным значением и
выведите в терминал полученный результат. Разрешается использовать
тернарный оператор ?:.
9. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений и число A. Сформируйте новый список, значения
элементов которого > A. Выведите в терминал полученный результат.
10. Напишите приложение, позволяющее пользователю вводить
список целочисленных значений и число A. Сформируйте новый список,
значения элементов которого <= A. Выведите в терминал полученный
результат.
11. Напишите приложение, позволяющее пользователю вводить
список целочисленных значений и число A. Сформируйте новый список,
значения элементов которого кратны A. Выведите в терминал полученный
результат.
12. Напишите приложение, позволяющее пользователю вводить
список вещественных значений. Округлите элементы вещественного
списка до ближайшего максимального целого значения и сформируйте
новый целочисленный список. Выведите в терминал полученный
результат.
13. Напишите приложение, позволяющее пользователю вводить
список вещественных значений. Округлите элементы вещественного

65
списка до ближайшего минимального целого значения и сформируйте
новый целочисленный список. Выведите в терминал полученный
результат.
14. Напишите приложение, позволяющее пользователю вводить два
списка. Сформируйте из них один новый список и выведите в терминал
полученный результат.
15. Напишите приложение, позволяющее пользователю вводить
список целочисленных значений и число A. Удалите из списка все
элементы, значения которых кратны A, а после найдите сумму оставшихся.
Выведите в терминал полученный результат.
16. Напишите приложение, позволяющее пользователю вводить
список строковых значений и два числа (например a и b). Программа
должна создать новый список из элементов, которые лежат в диапазоне от
индекса «a» по «b». Выведите в терминал полученный результат.
17. Напишите приложение, позволяющее пользователю вводить
список строковых значений и два числа (например a и b). Программа
должна удалить в списке все элементы, которые лежат в диапазоне от
индекса «a» по «b». Выведите в терминал полученный результат.
18. Напишите приложение, позволяющее пользователю вводить
список целочисленных значений. Сформируйте новый список, элементы
которого – четные значения. Выведите в терминал полученный результат.
19. Напишите приложение, позволяющее пользователю вводить
список целочисленных значений. Сформируйте новый список, элементы
которого – нечетные значения. Выведите в терминал полученный
результат.
20. Напишите приложение, позволяющее пользователю вводить
список целочисленных значений и два числа (например a и b). Посчитайте
сумму элементов списка, которые лежат в диапазоне от индекса «a» по «b».
Выведите в терминал полученный результат.
Таблица 1.2
Варианты работ
№ варианта Номера заданий к варианту
1 1, 2, 3, 4
2 5, 6, 7, 8
3 9, 10, 11, 12
4 13, 14, 15, 16
5 17, 18, 19, 20
6 1, 6, 8, 9
7 2, 4, 17, 19
8 3, 4, 9, 16
9 2, 4, 17, 20

66
№ варианта Номера заданий к варианту
10 3, 6, 9, 16
11 1, 12, 15, 17
12 3, 9, 15, 20
13 4, 5, 6, 12
14 4, 8, 14, 18
15 5, 9, 12, 15
16 6, 9, 13, 18
17 7, 13, 14, 16
18 10, 11, 16, 20
19 12, 16, 17, 20
20 4, 11, 13, 16

Часть 3. Задания по множествам


1. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений. Удалите все дублирующиеся значения и
вычислите сумму оставшихся. Выведите в терминал полученный результат.
2. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений. Удалите все дублирующиеся значения и
вычислите произведение оставшихся. Выведите в терминал полученный
результат.
3. Напишите приложение, позволяющее пользователю вводить строку.
Сформируйте и выведите в терминал список уникальных символов в
введенной строке.
4. Напишите приложение, позволяющее пользователю вводить два
списка. Сформируйте из них одно новое множество и выведите в терминал
полученный результат.
5. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений и число A. Сформируйте множество, значения
элементов которого > A. Выведите в терминал полученный результат.
6. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений и число A. Сформируйте множество, значения
элементов которого <= A. Выведите в терминал полученный результат.
7. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений и число A. Сформируйте множество, значения
элементов которого кратны A. Выведите в терминал полученный результат.
8. Напишите приложение, позволяющее пользователю вводить два
множества А и В. Проверьте, является ли множество А подмножеством
множества В и выведите в терминал полученный результат.

67
9. Напишите приложение, позволяющее пользователю вводить два
целочисленных списка. Найдите сумму их уникальных элементов и
выведите в терминал полученный результат.
10. Напишите приложение, позволяющее пользователю вводить два
целочисленных множества А и В. Найдите их пересечение, рассчитайте
сумму элементов, которые в него попадают и выведите в терминал
полученный результат.
11. Напишите приложение, позволяющее пользователю вводить два
целочисленных множества А и В. Найдите разницу А-В, уменьшите на 2
значения элементов нового множества и выведите в терминал полученный
результат.
12. Напишите приложение, позволяющее пользователю вводить два
множества А и В. Найдите разницу А-В, увеличьте 10 значения элементов
нового множества и выведите в терминал полученный результат.
13. Напишите приложение, позволяющее пользователю вводить
множество и число A. Проверьте входит ли A в множество и выведите в
терминал полученный результат.
14. Напишите приложение, позволяющее пользователю вводить
целочисленное множество и число A. Удалите из множества значение A,
после чего уменьшите хранящиеся в нем значения на А и посчитайте сумму
элементов. Выведите в терминал полученный результат.
15. Напишите приложение, позволяющее пользователю вводить строку.
Сформируйте новую строку, состоящую только из уникальных символов, и
выведите ее в терминал.
16. Напишите приложение, позволяющее пользователю вводить список
целочисленных значений и число A. Сформируйте множество, значения
элементов которого > A и вычислите сумму его элементов. Выведите в
терминал полученный результат.
17. Напишите приложение, позволяющее пользователю вводить два
множества А и В. Найдите разницу А-В и оставьте в получившимся
множестве только те элементы, значения которых > 10. Выведите в
терминал полученный результат.
18. Напишите приложение, позволяющее пользователю вводить два
множества А и В. Найдите разницу В-А и оставьте в получившимся
множестве только те элементы, значения которых <= 10. Выведите в
терминал полученный результат.
Таблица 1.3
Варианты работ
№ варианта Номера заданий к варианту
1 14, 16, 17, 18
2 9, 11, 13, 16
3 8, 10, 11, 12

68
№ варианта Номера заданий к варианту
4 4, 8, 10, 14
5 5, 14, 15, 17
6 5, 7, 10, 18
7 3, 5, 6, 10
8 1, 2, 7, 10
9 1, 3, 10, 13
10 1, 11, 14, 15
11 4, 8, 10, 12
12 5, 14, 17, 18
13 9, 11, 12, 17
14 10, 13, 14, 16
15 8, 14, 17, 18
16 7, 8, 14, 15
17 10, 12, 13, 15
18 6, 10, 11, 13
19 4, 8, 13, 14
20 3, 9, 16, 17

Часть 4. Задания по мэпам (таблицам / картам)

1. Напишите приложение, позволяющее пользователю вводить Map<int,


String> и число А. Удалите все элементы с ключами, значения которых
кратны А и выведите в терминал полученный результат.
2. Напишите приложение, позволяющее пользователю вводить Map<int,
int> и число А. Удалите все элементы с ключами которые кратны А, потом
вычислите сумму всех значений и выведите в терминал полученный
результат.
3. Напишите приложение, позволяющее пользователю вводить Map<int,
int> и два числа: А и В. Проверьте существует ли в словаре ключ А, значение
равное В и выведите полученный результат в терминал.
4. Напишите приложение, позволяющее пользователю вводить Map<int,
int> и два числа: А и В. Если в структуре нет элемента с ключом А, добавьте
в Map по ключу А значение В. Выведите Map и значение, хранящееся по
ключу А в терминал.
5. Напишите приложение, позволяющее пользователю вводить Map<int,
String> и строку А. Удалите все элементы значения которых равны А и
выведите в терминал полученный результат.
6. Напишите приложение, позволяющее пользователю вводить Map<int,
double> и число A. Удалите все элементы Map, значения которых > A и
выведите в терминал полученный результат.

69
7. Напишите приложение, позволяющее пользователю вводить 2 объекта
Map<int, int>. Объедините введенные структуры данных и выведите в
терминал полученный результат, а также количество хранимых в новом
объекте элементов.
8. Напишите приложение, позволяющее пользователю вводить Map<int,
String> и строку А. Удалите все элементы значения которых начинаются с
подстроки А и выведите в терминал полученный результат.
9. Напишите приложение, позволяющее пользователю вводить Map<int,
String> и строку А. Удалите все элементы значения которых заканчиваются
подстрокой А и выведите в терминал полученный результат.
10. Напишите приложение, позволяющее пользователю вводить Map<int,
double> и число A. Удалите все элементы значения ключей которых <= A.
Выведите в терминал полученный результат и сумму всех значений Map.
11. Напишите приложение, позволяющее пользователю вводить список
Map<int, int > и число A. Удалите все элементы значения и ключи которых
кратны А и выведите в терминал произведение оставшихся ключей и
значений.
Таблица 1.4
Варианты работ
№ варианта Номера заданий к варианту
1. 2, 7, 8
2. 7, 8, 11
3. 3, 4, 7
4. 3, 5, 8
5. 6, 8, 10
6. 2, 6, 11
7. 5, 7, 8
8. 4, 5, 10
9. 9, 10, 11
10. 6, 10, 11
11. 7, 9, 11
12. 3, 4, 5
13. 3, 4, 9
14. 1, 2, 3
15. 1, 3, 5
16. 4, 5, 10
17. 1, 6, 9
18. 5, 6, 7
19. 5, 6, 9
20. 1, 3, 7

70
Глава 2. Операторы, pattern matching и
управляющие конструкции

2.1. Основные операторы Dart


Операторы в Dart классифицируются следующим образом:
− арифметические операторы;
− операторы сравнения;
− операторы проверки;
− операторы присваивания;
− логические операторы;
− побитовые операторы;
− условные выражения;
− каскадная запись;
− другие операторы.
Арифметические операторы представлены в таблице ниже. К ним
также относятся как префиксные, так и постфиксные операторы
инкремента и декремента значения переменной:
Таблица 2.1
Арифметические операторы
Оператор Описание Примеры
10 + 5 = 15
+ Сложение
10 + -3 = 7
15 - 5 = 10
- Вычитание 25 - -3 =28
11.98 - 7 = 4.98
2*2=4
* Умножение 7 * 3.2 = 22.4
-2 * 4 = -8
12 / 4 = 3
/ Деление
7 / 3 = 2.334
4%2=0
% Деление по модулю 9%2=1
13.2 % 4 = 1.199
17 ~/ 5 = 3
~/ Целочисленное деление
10 ~/ 3 = 3
++var Префиксный инкремент var = var + 1;
var++ Постфиксный инкремент var = var + 1;
--var Префиксный декремент var = var - 1;
var-- Постфиксный декремент var = var - 1;

71
Далее рассмотрим операторы сравнения:
Таблица 2.2
Операторы сравнения
Оператор Описание Примеры
1 == 1 (true)
== Проверка на равенство true == false (false)
"test" == "test" (true)
1 != 2 (true)
!= Проверка на неравенство false != false (false)
"test" != "Test" (true)
Проверка на то, что значение
3 > 2 (true)
> левого операнда больше
2 > 3 (false)
правого
Проверка на то, что значение
3 < 2 (false)
< левого операнда меньше
2 < 3 (true)
правого
Проверка на то, что значение
3 >= 1 (true)
>= левого операнда больше или
3 >= 3 (true)
равно правого
Проверка на то, что значение
5 <= 5 (true)
<= левого операнда меньше или
-4 <= -21 (false)
равно правого

В следующей таблице представлены операторы проверки, которые


удобно использовать для проверки типов во время выполнения кода:

Таблица 2.3
Операторы проверки
Оператор Описание Примеры
Данный оператор
as используется для приведения
одного типа данных к другому
double a = 3.4;
true, если объект имеет
is print(a is double); // true
указанный тип
print(a is String); // false
double a = 3.4;
true, если объект не имеет
is! print(a is! double); // false
указанный тип
print(a is! String); // true

Перечень существующих операторов присваивания в Dart представлен


в таблице 2.4:

72
Таблица 2.4
Операторы присваивания
Оператор Описание Примеры
var a = 3;
= Оператор присваивания
var a = 2.3;
a +=b, var a = 3;
+=
что равносильно a = a+b; a +=2; // 5
var a = 3;
-= a -=b, что равносильно a = a-b;
a -= -2; // 5
a *=b, var a = 3;
*=
что равносильно a = a*b; a *=2; // 6
var a = 9;
/= a /=b, что равносильно a = a/b;
a /=3; // 3
a %=b, var a = 9;
%=
что равносильно a = a%b; a %=3; // 0
a ~/=b, var a = 9;
~/=
что равносильно a = a~/b; a ~/= 3; // 3
>>= a >>=b, var a = 8;
что равносильно a = a>>b; a >>= 2; // 2
<<= a <<=b, var a = 8;
что равносильно a = a<<b; a <<= 2; // 32
^= a ^=b, var a = 7;
что равносильно a = a^b; a ^= 2; // 5
&= a &=b, var a = 7;
что равносильно a = a&b; a &= 2; // 2
|= var a = 7;
a |=b, что равносильно a = a|b;
a |= 2; // 7

Для рассмотрения работы побитовых операторов необходимо ввести


два значения a и b. Примем a = 33 в десятичной системе счисления, что
эквивалентно 0010 0001 в двоичной системе счисления и b = 87 (0101 0111):

Таблица 2.5
Побитовые операторы
Оператор Описание Примеры
Побитовое логическое «И»
& a & b = 0000 0001 (1)
между операндами
Побитовое логическое
| a | b = 0111 0111 (119)
«ИЛИ» между операндами

73
Оператор Описание Примеры
Побитовое логическое
^ «исключающее ИЛИ» между a ^ b = 0111 0110 (118)
операндами
Логическое отрицание.
Инвертирует значения бит
~ ~a = 1101 1110 (-34)
операнда к которому
применяется
Побитовый сдвиг влево.
Применяется к одному
операнду. Эквивалентно a << 2 = 1000 0100
<<
умножению на 2n, где n – (33 * 22 = 132)
число, на которое
производится сдвиг
Побитовый сдвиг вправо.
Применяется к одному
a >> 2 = 0000 0100
>> операнду. Эквивалентно
(33 ~/ 22 = 8)
целочисленному делению на
2n
Беззнаковый побитовый
сдвиг вправо. Отличается от
оператора >> тем, что
выполняется логический
(сдвиг без знака), а не
арифметический сдвиг.
-a >> 2 = - 1001 (-9)
Младшие биты
>>> -a >>> 2 =
отбрасываются, а остальные
4611686018427387895
сдвигаются и старшие биты
заменяются нулем. Таким
образом, затирается знак
минус, если такой сдвиг
осуществляется на
отрицательном числе.

К логическим операторам относят обычные логические операции, как


«И», «ИЛИ» и «НЕ»:

74
Таблица 2.6
Логические операторы
Оператор Описание Примеры
true and true = true
Логическое «И» между
&& Во всех остальных
операндами
случаях false
false and false = false
Логическое «ИЛИ» между
|| Во всех остальных
операндами
случаях true
!false = true
! Логическое отрицание
!true = false
В следующей таблице приведены существующие в Dart условные
выражения:

Таблица 2.7
Условные выражения
Выражение Описание
Тернарный оператор. Если условие
истинно, то вычисляется и возвращается
condition ? expr1 : expr2
expr1, в противном случае вычисляется и
возвращается значение expr2
Если expr1 не равно null, возвращается
expr1 ?? expr2 его значение, иначе вычисляется и
возвращается значение expr2

Операторы, которые не вошли в перечисленную классификацию


операторов Dart относятся к категории «другие операторы»:

Таблица 2.8
Другие операторы
Оператор Описание
() Используется для вызова функций
Ссылается на значение в списке в соответствии с
[]
задаваемым индексом
. Позволяет обратиться к свойствам объекта
Как и оператор «.», только дополнительно выполняет
проверку на null. В том случае если объект хранит
?. значение null обращения к его свойству не
производится и не выбрасываются никакие
исключения

75
Последний вид операторов, который предоставляет Dart – это
каскадные операторы (.., ?..), которые позволяют выполнять
последовательность операций над одним и тем же объектом:
// ex2_1.dart
class Cat {
late final int _old;
late final String _name;
set old(int old) {
this._old = old;
}

set name(String name) {


this._name = name;
}

void helloMaster(){
print("Мяу-у-у-у!!!");
}
}

void main(List<String> arguments) {


var cat = Cat()
..name = 'Муся'
..old = 4
..helloMaster();
// аналогично записи ниже
var newCat = Cat();
newCat.name = 'Муся';
newCat.old = 4;
newCat.helloMaster();
}

2.2. Что такое Pattern Matching и Destructuring?


Прежде чем мы приступим к рассмотрению того, какими способами
можно управлять потоком выполнения программы, давайте затронем
такой механизм как Pattern Matching (сопоставление с шаблоном) и
Destructuring (деструктурирование). Следует отметить, что практическое
применение Pattern Matching будет рассмотрено в следующих разделах
главы, а деструктурирование в этом.
При переходе со второй версии на третью в Dart внесли довольно
существенные изменения, среди которых и был Pattern Matching.
Сопоставление с шаблоном используется для проверки того, соответствует
ли значение определенным характеристикам. Так, например, можно
проверить равно ли оно константе, имеет определенную форму, тип (форму

76
и тип) или его соответствие заданным критериям. Pattern Matching
поддерживает рекурсивное сопоставление с подшаблонами, благодаря
чему он может выполнять сопоставление свойств объекта или элементов
коллекции (List, Map). До Dart 3, чтобы выполнить такие проверки
приходилось писать больше кода и он не всегда был удобен для восприятия.
Данный механизм уже десятилетиями использовался в
функциональных языках программирования. Со временем он начал
появляться в языках программирования общего назначения (Java, Python и
т.д.) и вот, теперь, добрался до Dart. Что же такого дает Pattern Matching, что
его решили реализовать? А все просто, как 2х2. Более быстрое написание
читаемого кода! Если вы думаете, что такое не стоит потраченных усилий
разработчиков Dart, то вернитесь к своему любому проекту спустя полгода.
Это хорошо, если к нему будет документация. Но чаще всего на нее
забивают и обновляют в последнюю очередь (если вообще ведут). И вот в
этой ситуации сам код и является документацией! Если он читаем, то вы
быстро вспомните как тут все работает, а если нет… себя точно молодцом
не назовете =)
Деструктурирование шаблона (Pattern Destructuring) позволяет более
удобным способом извлекать данные из объекта. То есть, если объект
соответствует шаблону, его свойства или элементы можно преобразовать в
переменные. Ниже приведен пример, как осуществлялось
деструктурирование списка во второй версии Dart и насколько лаконичней
эта операция смотрится в третьей:
// ex2_2.dart
void main() {
// Dart 2
final myList = [1, 2];
var a = myList[0];
var b = myList[1];
print('a: $a, b: $b'); // a: 1, b: 2

// Dart 3
var [a1, b1] = myList; // или final [a1, b1] = myList;
print('a1: $a1, b1: $b1'); // a1: 1, b2: 2
}
Далее рассмотрим коллекции и объекты, к которым можно применять
данную операцию.

2.2.1. Деструктурирование списка


С деструктурированием списка из двух элементов вроде как
разобрались. А что делать, если их 3? Какой-то элемент нам вообще не
нужен или из большого списка посредством деструктурирования

77
необходимо извлечь конкретные элементы и записать их значения в
переменные? Давайте с этим разбираться.
Количество переменных, на которое распаковывается список должно
соответствовать его количеству элементов:
final myList = [1, 2];
final [a, ] = myList; // Bad state: Pattern matching error

Когда какой-то элемент для нас не важен, при деструктурировании, с


левой стороны выражения, следует использовать символ нижнего
подчеркивания:
// ex2_3.dart
var myList = [1, 2];
final [a, _] = myList;
print('a: $a'); // a: 1

myList = [1, 2, 3, 4];


final [b, _, c, _] = myList;
print('b: $b, c: $c'); // b: 1, c: 3

Если список содержит большое количество элементов, до для пропуска


какой-то части используется троеточие:
// ex2_4.dart
var myList = [1, 2, 3, 4, 5, 6, 7];
final [a, ..., b] = myList;
print('a: $a, b: $b'); // a: 1, b: 7

final [c, d, ...] = myList;


print('c: $c, d: $d'); // c: 1, d: 2

final [..., e, f] = myList;


print('e: $e, f: $f'); // e: 6, f: 7

Когда нужно пропускаемые таким образом элементы записать в


список, добавьте после троеточия имя:
// ex2_5.dart
var myList = [1, 2, 3, 4, 5, 6, 7];
final [a, ...b, c] = myList;
print('a: $a, b: $b, c: $c'); // a: 1, b: [2, 3, 4, 5, 6], c: 7

final [d, e, ...f] = myList;


print('d: $d, e: $e, f: $f'); // d: 1, e: 2, f: [3, 4, 5, 6, 7]

final [...g, h, i] = myList;


print('g: $g, h: $h, i: $i'); // g: [1, 2, 3, 4, 5], h: 6, i: 7

78
За одно деструктурирование списка в операции можно указать только
одно троеточие:
var myList = [1, 2, 3, 4, 5, 6, 7];
final [a, ...b, ...c] = myList;
// Error: At most one rest element is allowed in a
// list or map pattern.

В качестве «уличной магии» давайте рассмотрим, как


деструктурирование списка можно использовать для обмена значениями
между двумя списками:
// ex2_6.dart
var listA = [1, 2, 3];
var listB = [4, 5, 6];

[listA, listB] = [listB, listA];


print(listA); // [4, 5, 6]
print(listB); // [1, 2, 3]

2.2.2. Деструктурирование записи


По факту мы уже встречались с деструктурированием записи при
знакомстве с этим типом данных. Тогда это было названо распаковкой,
чтобы не напугать более точными определениями раньше времени:
// ex2_7.dart
var myRecord1 = (10, '-_-');

var (fist, second) = myRecord1;


print('$fist $second'); // 10 -_-

var (a, _) = myRecord1;


print('$a'); // 10

var myRecord2 = (3.14, cost: 10, smile: '-_-', 22);


var (fistPos, secondPos,
cost: costPos, smile: SmilePos) = myRecord2;
print('$fistPos, $secondPos, $costPos, $SmilePos');
// 3.14 22 10 -_-

var (b, _, cost: _, smile: c) = myRecord2;


print('$b, $c'); // 3.14 -_-

Перечислять то количество переменных при деструктурировании


записи, сколько у нее полей и указывать, какое из них для нас не важно,
порой, может быть тем еще удовольствием. В этом случае мы можем из
нужных полей записи сформировать новую и уже к ней применить
операцию деструктурирования:

79
// ex2_8.dart
var myRecord2 = (3.14, cost: 10, smile: '-_-', 22);
var (b, c) = (myRecord2.$1, myRecord2.smile);
print('$b, $c'); // 3.14 -_-

Если же перед вами стоит задача обмена значениями между


переменными, лучшего подхода, чем деструктурирование записи не найти:
// ex2_9.dart
var fisrt = 10;
var second = 20;
(fisrt, second) = (second, fisrt);
print('fisrt: $fisrt, second: $second');
// fisrt: 20, second: 10

2.2.3. Деструктурирование мэпы (таблицы)


Деструктурировать мэпу (таблицу) можно различными способами.
Самый простой случай – отсутствие вложений. Тут даже можно не
прибегать к символу нижнего подчеркивания:
// ex2_10.dart
final myMap = {"first": 1, "second": 2};
print(myMap); // {first: 1, second: 2}

final {"first": first, "second": second} = myMap;


print("$first, $second"); // 1, 2

final {"first": a} = myMap;


print("$a"); // 1

final {"second": b} = myMap;


print("$b"); // 2

При наличии вложений, когда в качестве значения по ключу хранится


список или еще одна мэпа, к ним также можно применять операцию
деструктурирования:
// ex2_11.dart
Map<String, List<int>> myMap = {
'first': [1, 2, 3],
'second': [4, 5, 6],
};

var {'first': [a, _, b]} = myMap;


print('a: $a, b: $b'); // a: 1, b: 3

Map<int, Map<String, int>> myMap2 = {


1: {'a': 1, 'b': 2},
2: {'c': 3, 'd': 4},

80
};

var {1: {'a': c, 'b': d}} = myMap2;


print('c: $c, d: $d'); // c: 1, d: 2

Чаще всего вам придется иметь дело с деструктурированием мэпы при


работе с JSON, когда данные хранятся в формате Map<String, dynamic>:
// ex2_12.dart
Map<String, dynamic> myMap = {
'person1': ['Alex', 22],
'person2': ['Max', 52],
'employee': {
'name': 'John',
'age': 25,
'salary': 1000,
'boss': {
'name': 'Alex',
'idEmployees': [1, 2, 3],
}
},
};

var {'person1': [name, age]} = myMap;


print('person1: $name, age: $age'); // person1: Alex, age: 22

var {
'employee': {
'name': empName,
'age': empAge,
'salary': empsalary,
'boss': {
'name': bossName,
},
}
} = myMap;
print('employee: $empName, age: $empAge, salary: $empsalary, boss:
$bossName');
// employee: John, age: 25, salary: 1000, boss: Alex

var {'employee': {'boss': {'idEmployees': [...ids]}}} = myMap;


print('ids: $ids'); // ids: [1, 2, 3]

2.2.4. Деструктурирование экземпляра класса


В данном случае нам снова придется забежать немного вперед и
рассмотреть, как осуществляется деструктурирование экземпляра класса.
Обязательным условием такой операции является указание имени класса,

81
экземпляр которого будет деструктурирован, за которым в круглых скобках
указываются его имена полей, которые будут распакованы в переменные:

// ex2_13.dart
class Employee {
final String name;
final int age;
final int salary;

Employee(this.name, this.age, this.salary);


}

void main() {
var employee = Employee("John", 25, 50000);
var Employee(name: empName, age: empAge,
salary: empSalary) = employee;
print("Name: $empName, Age: $empAge, Salary: $empSalary");
// Name: John, Age: 25, Salary: 50000

employee = Employee("Alex", 19, 3000);


var Employee(name: empName1, salary: empSalary1) = employee;
print("Name: $empName1, Salary: $empSalary1");
// Name: Alex, Salary: 3000
}

Имеется возможность сократить запись операции


деструктурирования. Для этого переменная, в которую будет распаковано
значение должна носить имя, идентичное полю класса:
// ex2_14.dart
var employee = Employee("John", 25, 50000);
var Employee(:name, :age, :salary) = employee;
print("Name: $name, Age: $age, Salary: $salary");
// Name: John, Age: 25, Salary: 50000

employee =Employee("Alex", 19, 3000);


Employee(:name, :salary) = employee;
print("Name: $name, Salary: $salary");
// Name: Alex, Salary: 3000

Employee(:age,) = employee;
print("Age: $age"); // Age: 19

Обратите внимание на то, что, так как переменные при первом


деструктурировании экземпляра класса были объявлены через var, то их
можно использовать и в последующих аналогичных операциях, не прибегая
к созданию новых.

82
2.3. Управление потоком выполнения кода
Для управления потоком выполнения кода в Dart используются
следующие виды операторов:
− Условный оператор if, if-case;
− Тернарный оператор;
− Оператор ??;
− Операторы циклов (for, for-in, while и do-while);
− Операторы потока выполнения (break, continue, return);
− Оператор выбора потока выполнения (switch-case) и switch-
выражения.

2.3.1. Условный оператор if


Оператор if, с необязательным оператором else, выбирает действия,
которые будут выполняться в процессе работы программы в зависимости
от условий (результата выполнения выражений), которые проверяются в
скобках, после объявления оператора if. Общая форма конструкции «если-
то-иначе» представлена ниже:
if (условие1){
блок1
}
else if (условие2){
блок 2
}

else if (условие(n-1)){
блок (n-1)
}
else{
блок (n)
}

Условие представляет собой проверку на истинность и если в


результате вычисления при проверке условия возвращается true, то будет
выполнен код из блока 1, иначе будет выполняться проверка в блоках else
if. В том случае, если не выполнилось ни одно условие, то выполнится код
из блока else:
// ex2_15.dart
void main(List<String> arguments) {
var a = 10;
var b = 30;
var c = 7;
if (a > b) {
print('a > b');

83
} else if (a > c){
print('a > c'); // a > c
}
else{
print('Ни то и ни другое');
}
}

Для примера того, что в ряде случаев можно обойтись и без


необязательных блоков, давайте напишем программу, сравнивающую
значения два числа и выводящую в терминал максимальное из них:
// ex2_16.dart
void main() {
var a = -3, b = 20;
var c = 0;

if (a > b) {
c = a;
} else if (a < b) {
c = b;
}else{
c = b;
}

print('Max: $c'); // Max: 20


}

В глаза сразу бросается то, что от блока else if можно отказаться и


получить тот же самый результат:
void main() {
var a = -3, b = 20;
var c = 0;

if (a > b) {
c = a;
} else{
c = b;
}

print('Max: $c'); // Max: 20


}

Но и блок else лишний, если переменную с проинициализировать


одним из проверяемых значений:
void main() {
var a = -3, b = 20;
var c = b;

84
if (a > b) {
c = a;
}
print('Max: $c'); // Max: 20
}

Выражение, возвращающее булевый результат в операторе if


способно быть как простым (рассмотренный ранее вариант), так и
сложносоставным. Для второго варианта между выражениями
используются логические операторы (&&, ||, !), со следующими таблицами
истинности:

Таблица 2.9
Таблица истинности логической операции «И»
a b a && b
false false false
true false false
false true false
true true true

Таблица 2.10
Таблица истинности логической операции «ИЛИ»
a b a || b
false false false
true false true
false true true
true true true

Таблица 2.11
Таблица истинности логической операции «Отрицание»
a !a
false true
true false

Представим ситуацию, что нам необходимо проверить, входит ли


число в необходимый диапазон значений:
// ex2_17.dart
void main() {
var a = 10;
if (a > 5 && a < 20) {
print('Значение входит в промежуток');
// Значение входит в промежуток

85
} else {
print('Значение не входит в промежуток');
}
}

Либо надо проверить, что переменная больше определенного


значения и в тоже время кратна другому:
// ex2_18.dart
void main() {
var a = 10;
if (a > 5 && a % 2 == 0) {
print('Значение удовлетворяет условиям');
// Значение удовлетворяет условиям
} else {
print('Значение не удовлетворяет условиям');
}
}

И если по первому блоку нужно двигаться в двух случаях, которые


могут быть равновероятны, используйте логическое «ИЛИ»:
// ex2_19.dart
void main() {
var a = -10;
if ((a > 5 && a % 2 == 0) || a < 0) {
print('Значение удовлетворяет условиям');
// Значение удовлетворяет условиям
} else {
print('Значение не удовлетворяет условиям');
}
}

Чем более сложное выражение для проверки, тем больше шансов


допустить какую-нибудь ошибку. Поэтому рекомендуют отдельные части
таких выражений заключать в скобки, чтобы можно было проследить
последовательность логических операций.

2.3.2. Оператор if-case


Начиная с третьей версии в Dart появилась поддержка оператора if-
case, позволяющего более удобным способом проверять объекты на
необходимый тип, форму и т.д. Общий вид принципа работы этого
оператора можно представить следующим образом:
if (значение case шаблон){
блок1
}
else if (значение case шаблон){

86
блок 2
}

else if (значение case шаблон){
блок (n-1)
}
else{
блок (n)
}

Представим, что у нас на входе список и необходимо проверить,


действительно ли в нем 2 значения, после чего деструктурировать его по
переменным:
// ex2_20.dart
void main() {
List<int> myList = [1, 2, 3];

if (myList case [int x, int y]){


print('2 значения $x и $y');
} else if (myList case [int x, ..., int y]){
print('3 и более значений'); // 3 и более значений
}

myList = [1, 2];


if (myList case [int x, int y]){
print('2 значения $x и $y'); // 2 значения 1 и 2
}
}

Или нужно валидировать JSON по одному или нескольким из его


полей, чье значения записать в переменные для последующей работы с
ними в рамках блока if-case:
// ex2_21.dart
void main() {
Map<String, dynamic> myMap = {
'person1': ['Alex', 22],
'person2': ['Max', 52],
'employee': {
'name': 'John',
'age': 25,
'salary': 1000,
'boss': {
'name': 'Alex',
'idEmployees': [1, 2, 3],
}
},
};

87
if (myMap case {'person1': [String name, int age]}) {
print('person1: $name, age: $age'); // person1: Alex, age: 22
}

if (myMap
case {
'employee': {
'name': String empName,
'age': int empAge,
'salary': int empsalary,
'boss': {
'name': String bossName,
},
}
}) {
print(
'employee: $empName, age: $empAge, salary: $empsalary, boss:
$bossName',
);
// employee: John, age: 25, salary: 1000, boss: Alex
}

if (myMap case {'employee': {'boss':


{'idEmployees': List<int> ids}}}) {
print('ids: $ids'); // ids: [1, 2, 3]
}

if (myMap case {'person2': [String name, String age]}) {


print('person1: $name, age: $age');
} else{
print('Ошибка вышла =('); // Ошибка вышла =(
}
}

При работе с классами оператор if-case можно использовать как для


проверки, относится ли объект к нужному типу данных, так и сразу для его
деструктурирования, чтобы перенести необходимые значения полей в
переменные, с которыми в блоке if и продолжится работа:
// ex2_22.dart
class Employee {
final String name;
final int age;
final int salary;

Employee(this.name, this.age, this.salary);


}

88
class Cat {
final String name;
final int age;

Cat(this.name, this.age);
}

void main() {
dynamic obj = Employee('John', 30, 1000);

if (obj case Cat(:String name, : int age)) {


print('Cat name is $name, age is $age');
}

if (obj case Employee(:String name, : int age, : int salary)) {


print(
'Employee name is $name, age is $age, salary is $salary'
);
} // Employee name is John, age is 30, salary is 1000

if (obj case Employee(:String name)) {


print('Employee name is $name'); // Employee name is John
}

obj = Cat('Tom', 20);


if (obj case Employee(:String name, : int age)) {
print('Employee name is $name, age is $age');
}

if (obj case Cat(:String name, : int age)) {


print('Cat name is $name, age is $age');
// Cat name is Tom, age is 20
}
}

На проверяемые объекты, а точнее на их значения, которые в процессе


сопоставления записываются в переменные можно накладывать Guard
clause (защитное условие). Для этого после case с указанием шаблона
следует ключевое слово when, за которым идет условное выражение:
// ex2_23.dart
class Cat {
final String name;
final int age;

Cat(this.name, this.age);
}

89
void main() {
Cat obj = Cat('Tom', 30);
if (obj case Cat(: int age) when age > 20) {
print('Cat name is ${obj.name}, age is $age');
}

obj = Cat('Tommy', 3);


if (obj case Cat(: int age) when age > 20) {
print('Cat name is ${obj.name}, age is $age');
}

var list = [8, 3];


if (list case [int a, int b] when a + b > 10) {
print('list sum is ${a + b}');
}
}

// Cat name is Tom, age is 30


// list sum is 11

2.3.3. Тернарный оператор ?:


Общий вид записи тернарного оператора можно представить
следующим способом:
condition ? expr1 : expr2

Если условие истинно, то вычисляется и возвращается expr1, в


противном случае вычисляется и возвращается значение expr2. Для
примера давайте посредством данного оператора реализуем поиск
максимального из двух значений:
// ex2_24.dart
var a = 5, b =10;
var c = a > b ? a : b;

print('Max is $c'); // 10

Тернарный оператор имеет более лаконичную запись, чем оператор if


и обычно применяется для простого ветвления (да/нет), когда результат
должен быть записан в переменную, передан в функцию/метод, либо
возвращен из них. Конечно, он может быть и вложенным, но тогда сильно
страдает читаемость кода:
// ex2_25.dart
var a = 25, b =10, c=17;
// максимум из трех чисел
var max = a > b ? a > c ? a : c < b ? b : c : b > c ? b : c;

90
print('Max is $max'); // 25

2.3.4. Оператор ??
Общий вид записи данного оператора можно представить следующим
способом:
expr1 ?? expr2

Если expr1 не равно null, возвращается его значение, иначе


вычисляется и возвращается значение expr2.
Ранее мы рассматривали использование оператора ?? как заглушку, но
у него куда больше вариантов применения. Например, если объекты
реализуют один интерфейс и expr1 в данный момент времени хранит null,
то создастся объект expr2, приведется к общему интерфейсу и присвоится
переменной интерфейсного типа, с которой потом идет взаимодействие в
клиентском коде.
Либо у нас имеется две функции и если первая возвращает null, то в
работу включается вторая. Для этого примера нам придется немного
забежать вперед и реализовать функцию с параметром по умолчанию,
равным null:
// ex2_26.dart
int? calculate([int? a]) {
if (a == null) {
return a;
}
return a * 7;
}

void main() {
var c = calculate() ?? calculate(10);
// попробуйте переписать для оператора ?:
print(c); // 70

var d = calculate(3) ?? calculate(10);


print(d); // 21
}

Без оператора ?? код, представленный выше, записывался бы


следующим образом:
// ex2_27.dart
void main() {
int? c;
if (calculate() != null){
c = calculate();

91
} else{
c = calculate(10);
}
print(c); // 70

if (calculate(3) != null){
c = calculate(3);
} else{
c = calculate(10);
}
print(c); // 21
}

2.3.5 Операторы циклов (for, for-in, while и do-while)


Цикл (оператор) for позволяет выполнить блок кода определенное
количество раз. В общем виде структуру этого цикла можно представить
следующим образом:
for (действие до начала цикла; условие выхода из цикла;
действие по завершению текущего шага цикла) {
// блок кода
}

Любой из элементов (действие или условие выхода) при объявлении


цикла может быть не задан. Так, например, следующий цикл является
«бесконечным», поскольку при его объявлении не указано условие
завершение цикла:
for(;;){
}

В следующем коде цикл for выполнится 5 раз, после чего напечатается


строка:
// ex2_28.dart
var str = '';
for(var i = 0; i <= 4; i++){
str += i.toString();
}
print(str); // 01234

Этот код может быть переписан следующим образом:


// ex2_29.dart
var str = '';
var i = 0;
for(; i <= 4;){
str += i.toString();
i++;

92
}
print(str); // 01234

По сути что один, что другой цикл выполняет одинаковые действия.


Все отличие заключается в чистоте и читаемости кода. В следующем
примере давайте используем цикл for для заполнения списка:
// ex2_30.dart
var myList = <int>[];
for(var i = 0; i <= 4; i++){
myList.add(i);
}
print(myList); // [0, 1, 2, 3, 4]

for(var i = 4; i >= 0; i--){


myList.add(i);
}
print(myList); // [0, 1, 2, 3, 4, 4, 3, 2, 1, 0]

Если использовать цикл for для обхода элементов списка, то код будет
смотреться следующим образом:
// ex2_31.dart
var myList = <int>[0, 1, 2, 3, 4, 4, 3, 2, 1, 0];
var sum = 0;
for(var i = 0; i < myList.length; i++){
sum += myList[i];
}

print('sum: $sum'); // 20

Но лучше для итерации по коллекциям использовать цикл for-in,


который позволяет перебирать значения, возвращаемые любым объектом,
поддерживающим итерацию: списки, множества и т. д., то есть объект
должен представлять собой коллекцию из элементов:
// ex2_32.dart
var myList = <int>[for (var i = 0; i<= 3; i++) i];
for (var it in myList){
print(it); // 0 1 2 3
}

var mySet = <int>{1, 2, 5, 6, 7, 8};


for (var it in mySet){
print(it); // 1 2 5 6 7 8
}

До Dart 3 итерация по объектам типа Map<K,V> могла осуществляться


несколькими способами:

93
// ex2_33.dart
var myMap = <int, String>{
1: 'Мама',
2: 'мыла',
3: 'раму'
};

myMap.forEach((key, value) {
print('$key => $value');
});
for (var it in myMap.entries) {
// it - MapEntry<int, String>, хранит ключ и значение
// текущего элемента итерации
print('${it.key} => ${it.value}');
}
1 => Мама
2 => мыла
3 => раму

Начиная с третьей версии цикл for-in поддерживает


деструктурирование элементов итерируемой коллекции, что позволяет
переписать предыдущий пример следующим образом:
for (var MapEntry(:key, :value) in myMap.entries) {
print('$key => $value');
}

Аналогично можно делать и с элементами списка:


// ex2_34.dart
class Cat {
final String name;
final int age;

Cat(this.name, this.age);
}

void main() {
var catList = <Cat>[for (var i = 0; i<= 3; i++)
Cat('Tommy$i', i+1)];

for (var Cat(:name, :age) in catList) {


print('$name is $age years old');
}
}
// Tommy0 is 1 years old
// Tommy1 is 2 years old
// Tommy2 is 3 years old
// Tommy3 is 4 years old

94
Для того, чтобы пройти по всем элементам строки используйте
следующий вид записи цикла:
// ex2_35.dart
var myStr = 'Hi!';
for(var i = 0; i <myStr.length; i++){
print(myStr[i]); // H i !
}

Также можно применить связку таких методов, как split() и


forEach():
// ex2_36.dart
var myStr = 'Hi!';
myStr.split('').forEach((element) {
print(element); // H i !
});

Или воспользоваться циклом for-in, вызвав у строки метод split():


// ex2_37.dart
var str = 'Hi!';
for (var str in str.split('')){
print(str); // H i !
}

Принципы работы циклов while и do-while довольно похожи.


Ключевое различие заключается в том, что цикл while может ни разу не
выполниться. Это связано с тем, что сначала проверяется условие его
выполнения. В том случае, если оно возвращает значение false, поток
выполнения кода переходит к командам и операторам, расположенным за
циклом. Цикл do-while выполнится хотя бы один раз, после чего уже идет
проверка условия: выполнить цикл по новой или выйти из цикла.
Структуру этих циклов можно представить следующим образом:
while (условие выхода из цикла) {
// блок кода
}

do{
// блок кода
}
while (условие выхода из цикла);

Ниже приведен пример, как можно использовать эти циклы вместо


цикла for:
// ex2_38.dart
var myStr = 'Hi!';
var i = 0;
while(i < myStr.length){

95
print(myStr[i]); // H i !
i++;
}

i = 0;
do{
print(i); // 0 1 2
i++;
}while(i < 3);

2.3.6 Операторы потока выполнения (break, continue, return)


Оператор continue используется для немедленного перехода в начало
цикла, в котором он был вызван:
// ex2_39.dart
var i = 13;
while(i > 0){
i--;
if (i % 2 == 0){
continue;
}
print(i); // 11 9 7 5 3 1
}

Оператор break используется для немедленного выхода из цикла, в


котором он был вызван. Представим ситуацию, что у нас имеется
вложенный цикл (цикл в цикле). При использовании оператора break
внутри вложенного цикла, поток управления перейдет к циклу верхнего
уровня, который продолжит выполняться. Ниже приведен пример выхода
из бесконечного цикла, посредством оператора break:
// ex2_40.dart
var i = 33;
while(true){
if (i <= 3){
break;
}
i--;
}
print(i); // 3

При использовании меток («название_метки:») с оператором break


можно сразу выйти из нескольких вложенных друг в друга циклов:
void main(List<String> arguments) {
// ex2_41.dart
mainLoop:
for (var i = 0; i < 3; i++) {

96
// метка «mainLoop:»
print('start main loop');
for (var x = 0; x < 3; x++) {
print('start second loop');
for (var y = 0; y < 3; y++) {
print('start external loop');
if (y >= 1) {
print('break external loop');
break mainLoop;
}
print('end external loop');
}
print('end second loop');
}
print('end main loop');
}
print('end loops');
start main loop
start second loop
start external loop
end external loop
start external loop
break external loop
end loops

Оператор return возвращает результат функции, switch-выражения,


метода класса и завершает их выполнение:
// ex2_42.dart
void main(List<String> arguments) {
var a = 10;
print(a); // 10
return;

// код ниже не имеет смысла,


// т.к. он не будет выполнен
var b = 20;
print(b);
}

2.3.7. Оператор выбора потока выполнения (switch-case)


Если блок кода в вашей программе состоит из большого числа цепочек
if-else if-else стоит призадуматься о использовании другой условной
конструкции управления потоков выполнения программы: switch-case. В
Dart 2 оператор switch-case позволял сравнивать целочисленные,
строковые переменные или константы времени компиляции с помощью
оператора сравнения «==». Начиная с Dart 3, сравнение осуществляется с

97
помощью шаблонов (Pattern Matching), указываемых после ключевого
слова case.
В общем виде конструкцию switch-case можно записать следующим
образом:
switch(объект):
case шаблон1:
блок1
case шаблон2:
блок2
case шаблон3:
блок3
...
default:
блок (n) # действие по умолчанию

Рассмотрим принципы работы switch-case, на примере, когда на вход


подается строковый тип данных:
// ex2_43.dart
void main(List<String> arguments) {
var command = 'close'; // проверяемое значение
switch (command) {
case 'close': // если значение в command == 'close'
print('closed'); // <- closed
case 'open': // если значение в command == 'open'
print('open');
default: // если не подошел ни один вариант
print('default');
}
}

Если мы хотим, чтобы между открытием и закрытием не было


разницы, немного модифицируем предыдущий пример:
// ex2_44.dart
var command = 'close';
switch (command) {
case 'close':
case 'open':
print('open/close'); // <- open/close
default:
print('default');
}

В том случае, когда необходимо после завершения одного из блоков


case перейти к выполнению другого, то есть ввести некоторый аналог
машины состояния, можно использовать оператор continue и метку
(«название_метки:»):

98
// ex2_45.dart
void main(List<String> arguments) {
var command = 'open';
switch (command) {
prepare:
case 'prepare':
print('prepare'); // 2 <- prepare
case 'close':
print('closed');
continue prepare;
case 'open':
print('open'); // 1 <- open
continue prepare;
default:
print('default');
}
}

Необязательный блок default выполнится в том случае, когда не


подойдет ни одно из значений из объявленных блоков case:
// ex2_46.dart
var command = 'Oo';
switch (command) {
prepare:
case 'prepare':
print('prepare');
case 'close':
print('closed');
continue prepare;
case 'open':
print('open');
continue prepare;
default: // или case _:
print('default'); // <- default
}

Помимо операторов break и continue блок case может завершаться


операторами return или throw.
А теперь перейдем к «уличной магии» switch-case, что свалилась на
разработчиков в Dart 3 и начнем с switch-выражения, результат которого
можно присваивать переменной, возвращать из функции и т.д.:
// ex2_47.dart
var a = 10;
var b = switch (a) {
2 => 5 + a,
3 => 4 + a,
_ => 10 - a, // значение по умолчанию

99
};

print(b); // 0

Такой способ работает только при замене case на =>, где в левой части
указывается шаблон для сравнения, а в правой возвращаемый результат.
Шаблоны могут быть различного вида и состоять из логических операций
«И», «ИЛИ» (логический шаблон) и операций сравнения (реляционный
шаблон):
// ex2_48.dart
void main() {
var myList = [1, 4, 5, 2, 33, 45, 90];

for (var element in myList) {


switch (element) {
case 2 || 3 || 5:
print('a ($element) is 2, 3, or 5');
case >= 30 && <= 40:
print('a ($element) is between 30 and 40');
default:
print('Default value: $element');
}
}
}
/* Default value: 1
Default value: 4
a (5) is 2, 3, or 5
a (2) is 2, 3, or 5
a (33) is between 30 and 40
Default value: 45
Default value: 90 */

// или

void main() {
var myList = [1, 4, 5, 2, 33, 45, 90];
var newList = <int>[];

for (var element in myList) {


newList.add(
switch (element) {
2 || 3 || 5 => element + 1,
>= 30 && <= 40 => element * 2,
< 50 => element - 5,
== 1 => element + 3,
_ => element,
});

100
}

print(newList); // [-4, -1, 6, 3, 66, 40, 90]


}

Так как оператор switch-case осуществляет сопоставление шаблонов,


то его можно использовать со списками, мэпами, записями и классами
(объектами). Для начала приведем пример полного сопоставления шаблона
с подаваемым списком:
// ex2_49.dart
void main() {
var myList = [
[],
[1],
[1, 2, 3],
[1, 2, 3, 4, 5],
];

var myStr = '';


for (var element in myList) {
switch (element) {
case [1]:
myStr += '1 ';
case [1, 2, 3]:
myStr += '3 ';
case []:
myStr += '0 ';
default:
myStr += '! ';
}
}

print(myStr); // 0 1 3 !
}

Значения какого-то элемента списка не является важным? Или все не


важны? Тогда используем нижнее подчеркивание:
// ex2_50.dart
void main() {
var myList = [
[],
[3],
[1, 5, 3],
[1, 2, 3, 4, 5],
];

var myStr = '';

101
for (var element in myList) {
switch (element) {
case [_]:
myStr += '1 ';
case [1, _, 3]:
myStr += '3 ';
case [_, _, _, 4, 5]:
myStr += '0 ';
default:
myStr += '! ';
}
}

print(myStr); // ! 1 3 0
}

Хочется работать со списками произвольной длины, акцентируя


внимание только на его начальных или последних элементах? Вспоминаем
про троеточие:
// ex2_51.dart
void main() {
var myList = [
[],
[3],
[1, 5, 3],
[1, 2, 3, 4, 5],
[7, 2, 3, 4, 5, 8],
[7, 2, 3, 4, 5, 8, 9],
];

var myStr = '';


for (var element in myList) {
switch (element) {
case [1, 2, ..., _]:
myStr += '5 ';
case [1, ..., 3]:
myStr += '3 ';
case [..., 9]:
myStr += '7 ';
case [7, ...]:
myStr += '6 ';
case [...]: // список любой длины
myStr += '0 ';
}
}
print(myStr); // 0 0 3 5 6 7
}

102
Нужно учесть ситуацию, что на определенном индексе списка могут
попадаться разные элементы? Это тоже не проблема:
// ex2_52.dart
void main() {
var myList = [
[1, 2, 3],
[3, 2, 3, 4, 5],
[7, 2, 3, 4, 5, 8],
[7, 4, 3, 4, 5, 8, 9],
];

var myStr = '';


for (var element in myList) {
switch (element) {
case [1 || 3 || 7, 2 || 4, ...]:
myStr += '5 ';
case [1, ..., 3]:
myStr += '3 ';
case [..., 9]:
case [...]: // список любой длины
myStr += '0 ';
}
}

print(myStr); // 5 5 5 5
}

К порядку сопоставления нужно подходить с особой осторожностью.


Если первым блоком case будет идти слишком общий шаблон, то все
остальные варианты просто не будут рассматриваться. Поэтому такие
варианты шаблонов лучше смещать в конец:
// ex2_53.dart
void main() {
var myList = [
[],
[3],
[1, 5, 3],
[1, 2, 3, 4, 5],
[7, 2, 3, 4, 5, 8],
[7, 2, 3, 4, 5, 8, 9],
];

var myStr = '';


for (var element in myList) {
switch (element) {
case [...]: // список любой длины
myStr += '0 ';

103
case [1, 2, ..., _]:
myStr += '5 ';
case [1, ..., 3]:
myStr += '3 ';
case [..., 9]:
myStr += '7 ';
case [7, ...]:
myStr += '6 ';
}
}

print(myStr); // 0 0 0 0 0 0
}

Теперь переключимся на сопоставление с мэпой (таблицей) и


реализуем как проверку на полное соответствие, так и на то, чтобы она
содержала необходимые ключи с хранимым по ним значениям:
// ex2_54.dart
void main() {
var myList = [
<int, String>{1: 'Oo', 2: '-_-'},
<int, int>{1: 1},
<String, double>{'^_^': 3.14, "(╯'□')╯︵ ┻━┻": 0.000001},
<String, dynamic>{
'person1': ['Alex', 22],
'person2': ['Max', 52],
'employee': {
'name': 'John',
'age': 25,
'salary': 1000,
'boss': {
'name': 'Alex',
'idEmployees': [1, 2, 3],
}
},
},
];

for (var element in myList) {


switch (element) {
case {1: 'Oo', 2: '-_-'}: // полное сопоставление
print('Full match');
case {"(╯'□')╯︵ ┻━┻": 0.000001}:
// ищем только одну пару "ключ-значение"
print("(╯'□')╯︵ ┻━┻");
case {'employee': {'name': 'John'}}:

104
// если есть элементы с такими данными
print('Hi, John!');
default:
print('No match');
}
}
}
// Full match
// No match
// (╯'□')╯︵ ┻━┻
// Hi, John!

Не важно какое значение хранится по ключу в указанном шаблоне?


Вспоминаем правила деструктурирования мэпы:
// ex2_55.dart
for (var element in myList) {
switch (element) {
case {1: 'Oo', 2: String smile}:
print('Full match with $smile');
case {"(╯'□')╯︵ ┻━┻": double zero}:
print("(╯'□')╯︵ ┻━┻");
case {
'employee': {
'name': String name,
'boss': {
'name': String bossName,
'idEmployees': List<int> idEmployees
},
},
}:
print(
'employee: $name, boss: $bossName, idEmployees: $idEmployees'
);
default:
print('No match');
}
}
// Full match with -_-
// No match
// (╯'□')╯︵ ┻━┻
// employee: John, boss: Alex, idEmployees: [1, 2, 3]

Так и чешутся руки добавить поддержку нескольких ключей или


значений при сопоставлении? Закатай губу Доставай попкорн и погнали:
// ex2_56.dart
for (var element in myList) {

105
switch (element) {
case {1: 'Oo'} || {2: '-_-'}:
print('Full match');
case {"(╯'□')╯︵ ┻━┻": double zero} && {'^_^': 3.14}:
print("(╯'□')╯︵ ┻━┻");
default:
print('No match');
}
}
// Full match
// No match
// (╯'□')╯︵ ┻━┻
// No match

Далее перейдем к работе с записями. Как и в рассмотренных ранее


случаях существуют разные способы задания шаблона, с которым будет
производиться сопоставление: полное и частичное. Начнем с первого вида
сопоставления:
// ex2_57.dart
void main() {
var myList = [
(10, '-_-'),
('^_^', 10, 20),
(5, smile: 'O_O'),
(5, smile: '-_-'),
(4, smile: '-_-', pruff: [23, 45]),
];

for (var element in myList) {


switch (element) {
case (10, '-_-') ||
(4, smile: '-_-', pruff: [23, 45]) ||
(5, smile: '-_-'):
print('Full match: $element');
default:
print('No match: $element');
}
}
}
// Full match: (10, -_-)
// No match: (^_^, 10, 20)
// No match: (5, smile: O_O)
// Full match: (5, smile: -_-)
// Full match: (4, pruff: [23, 45], smile: -_-)

106
Значения некоторых полей шаблонной записи не имеют значения?
Давайте отбросим их, оставив нижнее подчеркивание:
// ex2_58.dart
for (var element in myList) {
switch (element) {
case (10, '-_-') ||
(4, smile: '-_-', pruff: [23, 45]) ||
(_, smile: _):
print('Full match: $element');
default:
print('No match: $element');
}
}
// Full match: (10, -_-)
// No match: (^_^, 10, 20)
// Full match: (5, smile: O_O)
// Full match: (5, smile: -_-)
// Full match: (4, pruff: [23, 45], smile: -_-)

Необходимо, чтобы входное значение хотя бы частично


соответствовало шаблону и была возможность работать с пропускаемыми
при сопоставлении значениями? Правила деструктурирования записи тебе
в помощь
// ex2_59.dart
void main() {
var myList = [
(10, '-_-'),
('^_^', 10, 20),
(5, smile: 'O_O'),
(5, smile: '-_-'),
(4, smile: '-_-', pruff: [23, 45]),
(4, pruff: [23, 45, 50]),
];

for (var element in myList) {


switch (element) {
case (10, '-_-') ||
(4, smile: '-_-', pruff: [23, 45]):
print('Full match: $element');
case (5, smile: String smile):
print('Partial match with smile: $smile');
case (_, pruff: List<int> pruff):
print('Partial match with pruff: $pruff');
default:
print('No match: $element');
}
}

107
}
// Full match: (10, -_-)
// No match: (^_^, 10, 20)
// Partial match with smile: O_O
// Partial match with smile: -_-
// Full match: (4, pruff: [23, 45], smile: -_-)
// Partial match with pruff: [23, 45, 50]

Вы, наверное, уже думаете: «Да когда же закончится этот раздел


главы?». Ну…. потерпите совсем чуть-чуть… сейчас мы переходим к
шаблонам объектов и после них, сменим «уличную магию» на
экстерминатус вашего пукана обзор еще одного удивительного
нововведения для оператора switch-case в Dart 3.
Начнем с полного и частичного сопоставления шаблона. Так как у
объектов есть поля со значениями, то можно указать ожидаемые значения
всех полей или каких-то конкретно:
// ex2_60.dart
class Employee {
final String name;
final int age;
final String position;
final int salary;

Employee(this.name, this.age, this.position, this.salary);

@override
String toString() {
// чтобы печатать состояние объекта в терминале
return 'Employee{$name, $age, $position, $salary}';
}
}

void main() {
var myList = [
Employee('Max', 22, 'Tranee', 2000),
Employee('Alex', 30, 'Manager', 30000),
Employee('Anna', 27, 'Team Leader', 29000),
Employee('John', 22, 'Junior', 4000),
];

for (var element in myList) {


switch (element) {
case Employee(
name: 'Max',
age: 22,
position: 'Tranee',
salary: 2000,

108
) ||
Employee(
name: 'Alex',
age: 30,
position: 'Manager',
salary: 30000,
):
print('Full match: $element');
case Employee(
name: 'Anna',
salary: 29000,
):
print('Partial match: $element');
default:
print('No match: $element');
}
}
}
// Full match: Employee{Max, 22, Tranee, 2000}
// Full match: Employee{Alex, 30, Manager, 30000}
// Partial match: Employee{Anna, 27, Team Leader, 29000}
// No match: Employee{John, 22, Junior, 4000}

Нет дела до текущего значения поля класса при сопоставлении, но


хотите его использовать в вычислениях? Тогда настраиваем шаблон
следующим образом (указывая тип поля и переменную, куда запишется его
значение):
// ex2_61.dart
for (var element in myList) {
switch (element) {
case Employee(
name: 'Max',
age: int age, // или var age
position: 'Tranee',
salary: 2000,
) ||
Employee(
name: 'Alex',
age: int age,
position: 'Manager',
salary: 30000,
):
print('Full match (age - $age): $element');
case Employee(
name: String name, // или var name
salary: 29000,
) || Employee(

109
name: String name,
salary: 4000,
):
print('Partial match (name is $name): $element');
default:
print('No match: $element');
}
}
/* Full match (age - 22): Employee{Max, 22, Tranee, 2000}
Full match (age - 30): Employee{Alex, 30, Manager, 30000}
Partial match (name is Anna): Employee{Anna, 27, Team Leader, 29000}
Partial match (name is John): Employee{John, 22, Junior, 4000} */

А что, если мы не хотим работать с нищебродами необходимо


пропускать по сопоставлению только тех сотрудников, чей оклад
превышает определенное значение? При указании значения поля
используйте операторы сравнения:
// ex2_62.dart
for (var element in myList) {
switch (element) {
case Employee(
salary: >10000,
):
print('Элитальный сотрудник: $element');
// какие-то операции над сотрудником
case _ :
print('Нищеброд : $element');
}
}
// Нищеброд : Employee{Max, 22, Tranee, 2000}
// Элитальный сотрудник: Employee{Alex, 30, Manager, 30000}
// Элитальный сотрудник: Employee{Anna, 27, Team Leader, 29000}
// Нищеброд : Employee{John, 22, Junior, 4000}

Немного поменяем понятие элитарности и добавим условие, что


сотрудник при хорошем окладе еще и не должен быть старпером старше 30
лет:
// ex2_63.dart
for (var element in myList) {
switch (element) {
case Employee(
salary: >10000,
age: <30,
):
print('Элитальный сотрудник: $element');
// какие-то операции над сотрудником
case _ :

110
print('Что ты тут забыл? : $element');
}
}
// Что ты тут забыл? : Employee{Max, 22, Tranee, 2000}
// Что ты тут забыл? : Employee{Alex, 30, Manager, 30000}
// Элитальный сотрудник: Employee{Anna, 27, Team Leader, 29000}
// Что ты тут забыл? : Employee{John, 22, Junior, 4000}

Обратите внимание на то, что поля объекта можно указывать в


произвольном порядке и так как в Dart все объекты, то при помощи
ограничения значений полей достигается довольно интересный результат.
Для примера давайте напишем код, который будет работать со строками
или списками только определенной длины:
// ex2_64.dart
void main() {
var myList = [
'Мама мыла раму',
'Привет!',
'Как дела?',
'Синхрофазатрон',
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
];

for (var element in myList) {


switch (element) {
case String(length: > 10):
print('Строка > 10 символов: $element');
case List(length: <= 10):
print('Список длиной <= 10 : $element');
case _:
print('Элемент с не нужной длинной: $element');
}
}
}
// Строка > 10 символов: Мама мыла раму
// Элемент с не нужной длинной: Привет!
// Элемент с не нужной длинной: Как дела?
// Строка > 10 символов: Синхрофазатрон
// Список длиной <= 10 : [1, 2, 3, 4, 5]
// Список длиной <= 10 : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Элемент с не нужной длинной:
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

А теперь с этими знаниями вернемся к сотрудникам и уволим найдем


тех, чье имя по длине >= 5:

111
// ex2_65.dart
void main() {
var myList = [
Employee('Maxim', 22, 'Tranee', 2000),
Employee('Alexandr', 30, 'Manager', 30000),
Employee('Anna', 27, 'Team Leader', 29000),
Employee('John', 22, 'Junior', 4000),
];

for (var element in myList) {


switch (element) {
case Employee(
name: String(length: >= 5),
):
print('Full match: $element');
default:
print('No match: $element');
}
}
}
// Full match: Employee{Maxim, 22, Tranee, 2000}
// Full match: Employee{Alexandr, 30, Manager, 30000}
// No match: Employee{Anna, 27, Team Leader, 29000}
// No match: Employee{John, 22, Junior, 4000}

Но при таком подходе у нас нет большой вариативности. Так,


например, невозможно поставить ограничения на префикс, с которого
должно начинаться имя, рассчитать какую-то хитрую формулу, куда входит
возраст и оклад, после чего принять решение удовлетворяет ли объект
нужным требованиям, чтобы с ним работать дальше или нет и многое что
другое. Здесь нам на помощь приходит последняя фича оператора switch-
case - Guard clause (защитное условие). Оно позволяет накладывать
ограничения на значения, которые в процессе сопоставления
записываются в переменные. Для этого после case с указанием шаблона
следует ключевое слово when, за которым идет условное выражение:
// ex2_66.dart
for (var element in myList) {
switch (element) {
case Employee(
name: var name,
) when name.startsWith('A') && name.length >= 5:
print('Name match: $element');
case Employee(
salary: var salary,
age: var age,
) when salary / age >= 200:

112
print('Salary match: $element');
}
}
// Name match: Employee{Alexandr, 30, Manager, 30000}
// Salary match: Employee{Anna, 27, Team Leader, 29000}

При работе со строками мы таким образом можем проверять имеются


ли в них нужные подстроки, регулировать длину. Для списков проводить
предварительные вычисления, на основе которых будет приниматься
решение, обрабатывать данный шаблон или нет:
// ex2_67.dart
void main() {
var myList = [
'Мама мыла раму',
'Привет!',
'Привет! Как дела?',
'Синхрофазатрон',
[1, 2, 3, 4, 5],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
(10, 20),
(30, 15),
];

for (var element in myList) {


switch (element) {
case String str
when (str.contains('мы') && str.length > 10) ||
str.startsWith('При'):
print('String : $element');
case List<int> list
when list.reduce(
(value, element) => value + element) > 55:
print('List : $element');
case (int first, int second) when first > second:
print('Record : $element');
}
}
}
// String : Мама мыла раму
// String : Привет!
// String : Привет! Как дела?
// List : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
// Record : (30, 15)

113
Резюме по разделу
В данном разделе мы рассмотрели существующие операторы и
базовые синтаксические конструкции языка программирования Dart:
условный оператор, циклы и т.д. Они будут постоянно встречаться вам в
процессе написания приложений, поэтому понимание их принципов
работы снизит вероятность ошибок в логике разрабатываемого
программного продукта.
Также были затронуты шаблоны, как с ними работать при организации
потока выполнения программы и принципы их деструктурирования. Это
достаточно новый механизм, появившийся только в третьей версии Dart, но
уже зарекомендовавший себя, т.к. позволяет писать более лаконичный,
читаемый (когда привыкнешь к синтаксису) и элегантный код.

Вопросы для самопроверки


1. Какие операторы существуют в Dart?
2. Является ли блок else обязательным при использовании условного
оператора if?
3. Для чего используются циклы?
4. Какие операторы циклов существуют в Dart?
5. Чем отличается цикл for от for-in?
6. Какие способы для деструктурирования мэпы (таблиц) вы знаете?
Приведите примеры.
7. Чем отличается цикл while от do-while?
8. Для чего используются операторы break и continue?
9. Для чего используется оператор выбора потока выполнения switch-
case? Какие варианты его использования вы знаете?
10. Какие способы для деструктурирования объектов вы знаете?
Приведите примеры.
11. Какие возможности предоставляют метки при их использовании в
операторе выбора потока выполнения switch-case?
12. Что такое сопоставление с шаблоном и как оно реализовано в Dart?
13. Что такое деструктурирование шаблона? Приведите примеры для
коллекций, классов и записей.
14. Какой цикл необходимо использовать для итерации по объектам типа
Map<K, V>?
15. Какие способы для деструктурирования записей вы знаете? Приведите
примеры.
16. Как работает тернарный оператор? Когда его лучше использовать?
17. Какие возможности предоставляет оператор if-case? В чем его
отличие от обычного if?

114
18. Что такое Guard clause и с какими операторами используется?
19. Какие способы для деструктурирования списков вы знаете? Приведите
примеры.

Лабораторная работа № 2. Управляющие


конструкции, арифметические операции и шаблоны
Цель работы: познакомиться с основными способами работы с
арифметическими операциями, шаблонами и управляющими
конструкциями языка программирования Dart.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Для возведения в степень и т.д. используйте библиотеку
dart:math, добавив в начало файла с кодом: «import
'dart:math';»;
• Во всех заданиях необходимо предусмотреть проверку на
правильность вводимых данных с клавиатуры.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

Часть 1. Задания на арифметические операции

1. Пользователь вводит с клавиатуры 3 значения: val1, val2, val3. Найдите


их сумму и выведите полученный результат в терминал.
2. Пользователь вводит с клавиатуры 2 значения val1, val2. Посчитайте
их произведение и выведите полученный результат в терминал.
3. Пользователь вводит с клавиатуры 2 значения: val1, val2. Возведите
val1 в степень val2 и выведите полученный результат в терминал.
4. Пользователь вводит с клавиатуры 2 значения: val1, val2. Рассчитайте
результат следующего выражения (val1 * 3 + val1) / 4 – val2 и выведите его в
терминал.
5. Пользователь вводит с клавиатуры 2 значения: val1, val2. Осуществите
целочисленное деление на val1 на val2 и выведите полученный результат в
терминал.

115
6. Пользователь вводит с клавиатуры 2 значения: val1, val2. Осуществите
ее деление по модулю val1 на val2 и выведите полученный результат в
терминал.
7. Пользователь вводит с клавиатуры 3 значения: val1, val2, val3.
Вычислите следующее выражение (val1 + val2) / (val2 – val3) и выведите
полученный результат в терминал.
8. Пользователь вводит целочисленный список myList, минимум из 6
элементов. Найдите сумму его первого и последнего элемента и выведите
полученный результат в терминал.
9. Пользователь вводит целочисленный список myList из 8 элементов.
Найдите произведение его второго и среднего элемента и выведите
полученный результат в терминал.
На вход подается целочисленное значение n. Используя его, получите
решение для следующего выражения и выведите полученный результат в
терминал:
№ задания Выражение для решения
√𝑛 + √𝑛𝑛
10
7

11
√(𝑛 + 2.5𝑛)3
4
𝑛 − 20
12
√𝑛3
5𝑛 × cos 𝑛
13
√𝑛3
(𝑛2 + 5) × 16
14 25
3𝑛
tan 𝑛 − 2𝑛
15
√10 + 0.6𝑛
3sin 𝑛 − 15
16
√𝑛5
10 + 2 cos 𝑛
17
5 − √𝑛5

116
√21 + √3𝑛
18
3
sin 𝑛
2𝑛2 − 4𝑛 + 10
19
2𝑛
20
√𝑛3 − 𝑛
𝑛

Таблица 2.12
Варианты работ
№ варианта Номера заданий к варианту
1 1, 2, 4, 12, 16
2 1, 3, 10, 11, 18
3 2, 3, 6, 12, 18
4 2, 4, 8, 10, 12
5 2, 9, 15, 16, 19
6 3, 4, 8, 12, 16
7 3, 4, 11, 15, 17
8 4, 5, 10, 15, 20
9 4, 7, 9, 19, 20
10 5, 6, 8, 12, 19
11 4, 13, 16, 17, 19
12 5, 12, 14, 15, 19
13 6, 7, 14, 15, 17
14 7, 8, 10, 16, 17
15 8, 9, 13, 14, 16
16 9, 11, 12, 16, 20
17 10, 12, 14, 18, 19
18 12, 13, 14, 16, 18
19 8, 11, 13, 16, 19
20 9, 11, 13, 17, 18

Часть 2. Задания на шаблоны

1. Пользователь вводит строку. Используя шаблон выведите ее в


терминал, если строка удовлетворяет следующим условиям: 0 < длина < 15
и начинается на букву «W». Иначе выведите текст «Pattern no matched».

117
2. Пользователь вводит целочисленный список. Используя шаблон
выведите его в терминал, если список удовлетворяет следующим условиям:
0 < длина < 15, первый элемент равен 7, а последний 15. Иначе выведите
текст «Pattern no matched».
3. Пользователь вводит Map<int, String>. Используя шаблон выведите
его в терминал, если удовлетворяет следующим условиям: длина < 5,
имеется ключ со значением 999 или 666. Иначе выведите текст «Pattern no
matched».
4. Пользователь вводит строку. Используя шаблон выведите ее в
терминал, если строка удовлетворяет следующим условиям: начинается на
букву «T», а заканчивается на «!». Иначе выведите текст «Pattern no
matched».
5. Пользователь вводит целочисленный список. Используя шаблон
выведите его в терминал, если список удовлетворяет следующим условиям:
первый элемент равен последнему и сумма всех элементов списка больше
40. Иначе выведите текст «Pattern no matched».
6. Пользователь вводит Map<String, int>. Используя шаблон выведите
его в терминал, если удовлетворяет следующим условиям: имеется ключ со
значением «Key» и произведение всех хранимых значений больше 38.
Иначе выведите текст «Pattern no matched».
Используя деструктурирование организуйте извлечение значений из
объекта Map или List:
№ задания Структура объекта и что извлечь
{
"name": "Alex",
"age": 35,
"course": 2,
"single": true,
"description": [
"Мечтатель",
7 "Ленив",
"Студент",
"Постоянно жалуется на жизнь"
]
}

Извлеките список description, имя и возраст.


Выведите в терминал полученный результат.
{
"nickname": "Alex",
8 "age": 35,
"course": 2,
"ids": [
1,

118
2,
5
]
}

Извлеките список ids, course и nickname


{
"nickname": "Alex",
"age": 35,
"course": 2,
"teacher": {
"name": "Max",
"age": 40,
"courses": [
1,
9
2,
3
]
}
}

Извлеките список courses, nickname и возраст


преподавателя. Выведите в терминал полученный
результат.
[
{
"name": "tt",
"type": "file",
"paths": [
"tt.json",
"tt.js",
"c:/documents/tt.json",
]
},
{
10 "name": "Object",
"type": "executable",
"paths": [
"c:/documents/Object.exe",
"c:/documents/Object"
]
}
]

Извлеките из каждого элемента списка данные по


ключу paths и type. Выведите в терминал полученный
результат.

119
[1, 3, 4, 5, 6, -2, 7, -12, 22]

11 Извлеките все элементы списка кроме первого и


последнего и рассчитайте их сумму. Выведите в
терминал полученный результат.
[1, 3, 4, 5, 2, 4, 6, 6, -2, 7, -12, 22]

12 Извлеките все элементы списка кроме первого,


второго и третьего, последнего чего и рассчитайте их
произведение. Выведите в терминал полученный
результат.
[1, 3, 4, 5, 2, 4, 6, 6, -2, 7, -12, 22]

13 Извлеките все элементы списка кроме четырех


последний, последнего чего найдите максимальный
по значению. Выведите в терминал полученный
результат.
(23, 89, pef:'Oo', pruf: 3.976)

14 Извлеките из записи значение второго позиционного


поля, именнованного pruf и выведите в терминал их
произведение.
(23, 89, pef:'Oo', pruf: 3.976)
15
Извлеките из записи только позиционные поля и
выведите в терминал их разность
(23, 89, pef:'Oo', pruf: 3.976)
16
Извлеките из записи только именованные поля и
выведите в их терминал
{
"id": 25,
"box": {
"width": 15,
"height": 25,
17 "coords": {
"x": -7,
"y": 11
}
}
}

120
Извлеките координаты (х, у), рассчитайте сумму их
значений и выведите в терминал полученный
результат.
{
"id": 25,
"box": {
"width": 15,
"height": 25,
"length": 5
18
}
}

Извлеките данные по габаритам коробки, рассчитайте


ее объем и выведите в терминал полученный
результат.

Таблица 2.13
Варианты работ
№ варианта Номера заданий к варианту
1. 1, 3, 6, 9, 14
2. 1, 4, 8, 11, 14
3. 1, 5, 7, 15, 18
4. 1, 3, 9, 13, 17
5. 2, 5, 6, 7, 15
6. 2, 6, 10, 11, 16
7. 2, 8, 10, 15, 18
8. 3, 6, 14, 17, 18
9. 3, 6, 9, 12, 17
10. 4, 8, 10, 15, 16
11. 4, 10, 15, 16, 18
12. 5, 9, 11, 12, 17
13. 7, 8, 11, 13, 15
14. 7, 9, 11, 4, 14
15. 8, 10, 12, 14, 18
16. 6, 9, 11, 13, 16
17. 9, 13, 14, 15, 2
18. 5, 7, 9, 12, 16
19. 9, 11, 12, 16, 17
20. 7, 9, 10, 11, 17

121
Часть 3. Задания на управляющие конструкции
1. Напишите две версии программы (c if и switch), которая считывает
целое число (месяц) и выводит в терминал сезон, к которому этот месяц
относится (Зима, Лето, Осень, Весна, Ошибка ввода!).
2. Напишите две версии программы (c if и switch), в которой
пользователь вводит 2 значения: val1, val2. Если их произведение больше
400, то в терминал выводится получаемое значение, иначе выведите их
сумму.
3. На вход подается список, минимум из 6 элементов. Если его первый и
последний элементы равны, то выведите в терминал «True», иначе «False»
(без кавычек). Реализуйте 3 версии программы с разными подходами.
4. Напишите две версии программы (c if и switch), в которой
пользователь вводит 2 значения: val1, val2. Если val1 делится на val2 без
остатка, то выведите в терминал «Bingo!» (без кавычек), иначе выведите
получившийся остаток от деления.
5. Пользователь вводит свой оклад до вычета налога. Если он больше 100
тысяч рублей, то вычтите из него 13% НДФЛ, иначе – 6%. Выведите
полученный результат в терминал. Реализуйте 2 версии программы с
разными подходами.
6. Пользователь вводит с клавиатуры произвольный год. Напишите
программу, выводящую в терминал «YES» или «NO» (без кавычек) в
зависимости от того високосный год или нет. Реализуйте 2 версии
программы с разными подходами.
7. Пользователь вводит с клавиатуры целочисленный список, минимум
из 9 элементов. Если значение в середине списка больше, либо равно 10, то
выведите в терминал сумму его первого и последнего элемента, иначе
произведение первого и предпоследнего. Реализуйте 2 версии программы
с разными подходами (if и switch).
8. Пользователь вводит с клавиатуры целочисленное значение.
Проверьте лежит ли оно в диапазоне [-15, 10] и выведите в терминал «YES»
или «NO» (без кавычек) в зависимости от результата проверки. Реализуйте
2 версии программы с разными подходами (if и switch).
9. Пользователь вводит с клавиатуры 2 значения (x и y). Определите в
какой четверти находится точка с полученной координатой и выведите ее
в терминал (1, 2, 3 или 4). Реализуйте 2 версии программы с разными
подходами (if и switch).
10. Пользователь вводит с клавиатуры 3 значения (a, b, c). Решите
следующее квадратное уравнение ax2-bx+2c=0 и выведите в терминал
полученные корни.
11. Пользователь вводит с клавиатуры 2 значения (x и y), представляющие
собой координату точки в пространстве. Центр круга находится в центре
координат (0, 0), а его радиус равен 5. Проверьте, принадлежит ли данная

122
точка кругу и выведите в терминал «YES» или «NO» (без кавычек) в
зависимости от результата проверки. Реализуйте 2 версии программы с
разными подходами (if и switch).
12. Пользователь вводит с клавиатуры значение. Определите является оно
числом или нет и выведите в терминал «Number», либо «Other» (без
кавычек) в зависимости от результата проверки. Реализуйте 2 версии
программы с разными подходами (if и switch).
13. Пользователь вводит с клавиатуры букву алфавита. Определите
является она строчной или прописной и выведите в терминал «lowercase»,
либо «uppercase» (без кавычек) в зависимости от результата проверки.
Реализуйте 2 версии программы с разными подходами (if и switch).
14. Пользователь вводит с клавиатуры 2 значения: val1, val2. Если они
равны, то выведите в терминал «True», иначе «False» (без кавычек).
Реализуйте 3 версии программы с разными подходами (if, ?: и switch).
15. Пользователь вводит с клавиатуры список. Если сумма его элементов
больше 55, то выведите в терминал сумму его первого и последнего
элемента, иначе их произведение. Реализуйте 2 версии программы с
разными подходами (if и switch).
16. Пользователь вводит с клавиатуры номер месяца, а программа должна
вывести в терминал количество дней в нем. Реализуйте 2 версии
программы с разными подходами (if и switch).
17. Пользователь вводит с клавиатуры значение температуры в градусах
Цельсия 36.6с или в градусах Фаренгейта 36.6f. Если температура была
введена в градусах Цельсия, то конвертируйте ее в градусы Фаренгейта,
либо наоборот и выведите полученный результат в терминал. Реализуйте 2
версии программы с разными подходами (if и switch).
18. Пользователь вводит с клавиатуры курс валют банка в формате $1R60.4
(1 доллар можно купить по цене в 60.4 рубля) и сумму, которую он хочет
перевести в другую валюту: 3$ (доллары в рубли) или 39578R (рубли в
доллары). Выведите полученный результат в терминал. Например, 4$ при
курсе $1R60.4 -> 241.6R. Обратите внимание, что курс валют может быть
задан и следующим образом $3R170.05.

Таблица 2.14
Варианты работ
№ варианта Номера заданий к варианту
1. 1, 3, 9, 13, 17
2. 2, 5, 6, 7, 15
3. 2, 6, 10, 11, 16
4. 4, 8, 10, 15, 16
5. 4, 10, 15, 16, 18

123
6. 1, 3, 6, 9, 14
7. 9, 11, 12, 16, 17
8. 7, 9, 10, 11, 17
9. 1, 4, 8, 11, 14
10. 1, 5, 7, 15, 18
11. 5, 9, 11, 12, 17
12. 3, 6, 9, 12, 17
13. 6, 9, 11, 13, 16
14. 2, 8, 10, 15, 18
15. 3, 6, 14, 17, 18
16. 9, 13, 14, 15, 2
17. 8, 10, 12, 14, 18
18. 7, 9, 11, 4, 14
19. 7, 8, 11, 13, 15
20. 5, 7, 9, 12, 16

Лабораторная работа № 3. Циклы и битовые


операции
Цель работы: познакомиться с основными управляющими
конструкциями и циклами языка программирования Dart.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• ЗАПРЕЩЕНО обращаться к свойствам экземпляра класса
типа int для проверки на четность;
• Для возведения в степень и т.д. используйте библиотеку
dart:math, добавив в начало файла с кодом: «import
'dart:math';»;
• Во всех заданиях необходимо предусмотреть проверку на
правильность вводимых данных с клавиатуры.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

124
Часть 1. Задания на циклы
1. Пользователь вводит с клавиатуры список вещественных значений.
Используя цикл for, for-in и while найдите сумму его элементов и
выведите полученный результат в терминал.
2. Пользователь вводит с клавиатуры список целочисленных значений.
Используя цикл for, for-in и while найдите среднеарифметическое
значение списка и выведите полученный результат в терминал.
3. Пользователь вводит с клавиатуры список целочисленных значений.
Используя цикл for, for-in и while найдите произведение его элементов
и выведите полученный результат в терминал.
4. Пользователь вводит с клавиатуры список целочисленных значений.
Используя цикл for и while найдите сумму элементов с нечетным индексом
и выведите полученный результат в терминал.
5. Пользователь вводит с клавиатуры список вещественных значений.
Используя цикл for и while найдите сумму элементов с четным индексом
и выведите полученный результат в терминал.
6. Пользователь вводит с клавиатуры список целочисленных значений.
Используя цикл for, for-in и while найдите произведение элементов с
нечетным индексом и выведите полученный результат в терминал.
7. Пользователь вводит с клавиатуры список целочисленных значений.
Используя цикл for, for-in и while посчитайте количество вхождения в
него элементов кратных 2 и выведите в терминал полученный результат.
8. Пользователь вводит с клавиатуры список целочисленных значений.
Используя цикл for, for-in и while посчитайте количество вхождения в
него элементов кратных 5 и выведите в терминал полученный результат.
9. Пользователь вводит с клавиатуры текст. Используя цикл for, for-in
и while посчитайте количество вхождений каждого символа в строку и
выведите в терминал полученный результат.
10. Используя цикл for, do-while и while посчитайте сумму значений от
10 до 76, которые нацело делятся на 3 и выведите в терминал полученный
результат.
11. Используя цикл for, do-while и while посчитайте сумму значений от
минус 54 до 15, которые нацело делятся на 4 и выведите в терминал
полученный результат.
На вход подается целочисленное значение z. Используя его, получите
решение для следующего выражения и выведите полученный результат в
терминал:
№ задания Выражение для решения
𝑧
√𝑛 + √𝑛𝑛
12 ∑
7
𝑛=1

125
𝑧
√(𝑛 − 2.5𝑛)3
13 ∑
4
𝑛=1
𝑧
𝑛 − 20
14 ∑
𝑛=1
√𝑛3
𝑧
5𝑛 × cos 𝑛
15 ∑
𝑛=1
√𝑛3
∑𝑧𝑛=1(𝑛2 + 5) × 16
16 25
3𝑛
∑𝑧𝑛=1(tan 𝑛 − 2𝑛)
17
√10 + 0.6𝑛
3sin 𝑛 − 15
18 ∑𝑧𝑛=1 √𝑛5
10 + ∑𝑧𝑛=1 2 cos 𝑛
19
5 − √𝑛5

√21 + ∑𝑧𝑛=1 √3𝑛


20
3
sin 𝑛
𝑧
2𝑛2 − 4𝑛 + 10
21 ∑
2𝑛
𝑛=1
𝑧
√𝑛3 − 𝑛
22 ∑
𝑛
𝑛=1

Таблица 2.15
Варианты работ
№ варианта Номера заданий к варианту
1 1, 2, 12, 18, 21
2 1, 2, 16, 18, 22
3 1, 3, 9, 10, 20
4 1, 4, 6, 8, 17
5 1, 4, 9, 13, 15
6 1, 5, 7, 8, 16
7 3, 4, 11, 13, 19
8 3, 5, 8, 20, 22
9 3, 5, 10, 13, 15

126
10 3, 6, 7, 11, 14
11 3, 9, 13, 17, 21
12 3, 11, 15, 17, 19
13 3, 13, 17, 19, 22
14 4, 5, 19, 20, 21
15 4, 6, 12, 14, 20
16 4, 6, 13, 16, 22
17 5, 6, 12, 17, 19
18 5, 7, 10, 11, 16
19 5, 9, 11, 17, 20
20 8, 14, 16, 18, 21

Часть 2. Задания на побитовые операции


Примечание: для представления десятичного числа в двоичном
формате используйте метод .toRadixString(2) у переменной типа int.
Обратите внимание на то, что отрицательные значения хранятся в доп.
коде. В случае, если для вас тема битовых операций в новинку, можно
посмотреть мое видео на ютубе с примерами на Python и применить
полученные знания для решения задач на Dart
(https://youtu.be/HUTJvGyZask).

1. Пользователь вводит с клавиатуры положительное число. Определите,


установлен ли у него третий бит справа в 1. Если нет, то установите и
выведите полученный результат в двоичном формате в терминал.
2. Пользователь вводит с клавиатуры положительное число. Используйте
маску и операцию побитового исключающего ИЛИ для того, чтобы
инвертировать значения битов и выведите полученный результат в
двоичном формате в терминал.
3. Пользователь вводит с клавиатуры число. Используя побитовую
операцию умножьте значение на 16 и выведите полученный результат в
двоичном формате в терминал.
4. Пользователь вводит с клавиатуры положительное число. Проверьте
установлен ли ее 4-й бит справа в единицу или нет и выведите полученный
результат в терминал.
5. Пользователь вводит с клавиатуры положительное число. Проверьте
установлен ли ее правый бит в единицу или нет. Если нет, то установите и
выведите полученный результат в двоичном формате в терминал.
6. Пользователь вводит с клавиатуры число. Проверьте установлен ли ее
левый бит в единицу или нет и выведите в терминал полученный результат
и введенное число в двоичной системе счисления.

127
7. Пользователь вводит с клавиатуры нечетное число. Установите его
правый бит в ноль и выведите полученный результат в двоичном и
десятичном формате в терминал.
8. Пользователь вводит с клавиатуры большое число. Посредством цикла
и битовых операций посчитайте количество бит, установленных в единицу
и выведите полученный результат в терминал.
9. Пользователь вводит с клавиатуры большое число. Посредством цикла
и битовых операций посчитайте количество нулевых бит и выведите
полученный результат в терминал.
10. Пользователь вводит с клавиатуры большое число. Используя
побитовую операцию, разделите его на 4 и выведите полученный результат
в двоичном и десятичном формате в терминал.
11. Пользователь вводит с клавиатуры положительное число.
Инвертируйте значения бит и выведите полученный результат в терминал.
12. Пользователь вводит с клавиатуры два значения. Используя
побитовые операции и не прибегая к буферной переменной, поменяйте
значение этих переменных местами и выведите полученный результат в
терминал.
13. Пользователь вводит с клавиатуры число z. Используя побитовые
операции проверьте является ли оно четным и выведите полученный
результат в терминал.
14. Пользователь вводит с клавиатуры положительное число. Используя
операции сдвига установить 4 правых бита в ноль и выведите полученный
результат в терминал.
15. Пользователь вводит с клавиатуры положительное число. Посчитайте
количество занимаемых ей бит и выведите полученный результат в
терминал.

Таблица 2.16
Варианты работ
№ варианта Номера заданий к варианту
1 1, 2, 12, 14
2 1, 6, 11, 13
3 3, 5, 8, 10
4 6, 12, 13, 14
5 2, 6, 8, 12
6 5, 6, 9, 15
7 6, 7, 11, 15
8 2, 4, 10, 14
9 3, 7, 10, 15
10 3, 6, 7, 15
11 6, 11, 13, 15

128
12 2, 6, 7, 9
13 1, 3, 12, 13
14 1, 9, 10, 14
15 2, 4, 9, 12
16 4, 6, 7, 11
17 4, 10, 11, 14
18 5, 7, 9, 12
19 7, 9, 10, 14
20 8, 9, 11, 13

129
Глава 3. Функции, библиотеки, пакеты и их
тестирование
Поскольку Dart является объектно-ориентированным языком
программирования, то и функции в нем – это объекты. Хоть это и не
указывается явно, у них есть тип – Function, что делает функции довольно
гибким инструментом при разработке приложений. Так, например,
функцию можно присвоить переменной и вызывать ее через эту
переменную, а также передавать объявленную функцию в качестве
входного аргумента в другую функцию.
Работа любого приложения, написанного на Dart, начинается с
функции верхнего уровня main, которая представляет собой точку входа в
приложение. В виду этого любой файл, в котором содержится эта функция,
может выступать в роли того, с которого начинается запуск вашего
приложения.
Говоря более общими словами, функции представляют собой
инструмент, который позволяет вам переиспользовать код сколько угодно
раз в процессе выполнения программы и разбивать сложные системы на
составные части. Дополнительно к этому они являются альтернативой
любимого метода начинающих программистов: «copy&paste». Но у них
есть и темная сторона, о которой не многие задумываются. Поэтому начнем
же мы знакомство с функциями из далека, т.к. опыт преподавания
показывает, что обучающиеся программированию очень сложно
воспринимают понятие «абстракция», то, что она привносит в процесс
разработки программных продуктов и как связана с функциями. Первые
несколько разделов данной главы будут носить несколько философский
характер и здесь мы не напишем ни строчки кода, но они призваны
заложить фундамент понимания той «сакральной» части
программирования, к которому некоторые разработчики приходят спустя
годы или, к сожалению, не приходят вовсе.
Чем глубже вы будете погружаться в процесс написания программных
приложений, тем чаще у вас на пути будет появляться слово «абстракция»
и, уверяю вас, она применима не только к структурному или объектно-
ориентированному программированию, а к программированию в целом,
как, в принципе, и инкапсуляция, полиморфизм или наследование.
Возвращайтесь к этим разделам время от времени и обратите
внимание на то, как будет изменяться ваше восприятие к написанному, в
зависимости от уже полученного опыта написания программных
приложений.

130
3.1. Что такое абстракция?
К сожалению, в ответе на этот вопрос для ступающих на путь
программирования вряд ли помогут определения из словарей или
специализированной литературы. Для примера давайте рассмотрим
описание абстракции из толкового словаря русского языка Ожегова [9]:
1. Мысленное отвлечение, обособление от тех или иных сторон, свойств
или связей предметов и явлений для выделения существенных их
признаков.
2. Отвлеченное понятие, теоретическое обобщение опыта.
Ну как? Стало понятней? Что-то я в этом сомневаюсь! А ведь с
понятием абстракции тесно связано понятие абстрагирование. Здесь нам
на «помощь» придет Д. П. Горский с его трудом «Вопросы абстракции и
образование понятий» [10]: «Абстрагирование, процесс абстракции, состоит
в отвлечении, в мысленном отбрасывании (временном) тех предметов,
свойств и связей, которые затрудняют рассмотрение объекта исследования
в "чистом виде", необходимое на данном этапе изучения. Чтобы мысленно
воспроизвести предмет исследования в "чистом виде", "надо оставить в
стороне все отношения, не имеющие ничего общего с данным объектом
анализа"».
Так, стоп! Руки прочь от бутылки с горячительным! Столкнулись с
несколькими определениями и уже готовы сдаться? А ведь разработчики
каждый день сталкиваются с задачами или проблемами, которые не знают,
как решать и им платят не за то, что они пишут код, а именно за решение
проблем бизнеса, то есть сам код – это инструмент для решения проблем (а
порой и для их создания). Таким образом, чем раньше вы придете к
осознанию того, что путь разработчика программных продуктов – это путь
непрерывного самообразования и решения проблем, с которыми,
возможно, до вас никто и не сталкивался, то есть никто (даже интернет) не
будет способен дать вам точный ответ, тем быстрее сможете ответить на
такие вопросы, как: «Хочу ли я пройти по этому пути?», «Нравится ли мне
конечная точка, куда он меня заведет?» «Мое это или не мое?» и т. д.
Надеюсь, вы немного успокоились, и мы можем продолжить
погружаться в пучины абстракции. Давайте представим следующую
ситуацию: у вас поломался автомобиль и вы решили подыскать ему замену.
Для этого существует два пути: поехать к дилеру, где будет возможность
увидеть автомобили собственными глазами, а также посидеть за рулем и
прокатиться или начать поиск на какой-нибудь интернет-площадке. В
первом случае вы непосредственно контактируете с автомобилем и он
является для вас объектом реального мира, а во втором имеются только
фотографии и описания, то есть автомобили для вас представлены как
абстракции. В данный момент времени их нельзя пощупать, самому
посмотреть под капот или проехать, но за счет уже сформированного опыта

131
и представления о том, что такое автомобиль, можно понять, на какое из
предложений стоит обратить внимание и договориться с продавцом о
встрече.
Аналогично и с формулой ускорения из учебника физики. До тех пор,
пока мы не нажмем на педаль газа, чтобы прочувствовать ускорение, оно
для нас будет оставаться абстракцией, представленной на бумаге. Так же и
программный код, который мы не можем «потрогать», является
абстракцией. Иногда она описывает простые действия, иногда объекты или
действия, совершаемые над объектами, чье состояние и поведение из
предметной области было разработчиком перенесено в код. И здесь
возникает законный вопрос: «Почему человек, который привык в реальной
жизни постоянно взаимодействовать с абстракциями, т. е. читать,
любоваться котиками в интернете или выискивать очередную порцию
мемов, смотреть кино или мультипликационные фильмы и т. д., так трудно
взаимодействует с ними в процессе обучения программированию?». Как
один из возможных ответов – мы просто не воспринимаем все это, как
взаимодействие с абстракциями и те действия, которые при этом
совершаем для нас так же естественны, словно дыхание.
Дополнительной сложностью при знакомстве с абстракциями
является то, что они бывают различных уровней и более низкий уровень
представляет собой детали реализации более высокого. Но об этом
поговорим немного позже, когда приведем аналогию написания кода с
написанием какого-либо произведения, сочинения или очередного
переосмысления «Война и мир» под авторством себя любимого.
Согласно ГОСТ (ISO) под языком программирования понимают язык,
предназначенный для записи программ [11]. Таким образом, как и у любого
языка, у него имеется своя грамматика и семантика. Отличие от того языка,
на котором мы привыкли общаться, заключается только в том, что язык
программирования предназначен для написания программ, которые могут
управлять ЭВМ и лишен эмоциональной составляющей, позволяющей нам
взаимодействовать с другими людьми.
Любой язык программирования несмотря на то, что у него имеется
алфавит, ограничен набором ключевых слов. Иначе говоря, не все слова, что
можно организовать из имеющегося алфавита, позволят выстроить поток
выполнения разрабатываемой на нем программы. Представьте, что из
вашего родного языка выбросили все лишние слова, оставив только те,
которые позволяют описать действие или данные, с которыми предстоит
работать. Не очень-то и удобно жить с таким ограничением! Но для того,
чтобы сказать компьютеру что и откуда взять, как обработать, сохранить
или переслать этого набора вполне достаточно.
В состав любого языка программирования входят:

132
1. Алфавит (набор символов, которые можно использовать при
написании программы);
2. Лексема (минимальная смысловая единица: ключевое слово,
операция, идентификатор, константа, разделитель и т. д.);
3. Оператор (исполняемый и неисполняемый; первый выполняет
действие, например ветвление, а второй используется для
описания данных);
4. Выражение (может состоять из констант, функций, переменных
и т. д., всегда возвращает значение).
Все вышеперечисленные составляющие тесно переплетены между
собой и позволяют нам управлять ходом выполнения программы. А так как
чаще всего в основе языков программирования лежит английский алфавит,
представьте, что они – это упрощенная версия английского языка. Конечно,
используя такой упрощенный язык не получится свободно пообщаться со
своим лучшим другом на отвлеченные темы, вспоминая «сына маминой
подруги», но это совершенно не значит, что вы не сможете понять друг
друга. Каждый день сотни тысяч программистов общаются между собой с
его использованием! Только это происходит путем чтения и написания
кода. Именно поэтому очень важным аспектом является его читаемость,
которая позволяет понять изначально закладываемый смысл
разработчиком того или иного кусочка кода.

3.2. Функция как способ написания художественного


произведения
Как уже говорилось ранее, в процессе написания программ мы
оперируем абстракциями. Точно также писатель переносит из своего
воображения целые вселенные на чистые листы бумаги. Именно поэтому
можно провести аналогию процесса написания программы с написанием
художественного произведения!
Начнем с самого первого уровня абстракции, с которым встречается
начинающий программист – объявление переменной. Удивлены? Да,
переменная тоже является абстракцией, ведь за ее объявлением кроится
процесс выделения памяти и ее связывания с именем переменной. В
зависимости от типа данных выделяется разное количество памяти для
хранения значения и определяется формат его представления. Так,
например, целочисленные значения могут быть знаковыми и
беззнаковыми (только положительными), вещественные могут храниться в
формате с фиксированной или плавающей точкой. Но мы же не
задумываемся, где именно выделяется память и в каком объеме, в каком
бите устанавливается единица, а в каком ноль? Если бы и об этом каждый

133
раз приходилось думать программисту, то чем бы такая работа отличалась
от каторги!!!

3.2.1. Простой текст – это первый уровень абстракции


Обычное предложение в книге («Пейн, я ног не чувствую!») можно
воспринимать, как объявление каких-то переменных или выполнение
действий над ними. Предложения организуются в абзац, где описывается
инициализация переменных и действия над ними. Начиная знакомиться с
программированием, будущий разработчик пишет свой код в функции
main, которая является точкой входа в приложение, т. е. этот код всегда
выполняется в первую очередь.
На этом уровне абстракции мы можем писать довольно простые
программы: решение квадратного уравнения или других математических
задач, чтение данных из файла или запись в него и много чего другого. Даже
используя сторонние библиотеки (пакеты, модули), мы не переходим на
новый уровень, так как они являются зависимостями в проекте, а не
деталями его реализации. В то же самое время эти библиотеки помогают
упростить процесс решения той или иной задачи и остаются ровно на том
уровне абстракции, где их использует программист. Но что за книга из
сплошного текста? Как такому писателю смотреть в глаза своих преданных
фанатов?
Настало время разбить текст на разделы!

3.2.2. Раздел - новый уровень абстракции


Представим, что наш код сейчас выглядит следующим образом:
void main(){
объявление переменных
Хреналион строк действий для решения поставленной задачи
строка вывода результата в терминал
}

Одно только ознакомление с ним займет уйму времени, не говоря уже


о таких ситуациях, когда необходимо будет внести изменения в
проводимые вычисления. Вам может показаться глупым предыдущее
утверждение, ведь вы сами написали этот код, а значит должны
досконально в нем разбираться. Но будете ли вы также хорошо в нем
ориентироваться через неделю, месяц или год? Конечно нет! А если его
надо будет отдать другому разработчику, чтобы он использовал ваши
наработки в своем проекте? Да ему будет проще написать необходимую
часть кода самому, чем выискивать ее в вашем произведении.
Аналогично ситуация обстоит и с книгой, где только один текст и нет
оглавления для навигации по нему. Заводя новый раздел и помещая в него

134
часть текста из книги мы упрощаем читателю жизнь. Теперь он не бросит
ее в мусорное ведро, а сразу сможет найти необходимый для прочтения
материал. Все дело в том, что название раздела представляет собой
обобщение входящего в него текста. Таким образом вводится новый
уровень абстракции.
Примером раздела при написании книги в программировании
является функция или процедура. Их отличие заключается в том, что
функция возвращает значение, а процедура нет. Даже когда язык
программирования не имеет ключевого слова для объявления процедуры,
функция, которая не возвращает значение (void), может спокойно
рассматриваться как процедура. Далее по тексту мы будет оперировать
функциями. Чаще всего вам попадалось определение, что функция служит
для того, чтобы упростить повторное использование кода. Но только ли на
этом заканчиваются ее возможности? Функция также служит для вывода
процесса написания программы на новый уровень абстракции!
Представим, что в предыдущем примере кода мы использовали функции
только для того, чтобы убрать дублирование кода:
void main(){
объявление переменных
функция
200 строк кода
функция
3000 строк кода
функция
100 строк кода
функция
n-строк кода
строка вывода результата в терминал
}

Несмотря на то, что за счет введения функций сократилось количество


строк кода в main, он не стал более читаемым, т. к. остались довольно
большие куски, в реализации которых все также придется разбираться. А
теперь объединим те строки кода, которые не вошли в функции в новые
функции, дав им такое название, которое описывает то, что делает код,
перенесенный в них:
void main() {
объявление переменных
функция
функция для 200 строк кода
функция
функция для 3000 строк кода
функция
функция для 100 строк кода
функция

135
функция для n-строк кода
строка вывода результата в терминал
}

Смотрите, код стал более читаемым. Имя каждой функции говорит для
разработчика за что она отвечает и ему нет необходимости погружаться в
детали ее реализации, если на то нет веских причин. Мы вывели наше
приложение на новый уровень абстракции! Вместо того, чтобы разбираться
в огромном количестве кода, перед нашими глазами только функции,
которые своими именами абстрагируют нас от того кода, что в них был
перенесен. А если надо что-то изменить? Это становится куда проще, чем
кажется на первый взгляд. Для этого достаточно внести изменения в код
функции. По ее имени можно понять та эта функция или нет, после чего
разобраться в ее коде и найти место, где его модифицировать. Именно
поэтому в книгах и статьях часто рекомендуют, чтобы тела функций были
достаточно короткими, ведь в таком случае разработчику куда удобнее в
них ориентироваться. К сожалению, рекомендации и реальная жизнь две
разные вещи. В связи с этим готовьтесь, что в легаси проектах придется
разбираться с функциями или классами очень большой длины.
Книга поделена на разделы, по ним мы можем найти интересующий
нас текст, но их слишком много и не прослеживается структура, которая
могла бы как-то объединить разделы по их смысловому назначению. Чтобы
решить эту проблему давайте вспомним, что разделы могут быть
вложенными, где раздел, находящийся на более высоком уровне, имеет
обобщенное название для входящих в его состав вложенных разделов:

Рис. 3.1 – Раздел, как новый уровень абстракции

Также и с кодом программы. Функции могут содержать другие


функции. Так, например, новая функция своим именем, входными
параметрами и возвращаемым значением может объединять несколько
существующих, что еще более упростит нам чтение кода, а также выведет
код в функции main на новый уровень абстракции:
void main(){

136
объявление переменных
функция, объединяющая первую четверть программы
функция, объединяющая вторую четверть программы
функция, объединяющая третью четверть программы
функция, объединяющая четвертую четверть программы
строка вывода результата в терминал
}
Теперь те функции, которые вошли в более высокоуровневые будут
считаться деталями их реализации. И так можно продолжать до
бесконечности: путем введения более высокого уровня, содержащего более
обобщенное название для существующих ранее функций и строк кода мы
упрощаем чтение, поддержку и процесс изменения кодовой базы проекта.

3.2.3. Очередной уровень абстракции – главы и части


Несмотря на то, что мы сделали код в функции main более читаемым,
сам файл с именем main таковым пока назвать сложно, т. к. все
объявленные функции находятся внутри него:
Куча функций различного уровня абстракции

func main(){
объявление переменных
функция, объединяющая первую четверть программы
функция, объединяющая вторую четверть программы
функция, объединяющая третью четверть программы
функция, объединяющая четвертую четверть программы
строка вывода результата в терминал
}

Для того, чтобы исправить сложившуюся ситуацию необходимо


перейти на более высокий уровень абстракции. В книге за это отвечают
главы. По их названию можно понять, что описывается во входящих в их
состав разделах:

137
Рис. 3.2 – Глава, как новый уровень абстракции

Аналогом глав в программировании могут выступать модули,


библиотеки или просто файлы с необходимым расширением (в каждом
языке программирования по-своему) по названию которых разработчик
ориентируется при поиске реализации того или иного функционала
приложения. Поскольку у нас уже имеется 4 высокоуровневые функции в
теле main, давайте перенесем их и входящие в их состав функции более
низкого уровня абстракции в отдельные файлы, функционал которых
будем импортировать:
Импорт первого модуля
Импорт второго модуля
Импорт третьего модуля
Импорт четвертого модуля

func main(){
объявление переменных
функция, объединяющая первую четверть программы
функция, объединяющая вторую четверть программы
функция, объединяющая третью четверть программы
функция, объединяющая четвертую четверть программы
строка вывода результата в терминал
}

Готово! Теперь вместо кучи разнообразных функций в теле файла main


разработчика встречает 4 строчки кода, а структура проекта преобразуется
из одного файла в пять:
main
first_module
second_module
third_module
fourth_module

138
При этом по названию модулей он должен иметь возможность понять
какие обязанности на них возлагаются. Это позволяет сократить время
поиска участка кода, который необходимо модифицировать или исправить
логику его работы, а также упрощает чтение и поддержку кода самого
проекта.
Но это еще не все. В некоторых случаях книги состоят из частей,
названия которых представляют собой обобщение всех глав, входящих в их
состав, а сами части книги можно рассматривать как более высокий уровень
абстракции по отношению к ним. Аналогом частей книг в
программировании могут выступать пакеты, в состав которых может
входить различное количество модулей (библиотек). Обычно, когда
доходит дело до пакетов, предполагается, что код в них работает исправно
и покрыт тестами, чтобы тем разработчикам, которые его будут
использовать, не приходилось самостоятельно проверять правильность
работы, что освобождает их время и упрощает процесс разработки.
Заметьте, каждый раз повышая уровень абстракции мы упрощали себе
жизнь, поскольку пропадала необходимость запоминать огромный массив
информации, скрываемый за этим новым уровнем абстракции. Вводя
новую функцию, ее имя брало на себя обязанность описания того, для чего
она предназначена, снимая с нас нагрузку помнить, что за код в ней
написан. Теперь для разработчика большую роль играет что нужно подать
этой функции на вход и значение какого типа данных она вернет, чем сам
код тела функции.
Обратите внимание на то, что если в начале код программы смотрелся
следующим образом:
void main(){
объявление переменных
Хреналион строк действий для решения поставленной задачи
строка вывода результата в терминал
}

То вводя все новые уровни абстракции мы пришли к следующему:


Импорт первого модуля
Импорт второго модуля
Импорт третьего модуля
Импорт четвертого модуля

void main(){
объявление переменных
функция, объединяющая первую четверть программы
функция, объединяющая вторую четверть программы
функция, объединяющая третью четверть программы
функция, объединяющая четвертую четверть программы

139
строка вывода результата в терминал
}

В первом случае на поиск и исправление нужной части кода могло уйти


довольно много времени, а в последнем весь код программы
структурирован таким образом, что по названиям модулей и функций
можно понимать за что они отвечают и для чего используются. И когда нам
нужно что-то изменить, достаточно опускаться на более низкий уровень
абстракции до тех пор, пока не достигнем нужной части кода.

3.3. Функции в Dart


Для начала рассмотрим общий шаблон объявления функции:
[возвращаемый тип данных] имяФункции(
[ТипВходногогоАргумента1 имяАргумента1, …, n]
){
// тело функции
[return возвращаемое значение]
}

В квадратных скобках указаны необязательные элементы при


объявлении функции. Если в таком языке, как C++ нам необходимо явно
указать тип возвращаемого значения, то Dart может вывести его
автоматически, в связи с чем его можно не указывать при объявлении
функции. Несмотря на то, что это позволяет писать меньше кода, при
большой кодовой базе такой подход может сыграть злую шутку, так как код
проекта станем менее читаемым и понятным, особенно для новых
разработчиков в проекте. В связи с этим лучше указывайте тип
возвращаемого значения, даже если функция ничего не возвращает (тогда
пишем void).
Функции можно объявить практически в любой части кода. Обычно
принято это делать на верхнем уровне модуля:
void myFunction(){
print('Привет!!!');
}

void main(List<String> arguments) {


myFunction(); // Привет!!!
}

Когда при выполнении приложения в коде встречается вызов


функции, то поток управления переходит в нее и выполняет ее код, после
управление возвращается в точку вызова функции и продолжается
последовательная отработка кода основной программы.

140
Для передачи аргументов (параметров) в функцию необходимо после
объявления ее имени в круглых скобках указать тип и имя ее входного
аргумента, который свяжется с подаваемой при вызове функции на ее вход
переменной:
void myFunction(String name){
print('Привет, $name!');
}

void main(List<String> arguments) {


myFunction('Александр'); // Привет, Александр!
}

Если функция должна возвращать значение, используйте оператор


return:
String myFunction(String name){
var hello = 'Привет, $name!';
return hello;
}

void main(List<String> arguments) {


var myHelloString = myFunction('Александр');
print(myHelloString); // Привет, Александр!
}

Когда необходимо вернуть сразу несколько значений на помощь


приходят записи (Records):
(String, String) myFunction(String name){
return ('Привет', '$name!');
}

void main(List<String> arguments) {


var myHelloRec = myFunction('Александр');
print('${myHelloRec.$1} ${myHelloRec.$2}');
// Привет, Александр!
}

Для примера того, что компилятор Dart умеет выводить тип


возвращаемого функцией значения, давайте модифицируем наш
последний вариант кода следующим образом:
myFunction(String name){
var hello = 'Привет, $name!';
return hello;
}

void main(List<String> arguments) {


var myHelloString = myFunction('Александр');

141
print(myHelloString); // Привет, Александр!
print(myHelloString.runtimeType); // String
}

Как видно из результата, удаление типа возвращаемого функцией


значения не сказалось на работоспособности кода, но отразилось на его
читаемости.

3.3.1. Объявление входных аргументов функции


Входные аргументы функции Dart подразделяются на:
−позиционные;
−именованные;
−необязательные позиционные;
−необязательные именованные;
−комбинация из вышеперечисленных.
Самыми простыми в понимании являются позиционное аргументы,
так как последовательность передаваемых на вход функций переменных,
должна соответствовать последовательности и типу объявленных в
функции аргументов:
// ex3_1.dart
void myFunction(String name, int date, String monthName){
print('$name родился $date $monthName!');
}

void main(List<String> arguments) {


myFunction('Александр', 10, 'сентября');
}
// Александр родился 10 сентября!

Когда входных аргументов дюже много и становится неудобно читать


функцию, воспользуйтесь следующим способом – после каждого имени
объявляемого или передаваемого в функцию аргумента ставите запятую.
Это позволит вызвать в VS Code нажатием клавиш «Alt+Shift+F»
автоформатирование кода и перенесет каждый аргумент функции на
отдельную строку:
// ex3_2.dart
void myFunction(
String name,
int date,
String monthName,
int salary,
String workPosition,
) {
print('$name родился $date $monthName!');

142
print('$name заработал $salary рублей в месяц.');
print('$name работает в должности $workPosition.');
}

void main(List<String> arguments) {


myFunction(
'Александр',
10,
'сентября',
10000,
'бухгалтер',
);
}
// Александр родился 10 сентября!
// Александр заработал 10000 рублей в месяц.
// Александр работает в должности бухгалтер.

Для объявления того, что на вход функции аргументы передаются


именованным образом необходимо обернуть их в фигурные скобки «{ }».
После чего, в момент вызова функции явно указать какому именованному
аргументу какое значение передается. Если тип объявляемого аргумента
null-safety, т.е. не может хранить значение null, то перед объявляемым
аргументом используйте ключевое слово required:
// ex3_3.dart
void myFunction({
required String name,
required int date,
required String monthName,
}) {
print('$name родился $date $monthName!');
}

void main(List<String> arguments) {


myFunction(
date: 10,
name: 'Александр',
monthName: 'сентября',
);
}
// Александр родился 10 сентября!

Обратите внимание на порядок передаваемых в функцию значений.


Они могут передаваться в произвольной последовательности, так как мы
явно указываем какому из аргументов функции будет соответствовать то
или иное значение.

143
Если же значение передаваемого аргумента может хранить значение
null, то после указания его типа добавьте символ «?» и предусмотрите
проверку на null, который может передаться в двух случаях:
1. аргументу функции передали переменную, хранящую значение null;
2. именованному аргументу ничего не передавали, вследствие чего
значение аргумента по умолчанию становится равным null.
// ex3_4.dart
String myFunction({
String? name,
required int date,
required String monthName,
}) {
if (name != null) {
return '$name родился $date $monthName!';
}
return 'Не установлено имя новорожденного!';
}

void main(List<String> arguments) {


print(myFunction(
date: 10,
monthName: 'сентября',
));
print(myFunction(
date: 10,
monthName: 'сентября',
name: 'Иван',
));
}
// Не установлено имя новорожденного!
// Иван родился 10 сентября!

Когда мы указываем, что именованный аргумент может принимать


значение null, он становится необязательным при вызове функции. Но
когда, кровь из носу, запрещено давать такие вольности разработчику,
который будет использовать ваш код – пометьте такой именованный
аргумент, как required. Тогда у него не останется вариантов, кроме как
самому передать такому именованному аргументу вызываемой функции
null:
// ex3_5.dart
String myFunction({
required String? name,
required int date,
required String monthName,
}) {
if (name != null) {

144
return '$name родился $date $monthName!';
}
return 'Не установлено имя новорожденного!';
}

void main(List<String> arguments) {


// OK
print(myFunction(
date: 10,
monthName: 'сентября',
name: null,
));

// BAD
// Error: Required named parameter 'name' must be provided.
print(myFunction(
date: 10,
monthName: 'сентября',
));
}

Для того, чтобы указать необязательные аргументы при их


позиционном размещении, оберните их в квадратные скобки «[ ]»:
// ex3_6.dart
String myFunction(String name, int date, [String? monthName]){
if(monthName != null){
return '$name родился $date $monthName!';
}
return '$date числа, неустановленного месяца, родился $name!';
}

void main(List<String> arguments) {


print(myFunction('Александр', 20));
print(myFunction('Александр', 20, 'мая'));
}
// 20 числа неустановленного месяца родился Александр!
// Александр родился 20 мая!

Когда тип передаваемого в функцию аргумента не является


примитивным типом данных, то есть – List, Set, Map, пользовательский
тип данных и т.д., будьте предельно осторожны и не меняйте значения этих
объектов в блоке кода функции, если эти изменения не требует логика
работы приложения. Это связано с тем, что изменения останутся и после
завершения функции. Говоря другими словами, примитивные типы
данных передаются в функцию по значению, а все другие – по ссылке:

145
// ex3_7.dart
class Employee {
String name;
int age;
int salary;

Employee(this.name, this.age, this.salary);

@override
String toString() {
// чтобы печатать состояние объекта в терминале
return 'Employee{$name, $age, $salary}';
}
}

void changeSalary( Employee employee, int newSalary,) {


employee.salary = newSalary;
}

void addListElement(List<int> funcList, int b,) {


funcList.add(b);
}

void removeSetElement(Set<int> funcSet, int b,) {


funcSet.remove(b);
}

void updateMapValue(
Map<String, int> funcMap, {
required String key,
required int value,
}) {
funcMap[key] = value;
}

void stringUpdate(String funcString, String b) {


funcString = b;
}

void main(List<String> arguments) {


var myList = [10, 20];
addListElement(myList, 3);
print(myList); // [10, 20, 3]

var mySet = {10, 20};


removeSetElement(mySet, 20);
print(mySet); // {10}

146
var myMap = {
'a': 1,
'b': 2,
};
updateMapValue(myMap, key: 'b', value: 3);
print(myMap); // {'a': 1, 'b': 3}

var employee = Employee('Tom', 20, 100);


changeSalary(employee, 200);
print(employee); // Employee{Tom, 20, 200}

var str = 'Hello';


stringUpdate(str, 'World');
print(str); // Hello
}

3.3.2. Необязательные аргументы функции по умолчанию


Когда мы объявляем необязательные позиционные или именованные
аргументы, они по умолчанию инициализируются значением null (когда
при вызове функции им не задается отличное от null значение). Для того,
чтобы присвоим им значение по умолчанию, отличное от null, после
объявления имени аргумента добавьте оператор присваивание и само
значение, которое будет использоваться в коде функции всякий раз, пока
явно не передастся другое значение. При этом не имеет значения, тип
объявляемого аргумента – null-safety или нет:
// ex3_8.dart
String myFunction(
String name, [
int? date = 10,
String monthName = 'июля',
]) {
return '$name родился $date $monthName!';
}

void main(List<String> arguments) {


print(myFunction('Александр'));
print(myFunction('Александр', 24));
print(myFunction('Александр', 20, 'мая'));
}
// Александр родился 10 июля!
// Александр родился 24 июля!
// Александр родился 20 мая!

При использовании позиционного расположения аргументов


функции, те аргументы, которым присваиваются значения по умолчанию,

147
должны указываться в конце. Следующие объявления функций, где
значения по умолчанию задаются в других местах, приведут к ошибке:
// ex3_9.dart
String myFunction(
String name = 'Александр',
int date,
String monthName,
){
return '$name родился $date $monthName!';
}

String myFunction(
String name, [
int date=10,
String monthName,
]){
return '$name родился $date $monthName!';
}

Чтобы ввести значение по умолчанию для именованных аргументов,


достаточно просто присвоить ему значение и не использовать ключевое
слово required. К расположению значений по умолчанию для такого типа
аргументов требований нет, но лучше придерживаться устоявшейся
традиции и объявлять их в конце:
// ex3_10.dart
String myFunction({
required String name,
required String monthName,
int date = 10,
}) {
return '$name родился $date $monthName!';
}

void main(List<String> arguments) {


print(myFunction(name: 'Александр', monthName: 'мая'));
print(myFunction(
date: 14,
name: 'Александр',
monthName: 'мая',
));
}
// Александр родился 10 мая!
// Александр родился 14 мая!

148
3.3.3. Область видимости переменных
В языке программирования Dart используется лексическая область
видимости объектов. Это значит, что каждый блок кода имеет доступ к
переменным, которые были объявлены на уровне выше, то есть «над» ним.
В качестве примера приведем вложенные функции:
// ex3_11.dart
var topLevel = 'Сверх-доступная переменная';
void topLevelFunction(){
print(topLevel);
var firstLevel= 'Не очень доступная переменная';
void firstLevelFunction(){
print(topLevel);
print(firstLevel);
var secondLevel= 'Так себе доступная переменная';
void secondLevelFunction(){
print(topLevel);
print(firstLevel);
print(secondLevel);
}
secondLevelFunction();
}
firstLevelFunction();
}

void main(List<String> arguments) {


topLevelFunction();
}
// Сверх-доступная переменная
// Сверх-доступная переменная
// Не очень доступная переменная
// Сверх-доступная переменная
// Не очень доступная переменная
// Так себе доступная переменная

Если же попытаться из блока верхнего уровня обратиться к


переменной, объявленной на уровне ниже, то это вызовет ошибку времени
компиляции:
// ex3_12.dart
var topLevel = 'Сверх-доступная переменная';
void topLevelFunction(){
print(topLevel);
var firstLevel= 'Не очень доступная переменная';
void firstLevelFunction(){
print(topLevel);
print(firstLevel);
var secondLevel= 'Так себе доступная переменная';

149
}
print(secondLevel); // error: Undefined name 'secondLevel'.
}
void main(List<String> arguments) {
topLevelFunction();
}

Зачем вообще заострять внимание на области видимости? Все дело в


том, что функция запоминает объявленные объекты в объемлющей области
видимости и способна работать ними, даже если такую функцию передали
в качестве аргумента другой функции. Именно на этом и держится
механизм замыкания, который рассмотрим несколько позже.

3.3.4. Обращение к функции через переменную


Как уже говорилось ранее – функцию можно присваивать (связывать
с) переменной, после чего вызывать ее посредством работы с этой самой
переменной. Для примера напишем функцию сложения двух чисел, и будем
с ней работать посредством переменной:
// ex3_13.dart
int add(int a, int b) {
return a + b;
}

String season(int month) {


return switch (month) {
== 12 || > 0 && < 3 => 'Winter',
>= 3 && < 6 => 'Spring',
>= 6 && < 9 => 'Summer',
>= 9 && < 12 => 'Autumn',
_ => "WTF? (╯'□')╯︵ ┻━┻",
};
}

void main(List<String> arguments) {


var myAdd = add;
print(myAdd(10, 5)); // 15

var mySeason = season;


print(mySeason(10)); // Autumn
print(mySeason(-1)); // WTF? (╯'□')╯︵ ┻━┻
var o_O = (int a, int b){
return a * b;
};
print(o_O(10, 5)); // 50
}

150
3.3.5. Функция как входной аргумент другой функции
Теперь используем написанные в предыдущем разделе функции в
качестве входного аргумента другой функции. Для этого на понадобится в
качестве типа аргумента указать сигнатуру принимаемой на вход функции
(возвращаемый тип данных Function(тип1, … типN)):
// ex3_14.dart
int add(int a, int b) {
return a + b;
}

String season(int month) {


return switch (month) {
== 12 || > 0 && < 3 => 'Winter',
>= 3 && < 6 => 'Spring',
>= 6 && < 9 => 'Summer',
>= 9 && < 12 => 'Autumn',
_ => "WTF? (╯'□')╯︵ ┻━┻",
};
}

String myStrFunc(
String prefix,
int month,
String Function(int) func,
) {
return prefix + ' ' + func(month);
}

int sub(
int a,
int b, {
int c = 10,
int Function(int, int) func = add, }) {
return c - func(a, b);
}

void main(List<String> arguments) {


print(myStrFunc('ヽ༼ ಥ_ಥ༽ノ', 12, season));
// ヽ༼ ಥ_ಥ༽ノ Winter
print(myStrFunc("(҂ 'з´) ︻╦̵̵̵̵̿̿╤──", 0, season));
// (҂ 'з´) ︻╦̵̵̵̵̿̿╤── WTF? (╯'□')╯︵ ┻━┻

print(sub(3, 7)); // 0
print(sub(2, 4, c: 2)); // -4
print(sub(2, 4, c: 2, func: (int a, int b) {

151
return a * b;
})); // -6
}

Такой подход позволяет использовать либо уже существующие


функции, которые совпадают по сигнатуре с типом указанного аргумента,
либо задавать новые (анонимные) в процессе вызова исходной функции,
как показано в последнем примере. Тем самым предоставляя
разработчикам более гибкие механизмы управления потоком выполнения
программы:
// ex3_15.dart
int sub(
int a,
int b, {
int c = 10,
int Function(int, int)? func,
}) {
if (func == null) {
return 0;
}
return c - func(a, b);
}

void main(List<String> arguments) {


var a = 13, b = 12;
print(sub(
2,
4,
c: 2,
func: a < b
? (int a, int b) {
return a * b;
}
: (int a, int b) {
return a - b;
},
));
}
// 4 при a > b, a = 13, b = 12
// -6, когда a = 11, b = 12

3.3.6. Type Alias


Иногда бывают ситуации, что используемый разработчиком тип
данных, представляет собой довольно сложную комбинацию коллекций:
// ex3_16.dart
int myFunc(Map<String, Map<(int, List<int>), int>> data) {

152
var sum = 0.0;
for (var MapEntry(:value) in data.entries) {
for (var MapEntry(key: recKey, value: recValue)
in value.entries) {
var (int a, List<int> b) = recKey;
sum += (a * b.reduce(
(value, element) => value + element)
)/recValue;
}
}
return sum.floor();
}

void main(List<String> arguments) {


Map<String, Map<(int, List<int>), int>> map = {
'a': {
(1, [1, 2, 3]): 100,
(2, [2, -4, 9]): -98,
(3, [3, 4, 5]): 3,
},
'b': {
(10, [1, 0, 3]): 100,
(20, [6, -4, 2]): -98,
(30, [-3, 4, -5]): 3,
}
};
print(myFunc(map)); // -29
}

Когда у функции один аргумент с таким типом данных, то это еще


терпимо. А теперь представьте, что их 3 или более и все различные. Такая
функция станет ночным кошмаром! Так как просыпаться в холодном поту
– то еще удовольствие, разработчики Dart предусмотрели Type Alias
(псевдоним типа). Он позволяет в более компактной форме написать
объявление типов и функций. Изначально этот механизм был доступен
только для функций (псевдоним типа функций), но начиная с Dart 2.13 его
распространили и на типы данных. Согласно документации языка
программирования [12, 13] в отношении функций им рекомендуется
пользоваться в случаях, когда тип (сигнатура) функции особенно длинный
или часто используется в коде. Но важно понимать, что в большинстве
случаев, другие разработчики захотят увидеть, какой у функции тип на
самом деле. Это даст им больше понимания при работе с самой функцией.
Особенно, когда ее тип используется для задания типа входного аргумента
другой функции.

153
Для начала дадим типу Map<String, Map<(int, List<int>), int>>
псевдоним OMyMap. В этом нам поможет ключевое слово typedef:
// ex3_17.dart
typedef OMyMap = Map<String, Map<(int, List<int>), int>>;

int myFunc(OMyMap data) {


var sum = 0.0;
for (var MapEntry(:value) in data.entries) {
for (var MapEntry(key: recKey, value: recValue)
in value.entries) {
var (int a, List<int> b) = recKey;
sum += (a * b.reduce(
(value, element) => value + element)
)/recValue;
}
}
return sum.floor();
}

void main(List<String> arguments) {


OMyMap map = {
'a': {
(1, [1, 2, 3]): 100,
(2, [2, -4, 9]): -98,
(3, [3, 4, 5]): 3,
},
'b': {
(10, [1, 0, 3]): 100,
(20, [6, -4, 2]): -98,
(30, [-3, 4, -5]): 3,
}
};
print(myFunc(map)); // -29
}

Обратите внимание на то, на сколько код стал легче в восприятии!


Снижение когнитивной нагрузки при чтении кода – достаточно весомый
аргумент для использования описываемых возможностей Dart.
Для следующего примера, немного перепишем пример и используем
typedef применительно к типу функции:
// ex3_18.dart
typedef OMyMap = Map<String, Map<(int, List<int>), int>>;
typedef OMyMapFunc = int Function(OMyMap)?;

int myFunc(OMyMap data) {


var sum = 0.0;
for (var MapEntry(:value) in data.entries) {

154
for (var MapEntry(key: recKey, value: recValue)
in value.entries) {
var (int a, List<int> b) = recKey;
sum += (a * b.reduce(
(value, element) => value + element)
)/recValue;
}
}
return sum.floor();
}

int add(
int a,
OMyMap data, {
OMyMapFunc func,
}) {
if (func == null) {
return 0;
}
return a + func(data);
}

void main(List<String> arguments) {


OMyMap map = {
'a': {
(1, [1, 2, 3]): 100,
(2, [2, -4, 9]): -98,
(3, [3, 4, 5]): 3,
},
'b': {
(10, [1, 0, 3]): 100,
(20, [6, -4, 2]): -98,
(30, [-3, 4, -5]): 3,
}
};
print(add(22, map, func: myFunc)); // -7
}

Просто представьте, как читался бы код сложного проекта без Type


Alias! Теперь выдохните и возрадуйтесь, что с Dart 2.13 typedef позволяет
задавать не только псевдонимы типов функций, но и типов данных.

3.3.7. Анонимные и стрелочные функции


Помимо функций, которые содержат их явные названия, вы можете
создавать и анонимные функции (порой их еще называют «Лямбда-

155
функции»), то есть функции без имени. Их структуру записи можно
представить следующим образом:
([[Type] arg1[, …]]) {
// блок кода
};

Эти функции на свой вход могут принимать сколько угодно значений


или не принимать их вовсе. Все зависит от того, каким образом она была
объявлена. При этом тип входного аргумента в некоторых случаях можно
опускать. В качестве примера давайте создадим анонимную функцию для
вывода элементов списка и передадим ее в метод списка forEach:
// ex3_19.dart
void main(List<String> arguments) {
var myList = ['Привет!', 'Я', '-', 'анонимная', 'функция!'];
myList.forEach((item) {
print(
'По индексу ${myList.indexOf(item)} хранится значение => $item'
);
});
}
// По индексу 0 хранится значение => Привет!
// По индексу 1 хранится значение => Я
// По индексу 2 хранится значение => -
// По индексу 3 хранится значение => анонимная
// По индексу 4 хранится значение => функция!

Стрелочная функция предоставляет форму для объявления


однострочных именованных или анонимных функций. Их поведение
аналогично обычным функциям за тем исключением, что они по
умолчанию всегда возвращают значение, то есть оператор return в этих
функциях не используется, но его наличие подразумевается:
// ex3_20.dart
int add(int a, int b) => a + b;

int sub(
int c,
int a,
int b,
int Function(int, int) func,
) =>
c - func(a, b);

void main(List<String> arguments) {


print(sub(30, 21, 2, add)); // 7
}

156
Анонимные или стрелочные функции могут присваиваться
переменной, точно также как и обычные функции:
// ex3_21.dart
int add(int a, int b) => a + b;

void main(List<String> arguments) {


var newAddFunction = add;
var newSubFunction = (
int c,
int a,
int b,
int Function(int, int) func,
) {
return c - func(a, b);
};

print(newSubFunction(30, 21, 2, newAddFunction)); // 7


}

3.3.8. Замыкания
Замыкания представляют собой довольно мощный инструмент, в
основе которого лежит возможность функций запоминать значения
переменных из объемлющих областей видимости. То есть из тех областей
видимости, где данная функция была объявлена. В основе идеи замыкания
лежит то, что функция может возвращать функцию, которая в свою очередь
может на вход принимать совершенно отличные значения от тех, что
подаются функции верхнего уровня, но использует в своей работе данные,
определенные в функции верхнего уровня. Звучит немного запутанно, не
прав да ли?
Давайте представим, что в качестве функции верхнего уровня
выступает завод, производящий технику. Ему каждый день поставляют
оборудование и забирают изготовленные микроволновые печи, которые
развозят по магазинам и, в конечном счете, одна из них оказалась в доме
покупателя. Каждый из нас часто пользуется этим устройством, чтобы
разогреть или приготовить еду и даже не задумывается, а из каких деталей
она состоит, как они взаимодействуют друг с другом и т. д. Так вот, эта
микроволновая печь и есть возвращаемая вложенная функция. В нее мы
ставим тарелку с супом, задаем ей режим работы, а она уже выполняет
нагрев с использованием той аппаратной начинки, что использовалось на
производственной линии завода:
// ex3_22.dart
int indexMicrowave = 0;

Function factory(String nameMicrowave, int power){

157
// Function – обобщенный тип данных для функций, к
// которому их можно приводить
var model = '$nameMicrowave-RX-0003$indexMicrowave';
indexMicrowave++;
return (String dish, int mode){
var myStr = StringBuffer(
'Микроволновка "$model" мощностью $power Вт'
);
myStr.write(', греет блюдо "$dish" в режиме $mode');
return myStr;
};
}

void main(List<String> arguments) {


var microwave = factory('Scarlet', 750);
print(microwave('Борщ', 3));
print(microwave('Котлеты', 5));
var newMicrowave = factory('Scarlet', 1000);
print(newMicrowave('Рагу', 2));
}
// Микроволновка "Scarlet-RX-00030" мощностью 750 Вт, греет
// блюдо "Борщ" в режиме 3
// Микроволновка "Scarlet-RX-00030" мощностью 750 Вт, греет
// блюдо "Котлеты" в режиме 5
// Микроволновка "Scarlet-RX-00031" мощностью 1000 Вт, греет
// блюдо "Рагу" в режиме 2

Также замыкания могут применяться в программах, которым


необходимо генерировать обработчики событий на лету в ответ на условия,
сложившиеся во время выполнения.
В качестве еще одного примера реализуем функцию, принимающую
на вход значение степени, в которую будем возводить числа, подаваемые
на вход возвращаемой ей функции:
// ex3_23.dart
import 'dart:math'; // подключаем библиотеку math
// для использования функции pow

int Function(int value) degree(int degree){


return (int value) => pow(value, degree).toInt();
}

void main(List<String> arguments) {


var calculation = degree(3);
print(calculation(3)); // 27
print(calculation(2)); // 8
calculation = degree(8);
print(calculation(3)); // 6561

158
print(calculation(7)); // 5764801
}

Теперь модифицируем пример таким образом, чтобы в функции main


объявлялась переменная и функция, захватывающая и изменяющая ее
значение, а также выступающая входным аргументом другой функции,
которая в свою очередь посредством замыкания вернет нам еще одну
функцию для последующей работы с ней (ну… люблю я издеваться
над людьми ):
// ex3_24.dart
int Function(int) myClosure(
int a,
int b,
int Function(int, int) func,
) {
return (int value) => value - func(a, b);
}

void main(List<String> arguments) {


var globalValue = 99;
int mainFunc(int a, int b) {
globalValue--;
print('globalValue: $globalValue');
return a < b ?
globalValue - a + b :
-globalValue + b * a;
}

var calculation = myClosure(3, 5, mainFunc);


print(calculation(3)); // globalValue: 98, -97
print(calculation(2)); // globalValue: 97, -97
calculation = myClosure(6, -2, mainFunc);
print(calculation(3)); // globalValue: 96, 111
print(calculation(7)); // globalValue: 95, 114

print('Final globalValue: $globalValue');


// Final globalValue: 95
}

3.3.9. Рекурсия
В теле функции может вызываться эта же самая функция. Такой
механизм называется рекурсией. Он обычно используется как
альтернатива циклу, когда необходимо обходить структуры, которые
имеют произвольную, непредсказуемую форму и глубину.

159
Существует несколько видов организации рекурсии: прямая и
косвенная. Давайте разберемся с тем, как они организуются на примере
задачи суммирования элементов последовательности:
// ex3_25.dart
int addFunction(List<int> myList){
print(myList);
if (myList.length <=1){
return myList[0];
}
else{ return myList[0] + addFunction(myList.sublist(1)); }
}

void main(List<String> arguments) {


var myList = [10, 20, 30, 5, 3, 2];
print(addFunction(myList));
}
// [10, 20, 30, 5, 3, 2]
// [20, 30, 5, 3, 2]
// [30, 5, 3, 2]
// [5, 3, 2]
// [3, 2]
// [2]
// 70

В примере выше у нас приведена прямая рекурсия, когда функция


явно вызывает саму себя. Каждый раз при вызове функцией самой себя ей
на вход подается срез списка, содержащий только те элементы, которые
еще не участвовали в операции суммирования. Теперь реализуем
суммирование элементов списка посредством косвенной рекурсии:
// ex3_26.dart
int addFunction(List<int> myList){
print(myList);
if (myList.length <=1){
return myList[0];
}
else{
return anotherFunction(myList);
}
}

int anotherFunction(List<int> myList){


return myList[0] + addFunction(myList.sublist(1));
}

void main(List<String> arguments) {


var myList = [10, 20, 30, 5, 3, 2];
print(addFunction(myList));

160
}
// [10, 20, 30, 5, 3, 2]
// [20, 30, 5, 3, 2]
// [30, 5, 3, 2]
// [5, 3, 2]
// [3, 2]
// [2]
// 70

Основным требованием при организации рекурсии является наличие


условия выхода из нее. В случае его отсутствия произойдет переполнение
стека вызова функций (превышение лимита по количеству рекурсивного
вызова функции) и выполнение кода прервется, оповестив пользователя о
наличии проблемы следующим исключением: «Unhandled exception:
Stack Overflow».

3.3.10. Генераторные функции


Генераторные функции используются для ленивой генерации
последовательности значений по запросу. Они выступают отличным
подспорьем спискам, так мы в этом случае не храним в памяти массив
значений, а создаем объект с необходимым текущим значением
генерируемой последовательности только в момент обращения к
генераторному объекту.
Dart предоставляет два варианта реализации генераторных функций:
− Синхронная генераторная функция – функция, возвращающая
объект типа Iterable<T>, где T – тип генерируемого функцией
значения. Класс Iterable представляет собой интерфейс,
реализуемый итерируемым набором значений или «элементов», к
которым можно получить последовательный доступ. Для обозначения
синхронной генераторной функции, после ее объявления, перед телом
самой функции она помечается как «sync*».
− Асинхронная генераторная функция – функция, возвращающая
объект типа Stream<T>, где T – тип генерируемого функцией значения.
Класс Stream представляет собой источник событий асинхронных
данных. Для обозначения асинхронной генераторной функции, после
ее объявления, перед телом самой функции она помечается как
«async*». Более подробно асинхронное программирование обсудим в
одном из следующих разделов.
Еще одной отличительной чертой обычных функций от генераторных
является то, что для возвращения из нее значения используется оператор
yield, а не return. Именно благодаря этому оператору генераторные
функции автоматически приостанавливают и возобновляют свое
выполнение и состояние вокруг точки генерации значений, а не

161
прекращают свою работку, как обычные функции. В момент приостановки
выполнения генераторной функции, сохраняется информация о ее
состоянии, куда входят данные о местоположении точки выхода из
функции и локальной области видимости. Возобновляется работа
генераторной функции с оператора yield, то есть с той точки, в которой была
выполнена остановка ее выполнения.
В качестве примера давайте сгенерируем последовательность
значений от 0 до 5 и запишем ее в список:
// ex3_27.dart
Iterable<int> myGenerator() sync* {
var k = 0;
while (k < 5) {
yield k++;
}
}

void main(List<String> arguments) {


var result = <int>[];
for(var it in myGenerator()){
result.add(it);
}
print(result); // [0, 1, 2, 3, 4]
}

Для того, чтобы лучше понять принцип работы генераторной функции


перепишем ее код следующим образом:
// ex3_28.dart
Iterable<int> myGenerator() sync* {
yield 0;
yield 1;
yield 2;
yield 3;
yield 4;
}

void main(List<String> arguments) {


var result = <int>[];
for(var it in myGenerator()){
result.add(it);
}
print(result); // [0, 1, 2, 3, 4]
}

Как видно из примера, при первом проходе цикла for из генераторной


функции вернулось значение 0, после чего ее работа приостановилась до
момента второго прохода цикла. При последующем обращении к функции

162
она продолжила свою работу с точки предыдущего выхода из нее, то есть с
«yield 0;». После того, как последовательность операторов yield в теле
функции закончилась, прекратился и цикл for.
Теперь давайте реализуем генераторную функцию, которая будет
возвращать только значения, которые нацело делятся на 4, а диапазон по
какое значение должна генерироваться последовательность будет
задаваться пользователем. В этом нам поможет библиотека «dart:io»:
// ex3_29.dart
import 'dart:io';

Iterable<int> myGenerator(int n) sync* {


var k = 0;
while(k < n){
if (k % 4 == 0){
yield k;
}
k++;
}
}

void main(List<String> arguments) {


var result = <int>[];

print('Введите границу генерируемой послед-ти: ');


// читаем значение введенное с клавиатуры
var n = int.parse(stdin.readLineSync()!); // вводим 20

for(var it in myGenerator(n)){
result.add(it);
}
print(result); // [0, 4, 8, 12, 16]
}

Далее рассмотрим, как можно использовать генераторную функцию


вне циклов, осуществляя работу через возвращаемый ей при создании
объект типа Iterable<T>:
// ex3_30.dart
Iterable<int> myGenerator(int n) sync* {
var k = 0;
while(k < n){
if (k % 4 == 0){
yield k;
}
k++;
}
}

163
void main(List<String> arguments) {
var result = <int>[];
var it = myGenerator(20);
it.forEach((element) {result.add(element);});
var result1 = it.toList();
print(result); // [0, 4, 8, 12, 16]
print(result1); // [0, 4, 8, 12, 16]
}

Ниже приведен способ создания асинхронной генераторной функции,


выводящей в терминал значения генерируемой последовательности:
// ex3_31.dart
Stream<int> myAsyncGenerator(int n) async* {
var k = 0;
while(k < n){
if (k % 4 == 0){
yield k;
}
k++;
}
}

void main(List<String> arguments) {


Stream<int> sequence = myAsyncGenerator(30);
sequence.listen(print); // 0 4 8 … 28
}

Пользоваться асинхронными генераторными функциями без знания


принципов асинхронного программирования не стоит. На данный момент
пока ограничитесь использованием их синхронных вариантов.

3.4. Создание и импортирование библиотек


При написании многократно используемого кода его принято
выделять в отдельные модули, представляющие собой единицу
организации программ наивысшего уровня, которая упаковывает
программный код, данные и предоставляет изолированные пространства
имен, цель которых - свести к минимуму конфликты имен переменных
внутри программ.
Для модульного представления кода вашего проекта в Dart
используются библиотеки и пакеты. Библиотека может представлять собой
как один файл с кодом, подключаемый к основному приложению и
расположенный в той же директории, так и набор таких файлов,

164
организованных в каталог, с единой или множественной точкой доступа к
ним, позволяющей скрыть детали реализации.
Пакет представляет собой каталог, который может содержать в себе
любое количество библиотек. Обязательным условием при создании пакета
является наличие в нем файла «pubspec.yaml» на верхнем уровне пакета,
содержащем важную информацию о самом пакете и его зависимостях от
других пакетов. Поэтому любое приложение, написанное на Dart, можно
рассматривать как пакет.
Dart предоставляет некоторый базовый набор библиотек, который
можно подключить посредством оператора import:
− dart:core. Данная библиотека подключается автоматически в каждый
dart-файл и предоставляет доступ к встроенным типам, коллекциям и
другим основным функциям.
− dart:async. Используется для поддержки асинхронного
программирования.
− dart:math. Предоставляет доступ к математическим константам,
функциям и генератору случайных чисел.
− dart:convert. Используется для преобразования между различными
представлениями данных.
− dart:html. Предоставляет доступ к DOM (Document Object Model) html-
файлов и другим API для браузерных приложений.
− dart:io. Служит для ввода и вывода данных. Позволяет работать с
файлами, каталогами, сокетами, процессами, писать собственные
серверные и клиентские части приложений и т. д.
− dart:collection. Содержит классы и утилиты, дополняющие
dart:core.
− dart:typed_data. Предоставляет списки для эффективной работы с
данными фиксированного размера (например, целые числа размером
8 байт без знака) и числовые типы SIMD.
− dart:developer. Библиотека для программного взаимодействия со
средой выполнения Dart для отладки и проверки. У нее есть несколько
реализаций: Dart Web и Dart Native (VM) для целевых платформ
(Android, Windows и т.д.). Стоит отметить, что не все платформы
поддерживают весь набор операций, предоставляемых библиотекой.
− dart:ffi. Предоставляет интерфейсные функции для взаимодействия
с языком программирования C.
− dart:isolate. Используется для написания конкурентного кода на
Dart с использованием изолятов. Изоляты в отличие от потоков не
разделяют память, а взаимодействие с ними может осуществляться
только посредством сообщений. Данная технология (библиотека)
поддерживается только средой выполнения Dart и не имеет
реализаций под Dart Web и целевые платформы.

165
− dart:mirrors. Предоставляет разработчикам инструменты для работы
с рефлексией.
− и т.д. (еще ряд библиотек для работы с WEB).

3.4.1. Импортирование кода из файла с расширением «.dart»


Для начала разберемся с тем как подключать (импортировать) код из
другого файла с расширением «.dart» (библиотеки), расположенного в
основной директории разрабатываемого приложения. В качестве примера
реализуем файл, который содержит функции операций над числами:
умножение, деление, сложение и т. д.
На первом шаге создадим простой проект («Console Application»), с
именем «my_lib»:

Рисунок 3.3 – Структура созданного приложения

Далее в директории «bin», где расположен файл «my_lib.dart»


создадим директорию «src», куда добавим новый файл с названием
«my_calculator.dart»:

Рисунок 3.4 – Добавление файла «my_calculator.dart» в проект

Теперь можно приступать к его заполнению кодом:


// src/my_calculator.dart
import 'dart:math';

166
double add(double a, double b) => a + b;
double sub(double a, double b) => a - b;
double div(double a, double b) => a / b;
double mul(double a, double b) => a * b;
double pow2(double a) => a * a;
double powN(double a, double n) => pow(a, n).toDouble();

Импортируем код из файла «my_calculator.dart» в файл


«my_lib.dart» и выведем в терминал результаты выполнения некоторых из
функций:
import 'src/my_calculator.dart';

void main(List<String> arguments) {


print(add(3.5, 10)); // 13.5
print(mul(2.5, 4)); // 10.0
print(pow2(3)); // 9.0
}

Как видим из результата выполнения кода, функции, объявленные в


файле «my_calculator.dart» доступны в основном файле приложения по
их имени. Такое поведение при импортировании не всегда бывает
удобным. Особенно, когда несколько импортируемых файлов будут
содержать одинаковое объявление какой-либо функции или другого
объекта — это приведет к ошибке. Для того, чтобы в этом убедиться, в
директории «src» создадим файл «short_calculator.dart», в который
скопируем функцию сложения и вычитания:
double add(double a, double b) => a + b;
double sub(double a, double b) => a - b;

И импортируем его в файл «my_lib.dart»:


import 'src/my_calculator.dart';
import 'src/short_calculator.dart';

void main(List<String> arguments) {


print(add(3.5, 10));
}
// Error: 'add' is imported from both 'bin/src/my_calculator.dart'
// and 'bin/src/short_calculator.dart'

Чтобы избежать таких ошибок, после объявления импорта одной из


библиотек можно использовать ключевое слово as (префикс/Prefix) за
которым следует указать имя для обращения к функциям из библиотеки:
import 'src/my_calculator.dart' as calculator;
import 'src/short_calculator.dart';

167
void main(List<String> arguments) {
// вызов функции из short_calculator.dart
print(add(3.5, 10)); // 13.5
// вызов функции из my_calculator.dart
print(calculator.mul(2.5, 4)); // 10.0
print(calculator.pow2(3)); // 9.0
print(calculator.add(2.5, 4)); // 6.5
}

Часть функций в библиотеке можно объявлять приватными путем


добавления символа нижнего подчеркивания перед именем функции.
Тогда они останутся доступными для использования в самой библиотеке,
но станут не импортируемыми. Для примера перепишем файл
«my_calculator.dart» следующим образом:
import 'dart:math';

double add(double a, double b) => _add(a, b);


double sub(double a, double b) => a - b;
double div(double a, double b) => a / b;
double mul(double a, double b) => a * b;
double pow2(double a) => a * a;
double powN(double a, double n) => pow(a, n).toDouble();

double _add(double a, double b){


return (a + b) * 10;
}

И попробуем средствами IDE обратиться к приватной функции


библиотеки:

Рисунок 3.5 – Доступные для использования функции библиотеки

168
Как видим из рисунка 3.5, доступ к приватным функциям
импортируемых библиотек из основного кода приложения закрыт. А
попытка их вызова приведет к ошибке:
import 'src/my_calculator.dart' as calculator;
import 'src/short_calculator.dart';

void main(List<String> arguments) {


// вызов функции из short_calculator.dart
print(add(3.5, 10));
calculator._add(2.5, 4);
}
// Error: Method not found: '_add'.

3.4.2. Импортирование части функциональности


Порой бывают ситуации, когда из имеющейся в нашем распоряжении
библиотеки необходимо использовать одну или две функции, либо же
закрыть к одной из функций доступ, чтобы не было возможности к ней
обратиться из кода, где подключили библиотеку. На эти случаи Dart
предоставляет следующие ключевые слова:
− show – позволяет указать используемую часть импортируемой
библиотеки. К остальным частям доступ будет закрыт;
− hide – скрывает указанную часть импортируемой библиотеки, к
которой будет закрыт доступ. Остальные части библиотеки будут
доступны для использования.
Данные ключевые слова можно использовать как с ключевым словом
as (после его объявления), так и без него.
Для демонстрации принципа работы частичного импортирования с
использованием ключевого слова show возьмем предыдущий пример кода
и укажем, что из импортируемой библиотеки нам нужна только функция
умножения:

Рисунок 3.6 – Импортирование части библиотеки

169
Если необходимо импортировать несколько частей библиотеки, то
перечислите их через запятую:
import 'src/my_calculator.dart' as calculator show mul, add;

void main(List<String> arguments) {


print(calculator.mul(2.5, 4)); // 10.0
print(calculator.add(2.5, 4)); // 65.0
}

Если результат от вызова функции сложения вам кажется


подозрительным, то вспомните, что сейчас она вызывает выполнение
приватной функции _add:
double _add(double a, double b){
return (a + b) * 10;
}

Для демонстрации принципа работы частичного импортирования с


использованием ключевого слова hide укажем, что запрещаем
импортировать функцию сложения и умножения:

Рисунок 3.7 – Импортирование части библиотеки

Как видно из рисунка 3.7 после такого импортирования мы не можем


использовать в клиентском коде функции: mul и add.

3.4.3. Отложенная загрузка импортируемого файла или


библиотеки

Отложенная (ленивая) загрузка библиотеки позволяет приложению


загружать библиотеку по запросу. То есть, такая библиотека может в
процессе работы приложения и не загружаться вовсе, если используются
операторы управления потоком выполнения. Для объявления отложенной

170
загрузки импортируемой библиотеки используется ключевое слово
deferred в связке с префиксом as <name>.
Дополнительным условием использования данного механизма
является оборачивание отложенной загрузки библиотеки в асинхронную
функцию, возвращающую Future. Это связано с тем, что используемый
метод для самой ленивой загрузки – loadLibrary возвращает этот тип
данных.
В качестве примера выполним отложенную загрузку библиотеки и
вызовем функцию деления:
import 'src/my_calculator.dart' deferred as calculator;

void main(List<String> arguments) {


callLibrary(34, 5);
}

Future callLibrary(double a, double b) async{


await calculator.loadLibrary();
print(calculator.div(a, b)); // 6.8
}

При объявлении отложенной загрузки можно использовать ключевые


слова show и hide для регулирования доступа к частям импортируемой
библиотеки.

3.4.4. Создание и использование библиотеки


В Dart существует негласное соглашение относительно библиотек.
Несмотря на то, что создаваемая библиотека может иметь любую иерархию
каталогов с кодом, рекомендуется весь код, содержащий реализацию,
помещать в директорию «lib/src». Данный каталог является закрытым и
не рекомендуется явным образом импортировать из него файлы с кодом.
Для того, чтобы пользователь имел возможность взаимодействовать с
реализацией библиотеки, необходимо в самой директории «lib» поместить
файлы, которые экспортируют файлы с кодом из «lib/src» и представляют
собой API вашей библиотеки.
Для начала создадим новый консольный проект с названием
«my_new_lib», после чего добавим в каталог «lib» директорию «src» и
следующие файлы с кодом функций:
//my_add.dart
double add(double a, double b) => a + b;

//sub.dart
double sub(double a, double b) => a - b;

//mul.dart

171
double mul(double a, double b) => a * b;

//my_pow_n.dart
import 'dart:math';

double powN(double a, double n) => pow(a, n).toDouble();

Рисунок 3.8 – Текущее состояние проекта

При импорте таких файлов из библиотеки, в клиентском коде


приложения (файлы, расположенные в каталоге «bin»), необходимо в
начале указания пути использовать директиву «package:». Если мы в одном
файле библиотеки импортируем другой, который расположен так же в
директории «lib», то необходимо использовать относительные пути
(например, как в предыдущих примерах разделов 3.4.1 – 2).
Теперь перейдите в «bin» каталог и откройте «my_new_lib.dart».
Любой из объявленных ранее файлов импортируются из библиотеки
следующим образом:
import 'package:my_new_lib/src/my_add.dart';

void main(List<String> arguments) {


print(add(10, 34.2)); // 44.2
}

Обратите внимание, что после директивы package: указывается


название текущего приложения, а остальная часть пути берется из каталога
«lib». Но приведенный способ импортирования файлов из библиотеки не
является рекомендуемым, так как у нас нет файла, описывающего API
библиотеки, и мы получаем прямой доступ к реализациям файлов
библиотеки.
Для того, что бы привести структуру библиотеки в соответствии с
рекомендациями [14] в каталоге «lib» переименуем «my_new_lib.dart» в

172
«calculator.dart» (пока не обращайте внимание на директорию test, даже
если она стала красной) и укажем в нем экспортируемые из библиотеки
файлы:
export 'src/my_add.dart';
export 'src/my_mul.dart';
export 'src/my_pow_n.dart';
export 'src/my_sub.dart';

И перепишем предыдущий код:


import 'package:my_new_lib/calculator.dart';

void main(List<String> arguments) {


print(add(10, 34.2)); // 44.2
print(mul(10, 4.2)); // 42.0
print(sub(10, 4.8)); // 5.2
print(powN(10, 3)); // 1000.0
}

Как видно из кода, мы одной строчкой импортировали все доступные


функции, которые объявляли в различных файлах библиотеки и прописали,
как экспортируемые в файле «calculator.dart» каталога «lib».
При таком импортировании библиотеки доступны все ранее
рассмотренные механизмы, когда используются ключевые слова: show,
hide, as и deferred as.
В каталоге «lib» можно создавать неограниченное количество файлов,
которые могут предоставлять доступ либо к части, либо ко всем файлам
библиотеки. Для примера создадим еще один файл, который назовем
«short_calculator.dart» и посредством него экспортируем файлы
библиотеки только для сложения и вычитания:
export 'src/my_add.dart';
export 'src/my_sub.dart';

В случае импортирования этого файла нам станут доступны всего


лишь 2 функции, находящиеся в нашей библиотеке:

Рисунок 3.9 – Подключение библиотеки в клиентском коде приложения

173
Текущий проект «my_new_lib» можно рассматривать как уже готовый
пакет, готовый для импортирования в другой проект, т.к. на его верхнем
уровне имеется файл «pubspec.yaml», с информацией о самом пакете и его
зависимостях от других пакетов.

3.5. Тестирование функций


Можно привести много доводов за и против тестирования, но
необходимо понимать одно – покрытие проекта тестами уже многие годы
де-факто является стандартом в IT-индустрии. К сожалению, начинающие
разработчики рассматривают тестирование так достаточно нудный и
трудоемкий процесс, забирающий время от их любимого дела –
программирования. Но одно дело, если вы пишете проект «для себя», а
совсем другое, когда им будет пользоваться большое количество людей.
Поэтому выпускать на рынок продукт, не покрытый тестами – себе дороже.
К тому же существует методология разработки программного
обеспечения, где тестирование является основной составляющей – Test-
Driven Development (разработка через тестирование). Ключевая идея TDD –
написание кода теста перед самим процессом кодирования какого-либо
класса разрабатываемого программного продукта. Это позволяет довольно
хорошо покрыть код тестами и полностью автоматизировать сам процесс
тестирования, запуская его при каждом внесении изменений
программистом в реализацию того или иного компонента.
Дополнительным плюсом от применения TDD является упрощение
программной архитектуры, то есть, когда модуль, компонент или класс
проходит написанный под него тест, он считается готовым. При обычном
же подходе к разработке велика вероятность, что написанный код
получится избыточным, из-за того, что программист может пуститься «во
все тяжкие», аргументируя это фразой: «А что, если?».
Существует довольно большое количество видов тестирования, но в
документации Dart акцент делается только на трех [15]:
− Модульное тестирование. Такие тесты сосредоточены на проверке
мельчайших частей тестируемого программного обеспечения:
функции, методы или классы.
− Компонентное тестирование. Данные тесты необходимы для
проверки того, что компонент (обычно состоит из нескольких классов)
ведет себя должным образом. При компонентном тестировании часто
требует использование фиктивных объектов, которые могут
имитировать: действия пользователя, события и т. д.
− Интеграционное и сквозное тестирование. Эти тесты проверяют
поведение всего приложения или его большой части. Интеграционный
тест зачастую выполняется на реальном устройстве, симуляторе

174
операционной системы или в браузере. Он состоит из двух частей:
самого приложения и тестового приложения для его проверки.
Для написания тестов в Dart принято использовать пакет (библиотеку)
test, а когда необходимо использовать объекты заглушки (фиктивные
объекты) – mockito.
Перед тем, как перейдем к рассмотрению функциональности пакета
тестирования, как его устанавливать и организовывать тестовое окружение
кода приложения, создайте новый консольный проект с названием
«conquest_functions».

3.5.1. Установка пакета test в проект


Для установки пакета test (https://pub.dev/packages/test) в текущий
проект, пропишите его актуальную версию в качестве зависимости проекта
в файле pubspec.yaml:
dev_dependencies:
test: ^1.24.7 # актуальная версия на момент написания книги

Теперь сохраните изменения, нажав «Ctrl+S». Это запустит


автоматическое скачивание указанного пакета. Обычно, при создании
консольного проекта, пакет test автоматически прописывается как
зависимость, но его версия не всегда совпадает с актуальной.

3.5.2. Написание тестов


Название каждого файла с тестом должно заканчиваться постфиксом
_test. Перед тем, как приступить к написанию самих тестов, в директории
«lib» текущего проекта создадим 2 файла «my_math.dart» и «my_str.dart»,
в которых напишем следующий код:
// my_math.dart
import 'dart:math';

int add(int a, int b) => a + b;


int sub(int a, int b) => a - b;
int mul(int a, int b) => a * b;
double powN(double a, double n) => pow(a, n).toDouble();

// my_str.dart
List<String> splitString(String line, String splitter){
return line.split(splitter);
}

String stringToLowerCase(String line){


return line.toLowerCase();
}

175
String stringToUpperCase(String line){
return line.toUpperCase();
}

String deleteSurroundingSpaces(String line) => line.trim();

Далее в директории «test» создадим файл «my_functions_test.dart»


и определим функцию верхнего уровня main, в которой и будут писаться
тесты. Файлы «conquest_functions.dart» из директорий «bin», «lib» и
«test» пока удалите.
Для задания теста используется функция верхнего уровня test, где в
качестве первого аргумента передается описание проводимого
тестирования, а вторым аргументом выступает анонимная функция,
вызываемая при выполнении самого теста. Тело анонимной функции
должно заканчиваться тестовым утверждением expect, в который
передается результат работы функции, метода класса и т. д., а также
ожидаемый результат выполнения вычисления:
// my_functions_test.dart
import 'package:test/test.dart';

import 'package:conquest_functions/my_math.dart';
import 'package:conquest_functions/my_str.dart';

void main(){
test('Проверка сложения', (){
expect(add(3, 7), equals(10));
});

test('Удаление окружающих пробелов', (){


var line = ' oO ';
expect(deleteSurroundingSpaces(line), equals('oO'));
});
}

Рядом с каждым тестом должен появиться виджет для его запуска:

Рисунок 3.10 – Виджет запуска теста

176
Нажмите на него и дождитесь завершения теста. Если он завершился
успешно, то рисунок виджета сменится на зеленую галочку:

Рисунок 3.11 – Успешное завершение тестирования

В противном случае рисунок изменится на красный крест:

Рисунок 3.12 – Тест не пройден

Тесты могут быть сгруппированы. Для этого используется функция


group, первым аргументом в которую передается описание группы, а
вторым выступает анонимная функция, куда помещаются тесты. Внесем
следующие изменения в файл «my_functions_test.dart»:
import 'package:test/test.dart';

import 'package:conquest_functions/my_math.dart';
import 'package:conquest_functions/my_str.dart';

void main() {
group('Арифметические функции', () {
test('Проверка сложения', () {
expect(add(3, 7), equals(10));
});
test('Проверка умножения', () {
expect(mul(3, 7), equals(21));
});
});

group('Функции для работы со строками', () {


test('Удаление окружающих пробелов', () {
var line = ' oO ';

177
expect(deleteSurroundingSpaces(line), equals('oO'));
});
test('Перевод в нижний регистр', () {
var line = 'ПроВерка';
expect(stringToLowerCase(line), equals('проверка'));
});
});
}

Рядом с объявлением группы должна появиться иконка, по нажатию


на которую можно запустить все тесты данной группы:

Рисунок 3.13 – Группа тестов

Когда необходимо выполнять какие-то действия перед тестом или


освобождать ресурсы после его выполнения, на помощь приходят такие
функции, как setUp и tearDown. Их необходимо использовать в начале
функции main или группы тестов. В эти функции верхнего уровня
передается анонимная функция, которая в случае с setUp будет
выполняться перед каждым тестом в группе или набором тестов. А
анонимная функция, передаваемая в tearDown, будет выполняться после
теста, даже если он не будет пройден.
Давайте перепишем группу арифметических тестов следующим
образом:
group('Арифметические функции', () {
late int a;
late int b;
setUp((){
a = 3;
b = 7;
});
test('Проверка сложения', () {
expect(add(a, b), equals(a+b));

178
});
test('Проверка умножения', () {
expect(mul(a, b), equals(a*b));
});
});

Анонимные функции, в которых прописывается тест, могут быть


отмечены ключевым словом async. Это позволяет тестировать
асинхронную функциональность. Так, например, тест не будет считаться
завершенным, пока Future не вернет значение. Добавьте код ниже в группу
арифметических тестов:
test('Проверка отложенного умножения', () async {
var value = await Future.value(mul(a, b));
expect(value, equals(a*b));
});

3.5.3. Запуск тестов


Запускать тесты можно двумя способами: через команду терминала
или используя встроенные средства VS Code. При запуске тестов через
терминал имеется возможность запустить единственный файл на
тестирование или все файлы с тестовым окружением проекта,
расположенные в определенной директории. Для этого используется
следующая команда: dart test путь_до_директории_с_тестами. Т.к. по
умолчанию используется директория «test», то команду запуска тестов
можно сократить до dart test. Откройте терминал проекта в VS Code и
введите в него:
dart test

Рисунок 3.14 – Успешное завершение тестирования

Внесите в тест специальное искажение, чтобы он провалился и


повторно запустите команду. Например, пусть при сложении, тестом
ожидается результат на 1 больше, чем возвращает тестируемая функция:

179
Рисунок 3.15 – Тест не пройден

Во втором случае необходимо перейти в соответствующую панель и


запустить тесты:

Рисунок 3.16 – Запуск тестов средствами VS Code

В случае не пройденного теста, данная панель будет иметь следующий


вид:

180
Рисунок 3.17 – Тест функции сложения не пройден

3.5.4. Конфигурация тестов


Порой проверяемая функциональность еще не реализована, а перед
глазами маячит оповещение о том, что проверяющие ее тесты не пройдены.
В этом случае, до момента ее реализации, файл с тестовым набором можно
отметить аннотацией @Skip(‘Описание почему пропускается’), чтобы он
пропускался при очередном запуске тестов проекта:
@Skip('Часть функционала не реализована')
// @Skip обязательно прописывается в самой первой строке файла
import 'package:test/test.dart';
import 'package:my_project/my_functions.dart';

void main() {
// код тестов
}

Также имеется возможность пропускать отдельные тесты или их


группы, передав им на вход именованный параметр skip (можно просто
установить, как true, но лучше написать почему тест пропускается):
import 'package:test/test.dart';

import 'package:conquest_functions/my_math.dart';
import 'package:conquest_functions/my_str.dart';

void main() {
group('Арифметические функции', () {
late int a;
late int b;
setUp((){
a = 3;
b = 7;

181
});
test('Проверка сложения', () {
expect(add(a, b), equals(a+b));
}, skip: 'В левой пятке зачесалось!');
test('Проверка умножения', () {
expect(mul(a, b), equals(a*b));
});
test('Проверка отложенного умножения', () async {
var value = await Future.value(mul(a, b));
expect(value, equals(a*b));
});
});

group('Функции для работы со строками', () {


test('Удаление окружающих пробелов', () {
var line = ' oO ';
expect(deleteSurroundingSpaces(line), equals('oO'));
});
test('Перевод в нижний регистр', () {
var line = 'ПроВерка';
expect(stringToLowerCase(line), equals('проверка'));
});
}, skip: 'Еще в разработке');
}

Теперь при запуске тестов проекта часть из них будет отмечена как
пропущенные:

Рисунок 3.18 – Пропуск тестов

В ряде случаев может потребоваться настроить тест или тестовое


окружение под различные платформы. Это может быть связано как с
ограничениями самой платформы, так и с ее особенностями. Если
необходимо задать настройку для всего файла, в его начале используйте

182
аннотацию @OnPlatform({'платформа': настраиваемый параметр}). Либо
же в группу или сам тест передавайте аргумент onPlatform:
import 'package:test/test.dart';

import 'package:conquest_functions/my_math.dart';
import 'package:conquest_functions/my_str.dart';

void main() {
group('Арифметические функции', () {
late int a;
late int b;
setUp(() {
a = 3;
b = 7;
});
test('Проверка сложения', () {
expect(add(a, b), equals(a + b)); }, onPlatform: {
'windows': Skip('В левой пятке зачесалось!'),
});
test('Проверка умножения', () {
expect(mul(a, b), equals(a * b));
});
test('Проверка отложенного умножения', () async {
var value = await Future.value(mul(a, b));
expect(value, equals(a * b));
});
});

group('Функции для работы со строками', () {


test('Удаление окружающих пробелов', () {
var line = ' oO ';
expect(deleteSurroundingSpaces(line), equals('oO'));
});
test('Перевод в нижний регистр', () {
var line = 'ПроВерка';
expect(stringToLowerCase(line), equals('проверка'));
});
}, onPlatform: {
'linux': Skip('В левой пятке зачесалось!'),
});
}

Хорошим подспорьем в настройке конфигураций тестов под


различные платформы являются теги. Ими можно отметить, как сам файл
(используя аннотацию @Tags(['имя_тега']) ), так и отдельные тесты или
группу тестов, задав значение именованному аргументу tags. Для примера
отметим часть тестов меткой 'windows':

183
import 'package:test/test.dart';

import 'package:conquest_functions/my_math.dart';
import 'package:conquest_functions/my_str.dart';

void main() {
group('Арифметические функции', () {
late int a;
late int b;
setUp(() {
a = 3;
b = 7;
});
test('Проверка сложения', () {
expect(add(a, b), equals(a + b+1));
});
test('Проверка умножения', () {
expect(mul(a, b), equals(a * b));
}, tags: ['windows']);
test('Проверка отложенного умножения', () async {
var value = await Future.value(mul(a, b));
expect(value, equals(a * b));
});
});

group('Функции для работы со строками', () {


test('Удаление окружающих пробелов', () {
var line = ' oO ';
expect(deleteSurroundingSpaces(line), equals('oO'));
});
test('Перевод в нижний регистр', () {
var line = 'ПроВерка';
expect(stringToLowerCase(line), equals('проверка'));
});
}, tags: ['windows']);
}

Чтобы запустить тесты, отмеченные тегом, введите в терминале


следующую команду: dart test --tags "windows"

Рисунок 3.19 – Результат выполнения команды

184
Если нужно запускать только те тесты, которые не отмечены тегом,
используйте для этого флаг --exclude-tags или -x: dart test -x
"windows"

Рисунок 3.20 – Результат выполнения команды

Также имеется возможность задавать собственные теги. Для этого


достаточно заменить тег «windows» на пользовательский. Например:
«prevozmogun». Теперь запустим только те тесты, которые отмечены этим
тегом: dart test -t "prevozmogun"

Рисунок 3.21 – Результат выполнения команды

Для более подробного знакомства с пакетом test и его возможностями


обратитесь к документации.

3.4.5. Тестирование пользовательского ввода


Как тестируются функции мы с вами разобрали. А теперь представим
ситуацию, что нужно комплексно протестировать консольное приложение,
где пользователь вводит какие-нибудь данные и программа возвращает
результат в терминал. Для этого в директории «bin» предыдущего проекта
создайте файл с именем «main.dart» и добавьте в него код:
import 'dart:io';

import 'package:conquest_functions/my_math.dart';

void main() {
// пользователь вводит 2 числа через пробел
String? input = stdin.readLineSync();
List<String> inputValues = input!.split(' ');
List<int> numbers = inputValues.map(int.parse).toList();
print(add(numbers[0], numbers[1]));
}

185
С простого наскока тест для этого кода не написать. Нам потребуется
запустить файл с кодом в отдельном процессе, передать ему данные через
stdin, получить рассчитанный результат с использованием stdout и после
сравнить с тестовыми данными, получилось то, что мы ожидали на выходе
или нет.
Для начала добавим внешнюю зависимость в виде пакета async. Она
предоставляет асинхронные коллекции, одна из которых нам понадобится
для более удобного получения данных из запускаемого процесса. Откройте
файл pubspec.yaml и в конец dev_dependencies добавьте:
async: ^2.11.0

Теперь создайте в директории «test» файл с именем «main_test.dart»


и добавьте в него следующую заготовку:
import 'package:async/async.dart';
import 'package:test/test.dart';
import 'dart:convert';
import 'dart:io';

void main() {
test("main is checked", () async {

var answers = <(String, String)>[


('7 5', '12'),
('2 3', '5'),
('-2 4', '2'),
('3 -4', '-1'),
];

var dir = Directory.current; // директория проекта

for (var (input, output) in answers) {


// тут будет основной код теста
}
});
}

Для запуска файл в отдельном процессе будем использовать класс


Process библиотеки dart:io. Ему на вход подается путь до запускаемого в
процессе приложения и в квадратных скобках его конфигурация. Поскольку
мы добавили SDK Dart в переменные среды, то запуск файла с расширением
«.dart» не будет представлять сложности:
var process = await Process.start(
'dart',
['run', '${dir.path}\\bin\\main.dart'],
);

186
Прежде чем подать данные на вход запущенного процесса,
сконфигурируем коллекцию для получения результата работы
приложения:
var stdoutSplitter = StreamSplitter(
process.stdout
.transform(
utf8.decoder,
)
.transform(
const LineSplitter(),
),
);

Наше приложение ожидает на вход строку из двух чисел, разделенных


пробелами. Отправить эти данные в него можно через свойство stdin
процесса:
process.stdin.writeln(input);

Для получения и проверки на корректность работы запущенного


процесса, используйте следующий код:
var procOutput = await stdoutSplitter.split().first;
expect(procOutput, equals(output));

var exitCode = await process.exitCode;


// успешное завершение процесса?
expect(exitCode, equals(0));

Ниже приведен код всего теста:


// main_test.dart
import 'package:async/async.dart';
import 'package:test/test.dart';
import 'dart:convert';
import 'dart:io';

void main() {
test("main is checked", () async {
var answers = <(String, String)>[
('7 5', '12'),
('2 3', '5'),
('-2 4', '2'),
('3 -4', '-1'),
];
var dir = Directory.current;
print(dir.path);
for (var (input, output) in answers) {
var process = await Process.start(

187
'dart',
['run', '${dir.path}\\bin\\main.dart'],
);
var stdoutSplitter = StreamSplitter(
process.stdout
.transform(
utf8.decoder,
)
.transform(
const LineSplitter(),
),
);
process.stdin.writeln(input);
var procOutput = await stdoutSplitter.split().first;
expect(procOutput, equals(output));

var exitCode = await process.exitCode;


// успешное завершение процесса?
expect(exitCode, equals(0));
}
});
}

Запустите тест, нажав на иконку запуска в VS Code. В результате


успешного тестирования, панель должна выглядеть следующим образом:

Рисунок 3.22 – Успешное завершение тестирования

Поменяйте какой-нибудь ответ или специально исказите входные


данные и посмотрите, что произойдет, если тест окажется не пройденным.

3.6. Создание пакета и его подключение к проекту


Как уже и говорилось ранее, по умолчанию, любое приложение на Dart
можно рассматривать как пакет. Но на самом деле между приложением и
пакетом имеется весомая разница! Пакет изначально проектируют для
последующего переиспользования. Также имеется небольшое отличие в

188
структуре директорий. У пакета вместо каталога «bin» - «example»,
хранящий примеры использования его функционала.

3.6.1. Создание пакета


Мы не будем изобретать велосипед и возьмем уже готовые наработки,
полученные в результате изучения текущей главы. Тесты и примеры для
пакета тоже не будем писать. Они остаются для вас в качестве
факультативного упражнения
Создайте новый проект Dart и после выбора директории
расположения, выберите пакет, а не консольное приложение:

Рисунок 3.23 – Создание пакета

В качестве названия введите «mad_package» и нажмите Enter. В итоге,


после инициализации проекта, вас должна встретить следующая структура
проекта:

Рисунок 3.24 – Структура пакета

189
Удалите файлы из каталога «example», «test» и «src». Нам они сейчас
не понадобятся.
Теперь откройте реализованный ранее проект «my_new_lib» и
скопируйте файлы из его директории «src» каталога «lib» в аналогичную
папку текущего проекта:

Рисунок 3.25 – Текущая структура пакета

Далее откройте файл «mad_package.dart» и добавьте в него следующие


экспорты:
library;

export 'src/my_add.dart';
export 'src/my_mul.dart';
export 'src/my_pow_n.dart';
export 'src/my_sub.dart';

Так как мы не будем публиковать пакет на pub.dev, откройте файл


«pubspec.yaml» и явно укажите это:
name: mad_package
description: A starting point for Dart libraries or applications.
version: 1.0.0
publish_to: none

environment:
sdk: ^3.1.2

# Add regular dependencies here.


dependencies:
# path: ^1.8.0

dev_dependencies:
lints: ^2.0.0
test: ^1.21.0

190
Не беря в расчет отсутствие примеров и тестов, вас можно поздравить
с первым созданным пакетом! Далее мы разберем различные способы его
подключения к разрабатываемым проектам.

3.6.2. Локальное подключение пакета


Создадим в той же директории, что и ранее, новый консольный проект
с именем «my_app» и в его файле «pubspec.yaml» установим зависимость в
виде написанного ранее пакета «mad_package», с указанием относительного
пути его расположения (то есть от того места, где создается текущий
проект):
name: my_app
description: A sample command-line application.
version: 1.0.0

environment:
sdk: ^3.1.2

# Устанавливаем зависимости проекта


dependencies:
#На этом уровне имена подключаемых пакетов
mad_package:
# Настройки пакета
path: ../mad_package

dev_dependencies:
lints: ^2.0.0
test: ^1.21.0

В моем случае путь до пакета «mad_package» выглядит следующим


образом «../mad_package». Это связано с тем, что проект «packeges_test»
был создан в той же директории, что и «my_app».
Теперь импортируем из подключенного пакета файл
«mad_package.dart», который предоставляет API работы с подключаемым
пакетом:
import 'package:mad_package/mad_package.dart' as calc;

void main(List<String> arguments) {


print(calc.add(10, 34.2)); // 44.2
print(calc.mul(10, 4.2)); // 42.0
print(calc.sub(10, 4.8)); // 5.2
print(calc.powN(10, 3)); // 1000.0
}

191
Если имеется необходимость установить пакет, который находится в
менеджере пакетов pub, то достаточно в зависимостях (dependencies:)
файла «pubspec.yaml» прописать имя этого пакета и его версию.

3.6.3. Удаленное подключение пакета


Для этого расположите пакет «mad_package» в публичном github-
репозитории, либо воспользуйтесь тем, который заранее был мной
размещен.
Если захотите поработать со своим репозиторием, то в файле
«pubspec.yaml» пакета «mad_package» добавьте поле с указанием пути до
него:
repository: https://github.com/my_org/my_repo

Создайте новый проект «my_new_app» или воспользуйтесь


предыдущим, внеся в файл «pubspec.yaml» следующие изменения:
dependencies:
mad_package:
git: https://github.com/MADTeacher/mad_package.git

Вот и все! Достаточно просто, не правда ли?

Резюме по разделу
В данной главе мы рассмотрели, как объявлять и использовать
функции при написании кода. Какие способы передачи аргументов в
функции существуют и чем отличаются. Что такое замыкания и как они
реализованы в Dart. Поговорили про абстракцию, что она привносит в
процесс разработки, для чего нужна и как функции ее поддерживают.
Также рассмотрели как можно импортировать функции, переменные
и т.д., написанные в других модулях (файлах с расширением «.dart»), писать
свои библиотеки и пакеты.
Отдельно стоит отметить такой механизм, как генераторные функции,
которые используются для ленивой генерации последовательности
значений по запросу. Они представляют собой альтернативу спискам, так
как при их использовании в памяти не хранится массив значений и объект,
с необходимым нам текущим значением элемента последовательности,
создается только в момент обращения к генераторному объекту.
С особой осторожностью обращайтесь с такими типами передаваемых
аргументов в функцию, как: List, Set, Mat, пользовательский тип данных
и т.д. Это связано с тем, что такие аргументы передаются в функцию по
ссылке, а не значению и какое-либо их изменение в теле функции приведет
к тому, что они останутся и после завершения функции. Поэтому будьте

192
предельно осторожны и не меняйте значения этих объектов в блоке кода
функции, если эти изменения не требует логика работы приложения.
Дополнительно, в главе задели такую тему, как тестирование. Писать
тесты или не писать, решение за вами (или вашим руководителем). В любом
случаев от их наличия выиграют все, так как большинство ошибок будут
отлавливаться на стадии разработки, что скажется на удобстве
использования разрабатываемого приложения.
Если же у вас появилась мысль по поводу написания собственного
пакета и загрузки его в pub, то одним из обязательных условий является
наличие у него тестов.

Вопросы для самопроверки


1. Для чего используются функции?
2. Назовите функцию верхнего уровня, которая представляет собой
точку входа в приложение?
3. Перечислите все способы объявления аргументов функции.
4. Чем отличаются позиционные от именованных аргументов функции?
5. Для чего используется ключевое слово required?
6. Какая область видимости используется в Dart? Приведите пример.
7. Как обратиться к функции через переменную?
8. Можно ли функцию использовать в качестве входного аргумента
другой функции? Приведите пример.
9. Для чего используется ключевое слово typedef?
10. Что такое анонимные и стрелочные функции? Для чего они
используются?
11. Может ли функция в качестве возвращаемого значения возвращать
другую функцию? Приведите пример.
12. Что представляет собой механизм замыкания и зачем им
пользоваться?
13. Перечислите виды организации рекурсии. Чем они отличаются?
14. Для чего используются генераторные функции?
15. Какие варианты реализации генераторных функций существуют в
Dart?
16. Чем оператор yield отличается return?
17. Что лучше использовать: списки или генераторные функции? В каких
случаях?
18. Зачем писать тесты?
19. Что такое TDD? Какая ключевая идея лежит в ее основе?
20. Какие виды тестирования используются в Dart?
21. Для чего используется пакет test? Приведите пример.
22. Каким образом можно конфигурировать тесты?

193
23. Какой набор базовых библиотек входит в Dart?
24. Какая базовая библиотека автоматически подключается к dart-
файлам?
25. Какое ключевое слово позволяет подключать к вашему модулю код из
других модулей (файлов с расширением «.dart»)?
26. Для чего используется файл «pubspec.yaml»?
27. Как создать в проекте библиотеку?
28. Как организовать API для доступа к предоставляемой библиотекой
функциональности?
29. Для чего используются ключевые слова show и hide?
30. Как организовать отложенную загрузку подключаемой
библиотеки/модуля?
31. Как подключить пакет к проекту?

Лабораторная работа № 4. Функции


Цель работы: познакомиться с основными способами объявления и
принципами использования функций в Dart.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Во всех заданиях необходимо предусмотреть проверку на
правильность вводимых данных с клавиатуры;
• Каждое задание на функции должно сопровождаться
минимум тремя тестами;
• ЗАПРЕЩЕНО использовать рекурсию.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.
Задания:
1. Пользователь вводит с клавиатуры целочисленный список. Напишите
функцию, которая возвращает его максимальное значение. Полученный
результат выведите в терминал.
2. Пользователь вводит с клавиатуры целочисленный список. Напишите
функцию, которая возвращает произведение элементов списка.
Полученный результат выведите в терминал.

194
3. Пользователь вводит с клавиатуры два целочисленных списка.
Напишите функцию, которая возвращает сумму элементов списков.
Полученный результат выведите в терминал.
4. Пользователь вводит с клавиатуры произвольное количество чисел.
Напишите функцию, которая возвращает сформированный из них список.
Полученный результат выведите в терминал.
5. Пользователь вводит с клавиатуры целочисленный список и число val.
Напишите функцию, возвращающую номер индекса, по которому хранится
значение val. Если элемента с искомым значением нет в списке – верните
null. Полученный результат выведите в терминал.
6. Пользователь вводит с клавиатуры Map<int, String> и строку str.
Напишите функцию, возвращающую ключ по которому хранится строка.
Если элемента с искомым значением нет – верните null. Полученный
результат выведите в терминал.
7. Пользователь вводит с клавиатуры строку. Напишите функцию,
которая возвращает ее инвертированное представление. Полученный
результат выведите в терминал.
8. Пользователь вводит с клавиатуры строку. Напишите функцию,
которая проверяет является ли подаваемая на ее вход строка палиндромом
и возвращает значение булевского типа данных (true – да, нет – false).
Полученный результат выведите в терминал.
9. Пользователь вводит с клавиатуры 3 числа: A, B, C. Напишите
функцию, возвращающую целочисленный список в соответствии со
следующими правилами: первые два аргумента задают диапазон значений,
которые будут добавлены в формируемый список, а третий аргумент
отвечает за шаг. Полученный результат выведите в терминал.
10. Пользователь вводит с клавиатуры число. Напишите функцию,
возводящую его в куб и возвращающую полученный результат, который
затем выведите в терминал.
11. Пользователь вводит с клавиатуры две строки. Напишите функцию,
возвращающую true или false, в зависимости от того, являются ли
переданные значения анаграммами или нет. Полученный результат
выведите в терминал.
12. Пользователь вводит с клавиатуры 3 числа: A и B. Напишите функцию,
которая проверяет установлен ли у A бит под номером B в единицу или нет.
Результат проверки необходимо вернуть в виде булевского значения ( true
– да, нет – false) и вывести его в терминал.
13. Пользователь вводит с клавиатуры 3 числа: A и B. Напишите функцию,
которая проверяет установлен ли у A бит под номером B в ноль или нет.
Результат проверки необходимо вернуть в виде булевского значения ( true
– да, нет – false) и вывести его в терминал.

195
14. Пользователь вводит с клавиатуры число n. Напишите функцию,
которая возвращает сумму значений от нуля до n-1. Полученный результат
выведите в терминал.
15. Пользователь вводит с клавиатуры число вещественное число,
представляющее собой значение температуры в градусах Цельсия.
Напишите функцию для его перевода в градусы Фаренгейта. Полученный
результат выведите в терминал.
16. Пользователь вводит с клавиатуры число вещественное число,
представляющее собой значение температуры в градусах Фаренгейта.
Напишите функцию для его перевода в градусы Цельсия. Полученный
результат выведите в терминал.
17. Пользователь вводит с клавиатуры строку, содержащую символы в
различном регистре. Напишите функцию, возвращающую количество
прописных букв. Полученный результат выведите в терминал.
18. Пользователь вводит с клавиатуры число целое число,
представляющее собой номер месяца. Напишите функция, которая
возвращает количество дней в месяце. Если введен не корректный номер
месяца, то возвращается ноль. Полученный результат выведите в терминал.
19. Пользователь вводит с клавиатуры два числа. Напишите функцию,
возвращающую их наименьшее общее кратное. Полученный результат
выведите в терминал.
20. Пользователь вводит с клавиатуры два числа. Напишите функцию,
возвращающую их наибольший общий делитель. Полученный результат
выведите в терминал.
21. Пользователь вводит с клавиатуры длину, ширину и высоту коробки.
Напишите функцию, которая возвращает объем коробки и имеет два
аргумента по умолчанию (ширина = 10, высота = 7), на случай если
пользователь введет не три значения. Полученный результат выведите в
терминал.
22. Пользователь вводит с клавиатуры две Map<int, String> (point1 и
point2) вида: {'х' : 10, 'у' : 13}. Напишите функцию, которая возвращает
значение расстояния между заданными точками. Аргумент функции point2
должен иметь следующее значение по умолчанию {'х' : -7, 'у' : 3}, на случай
если пользователь введет не две Map. Полученный результат выведите в
терминал.
23. Пользователь вводит с клавиатуры строку, содержащую произвольное
количество открывающихся и закрывающихся скобок. Напишите функцию,
возвращающую true или false, в зависимости от того, имеется ли баланс
открывающих и закрывающих скобок. Когда в строке отсутствуют скобки
должно возвращаться true. Полученный результат выведите в терминал.
24. Пользователь вводит с клавиатуры целочисленный список. Напишите
функцию, которая возвращает возвращается значение элемента,

196
встречающегося наибольшее число раз. Если такого нет, то минимального
по значению. Полученный результат выведите в терминал.
25. Пользователь вводит с клавиатуры два числа N и k. Напишите
функцию, которая будет возвращать результат следующего выражения: 1k +
2k + 3k + … + Nk и выведите его в терминал.
Таблица 3.1
Варианты работ
№ варианта Номера заданий к варианту
1 1, 2, 15, 17, 22
2 1, 3, 7, 9, 11
3 1, 3, 5, 18, 25
4 1, 8, 12, 15, 16
5 2, 3, 5, 17, 19
6 2, 4, 6, 9, 14
7 2, 4, 10, 20, 22
8 2, 5, 9, 13, 23
9 2, 6, 9, 20, 24
10 3, 4, 12, 20, 21
11 3, 6, 10, 11, 17
12 6, 10, 14, 17, 20
13 7, 9, 11, 20, 25
14 7, 10, 12, 15, 19
15 8, 9, 10, 11, 25
16 8, 15, 17, 18, 21
17 10, 12, 15, 23, 25
18 10, 13, 19, 22, 24
19 11, 13, 17, 20, 21
20 12, 14, 18, 21, 23

Лабораторная работа № 5. Рекурсия


Цель работы: познакомиться с основными способами объявления и
использования рекурсивных функций, а также механизма замыканий в
Dart.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;

197
• Во всех заданиях необходимо предусмотреть проверку на
правильность вводимых данных с клавиатуры.
• Каждое задание на функции должно сопровождаться
минимум тремя тестами.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

Задания:
1. Пользователь вводит с клавиатуры число n. Напишите рекурсивную
функцию, вычисляющую сумму всех чисел от 1 до n. Полученный результат
выведите в терминал.
2. Пользователь вводит с клавиатуры целочисленный список. Напишите
рекурсивную функцию, возвращающую минимальное значение из списка.
Полученный результат выведите в терминал.
3. Пользователь вводит с клавиатуры целочисленный список. Напишите
рекурсивную функцию, возвращающую максимальное значение из списка.
Полученный результат выведите в терминал.
4. Пользователь вводит с клавиатуры строку. Напишите рекурсивную
функцию, которая проверяет является ли подаваемая на ее вход строка
палиндромом и возвращает значение булевского типа данных ( true – да,
нет – false). Полученный результат выведите в терминал.
5. Пользователь вводит с клавиатуры число n. Напишите рекурсивную
функцию для вычисления факториала f(n). Полученный результат выведите
в терминал.
6. Пользователь вводит с клавиатуры число n. Напишите рекурсивную
функцию для вычисления числа Фибоначчи fib(n). Полученный результат
выведите в терминал.
7. Пользователь вводит с клавиатуры два числа n и k. Напишите
рекурсивную функцию, возвращающую значение следующего вида: nk.
Полученный результат выведите в терминал.
8. Пользователь вводит с клавиатуры строку str и символ symbol.
Напишите рекурсивную функцию, удаляющую из строки все буквы,
соответствующие symbol. Полученный результат выведите в терминал.
9. Пользователь вводит с клавиатуры два числа. Напишите рекурсивную
функцию, возвращающую их наименьшее общее кратное. Полученный
результат выведите в терминал.
10. Пользователь вводит с клавиатуры два числа. Напишите рекурсивную
функцию, возвращающую их наибольший общий делитель. Полученный
результат выведите в терминал.

198
11. Пользователь вводит с клавиатуры строку, содержащую одну пару из
открывающейся и закрывающейся скобки. Напишите рекурсивную
функцию, возвращающую строку, состоящую из символов, находящихся в
скобках исходной строки. Полученный результат выведите в терминал.
12. Пользователь с клавиатуры вводит целое число N. Напишите
рекурсивную функцию проверяющую то, является ли введенное значение
точной степенью двойки. Если да – функция должна вернуть true, иначе
false. Полученный результат выведите в терминал.
13. Пользователь вводит с клавиатуры целочисленный список. Напишите
рекурсивную функцию возвращающую сумму отрицательных элементов
списка. Полученный результат выведите в терминал.
14. Пользователь вводит с клавиатуры целочисленный список и число n.
Напишите рекурсивную функцию возвращающую сумму элементов списка,
кратных n. Полученный результат выведите в терминал.
15. Пользователь вводит с клавиатуры целое число. Напишите
рекурсивную функцию, которая должна возвращать число, где цифры
расположены в обратном порядке. Например, 1789 -> 9871.

Таблица 3.2
Варианты работ
№ варианта Номера заданий к варианту
1 1, 2, 12, 14
2 1, 6, 11, 13
3 3, 5, 8, 10
4 6, 12, 13, 14
5 2, 6, 8, 12
6 5, 6, 9, 15
7 6, 7, 11, 15
8 2, 4, 10, 14
9 3, 7, 10, 15
10 3, 6, 7, 15
11 6, 11, 13, 15
12 2, 6, 7, 9
13 1, 3, 12, 13
14 1, 9, 10, 14
15 2, 4, 9, 12
16 4, 6, 7, 11
17 4, 10, 11, 14
18 5, 7, 9, 12
19 7, 9, 10, 14
20 8, 9, 11, 13

199
Лабораторная работа № 6. Замыкание
Цель работы: познакомиться с основными способами объявления и
принципами использования замыканий в Dart.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Во всех заданиях необходимо предусмотреть проверку на
правильность вводимых данных с клавиатуры.
• Каждое задание на функции должно сопровождаться
минимум тремя тестами.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

Задания:
1. Напишите функцию, на вход которой подается целочисленное
значение, устанавливающее начальное состояние счетчика. Она должна
возвращать другую функцию, при вызове которой будет возвращаться
обновленное значение счетчика, увеличивающееся на единицу.
Полученный результат выведите в терминал.
2. Напишите функцию, на вход которой подается целочисленное
значение n, которое в последующем будет возводиться в квадрат. Она
должна возвращать другую функцию, при вызове которой будет каждый раз
возвращаться результат выражения n=n2. Полученный результат выведите
в терминал.
3. Напишите функцию, на вход которой подается строка. Она должна
возвращать другую функцию, принимающую номер индекса и
возвращающую символ, располагаемый в строке по этому индексу. Если
задаваемый индекс выходит за пределы строки, то верните пустой символ.
Полученный результат выведите в терминал.
4. Напишите функцию, использующую механизм замыканий для
сложения двух чисел и возвращающую полученное значение. Например,
my_sum(1)(2) -> 3. Полученный результат выведите в терминал.
5. На вход функции поступает один из символов «>», «<», «=». Используя
механизм замыканий, сравните два значения, подаваемые на вход
возвращаемой функции. В результате должно возвращаться true, или
false. В том случае, когда на вход объемлющей функции подается

200
неизвестный символ – результат всегда false. Полученный результат
выведите в терминал.
6. На вход функции подается строка. Используя механизм замыканий,
удалите из строки задаваемый в возвращаемой функции символ и верните
полученный результат. Полученный результат выведите в терминал.
7. Напишите функцию, на вход которой подается строка. Функция
должна возвращать другую функцию, принимающую символ и
возвращающую количество его повторений. Полученный результат
выведите в терминал.
8. Напишите функцию, на вход которой подается номер проверяемого
бита. Функция должна возвращать другую функцию, принимающую число
и возвращающую true, если заданный бит в нем установлен в единицу,
иначе – false. Полученный результат выведите в терминал.
9. Напишите функцию, на вход которой подается список целочисленных
или вещественных значений. Используя механизм замыкания верните
функцию, принимающую значение степени, в которую необходимо
возвести каждый элемент списка и возвращающую полученный результат.
Полученный результат выведите в терминал.
10. Напишите функцию, на вход которой подается список целочисленных
значений. Используя механизм замыкания верните функцию,
принимающую на вход значение n и возвращающую список, в котором
удалены все элементы, что без остатка делятся на n. Полученный результат
выведите в терминал.

Таблица 3.3
Варианты работ
№ варианта Номера заданий к варианту
1 1, 2, 3
2 1, 3, 5
3 4, 5, 10
4 5, 6, 9
5 1, 3, 7
6 6, 10, 2
7 7, 9, 3
8 3, 4, 5
9 3, 4, 9
10 9, 10, 3
11 2, 7, 8
12 7, 8, 4
13 5, 7, 8
14 4, 5, 10
15 1, 6, 9

201
16 5, 6, 7
17 6, 8, 10
18 2, 6, 9
19 3, 4, 7
20 3, 5, 8

202
Глава 4. Объектно-ориентированное
программирование
Прежде чем начнем погружение в то, как концепции ООП реализованы
в Dart и какие механизмы для этого используются, давайте, как и в случае с
функциями, сначала поговорим об абстракции и ее месте в объектно-
ориентированном программировании.

4.1. Абстракция в объектно-ориентированном


программировании
В основе объектно-ориентированного программирования (ООП)
лежит понятие – объект, который представляет собой некоторую сущность
предметной области, в рамках которой ведется разработка приложения. В
некоторых случаях описываемый объект может не входить в предметную
область и является порождением нашего воображения, но на него
возлагается ряд обязанностей, что позволяет нам в целом упростить код
разрабатываемого приложения. Объекты описываются в коде классами.
Говоря другими словами, класс – это абстракция, позволяющая нам
перенести состояние и поведение объекта предметной области в код.
Под состоянием понимается некоторый набор атрибутов, которые
позволяют описать из чего состоит объект и какие характеристики ему
присущи. Например: марка автомобиля, ее цвет, объем двигателя, тип
топлива и т. д. Под поведением понимается некоторый набор действий,
которые объект может совершать над своим внутренним состоянием.
Автомобиль может стоять на месте, его можно завести, начать движение,
повернуть налево или направо, ускориться и т. д.
Состояние объекта, при его описании классом на языке
программирования, переводится в переменные реализуемого класса, а
поведение объекта – в методы.
При том не всегда один класс описывает только один объект. Все
зависит от структуры того объекта, который необходимо перенести в
программный код, в связи с чем некоторые объекты могут быть описаны не
одним, а целым набором классов.
До тех пор, пока разработчик явным образом не создал экземпляр
класса (вызвал конструктор), он существует как текст и только в момент его
создания мы может говорить, что разрабатываемое программное
приложение оперирует объектом, т. к. под него выделяется память,
инициализируются его состояния и появляется возможность их изменить
путем вызова методов объекта.

203
4.1.1. Основа абстракции в ООП – класс
Класс изначально является высокоуровневой абстракцией, в связи с
чем может рассматриваться как одновременное написание главы книги
вместе с ее разделами, где после объявление главы идет небольшой текст с
пояснениями для чего она предназначена, то есть описываются состояния
класса. Простые типы данных и операции над ними так и остаются на
первом уровне абстракции и используются в процессе написания
состояний классов, а также операций над ними в методах описываемого
класса. Каждый новый класс описывается в новом файле, имя которого
должно совпадать с именем класса, которое отвечает на вопросы: «Кто?»,
«Что?», то есть имя класса – существительное, а имена его методов –
глаголы. Иногда в одном файле может содержаться по несколько классов.
Как и в предыдущем случае, когда мы рассматривали разделы, они
могут быть вложенными. На верхнем уровне находятся те, по которым
читатель может спокойно ориентироваться (публичные методы, т. е.
интерфейс класса), в то время как разделы на более низких уровнях
абстракции сокрыты от его глаз и считаются деталями реализации класса
(приватные методы).

4.1.2. Сложносоставной класс: от деталей к обобщению на


примере автомобиля
Предыдущее описание класса подойдет если в код переносится какой-
нибудь простой объект, в состав которого не входят другие объекты.
Примером может служить лобовое стекло автомобиля, колесный диск и т.
д. В том же случае, когда мы описываем сложные объекты, как например,
трансмиссию, входящие в ее структуру составные части, которые также
описываются классами, становятся деталями реализации.
В общем виде автомобиль состоит из следующих узлов:
− Кузов;
− Ходовая часть;
− Подвеска;
− Двигатель;
− Трансмиссия;
− Рулевое управление.
В то же самое время доступ к их методам сокрыт абстракцией верхнего
уровня. Для примера возьмем двигатель. Он состоит из множества деталей
и преобразует тепловую энергию в механическую, но сидя за рулем
водитель не имеется прямого доступа, чтобы своими руками приводить
составные части двигателя в движение. Каждая часть двигателя имеет свое
состояние, поведение и описывается классом. Эти части могут не знать о
существовании друг друга, предоставляя только интерфейс (методы) для

204
изменения своих состояний. Все начинается с маленьких запчастей,
которые входят в состав более сложных, становясь деталями их реализации,
то есть переменными класса более высокого уровня. Доступ к изменению
состояний данных запчастей можно получить только через методы детали,
в состав которой входит эта запчасть. Так как двигатель довольно сложная
и закрытая система, то предоставление доступа напрямую к каким-то
запчастям может привести к фатальным последствиям.
А сами по себе приведенные выше узлы без кузова ни на что не
способны, т. к. он является связующим звеном, позволяющим закрепить их
и детали, необходимые для нормальной работы автомобиля.
И несмотря на то, что автомобиль состоит из огромного количества
составных частей, водителю нет необходимости знать все его устройство.
Достаточно просто выучить дорожные правила, уметь крутить руль, а также
различать педаль газа и тормоза. Автомобиль как класс, является
абстракцией очень высокого уровня, в состав которой входит множество
других классов. Он представляет собой обобщение, любой метод которого
приводит в движение огромное количество изменений состояний его
составных частей.
Наша задача, как разработчиков, научиться писать программы на
высоком уровне абстракции! Чем раньше мы выкинем из головы как
осуществляется работа с классом поршень и возложим обязанность по его
контролю на класс двигатель, чем раньше мы перестанем вручную считать
набираемую автомобилем скорость путем подсчета скорости вращения
колес, а возложим эту обязанность на класс спидометр, тем быстрее в
процессе написания кода выйдем на более высокий уровень абстракции и
будем способны разрабатывать системы различной сложности.

4.1.3. Наследование – новый уровень абстракции?


Наследование позволяет создавать новые классы на основе
существующих. Тот класс, от которого происходит наследование, принято
называть базовым классом (суперклассом, родительским классом), а класс,
который наследуется от базового – производным классом (подклассом,
дочерним классом). Производный класс наследует состояние и поведение
базового класса, что позволяет повторно использовать имеющийся код, но
при этом необходимо всегда придерживаться интерфейса, что задает
базовый класс. Также производный класс может объявлять собственные
переменные и методы, тем самым расширяя состояние и поведение
базового класса. Именно здесь и кроется ответ на вопрос. Абстракция – это
обобщение, а производный класс добавляет к базовому дополнительные
детали. Таким образом базовый класс, от которого производится
наследование, всегда будет более абстрактным нежели производный.

205
4.1.4. Три столпа абстракции в ООП: интерфейс, полиморфизм и
приведение
Полиморфизм очень тесно связан с наследованием и позволяет
менять (переопределять) поведение базовых классов в производных.
Допустим у нас имеется базовый класс машина и у которого прописали
метод для набора скорости в зависимости от того, как сильно водитель
зажимает педаль газа. Если наследоваться от данного класса и не
переопределять этот метод, то экземпляр производного класса будет
работать с реализацией метода, определенной в базовом классе. Но ведь у
нас существует множество различных марок машин, и они по-разному
разгоняются до 100 км/ч. Здесь на помощь и приходит полиморфизм,
позволяя переопределить реализацию метода базового класса, после чего
при создании экземпляра производного класса при вызове метода набора
скорости уже будет вызываться не реализация базового класса, а
переопределенная в производном.
Для лучшего понимания, почему так происходит давайте разберем это
при помощи рисунков. Как уже говорилось ранее производный класс
наследует от базового его поведение и состояния. Помимо этого, на
производный класс накладывается обязанность по созданию экземпляра
базового и его начальной инициализации. Говоря другими словами, при
создании экземпляра производного класса у нас создается не один, а два
экземпляра класса (производный и базовый), тесно связанные между
собой. Сначала разберем случай без переопределения метода базового
класса в производном:
Базовый класс Производный класс

состояние «а» состояние «а»


состояние «б» состояние «б»
состояние «в» состояние «в»
состояние «г»

метод «а» метод «а»


метод «б»

Рисунок 4.1 – Связь поведения и состояния производного и базового


класса

206
Обращаясь к таким переменным, как «а», «б», «в» производного класса,
разработчик на самом деле взаимодействует с переменными базового
класса, т. е. имена перечисленных переменных в производном классе
являются ссылками на переменные базового. Тоже касается и метода «а».
Переменная же «г» и метод «б» связаны только с производным классом. При
переопределении метода «а» в производном классе, приведенная на
рисунке связь разрушится и всегда, при работе с экземпляром
производного класса будет вызываться переопределенная в нем
реализация метода «а»:
Базовый класс Производный класс

состояние «а» состояние «а»


состояние «б» состояние «б»
состояние «в» состояние «в»
состояние «г»

метод «а» метод «а»


метод «б»

Рисунок 4.2 – Переопределение реализации метода «а» в производном


классе

Пока не прослеживается связь полиморфизма и абстракции?


Терпение, только терпение! Для того, чтобы ее проследить нам необходимо
разобрать следующие столпы абстракции и начнем с интерфейса. Под ним
можно понимать те переменные и методы класса, у которых публичная
область видимости. Это значит, что любой разработчик, который
импортирует ваш класс, может получить к ним доступ без опасения
поломать что-либо в логике его работы. Таким образом, интерфейс
приведенного на рисунке базового класса будет состоять из переменных
«а», «б», «в» и метода «а», а интерфейс производного класса из тех же
публичных переменных и метода базового класса, плюс к нему еще
добавится переменная «г» и метод «б». Именно это обстоятельство
позволяет нам работать с экземпляром производного класса через
интерфейс базового используя операцию приведения. Вы наверняка

207
встречали такие строчки кода, где переменной типа базового класса
присваивается создаваемый экземпляр производного:
Базовый_класс имя_переменной = Производный_класс();

Такой подход позволяет абстрагироваться от конкретной реализации,


работая с объектом только в рамках интерфейса базового класса, который
является обобщением для всех производных. Для начала давайте
посмотрим, как преобразуется предыдущий рисунок для объявленной
переменной в случае, когда в производном классе не производилось
переопределение реализации метода «а»:
Базовый класс Производный класс

состояние «а» состояние «а»


состояние «б» состояние «б»
состояние «в» состояние «в»
состояние «г»

метод «а» метод «а»


метод «б»

Рисунок 4.3 – Доступные переменные и методы у объявляемой


переменной

Как видим из рисунка, при приведении экземпляра производного


класса к базовому, у нас разорвалась связь между ними, и вся работа с
создаваемым экземпляром класса становится возможной только через
интерфейс базового. Это значит, что переменные и методы характерные
только для производного класса теперь скрыты и к ним нет возможности
получить доступ, пока разработчик не выполнит обратное приведение. В то
же самое время, так как метод «а» не был переопределен, будет
осуществляться вызов метода базового класса.
Теперь представим ситуацию, когда в производном классе
переопределяется метод «а». В этом случае связь между интерфейсом
базового и производного класса преобразуется следующим образом:

208
Базовый класс Производный класс

состояние «а» состояние «а»


состояние «б» состояние «б»
состояние «в» состояние «в»
состояние «г»

метод «а» метод «а»


метод «б»

Рисунок 4.4 – Связь методов производного и базового класса в случае их


переопределения

Обратите внимание на стрелку. Она означает, что теперь всякий раз,


когда у объекта будет вызываться метод «а» на самом деле будет
вызываться его переопределенная реализация в производном классе. При
этом в реализации могут вызываться специфичные для производного
класса методы и изменяться значения переменных, характерных только
для него.
Но как данное обстоятельство играет нам на руку? Представьте, что вы
проектируете автомобиль и закладываете возможность замены его
элементов: двигателя, аккумулятора, кресла водителя и т. д., что дает
возможность заводу рассматривать различных поставщиков. Главное
условие – соответствие интерфейсу! Именно это обстоятельство позволяет
проводить замену двигателя с одним количеством цилиндров на другое,
менять летние покрышки на зимние и наоборот, а также много что еще. У
вас на складе может быть множество реализаций двигателей,
соответствующих спроектированному для разрабатываемого автомобиля
интерфейсу и это дает огромную вариативность в количестве возможных
комплектаций производимых автомобилей! При проектировании классов,
закладывая возможность подмены реализации или сокрытия данных мы
используем более абстрактные сущности для объявляемых типов
переменных экземпляра класса. Это базовый класс или интерфейс. Но их
инициализация происходит не в момент объявления, а через аргументы
вызываемого конструктора проектируемого класса. Говоря другими
словами – это как в реальной жизни: где-то на другом заводе произвели

209
двигатель, потом доставили на завод, собирающий автомобили и
установили его. Так как он реализует необходимый интерфейс, то в момент
его установки (вызова конструктора с передачей ему объектов)
выполняется приведение к интерфейсу базового класса (либо просто
интерфейсу), после чего вся работа с ним будет осуществляться только в
рамках этого интерфейса.
В момент установки двигателя нам надо определиться кто будет
отвечать за его жизненный цикл: сам автомобиль или кто-то еще? То есть
использовать композицию или агрегацию. Это два довольно близких по
значению понятия, которые нам говорят, что можно использовать объекты
одного класса в другом (в виде состояний). Отличие заключается только в
том, что в случае агрегации класс, которому в конструкторе, либо
специальном методе передается объект, с которым он будет
взаимодействовать и использовать на протяжении всего своего жизненного
цикла не управляет жизненным циклом передаваемого ему объекта. В
момент удаления класса не удалится сам объект, который он использовал.
В качестве примера представим, что мы разбили машину, но двигатель
остался цел и перед ее утилизацией его вытащили, отправив на склад. При
использовании композиции класс, которому передается объект, берет на
себя управление его жизненным циклом (двигатель утилизируют с
автомобилем).
Если вы создаете класс под конкретную задачу, где нет необходимости
закладывать вариативность, производить сокрытие других методов его
внутренних объектов, то нет смысла прибегать к использованию более
абстрактных типов данных. Передавайте в его конструктор или методы для
установки объектов взаимодействия объекты только менее абстрактных
пользовательских типов данных.
Приведение к более абстрактным типам данных для последующего
взаимодействия с объектом через интерфейс позволяет разделять
приложение на слои, где сразу закладывается интерфейс сопряжения
между ними, после чего на классы ложится обязанность реализовать
соответствующее поведение. А связывание слоев производится путем
передачи объекта в конструктор создаваемого класса с его последующим
приведением к необходимому интерфейсу. При этом уменьшается
связность между слоями системы, что позволяет их делать заменяемыми.
По отдельности интерфейс, полиморфизм и приведение не
представляют ничего ультимативного, но, когда разработчик овладевает их
совместным использованием, у него в руках появляется очень гибкий
инструмент, открывающий дорогу в удивительный мир разработки
программного обеспечения, либо в ад, где уже для каждого программиста,
который бездумно его использует приготовлен отдельный котел. Как сказал

210
один из моих учеников: «Вот я и научился прятать говнокод за
интерфейсом. Теперь можно поднимать грейд до мидла» =)

4.1.5. Что такое абстрактный класс и интерфейс в рамках языка


программирования?
Не всегда базовый класс может содержать реализации всех
объявленных в нем методов. Так как такие классы представляют из себя
некоторое обобщение, то часть реализации их методов может отдаваться
на откуп производным классам. То есть базовый класс можно
рассматривать как некоторый обобщенный интерфейс, через который мы в
клиентском коде можем работать с экземплярами производных классов,
приводя их к базовому, что дает нам еще один уровень абстракции и
сокрытия. Так, например, после того как привели производный класс к
базовому мы можем вызывать только методы базового класса, так как
методы, специфичные для производного класса становятся не доступны.
Если же нам нужно вызвать один из таких методов производного класса, то
сперва необходимо выполнить проверку можно ли текущий экземпляр
класса, с которым осуществляется работа, привести к необходимому
производному классу.
Методы, которые объявлены, но не имеют реализацию, называются
чисто виртуальными методами. А класс, который содержит хоть один
виртуальный метод – абстрактный класс. Отличие абстрактного класса от
обычного заключается в том, что экземпляр абстрактного класса не может
быть создан. Но к таким классам мы можем приводить производные от них
классы. То есть что базовый, что абстрактный класс может выступать в роли
публичного интерфейса к экземплярам производных от них классов. В
каждом объектно-ориентированном языке программирования своя
специфика при их объявлении, но ключевые возможности и способы
применения – одни и те же.
По поводу интерфейса принято говорить, что «класс реализует такой-
то интерфейс». Его отличие от абстрактного или обычного класса
заключается в том, что он представляет собой чисто абстрактный класс (в
классическом представлении). Все объявляемые в интерфейсе методы –
чисто виртуальные. Так, например, в случае наследования нам не надо было
прописывать все переменные и методы базового класса в производном
заново, а только те методы, которые хотели переопределить. Здесь же все -
наоборот. Все методы, объявленные в интерфейсе, должны быть объявлены
и иметь реализацию в том классе, который этот интерфейс реализует. В
некоторых языках программирования, как например – Dart, классы могут
реализовывать интерфейс не только чисто абстрактных классов, но и
обычных. Что накладывает на разработчика дополнительные обязательства

211
реализовывать не только методы интерфейсного класса, но и
переопределять публичные значения переменных такого класса.
Таким образом, если в случае с наследованием мы наследуем состояние и
поведение, то в случае с интерфейсом – объявляем контракт, которому
должен следовать класс реализующий интерфейс.

4.1.6 Инкапсуляция и сокрытие


Во многих книгах инкапсуляция и сокрытие являются
тождественными понятиями, но на самом деле это не так. Под
инкапсуляцией понимается объединение данных и методов (функций)
работы с ними в одну оболочку. В таких объектно-ориентированных языках
как С++, Java, Dart и т. д. оболочкой обычно выступает класс, в Go - пакет,
через имя которого мы может обращаться к определенным в нем
функциям, типам данных и их методам или глобальным переменным, а в
Python инкапсуляция может производиться на уровне класса, модуля и
пакета.
Посредством сокрытия осуществляется настройка доступа к
инкапсулированным методам и данным. То есть ряд методов или
определенных в пакете типов может носить служебный характер и должны
использоваться только в рамках пакета, класса или модуля, в котором они
объявлены. Поэтому их имена начинаются с одного символа нижнего
подчеркивания (объявлены как приватные). А имена методов, функций или
типов данных, которые может использовать другой разработчик будут
начинаться со строчной буквы (объявлены как публичные).
Используя вместе инкапсуляцию и сокрытие, разработчик
обеспечивает инвариантность данных. Говоря другими словами, когда мы
заявляем, что класс инкапсулирован, то даем гарантию, что другим
разработчикам доступны только такие способы работы с ним, что он не
может случайным образом установить значения его полям, которые не
соответствуют онтологии (правилам) предметной области.
Чтобы лучше понять написанное, разберем следующий пример. У нас
есть класс, описывающий работника фирмы с его именем, возрастом и
прочими полями и методы работы с ними. Все поля класса публичные, но
их значения также можно получать или устанавливать посредством
методов. Из-за публичности полей класса какой-то разработчик может, не
вызывая необходимый метод, установить значение поля напрямую.
Например, установил возраст равным 9 годам. А это приведет к тому, что
данные потеряют свою инвариантность, так как в рамках предметной
области минимальный возраст сотрудника 18 лет.
Хорошо, пару раз наткнулись на такую ошибку и сделали все поля
(переменные) класса приватными, чтобы их значения не могли изменять
напрямую, а только через методы. Но не учли то, что в методах не

212
осуществляется проверка на корректность входных данных перед их
присваиванием. И снова имеется вероятность установить значение, из-за
которого данные экземпляра класса потеряют инвариантность!
Так вот, инкапсуляция помимо объединения данных и методов,
должна обеспечивать гарантию того, что данные не потеряют свою
инвариантность. Это достигается путем сокрытия служебных методов,
полей класса и введению дополнительных проверок в методы, которые в
случае чего приведут к падению приложения с оповещением об ошибке или
выбрасыванию исключения, но не позволят нарушить инвариантность
данных.
Наверное, из-за того, что инкапсуляция использует механизм
сокрытия для достижения своих целей, некоторыми разработчиками и
принято считать их тождественными определениями, но все куда сложнее,
чем кажется на первый взгляд.
В С++, Java и других языках программирования приватные
переменные или методы не видны за пределами методов класса, в котором
они определяются. То есть к ним нельзя обратиться через экземпляр класса.
Наличие модификаторов доступа позволяет использовать сокрытие при
проектировании программы, а сами приватные переменные и методы при
этом повышают уровень безопасности и надежности за счет ограничения
доступа к важным или критичным частям реализации объекта.

4.2. Объявление класса


Для объявления класса в Dart используется ключевое слово class. В
качестве примера, давайте посредством класса опишем один из объектов
предметной области ветеринарной клиники с его состоянием и поведением
– кота. Для этого создадим в новый файл «cat.dart»:
// ex4_1 - cat.dart
class Cat{
late final String name;
String address = 'Unknown';
int age = 0;
bool sleepState = true;

void sleep(){
if(!sleepState){
sleepState = true;
print('Кот засыпает: Хр-р-р-р-р...');
}
else{
print('Сон во сне... мммм...');
}
}

213
void wakeUp(){
if(sleepState){
sleepState = false;
print('Лениво потягиваясь, открывает глаза...');
}
}

void helloMaster(){
if(!sleepState){
print('Мя-я-я-я-у!!!');
}
}

void currentState(){
if(sleepState){
print('Кот спит');
}
else{
print('Кот бодрствует');
}
}
}

// ex4_1 - main.dart
import 'cat.dart';

void main(List<String> arguments) {


// создаем экземпляр класса Cat
var cat = Cat()
..age=3
..name='Тимоха'
..sleepState=false;

cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
cat.sleepState = true;
cat.currentState(); // Кот спит
cat.sleep(); // Сон во сне... мммм...
}

В приведенном коде мы объявили класс Cat, описывающий состояние


и поведения объекта кот. По умолчанию в Dart все переменные и методы
класса являются public (публичными) и именно поэтому к ним можно
обращаться и менять их значения из клиентского кода (в нашем случае –
функция main). Для объявления приватной переменной, либо метода, их

214
имя должно начинаться с символа нижнего подчеркивания «_». Обычно
приватные методы скрывают детали реализации, которые мы не хотим
выставлять на всеобщее обозрение, доступ к которым осуществляется через
публичные методы, а для доступа к приватным переменным, чтобы
получить или установить их значения принято использовать геттеры ( get)
и сеттеры (set).
Давайте перепишем класс Cat с учетом того, что часть переменных
состояния кота теперь будут приватными и класс не предоставляет
возможность для установки или чтения их значений:
// ex4_2 - cat.dart
class Cat{
late final String name;
String _address = 'Unknown';
int age = 0;
// по умолчанию кот, при создании экземпляра класса
// всегда бодрствует
bool _sleepState = false;

void sleep(){
if(!_sleepState){
_sleepState = true;
print('Кот засыпает: Хр-р-р-р-р...');
}
else{
print('Сон во сне... мммм...');
}
}

void wakeUp(){
if(_sleepState){
_sleepState = false;
print('Лениво потягиваясь, открывает глаза...');
}
}

void helloMaster(){
if(!_sleepState){
print('Мя-я-я-я-у!!!');
}
}

void currentState(){
if(_sleepState){
print('Кот спит');
}
else{
print('Кот бодрствует');

215
}
}
}

// ex4_2 - main.dart
import 'cat.dart';

void main(List<String> arguments) {


// создаем экземпляр класса Cat
var cat = Cat()
..age=3
..name='Тимоха';

/*
// ошибка
var cat = Cat()
..age=3
..name='Тимоха'
.._sleepState=true;
*/

cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
cat.sleep(); // Кот засыпает: Хр-р-р-р-р...
cat.sleep(); // Сон во сне... мммм...
cat.wakeUp(); // Лениво потягиваясь, открывает глаза...
}

Теперь из модуля, где объявлена функция main нельзя обращаться к


приватной переменной класса Cat - _sleepState или _address. Но это
действительно только в том случае, если описываемый класс
импортируется. То есть символ «_» скрывает переменные и методы класса
от других элементов приложения при его импортировании. В рамках того
модуля (dart-файла), где описывается класс с его приватными
переменными они остаются общедоступными. Для примера перенесем
класс Cat, в котором имеется приватная переменная из файла «cat.dart» в
файл с функцией верхнего уровня «main» и изменим из него значение
переменной _sleepState экземпляра класса Cat:
// ex4_3.dart
class Cat{
// без изменений относительно предыдущего примера
}

void main(List<String> arguments) {


// создаем экземпляр класса Cat
var cat = Cat()

216
..age=3
..name='Тимоха'
.._sleepState=false;

cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
cat._sleepState = true;
cat.currentState(); // Кот спит
cat.sleep(); // Сон во сне... мммм...
cat._sleepState = false;
cat.currentState(); // Кот бодрствует
}

В связи с такой особенностью поведения приватных методов и


переменных необходимо помнить о том, что относительно того модуля
(файла), где они были объявлены, что переменные класса или методы, что
приватные переменные модуля или функции, остаются публичными.
Вернем класс в файл «cat.dart» и напишем геттер и сеттер для
установки и чтения значения переменной - _sleepState и _address:
// ex4_4 - cat.dart
class Cat {
late final String name;
String _address = 'Unknown';
int age = 0;
bool _sleepState = false;

bool get isSleep => _sleepState;


set setSleepState(bool val) => _sleepState = val;

String get address => _address;


set address(String val) => _address = val;

// остальные методы не изменялись


}

// ex4_4 - main.dart
import 'cat.dart';

void main(List<String> arguments) {


// создаем экземпляр класса Cat
var cat = Cat()
..age = 3
..name = 'Тимоха';

print(cat.address); // Unknown
cat.address = 'Москва';
print(cat.address); // Москва

217
cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
print(cat.isSleep); // false

cat.setSleepState = true;
cat.currentState(); // Кот спит
cat.sleep(); // Сон во сне... мммм...
}

Если смотреть на представленный пример более концептуально, то


показанные геттеры и сеттеры не привносят никакой пользы и просто
дублируют функционал публичной переменной. Давайте это изменим для
поля адреса и перепишем код следующим образом:
// ex4_5 - cat.dart
class Cat {
late final String name;
String _address = 'Unknown';
int age = 0;
bool _sleepState = false;

bool get isSleep => _sleepState;


set setSleepState(bool val) => _sleepState = val;

String get address{


if (_address == 'Unknown') {
return 'Default City';
}
return _address;
}

set address(String val){


if (val == '') {
return;
}
_address = val;
}
// остальные методы не изменялись
}

// ex4_5 - main.dart
import 'cat.dart';

void main(List<String> arguments) {


// создаем экземпляр класса Cat
var cat = Cat()
..age = 3

218
..name = 'Тимоха';

print(cat.address); // Default City


cat.address = '';
print(cat.address); // Default City
cat.address = 'Москва';
print(cat.address); // Москва

cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
}

4.3. Конструктор класса


Посредством конструктора создается экземпляр класса и когда он не
объявляется явно, то компилятор создает конструктор по умолчанию,
который не принимает на свой вход аргументы для инициализации
состояния создаваемого экземпляра класса.
Давайте рассмотрим несколько способов объявления конструктора
класса в Dart. В первом случае мы можем использовать стандартное
объявление из языков программирования семейства Си:
// ex4_6 - cat.dart
class Cat{
late final String name;
String _address = 'Unknown';
int age = 0;
bool _sleepState = false;

Cat(String name, int age, String address) {


this.name = name;
this.age = age;
this._address = address;
}

// остальные методы не изменялись


}

// ex4_6 - main.dart
import 'cat.dart';

void main(List<String> arguments) {


// создаем экземпляр класса Cat
var cat = Cat('Тимоха', 5, 'Москва');
print(cat.name); // Тимоха
print(cat.age); // 5
print(cat.address); // Москва

219
cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
}

Так как имена аргументов конструктора и переменных класса


идентичны, то мы должны явным образом, через ссылку на текущий
экземпляр класса - this, указать какой переменной значение какого
аргумента конструктора присваивается. Если названия имен переменных
класса и аргументов конструктора отличаются, то конструктор можно
объявить следующим образом:
class Cat{
late final String name;
String _address = 'Unknown';
int age = 0;
// по умолчанию кот, при создании экземпляра класса
// всегда бодрствует
bool _sleepState = false;

Cat(String n, int i, String a) {


name = n;
age = i;
_address = a;
}

// остальные методы не изменялись


}

Еще один способ объявления конструктора позволяет его записать


более компактно. В этом случае производится явное связывание имен
аргументов конструктора с переменными экземпляра создаваемого класса:
class Cat{
late final String name;
String _address = 'Unknown';
int age = 0;
// по умолчанию кот, при создании экземпляра класса
// всегда бодрствует
bool _sleepState = false;

Cat(this.name, this.age);

// остальные методы не изменялись


}

Аналогичным образом мы можем через конструктор


инициализировать значения приватных переменных экземпляра класса:

220
// ex4_7 - cat.dart
class Cat{
late final String name;
String _address = 'Unknown';
int age = 0;
// по умолчанию кот, при создании экземпляра класса
// всегда бодрствует
bool _sleepState = false;

Cat(
this.name,
this.age,
this._address,
this._sleepState,
);
// остальные методы не изменялись
}

// ex4_7 - main.dart
import 'cat.dart';

void main(List<String> arguments) {


// создаем экземпляр класса Cat
var cat = Cat('Тимоха', 3, 'Москва', true);

print(cat.name); // Тимоха
print(cat.age); // 3
print(cat.address); // Москва

cat.helloMaster();
cat.currentState(); // Кот спит
}

К конструкторам и методам класса применимы все те же способы


передачи аргументов, что и к функциям. Для примера сделаем передачу в
конструктор флага бодрствования кота и его возраста не обязательными:
// ex4_8 - cat.dart
class Cat{
late final String name;
String _address = 'Unknown';
int age = 0;
// по умолчанию кот, при создании экземпляра класса
// всегда бодрствует
bool _sleepState = false;

Cat(
this.name,

221
this._address, [
this.age = 4,
this._sleepState = true,
]);
// остальные методы не изменялись
}

// ex4_8 - main.dart
import 'cat.dart';

void main(List<String> arguments) {


var cat = Cat('Тимоха', 'Москва');
print('Возраст кота "${cat.name}" = ${cat.age}');
// Возраст кота "Тимоха" = 4
print(
'Город прожавания кота "${cat.name}" = ${cat.address}',
); // Город прожавания кота "Тимоха" = Москва
cat.helloMaster();
cat.currentState(); // Кот спит
}

Теперь, вместо позиционных используем именованные аргументы.


Так как их имя не может начинаться с символа нижнего подчеркивания,
необходимо будет изменить имя аргумента, который отвечает за передачу
флага бодрствования кота и явно указать, что приватная переменная
инициализируется значением этого аргумента конструктора:
// ex4_9 - cat.dart
class Cat{
late final String name;
String _address = 'Unknown';
int age = 0;
// по умолчанию кот, при создании экземпляра класса
// всегда бодрствует
bool _sleepState = false;

Cat({
required this.name,
required bool sleepState,
this.age = 2,
String address = 'Unknown',
}) : _sleepState = sleepState,
_address = address;

// остальные методы не изменялись


}

// ex4_9 - main.dart

222
import 'cat.dart';

void main(List<String> arguments) {


var cat = Cat(name: 'Тимоха', sleepState: true);

print('Возраст кота "${cat.name}" = ${cat.age}');


// Возраст кота "Тимоха" = 2
print(
'Город прожавания кота "${cat.name}" = ${cat.address}',
); // Город прожавания кота "Тимоха" = Default City
cat.helloMaster();
cat.currentState(); // Кот спит
}

4.3.1. Именованный конструктор


В отличие от С-образных языков программирования Dart не
поддерживает перегрузку конструктора, то есть мы не можем объявить
несколько конструкторов с одним именем, но принимающие на вход
различное количество аргументов или отличающиеся типом принимаемых
аргументов. Вместо механизма перегрузки конструкторов программисту
предоставляется возможность объявлять именованный конструктор:
// ex4_10 - cat.dart
class Cat{
late final String name;
String _address = 'Unknown';
int age = 0;
// по умолчанию кот, при создании экземпляра класса
// всегда бодрствует
bool _sleepState = false;

Cat({
required this.name,
this.age = 2,
required bool sleepState,
String address = 'Unknown',
}) : _sleepState = sleepState,
_address = address;

Cat.onlyName(this.name);

Cat.fromNameAndAddress(
this.name,
String address,
) : _address = address;

// остальные методы не изменялись

223
}

// ex4_10 - main.dart
import 'cat.dart';

void catProcessing(Cat cat) {


print('Возраст кота "${cat.name}" = ${cat.age}');
print(
'Город прожавания кота "${cat.name}" = ${cat.address}',
);
cat.helloMaster();
cat.currentState();
}

void main(List<String> arguments) {


var cat = Cat(
name: 'Тимоха',
sleepState: true,
address: 'Питер',
);
catProcessing(cat);
print('*' * 20);

cat = Cat.onlyName('Тимоха');
catProcessing(cat);
print('*' * 20);

cat = Cat.fromNameAndAddress('Тимоха', 'Москва');


catProcessing(cat);
}
/* Возраст кота "Тимоха" = 2
Город прожавания кота "Тимоха" = Питер
Кот спит
********************
Возраст кота "Тимоха" = 0
Город прожавания кота "Тимоха" = Default City
Мя-я-я-я-у!!!
Кот бодрствует
********************
Возраст кота "Тимоха" = 0
Город прожавания кота "Тимоха" = Москва
Мя-я-я-я-у!!!
Кот бодрствует */

Также имеется возможность из именованного конструктора вызывать


основной конструктор класса. Данный механизм называется
перенаправлением конструктора:

224
// ex4_11 - cat.dart
class Cat{
late final String name;
String _address = 'Unknown';
int age = 0;
// по умолчанию кот, при создании экземпляра класса
// всегда бодрствует
bool _sleepState = false;

Cat({
required this.name,
this.age = 2,
required bool sleepState,
String address = 'Unknown',
}) : _sleepState = sleepState,
_address = address;

Cat.onlyName(String name)
: this(
name: name,
sleepState: false,
);

Cat.defaultCat()
: this(
name: 'Мурзик',
sleepState: false,
address: 'Пятигорск',
age: 3,
);

Cat.fromNameAndAddress(String name, String address)


: this(
name: name,
sleepState: false,
address: address,
);

// остальные методы не изменялись


}

// ex4_11 - main.dart
import 'cat.dart';

void catProcessing(Cat cat) {


print('Возраст кота "${cat.name}" = ${cat.age}');
print(
'Город прожавания кота "${cat.name}" = ${cat.address}',

225
);
cat.helloMaster();
cat.currentState();
}

void main(List<String> arguments) {


var cat = Cat(
name: 'Тимоха',
sleepState: true,
address: 'Питер',
);
catProcessing(cat);
print('*' * 20);

cat = Cat.onlyName('Твикс');
catProcessing(cat);
print('*' * 20);

cat = Cat.defaultCat();
catProcessing(cat);
print('*' * 20);

cat = Cat.fromNameAndAddress('Тимоха', 'Москва');


catProcessing(cat);
}
/*
Возраст кота "Тимоха" = 2
Город прожавания кота "Тимоха" = Питер
Кот спит
********************
Возраст кота "Твикс" = 2
Город прожавания кота "Твикс" = Default City
Мя-я-я-я-у!!!
Кот бодрствует
********************
Возраст кота "Мурзик" = 3
Город прожавания кота "Мурзик" = Пятигорск
Мя-я-я-я-у!!!
Кот бодрствует
********************
Возраст кота "Тимоха" = 2
Город прожавания кота "Тимоха" = Москва
Мя-я-я-я-у!!!
Кот бодрствует */

226
4.3.2. Константный конструктор
Такой вид конструктора необходимо использовать в тех случаях, когда
значения объектов и переменных класса не меняются, то есть все они
объявлены через final. Для использования такого объявления
конструктора, перед ним следует добавить ключевое слово const:
// ex4_12 - cat.dart
class ImmutableCat {
final String name;
final int age;

const ImmutableCat(this.name, this.age);

void helloMaster(){
print('Мя-я-я-я-у!!!');
}
}

// ex4_12 - main.dart
import 'cat.dart';

void main(List<String> arguments) {


var cat = const ImmutableCat('Тимоха', 3);
var newcat = const ImmutableCat('Тимоха', 3);

var barsik = const ImmutableCat('Барсик', 2);

print(identical(cat, newcat)); // cat == newcat


print(identical(cat, barsik)); // cat != barsik
barsik.helloMaster(); // Мя-я-я-я-у!!!
}

Переменная cat и newcat хранят ссылку на один и тот же объект, это


связано с тем, что при их создании использовались идентичные аргументы
для инициализации экземпляра класса. При этом, необходимо не забывать
при создании экземпляра класса перед его конструктором использовать
ключевое слово const, иначе будет создаваться новый экземпляр класса,
даже в том случае, когда производится инициализация одинаковыми
значениями:
// ex4_13 - main.dart
import 'cat.dart';

void main(List<String> arguments) {


var cat = const ImmutableCat('Тимоха', 3);
var newcat = ImmutableCat('Тимоха', 3);

227
var barsik = const ImmutableCat('Барсик', 2);

print(identical(cat, newcat)); // cat != newcat


print(identical(cat, barsik)); // cat != barsik
}

4.3.3. Фабричный конструктор


Обычные конструкторы отвечают за создание и инициализацию
новых экземпляров класса. А что, если нам необходимо, чтобы конструктор
в случае создания объекта с параметрами, по которым уже ранее создавался
объект, возвращал именно его? Или в системе присутствовал только один
объект (экземпляр) нужного нам класса?
Для этих случаев необходимо использовать фабричные конструкторы,
путем добавления перед конструктором класса ключевого слова factory.
Такой конструктор от обычных отличается еще тем, что в нем явно должен
осуществляться возврат экземпляра класса посредством return.
Для начала приведем пример, когда в разрабатываемой программной
системе необходимо обеспечить наличие всего лишь одного экземпляра
класса (паттерн Singleton/Одиночка). Представим, что у нас в квартире
может существовать только один кот. Он полноправный хозяин в квартире
и не потерпит наличие конкуренции:
// ex4_14 - singlecat.dart
class SingleCat{
String _name;
int age;
static SingleCat _singleCat = SingleCat.fromName('',0);

SingleCat.fromName(this._name, this.age);

factory SingleCat(String name, int age){


if(_singleCat._name == ''){
_singleCat = SingleCat.fromName(name, age);
print('Создаем экземпляр класса кота');
}
else{
print('Экземпляр класса кота был создан ранее!');
}
return _singleCat;
}
String get name => _name;
}

// ex4_14 - main.dart
import 'singlecat.dart';

228
void main(List<String> arguments) {
var cat = SingleCat('Тимоха', 2);
var newCat = SingleCat('Барсик', 4);
print(newCat.name);
}
/* Создаем экземпляр класса кота
Экземпляр класса кота был создан ранее!
Тимоха */

Если вы внимательно ознакомились с предыдущим примером, то ваш


вопрос в стиле: «Разве это шаблон Singleton?» полностью оправдан! В
приведенном коде нет приватного конструктора, а это значит, что объект
может быть спокойно создан в обход фабричного конструктора:
void main(List<String> arguments) {
var cat = SingleCat('Тимоха', 2);
var newCat = SingleCat.fromName('Твикс', 2);
print(newCat.name);
}
// Создаем экземпляр класса кота
// Твикс

Для того, чтобы восстановить справедливость, перепишем пример


следующим образом:
// ex4_15 - singlecat.dart
class SingleCat{
String _name;
int age;
static SingleCat? _singleCat;

SingleCat._(this._name, this.age); // приватный конструктор

factory SingleCat([String name='', int age=0]){


return _singleCat ??= SingleCat._(name, age);
}

String get name => _name;


}

// ex4_15 - main.dart
import 'singlecat.dart';

void main(List<String> arguments) {


var cat = SingleCat('Тимоха', 2);
var newCat = SingleCat('Твикс', 3);
print(cat.name); // Тимоха
print(newCat.name); // Тимоха

229
var newCat2 = SingleCat();
print(newCat2.name); // Тимоха

print(identical(cat, newCat2)); // true


print(identical(newCat, newCat2)); // true
}

В приведенном примере при первом вызове конструктора можно


передать инициализирующие данные. Во всех следующих попытках
создать экземпляр класса будет возвращаться один и тот же объект. Обычно
в процессе реализации такого паттерна проектирования на вход
шаблонного конструктора или специализированного метода для создания
одиночного объекта ничего не подается.
Теперь же представим ситуацию, что у нас в системе огромное
количество книг, и чтобы не создавать дубликаты, мы в случае запроса на
создание экземпляра класса с параметрами, по которым был ранее создан
экземпляр класса книги, будем возвращать его. А если экземпляр класса
книги с необходимыми параметрами не создавался, то мы его создадим,
сделаем себе об этом заметку и вернем объект в клиентский код:
// ex4_16.dart
class Book{
final String name;
final int pages;
static var _booksMap = <String, Book>{};

Book.fromSettings(this.name, this.pages);

factory Book(String name, int pages){


var cache = name.toLowerCase() + pages.toString();
return _booksMap.putIfAbsent(cache,
() => Book.fromSettings(name, pages));
}
}

void main(List<String> arguments) {


var book1 = Book('Война и Мир т.1', 1234);
var book2 = Book('Тихий Дон т.1', 400);
var book3 = Book('Евгений Онегин', 250);
var book4 = Book('Война и Мир т.1', 1234);
print(identical(book2, book3)); // false
print(identical(book1, book4)); // true
}

230
4.4. Статические переменные и методы класса
В предыдущих примерах использовались статические переменные
класса. Их отличие от обычных заключается в том, что они будут хранить
одно и то же значение вне зависимости от того, с каким экземпляром класса
сейчас производится работа. Также к ним можно обращаться только через
имя самого класса (без создания экземпляра класса), либо прописав
сеттеры и геттеры. Отдельно стоит обратить внимание на то, что
статическая переменная должна быть инициализирована до момента ее
использования (обращения к ней):
// ex4_17.dart
class Book{
static var bookPages = 10;

int get pages => bookPages;


}

void main(List<String> arguments) {


var book1 = Book();
print(book1.pages); // 10
Book.bookPages = 20; // меняем значение
var book2 = Book();
print(book2.pages); // 20
}

Статические методы можно вызывать, обращаясь к ним через имя


класса, без создания самого экземпляра класса или написать отдельный
метод для экземпляра класса, который будет переадресовывать вызов
статическому методу:
// ex4_18.dart
class Calc{
static int add(int a, int b){
return a + b;
}
int sum(int a, int b) => add(a, b);
}

void main(List<String> arguments) {


print(Calc.add(3, 5)); // 8
print(Calc.add(13, 5)); // 18
var calc = Calc();
print(calc.sum(13, 5)); // 18
}

231
4.5. Перегрузка операторов
Dart предоставляет возможность программисту перегружать
стандартные операторы (см. главу 2), тем самым создавая более гибкие
классы. Иногда перегрузку путают с переопределением, так вот, с
академической колокольни – операторы могут только перегружаться, а не
переопределяться. Так уж исторически повелось, поскольку они не
привязаны только к какому-то конкретному типу данных.
Ряд перегрузок операторов мы с вами рассмотрим, взяв за основу
рубли, где значения будут храниться в копейках. Для боевого проекта такой
код не очень подойдет, но даст полноценное представление о том, как
реализуются перегрузки операторов.

4.5.1. Перегрузка арифметических операторов


Ниже представлены варианты перегрузок основных арифметических
операций и пример их использования:
// ex4_19.dart
class Rub {
late final int kopek;

Rub._(this.kopek);

factory Rub(String rub) {


var localRub = (double.parse(rub) * 100).toStringAsFixed(0);
return Rub._(int.parse(localRub));
}

// перегрузка сложения
Rub operator +(Rub other) {
return Rub._(kopek + other.kopek);
}

// перегрузка вычитания
Rub operator -(Rub other) {
var temp = 0;
if (kopek - other.kopek >= 0) {
temp = kopek - other.kopek;
} else{
print("(╯'□')╯︵ ┻━┻ Банкрот!!!");
}
return Rub._(temp);
}

// перегрузка умножения
Rub operator *(int value) {

232
return Rub._(kopek * value);
}

// перегрузка деления
Rub operator /(int value) {
// осуществляем целочисленное деление
return Rub._(kopek ~/ value);
}

// перегрузка остатка от деления


Rub operator %(int value) {
return Rub._(kopek % (value*100));
}

// перегрузка унарного минуса


Rub operator -() {
print("(╯'□')╯︵ ┻━┻ Банкрот!!!");
return Rub._(-kopek);
}

// переопределение
@override
String toString() {
var rub = (kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}
}

void main(List<String> arguments) {


var rub1 = Rub('10');
var rub2 = -Rub('10'); // (╯'□')╯︵ ┻━┻ Банкрот!!!
var rub3 = rub1 + rub2;
print(rub3); // Rub(0.00)
print(rub1 + Rub('4.55')); // Rub(14.55)
print(rub3 - Rub('2')); // (╯'□')╯︵ ┻━┻ Банкрот!!! Rub(0.00)
print(rub1 * 3); // Rub(30.00)
print(rub1 / 2); // Rub(5.00)
print(rub1 % 4); // Rub(2.00)
}

Чтобы на экземпляре класса можно было применять на


арифметические операции с различными типами данных, необходимо
использовать более абстрактный тип в качестве аргумента перегружаемого
оператора. В нашем случае – Object. Для примера немного перепишем
перегрузку сложения:

233
// ex4_20.dart
class Rub {
late final int kopek;

Rub._(this.kopek);

factory Rub(String rub) {


var localRub = (double.parse(rub) * 100).toStringAsFixed(0);
return Rub._(int.parse(localRub));
}

// перегрузка сложения
Rub operator +(Object other) {
if (other is Rub) {
return Rub._(kopek + other.kopek);
} else if(other is int){
return Rub._(kopek + other*100);
}else if (other is double){
var localRub = (other * 100).toStringAsFixed(0);
return Rub._(kopek + int.parse(localRub));
}else{
print("(╯'□')╯︵ ┻━┻ WTF!!!");
return Rub._(0);
}
}

// переопределение
@override
String toString() {
var rub = (kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}
}

void main(List<String> arguments) {


var rub1 = Rub('10');
print(rub1 + Rub('4.55')); // Rub(14.55)
print(rub1 + 2); // Rub(12.00)
print(rub1 + 5.5); // Rub(15.50)
print(rub1 + "33"); // (╯'□')╯︵ ┻━┻ WTF!!! Rub(0.00)
}

Наличие перегруженных операторов, возвращающих экземпляр


класса, позволяет без каких-либо манипуляций использовать их с
операцией присваивания:
void main(List<String> arguments) {
var rub1 = Rub('10');

234
rub1 += 10;
print(rub1); // Rub(20.00)

rub1 += Rub('2');
print(rub1); // Rub(22.00)

rub1 += 3.4;
print(rub1); // Rub(25.40)
}

4.5.2. Перегрузка операторов сравнения


Ниже представлены варианты перегрузок операторов проверки на
равенство и неравенство и пример их использования:
// ex4_21.dart
class Rub {
late final int kopek;

Rub._(this.kopek);

factory Rub(String rub) {


var localRub = (double.parse(rub) * 100).toStringAsFixed(0);
return Rub._(int.parse(localRub));
}

@override
int get hashCode => kopek.hashCode;
// Переопределение hashCode необходимо для правильной
// проверки равенства объектов

// перегрузка проверки на равенство


@override
bool operator ==(Object other) {
if (other is Rub) {
return kopek == other.kopek;
} else if (other is int) {
return kopek == other * 100;
} else if (other is double) {
var localRub = (other * 100).toStringAsFixed(0);
return kopek == int.parse(localRub);
} else {
print("(╯'□')╯︵ ┻━┻ WTF!!!");
return false;
}
}

235
bool operator >(Rub other) {
return kopek > other.kopek;
}

bool operator <(Rub other) {


return kopek < other.kopek;
}

bool operator >=(Rub other) {


return kopek >= other.kopek;
}

bool operator <=(Rub other) {


return kopek <= other.kopek;
}

// переопределение
@override
String toString() {
var rub = (kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}
}

void main(List<String> arguments) {


var rub1 = Rub('10');
print(rub1 == Rub('10')); // true
print(rub1 != Rub('10')); // false
print(rub1 == 4); // false
print(rub1 == 10.0); // true
print(rub1 == '10.0'); // (╯'□')╯︵ ┻━┻ WTF!!!
print(rub1 >= Rub('9')); // true
print(rub1 <= Rub('15')); // true
print(rub1 > Rub('10')); // false
print(rub1 < Rub('11')); // false
}

Обратите внимание на то, что проверку на неравенство типа !=


перегружать не пришлось. Да и нет такого оператора для перегрузки. Но его
можно спокойно использовать при сравнении экземпляра класса с другими
объектами, т.к. в основе данной операции лежит оператор проверки на
равенство ==.

236
4.5.3. Перегрузка битовых операторов
Цифровой век диктует свои законы, поэтому даешь битовые операции
для цифрового рубля!
// ex4_22.dart
class Rub {
late final int kopek;

Rub._(this.kopek);

factory Rub(String rub) {


var localRub = (double.parse(rub) * 100).toStringAsFixed(0);
return Rub._(int.parse(localRub));
}

// перегрузка побитового И
Rub operator &(Object other) {
if (other is Rub) {
return Rub._(kopek & other.kopek);
} else if(other is int){
return Rub._(kopek & other*100);
}else if (other is double){
var localRub = (other * 100).toStringAsFixed(0);
return Rub._(kopek & int.parse(localRub));
}else{
print("(╯'□')╯︵ ┻━┻ WTF!!!");
return Rub._(0);
}
}

// перегрузка побитового ИЛИ


Rub operator |(int other) {
return Rub._(kopek | (other*100));
}

// перегрузка побитового исключающего ИЛИ


Rub operator ^(int other) {
return Rub._(kopek ^ (other*100));
}

// перегрузка побитового НЕ
Rub operator ~() {
return Rub._(~kopek);
}

// перегрузка сдвига влево


Rub operator <<(int other) {
return Rub._(kopek << other);

237
}

// перегрузка сдвига вправо


Rub operator >>(int other) {
return Rub._(kopek >> other);
}

// перегрузка беззнакового сдвига вправо


Rub operator >>>(int other) {
return Rub._(kopek >>> other);
}

// переопределение
@override
String toString() {
var rub = (kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}
}

void main(List<String> arguments) {


var rub1 = Rub('109');
print(rub1 & 100); // Rub(87.20)
print(rub1 & Rub('20')); // Rub(6.56)
print(rub1 & 30.7); // Rub(27.08)
print(rub1 & '30.7'); // (╯'□')╯︵ ┻━┻ WTF!!! Rub(0.00)
print(rub1 | 100); // Rub(121.80)
print(rub1 ^ 86); // Rub(28.28)
print(~rub1); // Rub(-109.01)
print(rub1 << 2); // Rub(436.00)
print(rub1 >> 2); // Rub(27.25)
print(rub1 >>> 1); // Rub(54.50)
}

4.5.4. Перегрузка индексных операторов


Для демонстрации перегрузки индексных операторов рассмотрим
ситуацию, что у нас есть книги и коробка, в которую можем их помещать. В
коде это будет выглядеть следующим образом:
// ex4_23.dart
class Book {
final String name;
final int pages;

Book(this.name, this.pages);

Box operator +(Book otherBook) {

238
return Box([this, otherBook]);
}

@override
String toString() {
return 'Book($name, $pages)';
}
}

class Box {
final List<Book> _items;
Box(this._items);

int get size => _items.length;

String _printBooks() {
var str = '[';
for (var element in _items) {
str += ('$element, ');
}
str += ']';
return str;
}

void operator +(Object book) {


if (book is Book) {
_items.add(book);
}
if (book is Box) {
_items.addAll(book._items);
}
}

Book operator [](int index) {


if (index < 0 || index >= _items.length) {
throw RangeError.range(index, 0, _items.length);
}
return _items[index];
}

void operator []=(int index, Book book) {


if (index < 0 || index >= _items.length) {
throw RangeError.range(index, 0, _items.length);
}
_items[index] = book;
}

@override

239
String toString() {
return _printBooks();
}
}

void main(List<String> arguments) {


var book1 = Book('ВиМ т.1', 1234);
var book2 = Book('ТД т.1', 400);
var box = book1 + book2;
print(box);
print('-' * 30);

box + Book('ЕО', 250);


// box += Book('Евгений Онегин', 250); // error
print(box);
print('-' * 30);

var box2 = Box([


Book('Мы', 233),
Book('Честь имею', 600),
]);
box2 + box;
print(box2);
print('-' * 30);

print(box2[2]);
box2[2] = Book('Матан', 666);
print(box2);
}
/*
[Book(ВиМ т.1, 1234), Book(ТД т.1, 400), ]
------------------------------
[Book(ВиМ т.1, 1234), Book(ТД т.1, 400), Book(ЕО, 250), ]
------------------------------
[Book(Мы, 233), Book(Честь имею, 600), Book(ВиМ т.1, 1234), Book(ТД
т.1, 400), Book(ЕО, 250), ]
------------------------------
Book(ВиМ т.1, 1234)
[Book(Мы, 233), Book(Честь имею, 600), Book(Матан, 666),
Book(ТД т.1, 400), Book(ЕО, 250), ]
*/

4.6. Методы расширения (Extension methods)


Иногда, используя тот или иной встроенный или пользовательский
тип данных так и хочется спросить: «Где живет разрабатывавший это
человек? Почему он не добавил таких-то методов?» Не думайте, что вы

240
одиноки в этих вопросах, но та щепотка магии, о которой далее пойдет
речь, сгладит острые углы неприятия сложившейся ситуации.
Как гласит народная мудрость: «Не можешь бороться – возглавь!».
Если переложить ее на IT-сленг, то звучать она, скорее всего, будет
следующим образом: «Что-то не нравится – напиши свой
велосипедокостыль!» Как раз такой функционал и предоставляют
разработчику методы расширения, обобщенный вид объявления которых
можно представить следующим образом:
extension <ИмяРасширения>? on <тип> {
(<добавляемые методы>)*
}

В качестве своей первой жертвы давайте возьмем встроенный тип


данных (он же класс) – int и добавим ему метод возведения в квадрат и
проверку на то, установлен ли заданный бит в 1:
// ex4_24.dart
extension MyInt on int {
int pow2() {
return this<<1;
}

bool isSetBit(int bit) {


return (this & (1 << bit)) != 0;
}
}

void main(List<String> arguments) {


var value = 11;
print(value.pow2()); // 22
print(value.isSetBit(2)); // false
print(value.isSetBit(0)); // true
}

Теперь поборемся со строковым типом данных:


// ex4_25.dart
extension MyString on String {
bool isUrl() {
return startsWith("http://") || startsWith("https://");
}

List<String> toList() => split('');


}

void main(List<String> arguments) {


var url = "https://dart.dev";
print(url.toList());

241
// [h, t, t, p, s, :, /, /, d, a, r, t, ., d, e, v]

print(url.isUrl()); // true

url = '-_-';
print(url.isUrl()); // false
}

Далее добавим для целочисленного списка методы расчета суммы его


элементов и количества вхождения в список элемента с задаваемым
значением:
// ex4_26.dart
extension MyList on List<int> {
int sum() {
return reduce((value, element) => value + element);
}

int count(int value){


return where((element) => element == value).length;
}
}

void main(List<String> arguments) {


var myList = [1, 2, 3, 4, 3, 5, 3];
print(myList.sum()); // 21
print(myList.count(3)); // 3
}

Методы расширений можно объявлять в отдельной библиотеке


(файле) и импортировать в нужный файл проекта. Если их API частично
совпадает с еще одним импортируемым файлом, где объявлены методы
расширений, обратитесь к официальной документации, где подробно
рассмотрено, как действовать в этой ситуации
(https://dart.dev/language/extension-methods#api-conflicts).
Напоследок давайте вспомним последний пример с перегрузкой
операторов и то, что добавление в коробку осуществлялось через сложение.
Такое себе решение… поэтому добавим через расширение к классу Box
метод add:
// ex4_27.dart
class Book {
// без изменений
}

class Box {
// без изменений
}

242
extension TrueBox on Box {
void add(Object book){
this + book;
}
}

void main(List<String> arguments) {


var book1 = Book('ВиМ т.1', 1234);
var book2 = Book('ТД т.1', 400);
var box = book1 + book2;

var box2 = Box([


Book('Мы', 233),
Book('Честь имею', 600),
]);
box2.add(box);
print(box2);
}
// [Book(Мы, 233), Book(Честь имею, 600),
// Book(ВиМ т.1, 1234), Book(ТД т.1, 400), ]

4.7. Наследование и переопределение методов


В Dart нет множественного наследования, то есть наследоваться
можно только от одного базового класса. В тоже самое время класс может
реализовывать множество интерфейсов. Для начала давайте рассмотрим
классический пример обобщения сотрудников в организации. Для начала
объявим классы, описывающие сантехника и строителя:
// ex4_28.dart
class Plumber {
final String name;
final int id;
int _age;
int _salary;
int _yearsExperience;

Plumber(
this.name,
this._age,
this.id,
this._salary,
this._yearsExperience,
);

Plumber.withMinSalary(
this.name,

243
this._age,
this.id,
this._yearsExperience,
) : _salary = 1000;

int get salary => _salary;


int get age => _age;
int get experience => _yearsExperience;

void ageIncrease() {
_age++;
}

void yearsExperienceIncrease() {
_yearsExperience++;
}

void salaryDown(int percent) {


// штрафуем сотрудника
_salary -= ((_salary / 100) * percent).toInt();
}

void salaryUp(int percent) {


// премируем сотрудника
_salary += ((_salary / 100) * percent).toInt();
}

@override
String toString() {
return 'Plumber($name, $age, $id, $_salary)';
}
}

class Builder {
final String name;
final int id;
int _age;
int _salary;
int _yearsExperience;
int _category;

Builder(
this.name,
this._age,
this.id,
this._salary,
this._yearsExperience,
this._category,

244
);

Builder.withMinSalary(
this.name,
this._age,
this.id,
this._yearsExperience,
this._category,
) : _salary = 1000;

int get salary => _salary;


int get age => _age;
int get experience => _yearsExperience;
int get category => _category;

void ageIncrease() {
_age++;
}

void yearsExperienceIncrease() {
_yearsExperience++;
}

void salaryDown(int percent) {


// штрафуем сотрудника
_salary -= ((_salary / 100) * percent).toInt();
_category--;
}

void salaryUp(int percent) {


// премируем сотрудника
_salary += ((_salary / 100) * percent).toInt();
_category++;
}

@override
String toString() {
return 'Builder($name, $age, $id, $_salary, $_category)';
}
}

К сожалению, если придется увеличивать количество классов


работников, такой подход приводит к появлению большого количества
дублирующего кода. При этом возникнет трудность, если мы захотим
осуществить работу с различными экземплярами классов сотрудников в
рамках одного интерфейса. Поэтому давайте абстрагируемся от

245
конкретных должностей и введем класс «Сотрудник», от которого будет
производится наследование:
// ex4_29.dart
class Employee {
final String name;
final int id;
int _age;
int _salary;
int _yearsExperience;

Employee(
this.name,
this._age,
this.id,
this._salary,
this._yearsExperience,
);

int get salary => _salary;


int get age => _age;
int get experience => _yearsExperience;

void ageIncrease() {
_age++;
}
void yearsExperienceIncrease() {
_yearsExperience++;
}

void salaryDown(int percent) {


// увеличиваем оклад
_salary -= ((_salary / 100) * percent).toInt();
}

void salaryUp(int percent) {


// уменьшаем оклад
_salary += ((_salary / 100) * percent).toInt();
}

@override
String toString() {
return 'Employee($name, $age, $id, $_salary)';
}
}

class Plumber extends Employee { // наследование


Plumber(
String name,

246
int age,
int id,
int salary,
int yearsExperience,
) : super(name, age, id, salary, yearsExperience);

Plumber.withMinSalary(
String name,
int age,
int id,
int yearsExperience,
) : super(name, age, id, 1000, yearsExperience);

@override
String toString() {
return 'Plumber($name, $age, $id, $_salary)';
}
}

class Builder extends Employee { // наследование


int _category;

Builder(
this._category, {
required String name,
required int age,
required int id,
required int salary,
required int yearsExperience,
}) : super(name, age, id, salary, yearsExperience);

Builder.withMinSalary({
required String name,
required int age,
required int id,
required int yearsExperience,
required int category,
}) : _category = category,
super(name, age, id, 3000, yearsExperience);

int get category => _category;

@override
void salaryDown(int percent) {
// штрафуем сотрудника
super.salaryDown(percent);
_category--;
}

247
@override
void salaryUp(int percent) {
// премируем сотрудника
super.salaryUp(percent);
_category++;
}

@override
String toString() {
return 'Builder($name, $age, $id, $_salary, $_category)';
}
}

Класс Employee является базовым по отношению к Plumber и Builder.


А они, в свою очередь, считаются производными классами от Employee, что
задается использованием ключевого слова extends. Так как Employee более
абстрактно описывает сотрудника, он задает основные его свойства и
поведение (поля и методы). В случае с сантехником (Plumber), производных
класс наследуется от базового и не имеет никаких новых полей и отличного
от базового класса поведения, а только переопределенный метод toString.
В классе, описывающем строителя (Builder), добавилось поле, отвечающее
за его категорию, которая уменьшается или увеличивается в зависимости
от того, что происходит с окладом. Поэтому для него обязательно нужно
переопределить методы увеличения и уменьшения оклада, а т.к. они
завязаны не реализации базового класса, то она вызывается через ключевое
слово super, которое позволяет нам работать с поведением и состоянием
базового класса.
При создании производного класса обязательно нужно вызвать
конструктор базового и передать ему необходимые параметры. В нашем
случае это делается после двоеточия, где указывается ключевое слово super
и передаются аргументы, ожидаемые на вход конструктора базового класса.
Начиная с Dart 2.17, посредством супер параметров
(https://dart.dev/language/constructors#super-parameters), можно упростить
процесс передачи аргументов для инициализации базового класса, от
которого производилось наследования и явно не вызывать конструктор
базового класса. Для этого достаточно объявить аргументы в конструкторе
производного класса через ключевое слово super:
Plumber(
super.name,
super.age,
super.id,
super.salary,
super.yearsExperience,
);

248
Но большей гибкости можно добиться при использовании
именованных аргументов в конструкторе производного и базового класса:
// ex4_30.dart
class Employee {
final String name;
final int id;
int _age;
int _salary;
int _yearsExperience;

Employee(
this.name,
this._age,
this.id,
this._salary,
this._yearsExperience,
);

Employee.named({
required String name,
required int age,
required int id,
required int salary,
required int yearsExperience,
}) : this(name, age, id, salary, yearsExperience);

// остальные методы не изменялись


}

class Plumber extends Employee {


Plumber(
super.name,
super.age,
super.id,
super.salary,
super.yearsExperience,
);

Plumber.withMinSalary({
required super.name,
required super.age,
required super.id,
required super.yearsExperience,
}) : super.named(salary: 1000);

// остальные методы не изменялись


}

249
class Builder extends Employee {
int _category;

Builder(
super.name,
super.age,
super.id,
super.salary,
super.yearsExperience,
this._category,
);

Builder.withMinSalary({
required super.name,
required super.age,
required super.id,
required super.yearsExperience,
required int category,
}) : _category = category,
super.named(salary: 3000);
// остальные методы не изменялись
}

При таком написании конструкторов важно следовать правилу, что


сначала через super указываются аргументы базового класса, а потом, через
this производного.
Теперь перейдем к созданию экземпляра производного класса, а также
разберем, как его приводить к базовому, чтобы вся последующая работа с
ним осуществлялась через интерфейс базового класса:
// ex4_30.dart
void main() {
var builder = Builder.withMinSalary(
name: 'Ivan',
age: 30,
id: 1,
yearsExperience: 7,
category: 2,
);

var plumber = Plumber.withMinSalary(


name: 'Max',
age: 22,
id: 4,
yearsExperience: 1,
);

print(builder); // Builder(Ivan, 30, 1, 3000, 2)

250
print(plumber); // Plumber(Max, 22, 4, 1000)

Employee employee = builder; // неявное приведение к Employee


print(employee); // Builder(Ivan, 30, 1, 3000, 2)
employee.salaryDown(50);
print(builder); // Builder(Ivan, 30, 1, 1500, 1)

// проверка на тип объекта


if (employee is Plumber){
print(employee); // ничего не выведет
}

if (employee is Builder){
// появляется доступ к полям и методам Builder
print(employee.category); // 1
}

employee = plumber as Employee; // явное приведение к Employee


print(employee); // Plumber(Max, 22, 4, 1000)

var employee2 = plumber as Employee;


employee2.salaryUp(30);
print(employee2); // Plumber(Max, 22, 4, 1300)
}

Работа через интерфейс базового класса позволяет нам осуществить


сокрытие реализации производного, т.е. таким образом мы работаем с
объектом на более абстрактном уровне, что позволяет нам, например, не
заводить список типа dynamic или Object для хранения в нем производных
классов, а использовать тип базового:
// ex4_31.dart
void main() {
var listEmployee = <Employee>[
Builder('Alex', 22, 1, 2000, 1, 1),
Plumber('John', 27, 4, 9000, 3),
Builder('Max', 33, 2, 12000, 10, 3),
Plumber('Kate', 23, 4, 9000, 3),
];

for (var it in listEmployee){


if (it is Plumber){
it.salaryDown(10);
}
if (it is Builder){
it.salaryUp(10);
}
print(it);

251
}
}
// Builder(Alex, 22, 1, 2200, 2)
// Plumber(John, 27, 4, 8100)
// Builder(Max, 33, 2, 13200, 4)
// Plumber(Kate, 23, 4, 8100)

Как можно заметить по работе кода со строителями, вне зависимости


от того, что мы привели производный класс к базовому, при повышении
или понижении оклада будет вызываться метод производного класса. Это
связано с тем, что его переопределили, используя аннотацию @override.
Иногда на базовый класс можно возложить обязанность создавать
производные классы и сразу приводить к базовому. Для этого используется
фабричный конструктор:
// ex4_32.dart
class Employee {
final String name;
final int id;
int _age;
int _salary;
int _yearsExperience;

Employee(
this.name,
this._age,
this.id,
this._salary,
this._yearsExperience,
);

Employee.named({
required String name,
required int age,
required int id,
required int salary,
required int yearsExperience,
}) : this(name, age, id, salary, yearsExperience);

factory Employee.createChild({
required String name,
required int age,
required int id,
required int salary,
required int yearsExperience,
required int typeChild,
}) {
return switch (typeChild) {

252
1 => Builder(
name,
age,
id,
salary,
yearsExperience,
(yearsExperience ~/ 3) == 0 ? 1 : (yearsExperience ~/ 3),
),
_ => Plumber(name, age, id, salary, yearsExperience),
};
}

// остальные методы не изменялись


}

void main() {
var listEmployee = <Employee>[
Employee.createChild(
name: 'Alex',
age: 22,
id: 1,
salary: 2000,
yearsExperience: 1,
typeChild: 1,
),
Employee.createChild(
name: 'John',
age: 27,
id: 4,
salary: 9000,
yearsExperience: 10,
typeChild: 2,
),
];

for (var it in listEmployee) {


if (it is Plumber) {
it.salaryDown(10);
}
if (it is Builder) {
it.salaryUp(10);
}
print(it);
}
}
// Builder(Alex, 22, 1, 2200, 2)
// Plumber(John, 27, 4, 8100)

253
Если у вас уже есть опыт разработки и использования шаблонов
проектирования GoF, то наверняка заметили, что мы с вами реализовали
фабричный метод в «Dart-стиле».
Что касается сокрытия свойств и методов производного класса при
приведении к базовому, это можно посмотреть следующим образом.
Переместите код с объявлением классов в отдельный файл, импортируйте
его, создайте экземпляр класса Builder, приведите к Employee и
попробуйте отыскать доступ к его свойству category (дисклеймер – его не
будет видно):

Рисунок 4.5 – Сокрытие свойств (полей) и методов при приведении

4.8. Абстрактный класс и интерфейс


Дисклеймер: здесь говорится в целом про концепцию: что такое
абстрактный класс и что обычные классы в Dart могут использоваться, как
интерфейсные. Рассматривается только модификатор abstract, который и
так присутствовал в Dart до третьей версии.
Не всегда базовый класс может содержать реализации всех
объявленных в нем методов. Так как такие классы представляют из себя
некоторое обобщение, то часть реализации их методов может отдаваться
на откуп производным классам. То есть базовый класс можно
рассматривать как некоторый обобщенный интерфейс, через который мы в
клиентском коде можем работать с экземплярами производных классов,
приводя их к базовому. Это дает нам еще один уровень инкапсуляции. Так,
например, после того как привели производный класс к базовому мы
можем вызывать только методы базового класса, так как методы,
специфичные для производного класса становятся не доступны. Если же
нам нужно вызвать один из таких методов производного класса, то сперва
необходимо выполнить проверку можно ли текущий экземпляр класса, с

254
которым осуществляется работа, привести к необходимому производному
классу.
Методы, которые объявлены, но не имеют реализацию, называются
чисто виртуальными методами. А класс, который содержит хоть один
виртуальный метод – абстрактный класс. Отличие абстрактного класса от
обычного заключается в том, что экземпляр абстрактного класса не может
быть создан (исключение – фабричный конструктор). Но к таким классам
мы можем приводить производные от них классы. То есть что базовый, что
абстрактный класс может выступать в роли публичного интерфейса к
экземплярам производных от них классов.
Давайте перепишем класс Employee таким образом, чтобы он стал
абстрактным базовым классом, передав на откуп производным классам
расчет премии для сотрудников:
// ex4_33.dart
abstract class Employee {
final String name;
final int id;
int _age;
int _salary;
int _yearsExperience;

// конструкторы класса не изменялись

int calculateBonus(); // чисто виртуальный метод


int calculateBonusWithParam(int percent); // аналогично

// остальные методы не изменялись


}

После объявления Employee абстрактным классом, на его производные


классы Plumber и Builder ложится обязанность реализовать метод
calculateBonus и calculateBonusWithParam. В противном случае они тоже
будут считаться абстрактными:
// ex4_33.dart
class Plumber extends Employee {
// конструкторы и методы не изменялись

@override
int calculateBonus() {
return ((_salary / 100) * _yearsExperience).toInt();
}

@override
int calculateBonusWithParam(int percent) {

255
return ((_salary / 100) * percent).toInt();
}
}

class Builder extends Employee {


// конструкторы, поля и методы не изменялись

@override
int calculateBonus() {
return calculateBonusWithParam(50);
}

@override
int calculateBonusWithParam(int percent) {
return ((_salary / 100) * percent).toInt();
}
}

При использовании прошлого примера, где мы использовали


фабричный конструктор базового класса, все будет работать в штатном
режиме:
// ex4_33.dart
void main() {
var listEmployee = <Employee>[
Employee.createChild(
name: 'Alex',
age: 22,
id: 1,
salary: 2000,
yearsExperience: 1,
typeChild: 1,
),
Employee.createChild(
name: 'John',
age: 27,
id: 4,
salary: 9000,
yearsExperience: 10,
typeChild: 2,
),
];

for (var it in listEmployee) {


if (it is Plumber) {
it.salaryDown(10);
print('$it с премией ${it.calculateBonusWithParam(20)}');
}

256
if (it is Builder) {
it.salaryUp(10);
print('$it с премией ${it.calculateBonus()}');
}
}
}
// Builder(Alex, 22, 1, 2200, 2) с премией 1100
// Plumber(John, 27, 4, 8100) с премией 1620

Но при попытке создания экземпляра абстрактного класса, Dart


пропишет следующие болты:
void main() {
var employee = Employee.named(
name: 'John',
age: 27,
id: 4,
salary: 9000,
yearsExperience: 10,
);
}
// Error: The class 'Employee' is abstract
// and can't be instantiated.

По поводу интерфейса принято говорить, что «класс реализует такой-


то интерфейс». Его отличие от абстрактного или обычного класса
заключается в том, что определенные в нем состояние и поведение должны
быть также реализованы в том классе, который его реализует. В случае
наследования нам не надо было прописывать все переменные и методы
базового класса в производном заново, а только те методы, которые хотели
переопределить. Здесь же все - наоборот. Все переменные и методы,
объявленные в интерфейсе, должны быть объявлены и иметь реализацию в
том классе, который этот интерфейс реализует.
Таким образом, если в случае с наследованием мы наследуем состояние
и поведение, то в случае с интерфейсом – объявляем контракт, которому
должен следовать класс реализующий интерфейс.
Каждый класс в Dart неявно определяет интерфейс и когда нам
необходимо, чтобы разрабатываемый класс А поддерживал API другого
класса В, не наследуя его реализацию, то класс A должен реализовать
интерфейс B.
В отличие от наследования, класс может реализовывать сколько
угодно интерфейсов. Для указания того, что текущий класс реализует
интерфейс другого класса, используется ключевое слово implements.
В качестве примера рассмотрим ситуацию, когда у нас есть коробка
для хранения вещей и шкаф. Оба этих объекта представляют собой систему

257
хранения. Мы можем добавлять в них вещи, забирать последнюю
добавленную вещи и считать общую сумму веса тех вещей, которые в них
хранятся. Так как это объекты из различной предметной области, то они не
будут наследоваться от базового абстрактного класса СистемаХранения, но
будут реализовывать его интерфейс:
// ex4_34.dart
class Item{
final String name;
final double weight;

Item(this.name, this.weight) ;
}

abstract class StorageSystem {


void addItem(Item item);

Item popItem();

double systemWeight();
}

class Box implements StorageSystem {


var itemsList = <Item>[];
final double weightLimit;

Box(this.weightLimit);

@override
void addItem(Item item) {
var currentSystemWeight = systemWeight();
if((currentSystemWeight+item.weight) < weightLimit){
itemsList.add(item);
print('${item.name} добалнен(о/а) в коробку!');
}
else{
print('${item.name} не помещается в коробку!');
}
}

@override
Item popItem() {
return itemsList.removeLast();
}

@override
double systemWeight() {

258
var sum = 0.0;
for (var element in itemsList) {
sum += element.weight;
}
return sum;
}
// методы, характерные для коробки
}

class Cupboard implements StorageSystem {


var itemsList = <Item>[];

@override
void addItem(Item item) {
itemsList.add(item);
print('${item.name} добалнен(о/а) в шкаф!');
}

@override
Item popItem() {
return itemsList.removeLast();
}

@override
double systemWeight() {
var sum = 0.0;
for (var element in itemsList) {
sum += element.weight;
}
return sum;
}
// методы, характерные для шкафа
}

void main(List<String> arguments) {


var box = Box(18);
var cupboard = Cupboard();
StorageSystem? storageSystem = box;
storageSystem.addItem(Item('Книга', 2.6));
storageSystem.addItem(Item('Чайник', 3.9));
storageSystem.addItem(Item('Гантеля', 10));
storageSystem.addItem(Item('Монитор', 4));

print(storageSystem.popItem().name);
print(storageSystem.systemWeight());

storageSystem = cupboard;
print(storageSystem.systemWeight());

259
storageSystem.addItem(Item('Монитор', 4));
storageSystem.addItem(Item('Чайник', 3.9));
print(storageSystem.systemWeight());
}
/* Книга добалнен(о/а) в коробку!
Чайник добалнен(о/а) в коробку!
Гантеля добалнен(о/а) в коробку!
Монитор не помещается в коробку!
Гантеля
6.5
0.0
Монитор добалнен(о/а) в шкаф!
Чайник добалнен(о/а) в шкаф!
7.9 */

Как видно из примера, посредством приведения к интерфейсу мы


можем работать с любой системой хранения, которая реализует этот
интерфейс и нам не надо прописывать в функции main код для добавления,
изъятия или подсчета веса хранимых вещей для каждого из классов,
реализующих этот интерфейс. Достаточно привести экземпляр класса к
интерфейсу и работать через него. Таким образом мы можем достаточно
просто переключаться между шкафом или коробкой, и если добавится еще
один объект, реализующий интерфейс системы хранения, не возникнет
никаких трудностей при взаимодействии с ним из клиентского кода.
В качестве интерфейса не обязательно использовать абстрактный
класс, им может выступать и обычный:
// ex4_35.dart
class Person {
final String _name;
int age;
Person(this._name, this.age);

int howMuchOlder(Person person) => age - person.age;


String greet(Person person){
return 'Привет, ${person._name}!!!. Меня зовут $_name.';
}
}

class Alan implements Person{


@override
int age = 33;

@override
String get _name => 'Алан';

@override

260
String greet(Person person) {
return 'Привет, ${person._name}!!! Меня зовут $_name.';
}

@override
int howMuchOlder(Person person) {
return age*2 - person.age;
}
}

class Impostor implements Person {


@override
int age = 0;

@override
String get _name => '';

@override
String greet(Person person) {
return 'Вот ты и попался, ${person._name}!!!';
}

@override
int howMuchOlder(Person person) {
return -1;
}
}

String greet(Person firstperson, Person secondPerson){


return firstperson.greet(secondPerson);
}

int checkAge(Person firstperson, Person secondPerson){


return firstperson.howMuchOlder(secondPerson);
}

void main(List<String> arguments) {


var maxim = Person('Макс', 45);
var alan = Alan();
var impostor = Impostor();

print(greet(maxim, alan)); // Привет, Алан!!!. Меня зовут Макс.


print(greet(impostor, maxim)); // Вот ты и попался, Макс!!!
print(checkAge(maxim, alan)); // 12
print(checkAge(alan, maxim)); // 22
}

261
4.9. Продвижение приватных полей (Private field
promotion)
Данный механизм для работы с приватными null-safety полями
класса был добавлен в Dart 3.2, что позволило после проверки поля на null
не использовать символ восклицательного знака для присваивания его
значения не null-safety переменной, либо при обращении к значению
этого поля. Для начала посмотрим, как к с такими полями работали раньше:
// ex4_36.dart
class MyClass {
final int? _privateField;
MyClass(this._privateField);

void someMethod1() {
if (_privateField != null) {
int i = _privateField!; // OK
// int j = _privateField; // A value of type 'int?' can't
// be assigned to a variable of type 'int'.
}
}

void someMethod2() {
if (_privateField is int) {
int i = _privateField!; // OK
}
}
}

Начиная с Dart 3.2, после проверки на null не нужно использовать «!»:


// ex4_37.dart
class MyClass {
final int? _privateField;
MyClass(this._privateField);

void someMethod1() {
if (_privateField != null) {
int i = _privateField; // OK
}
}

void someMethod2() {
if (_privateField is int) {
int i = _privateField; // OK
}
}
}

262
У механизма продвижения приватных полей имеется ряд исключений.
Например, он не будет работать в следующих случаях (тут придется по
старинке):
− поле не обозначено как final, т.к. такие поля могут менять свое
значение в процессе работы приложения;
− поле переопределено геттером или не final полем;
− поле не является приватным, т.к. такие поля могут быть
переопределены в другом месте приложения;
− у поля такое же имя, как и у геттера или не final поля в другом
несвязанном классе библиотеки;
− в библиотеке есть любой класс, интерфейс которого содержит
объявление геттера с тем же именем, но у него нет его реализации.

4.10. Модификаторы класса


До Dart 3 у класса был всего один модификатор – abstract. Его
использовали как для объявления базового абстрактного класса, так и
интерфейса. Но этого разработчикам языка программирования показалось
мало и они добавили еще 5. Аргументировалось это желанием поджечь
пердаки подготовкой к будущему обновлению Dart и стабилизацией API
библиотек. А поскольку библиотекой в этом языке программирования
считается каждый импортируемый файл, повеселились они знатно…
Ниже приведен список из существующих на данный момент
модификаторов классов:
− abstract
− base
− interface
− final
− sealed
− mixin (как ключевое слово был в Dart, но не как модификатор)
Что важно знать про них знать? Модификаторы накладывают
ограничения только за пределами библиотеки (файла с расширением
«.dart»), тогда как в рамках самой библиотеки (файла) программист может
делать все что угодно.
И чтобы все не было настолько просто, разработчики Dart, решили
подкинуть дров разрешили комбинировать модификаторы и представили
сообществу следующую таблицу:

263
Таблица 4.1
Комбинация модификаторов класса
Объявление Construct Extend? Implement? Mix in? Exhaustive?
?
class Yes Yes Yes No No
base class Yes Yes No No No
interface class Yes No Yes No No
final class Yes No No No No
sealed class No No No No Yes
abstract class No Yes Yes No No
abstract base No Yes No No No
class
abstract No No Yes No No
interface class
abstract final No No No No No
class
mixin class Yes Yes Yes Yes No
base mixin Yes Yes No Yes No
class
abstract mixin No Yes Yes Yes No
class
abstract base No Yes No Yes No
mixin class
mixin No No Yes Yes No
base mixin No No No Yes No

В общей сложности 15 комбинаций стрельбы в ногу! Не сказать, чтобы


все обрадовались такому стечению обстоятельств. Но, коль, с этим ничего
уже не поделать, придется страдать смириться и хотя бы иметь
представление, какой модификатор за что отвечает.
Прежде чем перейдем к рассмотрению самих модификаторов, давайте
разберемся со столбцами таблицы. Столбец Construct сигнализирует нам о
том, можно ли создать экземпляр класса, используя простой конструктор
(не фабричный). Extend – можно ли от класса с такой комбинацией
модификаторов наследоваться. Implement – поддерживает ли класс его
использование в качестве интерфейса (вспоминаем пример с Impostor).
Mix in – можно ли создавать примеси (миксины, mixin) на основе
класса. Exhaustive (исчерпываимость) – поддерживает ли класс создание
перечисляемого набора подтипов.

4.10.1. Отсутствие модификатора


Отсутствие модификаторов не накладывает на вас, при импорте файла
с классом, никакие ограничения, кроме одного – запрет на использование

264
класса для создания примесей. Также его нельзя использовать для передачи
в конструкцию switch-case, т.е. такие классы не обладают
исчерпываимостью. Но это сложно назвать ограничением.
Когда такой класс импортируется из библиотеки (одного файла в
другой), при наследовании нет доступа к приватным полям и методам
базового класса. Для начала давайте рассмотрим пример, когда базовый и
производный класс размещены в одной библиотеке:
// ex4_37.dart
class User{
final String name;
int _age;
final int id;

User(this.name, this._age, this.id);

String _privateHello(){
return 'Private Hello!';
}

String publicHello(){
return 'Public Hello!';
}
}

class Moderator extends User{


Moderator(super.name, super.age, super.id);

@override
String publicHello(){
return 'Moderator ${super._privateHello()}';
}

@override
String toString() {
return 'Moderator($name, $_age, $id)';
}
}

void main() {
var user = User('Alex', 22, 1);
var moderator = Moderator('Max', 32, 5);

print(user.publicHello()); // Public Hello!


print(moderator.publicHello()); // Moderator Private Hello!
print(moderator); // Moderator(Max, 32, 5)

print(user._privateHello()); // Private Hello!

265
print(user._age); // 22
}

В данном случае производный класс имеет доступ ко всей реализации


базового класса. Даже из функции main мы можем напрямую обратиться к
приватным полям и методам класса.
Теперь выделим базовый класс в отдельную библиотеку с именем
«lib_a.dart», импортируем ее и получим ошибку при попытке обратиться
к приватному полю и методу базового класса из производного:
// ex4_38.dart
import 'lib_a.dart';

class Moderator extends User{


Moderator(super.name, super.age, super.id);

@override
String publicHello(){
return 'Moderator ${super._privateHello()}';
// Error: Superclass has no method named '_privateHello'.
}

@override
String toString() {
return 'Moderator($name, $_age, $id)';
// Error: The getter '_age' isn't defined
// for the class 'Moderator'.
}
}

void main() {
var user = User('Alex', 22, 1);
var moderator = Moderator('Max', 32, 5);

print(user.publicHello());
print(moderator.publicHello());
}

При использовании такого класса, как интерфейсного через


объявление ключевого слова implements вместо extends будут действовать
те же самые правила. Когда все находится в одной библиотеке придется
реализовывать как публичные поля и методы, так и приватные. А в случае
разбиения кода по нескольким библиотекам – только публичные.
Но мы же с вами практикуем «уличную магию» Поэтому нет ничего
не возможного! Давайте добавим в библиотеку «lib_a.dart» пару
публичных функций и пошлем куда подальше имеющиеся ограничения:

266
// ex4_39 - lib_a.dart
class User{
// без изменений
}

int userAge(User user){


return user._age;
}

String userPrivateHello(User user){


return user._privateHello();
}

// ex4_39 - main.dart
import 'lib_a.dart';

class Moderator extends User{


Moderator(super.name, super.age, super.id);

@override
String publicHello(){
return 'Moderator ${userPrivateHello(this)}';
}

@override
String toString() {
return 'Moderator($name, ${userAge(this)}, $id)';
}
}

void main() {
var user = User('Alex', 22, 1);
var moderator = Moderator('Max', 32, 5);

print(user.publicHello()); // Public Hello!


print(moderator.publicHello()); // Moderator Private Hello!
print(moderator); // Moderator(Max, 32, 5)
}

4.10.2. Модификатор abstract


В данном случае нет ничего нового, относительно того, что мы уже
рассмотрели. Экземпляры абстрактного класса не могут быть созданы, но
можно использовать фабричный конструктор, который создаст в своем
теле производный класс, приведет к базовому и вернет в клиентский код.

267
Как и при отсутствии модификатора, такие классы запрещено
использовать для создания примесей, и они не обладают
исчерпываимостью.
На абстрактный класс также распространяются правила,
рассмотренные ранее, в одном файле имеется доступ к приватным полям и
методам абстрактного базового класса, при импортировании – нет. Ну, и
обходятся эти ограничения аналогичным образом…
Используйте данный модификатор, если хотите отдать на откуп
производным классам реализацию ряда методов базового класса.

4.10.3. Модификатор base


Возможность использовать простой класс в качестве интерфейсного –
бич Dart, т.к. можно наворотить такого, что девиз программистов,
работающих по методологии Panic-Driven Development (разработка через
панику): «Трешь, угар и содомия!», покажется детской забавой, не
достойной внимания истинных сэров от мира IT.
Для того, чтобы исправить эту ситуацию, хотя бы на уровне
импортирования, и был введен модификатор base. По свой сути, объявляя
класс базовым, вы гарантируете, что его нельзя использовать в качестве
интерфейсного и переопределять его методы. То есть от него можно
наследоваться, к нему можно приводить производные классы, его можно
расширять, но никак не реализовывать «заглушку» с использованием
ключевого слова implements и переопределять методы базового класса (за
исключением тех, которые имеются у класса Object – toString т.д.). Тем
самым, у класса с модификатором base не нарушается свойство
транзитивности, как в других языках программирования (Java, C# и т.д.).
Для примера рассмотрим, как можно нарушить транзитивность класса,
когда мы не объявляем модификаторов:
// ex4_40.dart
class User{
final String name;
final int id;

User(this.name, this.id){
print('User created');
}

String publicHello(){
return 'Public Hello!';
}

@override
String toString() {

268
return 'User($name, $id)';
}
}

class Moderator implements User{


@override
int get id => 1;

@override
String get name => 'Max';

@override
String publicHello() {
return 'Moderator Public Hello!';
}

@override
String toString() {
return 'Moderator($name, $id)';
}
}

void main() {
var user = User('Alex', 22); // User created
var moderator = Moderator();

print(user.publicHello()); // Public Hello!


print(moderator.publicHello()); // Moderator Private Hello!
print(moderator); // Moderator(Max, 1)
}

Создание экземпляра класса Moderator не приводит к созданию


экземпляра класса User, что нарушает как его транзитивность, так и
потенциальных производных классов от User.
Ну и раз мы исследуем дно, на возможность падения еще ниже, то
давайте, в рамках одной библиотеки, используем модификатор base для
класса User и Moderator:
// ex4_41.dart
base class User{
// ничего не изменяли
}

base class Moderator implements User{


// ничего не изменяли
}

void main() {

269
var user = User('Alex', 22); // User created
var moderator = Moderator();
print(user.publicHello()); // Public Hello!
print(moderator.publicHello()); // Moderator Private Hello!
print(moderator); // Moderator(Max, 1)
}

Вы, наверное, спросите: «Как так? Ведь модификатор base запрещает


использование ключевого слова implements!!!» И отчасти будете правы.
Почему отчасти? Да потому, что он начинает работать при
импортировании, а поскольку весь код примера написан в одной
библиотеке, мы способны с ним творить различные непотребства…
Теперь вынесем код класса User в отдельную библиотеку
«lib_a.dart», импортируем его и запустим:
// ex4_42 - lib_a.dart
base class User{
// ничего не изменяли
}

// ex4_42 - main.dart
base class Moderator implements User{
// ничего не изменяли
}

void main() {
var user = User('Alex', 22);
var moderator = Moderator();
}
// Error: The class 'User' can't be implemented outside
// of its library because it's a base class.

Вот теперь модификатор base показал себя во всей красе, запретив


использовать класс User в качестве интерфейсного! Давайте перепишем
класс Moderator, таким образом, чтобы он наследовался от User:
// ex4_43 - lib_a.dart
base class User{
// ничего не изменяли
}

// ex4_43 - main.dart
import 'lib_a.dart';

base class Moderator extends User{


Moderator(super.name, super.id){
print('Moderator created');
}

270
@override
String toString() {
return 'Moderator($name, $id)';
}
}

void main() {
var user = User('Alex', 22); // User created
var moderator = Moderator('Max', 1);
// User created Moderator created

print(user.publicHello()); // Public Hello!


print(moderator); // Moderator(Max, 1)
}

Поскольку все классы негласно наследуются от Object, то


переопределение toString не приводит к ошибке и программа завершится
корректно. А если мы попытаемся переопределить метод publicHello
базового класса User в Moderator, то в момент его вызова у экземпляра
класса Moderator, приложение экстренно завершится:
// ex4_44 - main.dart
import 'lib_a.dart';

base class Moderator extends User{


Moderator(super.name, super.id){
print('Moderator created');
}

@override
String publicHello(){
return 'Moderator ${publicHello()}';
}

@override
String toString() {
return 'Moderator($name, $id)';
}
}

void main() {
var user = User('Alex', 22); // User created
var moderator = Moderator('Max', 1); // User created Moderator
created

271
print(user.publicHello()); // Public Hello!
print(moderator.publicHello()); // <- здесь будет падение
print(moderator); //
}
/* ===== CRASH =====
ExceptionCode=-1073741819, ExceptionFlags=0,
ExceptionAddress=00007FF76BC2697E
version=3.1.3 (stable) (Tue Sep 26 14:25:13 2023 +0000) on
"windows_x64"
pid=2544, thread=8312, isolate_group=main(000002B503D6D910),
isolate=main(000002B503D8CD70)
os=windows, arch=x64, comp=no, sim=no
isolate_instructions=7ff76baf6ca0, vm_instructions=7ff76baf6cb0
fp=0, sp=f3a24fefb0, pc=7ff76bc2697e
Stack dump aborted because GetAndValidateThreadStackBounds failed.
pc 0x00007ff76bc2697e fp 0x0000000000000000
Dart_IsPrecompiledRuntime+0x1451fe */

Так как мы переопределили метод базового класса, то поменялось


поведение и нарушилась транзитивность. Этим и вызвана ошибка времени
выполнения при попытке обратиться к такому методу.
Немного перепишем код, убрав переопределение и расширив
функционал производного класса:
// ex4_45 - main.dart
import 'lib_a.dart';

base class Moderator extends User{


final int levelAccess;

Moderator(super.name, super.id, this.levelAccess){


print('Moderator created');
}

String moderPublicHello(){
return 'Moderator ${publicHello()}';
}

@override
String toString() {
return 'Moderator($name, $id, $levelAccess)';
}
}

void main() {
var user = User('Alex', 22); // User created
var moderator = Moderator('Max', 1, 0);

272
// User created Moderator created

print(user.publicHello()); // Public Hello!


print(moderator.publicHello()); // Public Hello!
print(moderator.moderPublicHello()); // Moderator Public Hello!
print(moderator); // Moderator(Max, 1, 0)

// доступны только методы класса User


User newUser = moderator;
print(newUser); // Moderator(Max, 1, 0),
// т.к. переопределяли метод Object
}

Производным же классом, от помеченного модификатором base,


может выступать класс с таким же модификатором, либо модификатором
final или sealed.

4.10.4. Модификатор interface и abstract interface


В Dart 2 для объявления интерфейса использовался абстрактный класс
с чисто виртуальными методами. Это позволяло программистам как
наследоваться от него, так и указывать, что описываемый класс реализует
такой-то интерфейс. В связи с этим, приходилось более подробно изучать
код используемой библиотеки, поскольку язык не предоставлял
возможности понять, с каким из подходов осуществляется работа. Иногда
на помощь разработчикам приходил опыт использования интерфейсов из
других языков программирования, где первой буковой в названии
объявляемого интерфейса выступала «I».
Модификатор класса interface решает эту проблему, запрещая
наследоваться от класса, делая возможным только реализацию интерфейса.
Но есть в этой бочке меда и полбочки ложка дегтя… Про возможность
наследования в файле объявления, думаю и так понятно, но из покон веков
в программирование было правило – нельзя создать экземпляр
интерфейса, т. к. это может приводить к очень плачевным последствиям.
Например, передачу в какой-нибудь метод или конструктор экземпляр
интерфейса, а не объекта, приведенного к интерфейсу.
Прежде чем перейдем к этюдам уличной магии, стоит отметить, что
все методы объявляемого интерфейса рассматриваются как методы по
умолчанию и должны содержать либо реализацию, либо пустое тело
метода. При этом они обязательно должны быть переопределены в классе,
реализующем интерфейс.
Для начала рассмотрим то, как делать не надо… Объявим в одном
файле интерфейсный класс, от наследуемся от него, импортируем эту

273
библиотеку в другой файл и реализуем наследование от класса, который
наследуется от интерфейса:
// ex4_46 - lib_a.dart
interface class Money {
late final int _val;

Money._(this._val);

Money.fromInt(int value) : this._(value);

Money.fromString(String value)
: this._(
(double.parse(value) * 100).toInt(),
);

double value() {
return _val / 100;
}

Money operator +(Money other) {


return Money._(_val + other._val);
}

Money operator -(Money other) {


var temp = 0;
if (_val - other._val >= 0) {
temp = _val - other._val;
} else {
print("(╯'□')╯︵ ┻━┻ Банкрот!!!");
}
return Money._(temp);
}

@override
String toString() {
var money = (_val / 100).toStringAsFixed(2);
return 'Money($money)';
}
}

class Rub extends Money {


Rub(String value) : super.fromString(value);

@override
Money operator +(Money other) {
return Money._(_val + other._val);
}

274
}

// ex4_46 - main.dart
import 'lib_a.dart';

class MyMoney extends Rub{


MyMoney(super.value);

@override
String toString() {
return 'MyMoney(${value()})';
}
}

void main() {
var money = Money.fromInt(100);
print(money); // Money(1.00)

var myMoney = MyMoney('100');


print(myMoney); // MyMoney(100.0)
}

Стул еще не пригорает от того, какие непотребства разрешает творить


Dart?) Но в тоже самое время стоит отдать ему должное. Если изменить код
файла «main.dart», указав в нем, что теперь наследуемся от интерфейсного
класса Money, то в процессе сборки приложения выведется следующая
ошибка:
import 'lib_a.dart';

class MyMoney extends Money{


MyMoney(super.value);
}
// Error: The class 'Money' can't be extended outside
// of its library because it's an interface class.

В приведенном примере код имеет один очень серьезный недочет.


Интерфейсный класс не должен иметь конструктора, его главная задача
описать рамки поведения классов, которые его будут реализовывать, не
привнося в них лишние свойства. В то время как мы использовали
интерфейсный класс в качестве обычного. Давайте это исправим:
// ex4_47 - lib_a.dart
interface class IMoney {
int get value => 0;

IMoney operator +(IMoney other) => IMoney();

275
IMoney operator -(IMoney other) {
return IMoney();
}

@override
String toString() {
var money = (value / 100).toStringAsFixed(2);
return 'Money($money)';
}
}

// ex4_47 - main.dart
import 'lib_a.dart';

class Rub implements IMoney {


late final int _kopek;

Rub._(this._kopek);

factory Rub(String rub) {


var localRub = (double.parse(rub) * 100).toStringAsFixed(0);
return Rub._(int.parse(localRub));
}

@override
IMoney operator +(IMoney other) {
if (other is Rub) {
return Rub._(_kopek + other.value);
}
return Rub._(0);
}

@override
IMoney operator -(IMoney other) {
if (other is Rub) {
var temp = 0;
if (_kopek - other.value >= 0) {
temp = _kopek - other.value;
} else {
print("(╯'□')╯︵ ┻━┻ Банкрот!!!");
}
return Rub._(temp);
}
return Rub._(0);
}

@override
int get value => _kopek;

276
@override
String toString() {
var rub = (_kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}
}

void main() {
var money = IMoney();
print(money); // Money(0.00)

IMoney rub = Rub('100');


print(rub - Rub('4.6')); // Rub(95.40)
print(rub + IMoney()); // Rub(0.00)
}

Возможность создавать экземпляры интерфейсных классов призвана


помочь более простым образом реализовывать в коде разрабатываемого
приложения заглушки на ту часть функционала, которая еще не готова,
либо для его тестирования.
Давайте представим ситуацию, что у нас имеется кошелек и в нем
хранятся банкноты только двух валют – рубли и доллары. Наша задача
сделать таким образом, чтобы при взаимодействии с классом кошелька
можно было рассчитать всю хранящуюся сумму в долларах или рублях. Эти
валюты запрещено хранить в отдельных списках, поэтому будем их
приводить к общему интерфейсу и уже эти объекты помещать в кошелек.
Внесите в файл «main.dart» следующие изменения:
// ex4_48 - main.dart
import 'lib_a.dart';

class Rub implements IMoney {


late final int _kopek;

Rub._(this._kopek);
Rub.fromInt(int value) : this._(value);

factory Rub(String rub) {


var localRub = (double.parse(rub) * 100).toStringAsFixed(0);
return Rub._(int.parse(localRub));
}

@override
Rub operator +(IMoney other) {
return Rub._(_kopek + other.value);
}

277
@override
int get value => _kopek;

@override
String toString() {
var rub = (_kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}

@override
IMoney operator -(IMoney other) {
// TODO: implement -
throw UnimplementedError();
}
}

class USD implements IMoney {


late final int _cent;

USD._(this._cent);
USD.fromInt(int value) : this._(value);

factory USD(String usd) {


var localCent = (double.parse(usd) * 100).toStringAsFixed(0);
return USD._(int.parse(localCent));
}

@override
USD operator +(IMoney other) {
if (other is USD) {
return USD._(_cent + other.value);
}
return USD._(0);
}

@override
int get value => _cent;

@override
String toString() {
var usd = (_cent / 100).toStringAsFixed(2);
return 'USD($usd)';
}

@override
IMoney operator -(IMoney other) {
// TODO: implement -

278
throw UnimplementedError();
}
}

class Wallet {
var money = <IMoney>[];
final int rub2usd;

Wallet(this.rub2usd);

void addMoney(IMoney money) {


this.money.add(money);
}

void removeMoney(IMoney money) {


this.money.removeWhere(
(element) => element.value == money.value,
);
}

Rub allConverToRub() {
var rub = Rub('0');
for (var it in money) {
if (it is Rub) {
rub += it;
}else if (it is USD){
rub += Rub.fromInt(it.value * rub2usd);
}
}
return rub;
}

USD allConverToUsd() {
var usd = USD('0');
for (var it in money) {
if (it is USD) {
usd += it;
}else if (it is Rub){
usd += USD.fromInt((it.value / rub2usd).ceil());
}
}
return usd;
}

@override
String toString() {
var rub = Rub('0');
var usd = USD('0');

279
for (var it in money) {
if (it is Rub) {
rub += it;
} else if (it is USD) {
usd += it;
}
}
return 'Wallet($rub, $usd)';
}
}

void main() {
var wallet = Wallet(100);
wallet.addMoney(Rub('1000'));
wallet.addMoney(USD('100'));
wallet.addMoney(Rub('43'));
wallet.addMoney(USD('21'));

print(wallet); // Wallet(Rub(1043.00), USD(121.00))

print(wallet.allConverToRub()); // Rub(13143.00)
print(wallet.allConverToUsd()); // USD(131.43)
}

Конечно, с деньгами так работать нельзя, но сам пример хорошо


демонстрирует принцип работы через интерфейс с объектами коллекции.
К тому же, обратите внимание на тот факт, что в интерфейсном классе
оператор сложения перегружается следующим образом:
IMoney operator +(IMoney other) => IMoney();

А в реализующих его классах:


@override
Rub operator +(IMoney other) {
return Rub._(_kopek + other.value);
}

Такое свойство, когда вместо базового (более общего) типа T мы


можем подставлять его производный G, в программировании называется
ковариантность. Оно работает как с наследованием, так и с интерфейсами.
Иначе вместо строк, типа:
rub += it;
пришлось бы писать так:
rub = (rub + it) as Rub;

Когда же вместо конкретного типа G мы можем подставить более


общий T – это контрвариантность. Ее часто используют, приводя

280
производный класс к базовому или интерфейсу при передаче в конструктор
или метод какого-нибудь объекта. Если вы уже читали про SOLID, то вот вам
ответ на то, на каких свойствах строится принцип подстановки Барбары-
Лисков .
Что касается модификатора класса abstract interface, то он
позволяет, используя виртуальные методы, более лаконичным образом
описывать интерфейс и запрещает создавать экземпляры интерфейсного
класса (ну, разве что не через фабричный конструктор):
abstract class IMoney {
int get value;
IMoney operator +(IMoney other);
IMoney operator -(IMoney other);
}

4.10.5. Модификатор final


Данный модификатор следует объявлять перед классом, если вы
хотите запретить наследоваться от него после импортирования файла или
использовать в качестве интерфейсного. Само собой, данные ограничения
не будут действовать в рамках одной библиотеки, что способно привести к
неприятным последствиям, наподобие рассмотренных ранее.
По заявлениям разработчиков Dart, цель модификатора final –
стабилизация API пакета или библиотеки. Он призван помочь добавлять в
них точечные изменения, без возможности создания производных или
классов заглушек, где можно переопределять поведение.

4.10.6. Модификатор sealed


Sealed (запечатанные) классы не являются каким-то ноу-хау Dart, они
поддерживаются различными языками программирования. Например, C#
и Kotlin. От классов с таким модификатором нельзя наследоваться за
пределами его библиотеки (файла) или использовать их как интерфейсные.
Еще одной чертой sealed-классов является запрет на создание их
экземпляров. То есть экземпляр такого класса может быть создан только с
использованием фабричного конструктора.
Поскольку класс, объявленный с модификатором sealed, считается
исчерпывающим, то экземпляры производных от него классов можно
использовать в конструкции switch-case. Поэтому производные классы
должны быть определены в одной библиотеке с базовым. Данное
ограничение позволяет Dart знать о всех производных классах,
наследующихся от sealed-класса и требовать их присутствие в switch-
case.

281
Сначала мы рассмотрим простые варианты реализации sealed-
классов, а только потом перейдем к примерам с щепоткой «уличной
магии». Давайте вернемся к концепции кошелька и первым делом объявим
sealed-класс Money и его производные классы:
sealed class Money {}
class RUB extends Money {}
class USD extends Money {}
class EUR extends Money {}

Если попытаться привычным образом создать экземпляр класса Money,


Dart не даст это сделать:
void main() {
var money = Money();
}
// Error: The class 'Money' is abstract
// and can't be instantiated.
Но это не распространяется на производные классы:
void main() {
var money = RUB();
print(money); // Instance of 'RUB'
}
Теперь попробуем добавить конструкцию switch-case и вывести в
терминал тип входной валюты:
void main() {
Money money = RUB();
switch (money) {
case RUB():
print('RUB');
case USD():
print('USD');
}
}
// Error: The type 'Money' is not exhaustively matched
// by the switch cases since it doesn't match 'EUR()'

Так как мы не указали все производные классы от Money, это привело


к ошибке времени компиляции. Для начала исправим этот недочет:
// ex4_49.dart
void main() {
Money money = RUB();
switch (money) {
case RUB():
print('RUB'); // RUB
case USD():
print('USD');
case EUR():

282
print('EUR');
}
}

Ну, что ж… начнем потихоньку подтаскивать тяжелую артиллерию. По


своей сути sealed-класс ничем не отличается от абстрактного, что
позволяет в нем определять поля, методы или виртуальные методы,
которые будут переопределяться в производных классах. С его изменения
мы и начнем:
// ex4_50.dart
sealed class Money {
late final int _val;

Money(this._val);

int get value => _val;


Money operator +(Money other);
}

Далее перепишем класс RUB, добавив в него конструктор,


переопределение сложения и метода toString:
// ex4_50.dart
class RUB extends Money {
RUB(super.val);

factory RUB.fromStr(String value){


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return RUB(int.parse(rub));
}

@override
RUB operator +(Money other) {
return RUB(value + other.value);
}

@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
return 'RUB($rub)';
}
}

При переопределении сложения класса USD учтем, что он может


складываться с рублями, поэтому захардкодим конвертацию номинала
этой валюты в долларовый эквивалент:

283
// ex4_50.dart
class USD extends Money {
USD(super.val);
factory USD.fromStr(String value){
var usd = (double.parse(value) * 100).toStringAsFixed(0);
return USD(int.parse(usd));
}

@override
USD operator +(Money other) {
if (other is RUB) {
return USD(value + other.value ~/ 100);
}
else{
return USD(value + other.value);
}
}

@override
String toString() {
var usd = (value / 100).toStringAsFixed(2);
return 'USD($usd)';
}
}

Класс EUR, по своей сути, не будет отличаться от RUB:


// ex4_50.dart
class EUR extends Money {
EUR(super.val);
factory EUR.fromStr(String value){
var eur = (double.parse(value) * 100).toStringAsFixed(0);
return EUR(int.parse(eur));
}

@override
EUR operator +(Money other) {
return EUR(value + other.value);
}

@override
String toString() {
var eur = (value / 100).toStringAsFixed(2);
return 'EUR($eur)';
}
}

284
На очереди у нас написание функции, которая будет принимать на
вход запись (record), состоящую из двух экземпляров производных классов,
приведенных к базовому и осуществляющую их сложение в зависимости от
того, какая пара валют была ей передана:
// ex4_50.dart
void addMoney((Money, Money) money) {
switch (money) {
case (RUB(), RUB()):
print(money.$1 + money.$2);
case (USD(), USD()):
print(money.$1 + money.$2);
case (EUR(), EUR()):
print(money.$1 + money.$2);
case (RUB(), USD()):
print(money.$2 + money.$1);
case _:
print('Разные валюты');
}
}

Осталось только объявить функцию main и запустить приложение:


// ex4_50.dart
void main() {
addMoney((RUB.fromStr('200'), RUB.fromStr('100')));
addMoney((USD.fromStr('200'), USD.fromStr('100')));
addMoney((EUR.fromStr('200'), EUR.fromStr('100')));
addMoney((RUB.fromStr('2000'), USD.fromStr('10')));
addMoney((EUR.fromStr('200'), RUB.fromStr('100')));
}
// RUB(300.00)
// USD(300.00)
// EUR(300.00)
// USD(30.00)
// Разные валюты

Теперь добавим в базовый sealed-класс Money фабричный


конструктор и перепишем тело функции main:
// ex4_51.dart
sealed class Money {
late final int _val;

Money(this._val);
factory Money.fromStr(String currency, String value) {
var money = (double.parse(value) * 100).toStringAsFixed(0);
switch (currency.toLowerCase()) {
case 'rub':
return RUB(int.parse(money));

285
case 'usd':
return USD(int.parse(money));
case 'eur':
return EUR(int.parse(money));
};
throw UnsupportedError('Неподдерживаемая валюта');
}

int get value => _val;


Money operator +(Money other);
}

// остальной код без изменений

void main() {
addMoney((
Money.fromStr('rub', '200'),
Money.fromStr('rub', '100'),
)); // RUB(300.00)
addMoney((
Money.fromStr('usd', '200'),
Money.fromStr('usd', '100'),
)); // USD(300.00)
addMoney((
Money.fromStr('eur', '200'),
Money.fromStr('eur', '100'),
)); // EUR(300.00)
addMoney((
Money.fromStr('rub', '2000'),
Money.fromStr('usd', '10'),
)); // USD(30.00)
addMoney((
Money.fromStr('eur', '200'),
Money.fromStr('rub', '100'),
)); // Разные валюты
}
Далее ничего не мешает нам переписать функцию addMoney, добавив
туда условие, что значение первого или второго аргумента слагаемого (а
может и обоих) было >= определенного значения (не забывая, что значения
хранятся в минимальной единице валюты, т.е. для рубля – копейки, для
доллара – цент и т.д.):
// ex4_52.dart
void addMoney((Money, Money) money) {
switch (money) {
case (RUB(value: > 30000), RUB(value: <= 500)):
print(money.$1 + money.$2);
case (USD(), USD(value: <= 300)):

286
print(money.$1 + money.$2);
case (EUR(value: > 20000), EUR()):
print(money.$1 + money.$2);
case (RUB(value: > 50000), USD()):
print(money.$2 + money.$1);
case _:
print('╭∩╮( •̀_•́ )╭∩╮');
}
}

void main() {
addMoney((
Money.fromStr('rub', '200'), Money.fromStr('rub', '100'),
)); // ╭∩╮( •̀_•́ )╭∩╮
addMoney((
Money.fromStr('usd', '200'),
Money.fromStr('usd', '3'),
)); // USD(203.00)
addMoney((
Money.fromStr('eur', '200'),
Money.fromStr('eur', '100'),
)); // ╭∩╮( •̀_•́ )╭∩╮
addMoney((
Money.fromStr('rub', '2000'),
Money.fromStr('usd', '10'),
)); // USD(30.00)
addMoney((
Money.fromStr('eur', '200'),
Money.fromStr('rub', '100'),
)); // ╭∩╮( •̀_•́ )╭∩╮
}

Прежде чем перейдем к следующему «уличному заклинанию», давайте


остановимся и задумаемся: «А удобно ли поддерживать такой код?». На
самом деле – не очень. Модификатор sealed накладывает на нас
ограничение, заставляя писать код в одном файле. Когда его не много – это
не проблема, но при увеличении количества производных классов и их
функционала, особенно, когда один из производных классов тоже
помечается как sealed и может иметь свою цепочку полноты (производных
классов), такую библиотеку будет сложно поддерживать. А у новичка в
проекте, при первом взгляде на нее, волосы встанут дыбом до такой
степени, что не удобно будет сидеть на стуле… Здесь нам на помощь
приходит еще одна фишка Dart при работе с библиотеками, которая была
придержана до текущего момента – разделять библиотеку по нескольким
файлам, используя ключевое слово «part».

287
Создайте новый консольный проект «sealed_example» со следующей
структурой директорий и файлов:

Рисунок 4.6 – Структура проекта «sealed_example»

В файл «sealed_example.dart» директории «lib» добавьте строчку


экспорта только для «money.dart»:
export 'src/money.dart';

В «money.dart» перенесем класс Money и укажем в качестве приватных


частей для этой библиотеки остальные файлы каталога «src»:
part 'eur.dart';
part 'usd.dart';
part 'rub.dart';

sealed class Money {


late final int _val;

Money(this._val);
factory Money.fromStr(String currency, String value) {
var money = (double.parse(value) * 100).toStringAsFixed(0);
switch (currency.toLowerCase()) {
case 'rub':
return RUB(int.parse(money));
case 'usd':
return USD(int.parse(money));
case 'eur':
return EUR(int.parse(money));
}
throw UnsupportedError('Неподдерживаемая валюта');

288
}

int get value => _val;


Money operator +(Money other);
}

Откройте файл «usd.dart», перенеся туда одноименный класс, указав,


что этот файл является частью «money.dart»:
part of 'money.dart';

class USD extends Money {


USD(super.val);

factory USD.fromStr(String value) {


var usd = (double.parse(value) * 100).toStringAsFixed(0);
return USD(int.parse(usd));
}

@override
USD operator +(Money other) {
if (other is RUB) {
return USD(value + other.value ~/ 100);
} else {
return USD(value + other.value);
}
}

@override
String toString() {
var usd = (value / 100).toStringAsFixed(2);
return 'USD($usd)';
}
}

Проделаем те же самые действия для файла «rub.dart»:


part of 'money.dart';

class RUB extends Money {


RUB(super.val);

factory RUB.fromStr(String value) {


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return RUB(int.parse(rub));
}

@override
RUB operator +(Money other) {

289
return RUB(value + other.value);
}

@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
return 'RUB($rub)';
}
}

И файла «eur.dart» из каталога «src»:


part of 'money.dart';

class EUR extends Money {


EUR(super.val);

factory EUR.fromStr(String value) {


var eur = (double.parse(value) * 100).toStringAsFixed(0);
return EUR(int.parse(eur));
}

@override
EUR operator +(Money other) {
return EUR(value + other.value);
}

@override
String toString() {
var eur = (value / 100).toStringAsFixed(2);
return 'EUR($eur)';
}
}

В «money.dart» мы объявили, что часть его функционала будет


вынесена по другим файлам, которые указали после ключевого слова
«part». А в эти файлы связали с основным через «part of 'money.dart'».
Теперь перейдем в «sealed_example.dart» директории «bin» и
перенесем в него функцию addMoney:
import 'package:sealed_example/sealed_example.dart';

void addMoney((Money, Money) money) {


switch (money) {
case (RUB(value: > 30000), RUB(value: <= 500)):
print(money.$1 + money.$2);
case (USD(), USD(value: <= 300)):
print(money.$1 + money.$2);
case (EUR(value: > 20000), EUR()):

290
print(money.$1 + money.$2);
case (RUB(value: > 50000), USD()):
print(money.$2 + money.$1);
case _:
print('╭∩╮( •̀_•́ )╭∩╮');
}
}

void main(List<String> arguments) {


addMoney(
(Money.fromStr('rub', '200'), Money.fromStr('rub', '100'))
);
addMoney(
(Money.fromStr('usd', '200'), Money.fromStr('usd', '3'))
);
addMoney(
(Money.fromStr('eur', '200'), Money.fromStr('eur', '100'))
);
addMoney(
(Money.fromStr('rub', '2000'), Money.fromStr('usd', '10'))
);
addMoney(
(Money.fromStr('eur', '200'), Money.fromStr('rub', '100'))
);
}

После запуска приложения в терминале должен получиться


следующий результат:
╭∩╮( •̀_•́ )╭∩╮
USD(203.00)
╭∩╮( •̀_•́ )╭∩╮
USD(30.00)
╭∩╮( •̀_•́ )╭∩╮

Такое расслоение одной библиотеки (файла) на несколько частей


позволяет писать более чистый и поддерживаемый код.
Давайте представим, что рубль у нас может быть представлен 2
вариантами: бумажный и цифровой. Чтобы реализовать эту задумку
добавьте в директорию «src» еще несколько файлов: «paper_rub.dart» и
«digital_rub.dart». От исходного класса RUB, его варианты будут
отличаться названием и более широким вариантов конструкторов:

// paper_rub.dart
part of 'money.dart';

class PaperRUB extends RUB {

291
PaperRUB._(super.val);

PaperRUB.rub50() : super(5000);
PaperRUB.rub100() : super(10000);
PaperRUB.rub500() : super(50000);
PaperRUB.rub1000() : super(100000);
PaperRUB.rub5000() : super(500000);

@override
PaperRUB operator +(Money other) {
return PaperRUB._(value + other.value);
}
}

// digital_rub.dart
part of 'money.dart';

class DigitalRUB extends RUB {


DigitalRUB._(super.val);

factory DigitalRUB.fromStr(String value) {


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return DigitalRUB._(int.parse(rub));
}

@override
DigitalRUB operator +(Money other) {
return DigitalRUB._(value + other.value);
}

@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
return 'DigitalRUB($rub)';
}
}

Сам класс RUB перепишем следующим образом:


// rub.dart
part of 'money.dart';

sealed class RUB extends Money {


RUB(super.val);

factory RUB.create(String value, [bool isDigital = true]) {


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return switch (isDigital) {
true => DigitalRUB._(int.parse(rub)),

292
false => switch(value){
'50' => PaperRUB.rub50(),
'100' => PaperRUB.rub100(),
'500' => PaperRUB.rub500(),
'1000' => PaperRUB.rub1000(),
'5000' => PaperRUB.rub5000(),
_ => DigitalRUB._(int.parse(rub))
},
};
}

@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
return 'RUB($rub)';
}
}

Обозначьте созданные файлы частью библиотеки в «money.dart» и


внесите небольшие изменения в процесс создания экземпляра рубля:
part 'eur.dart';
part 'usd.dart';
part 'rub.dart';
part 'paper_rub.dart';
part 'digital_rub.dart';

sealed class Money {


late final int _val;

Money(this._val);
factory Money.fromStr(String currency, String value) {
var money = (double.parse(value) * 100).toStringAsFixed(0);
switch (currency.toLowerCase()) {
case 'rub':
return RUB.create(value);
case 'usd':
return USD(int.parse(money));
case 'eur':
return EUR(int.parse(money));
}
throw UnsupportedError('Неподдерживаемая валюта');
}

int get value => _val;


Money operator +(Money other);
}

293
Если сейчас попробуете запустить приложение, то не увидите никаких
изменений, т.к. в терминале увидите предыдущий результат. Что нам дает
объявление класса RUB как sealed? Мы можем его производные классы
использовать в switch-case как с классами USD и EUR, так и отдельно от них.
Начнем с совместного использования. Откройте файл
«sealed_example.dart» директории «bin» и добавьте в него следующий код:
import 'package:sealed_example/sealed_example.dart';

void wtfIsMoney(Money money) {


switch (money) {
case DigitalRUB():
print('Цифровой рубль - $money');
case PaperRUB():
print('Бумажный рубль - $money');
case USD():
print('Доллар - $money');
case EUR():
print('Евро - $money');
}
}

void main(List<String> arguments) {


wtfIsMoney(RUB.create('98'));
wtfIsMoney(USD.fromStr('3'));
wtfIsMoney(EUR.fromStr('100'));
wtfIsMoney(DigitalRUB.fromStr('1000'));
wtfIsMoney(PaperRUB.rub50());
}
/* Цифровой рубль - DigitalRUB(98.00)
Доллар - USD(3.00)
Евро - EUR(100.00)
Цифровой рубль - DigitalRUB(1000.00)
Бумажный рубль - RUB(50.00) */

А теперь напишем функцию, где в switch-case используются только


рубли:
import 'package:sealed_example/sealed_example.dart';

void rubWorker(RUB rub) {


switch (rub) {
case DigitalRUB(value: > 300000):
print('Цифровой рубль - $rub');
case PaperRUB(value: == 50000):
print('Бумажный рубль - $rub');
case _:
print('Это не правильные рубли ╭∩╮( •̀_•́ )╭∩╮');
}

294
}

void main(List<String> arguments) {


rubWorker(RUB.create('1000', false));
rubWorker(RUB.create('5000'));
rubWorker(PaperRUB.rub500());
rubWorker(PaperRUB.rub5000());
}
/* Это не правильные рубли ╭∩╮( •̀_•́ )╭∩╮
Цифровой рубль - DigitalRUB(5000.00)
Бумажный рубль - RUB(500.00)
Это не правильные рубли ╭∩╮( •̀_•́ )╭∩╮ */

Таким образом, модификатор sealed в основном влияет на то, как


можно использовать базовый тип. Он не накладывает особых ограничений
на производные классы, за исключением того, что они должны быть
определены в одной библиотеке.

4.10.7. Миксины и модификатор класса mixin


Так как в Dart отсутствует множественное наследование, он
предоставляет ряд механизмов по расширению возможностей классов.
Одним из таких и являются mixins (примеси). Они позволяют подмешать к
классу новые методы или поля, без необходимости их переопределения.
Давайте представим, что у нас есть два класса животных, каждый из
которых обладает собственным набором поведения. Самое первое, что
придет на ум – выделить общее состояние и поведение в базовый класс.
Пока в системе два класса животных – все работает отлично, но вот нам
понадобился класс птицы. Казалось бы, у базового класса животных и
птицы много общего, но вот летать животные не умеют, хотя также бегают,
едят, купаются и т. д., у них имеется атрибут возраста и названия вида, к
которому принадлежат. Очевидно, что хочется найти наиболее
эффективную структуру для написания этих классов с максимальной
возможностью повторного использования кода. Тут на помощь и приходят
примеси, поскольку они позволяют вставлять блоки кода в класс, без
необходимости создания производного класса.
Для начала давайте объявим миксину, которую можно будет
использовать и для класса денежной валюты и для банковской ячейки:
mixin Inflation{
int pecent = 14;

RUB inflation(RUB rub){


return switch(pecent){
!= 0 => RUB.kopek(
rub.value - (rub.value * pecent / 100).ceil(),

295
),
_ => RUB.kopek(rub.value),
};
}
}

Чтобы подмешать объявленную миксину Inflation к классу RUB, после


его объявления используйте ключевое слово with:
// ex4_53.dart
class RUB with Inflation {
int _val;
RUB._(this._val);

factory RUB.fromStr(String value) {


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return RUB._(int.parse(rub));
}

RUB.kopek(this._val);

int get value => _val;

RUB operator +(RUB other) {


return RUB._(value + other.value);
}

@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
return 'RUB($rub)';
}
}

void main() {
RUB rub = RUB.fromStr('100');
print(rub.inflation(rub)); // RUB(86.00)
}

Пока пользоваться миксиной не слишком удобно, но у класса


появилась необходимая функциональность, позволяющая нам понять,
насколько обесценились 100 рублей при инфляции в 14%. Если хотите
регулировать данный процент при создании экземпляра класса рубля, то
это можно сделать через его конструктор (или метод):
class RUB with Inflation {
int _val;
RUB._(this._val, [inflationPecent = 14]){
pecent = inflationPecent; // обновляем значения у поля примеси

296
}

factory RUB.fromStr(String value, [inflationPecent = 14]) {


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return RUB._(int.parse(rub), inflationPecent);
}

RUB.kopek(this._val, [inflationPecent = 14]) {


pecent = inflationPecent;
}
// остальной код не изменился
}

void main() {
RUB rub = RUB.fromStr('100', 55);
print(rub.inflation(rub)); // RUB(45.00)
}

Экземпляр миксины нельзя создать, но к ней можно привести


созданный с ее использованием экземпляр класса рубля. То есть Dart
позволяет использовать их, как интерфейс:
void main() {
// var infl = Inflation(); // error
Inflation infl = RUB.fromStr('100', 55);
print(infl.inflation(RUB.fromStr('100'))); // RUB(45.00)
}

А такое поведение дает нам возможность при объявлении миксины


указать ее методы как чисто виртуальные (либо часть из них), тем самым
возложив обязанность по их реализации на использующий эту миксину
класс:
// ex4_54.dart
mixin Inflation {
int pecent = 14;

RUB inflation([RUB? rub]);


}

class RUB with Inflation {


int _val;
RUB._(this._val, [inflationPecent = 14]) {
pecent = inflationPecent;
}

factory RUB.fromStr(String value, [inflationPecent = 14]) {


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return RUB._(int.parse(rub), inflationPecent);

297
}

RUB.kopek(this._val, [inflationPecent = 14]) {


pecent = inflationPecent;
}

int get value => _val;

RUB operator +(RUB other) {


return RUB._(value + other.value);
}

@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
return 'RUB($rub)';
}

@override
RUB inflation([RUB? rub]) {
if (rub == null) {
return switch (pecent) {
!= 0 => RUB.kopek(value - (value * pecent / 100).ceil()),
_ => RUB.kopek(value),
};
}else{
return switch (pecent) {
!= 0 => RUB.kopek(
rub.value - (rub.value * pecent / 100).ceil()
),
_ => RUB.kopek(rub.value),
};
}
}
}

void main() {
var rub = RUB.fromStr('100', 55);
print(rub.inflation()); // RUB(45.00)
print(rub.inflation(RUB.fromStr('200'))); // RUB(90.00)
}

Таким образом, в классе мы можем переопределять объявленные в


мексине методы, что дает не только больше гибкости, но и проблем.
Особенно, если при переопределении закрадется ошибка в коде.
К тому же надо быть внимательным при наличии в миксинах
одноименных методов, т.к. при их совместном использовании с одним

298
классом, это приведет к затиранию реализации одного из них. Если быть
точнее – метод последней миксины затрет методы предыдущих:
// ex4_55.dart
mixin Inflation {
void inflation(){
print('Inflation');
}
}

mixin GlobalInflation {
void inflation(){
print('GlobalInflation Inflation');
}
}

class RUB with Inflation, GlobalInflation {}


class USD with GlobalInflation, Inflation {}

void main() {
var rub = RUB();
rub.inflation(); // GlobalInflation Inflation

var usd = USD();


usd.inflation(); // Inflation
}

Имеется способ дать объявляемой миксине доступ к полям и методам


класса. Для этого после ее имени используется ключевое слово on с
указанием имени класса. При таком раскладе нам придется от данного
класса создать производный с указанием используемых миксин:
// ex4_56.dart
class RUB {
int _val;
RUB._(this._val);

factory RUB.fromStr(String value) {


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return RUB._(int.parse(rub));
}

RUB.kopek(this._val);

int get value => _val;

@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);

299
return 'RUB($rub)';
}
}

mixin Inflation on RUB {


RUB inflation(int pecent) {
return switch (pecent) {
!= 0 => RUB.kopek(value - (value * pecent / 100).ceil()),
_ => RUB.kopek(value),
};
}
}

mixin YearPecent on RUB {


int pecent = 14;
RUB yearEnd() {
return RUB.kopek(value + (value * pecent / 100).ceil());
}
}

class PaperRUB extends RUB with Inflation {


PaperRUB._(int value) : super.kopek(value);
PaperRUB.kopek(int value) : super.kopek(value);
PaperRUB.rub50() : this._(5000);
PaperRUB.rub100() : this._(10000);
}

class BankBox extends RUB with YearPecent, Inflation {


BankBox._(int value, int bankPecent) : super.kopek(value){
pecent = bankPecent;
}

factory BankBox.fromStr(String value, int bankPecent) {


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return BankBox._(int.parse(rub), bankPecent);
}
}

void main() {
var rub = PaperRUB.rub100();
print(rub); // RUB(100.00)
print(rub.inflation(-13)); // RUB(113.00)
print(rub.inflation(13)); // RUB(87.00)
print(rub.inflation(0)); // RUB(100.00)
var bankBox = BankBox.fromStr('1000000', 23);
print(bankBox); // RUB(1000000.00)
print(bankBox.yearEnd()); // RUB(1230000.00)
}

300
Начиная с Dart 3 обычные классы запрещено использовать в качестве
миксин. Теперь, если вы хотите для этих целей использовать объявляемый
класс, его надо пометить модификатором mixin. Экземпляры таких классов
можно создавать только конструктором по умолчанию (т.е. поля класса
должны быть явно проинициализированы при его объявлении), от них
можно наследоваться или использовать в качестве интерфейсных. Но это,
признаюсь, то еще удовольствие…
К тому же, классы с данным модификатором не могут быть
производными классами и их нельзя использовать с ключевым словом on:
// ex4_57.dart
mixin class Inflation{
int pecent = 14;

// Inflation(this.pecent); // error
// The class 'Inflation' can't be used as a mixin
// because it declares a constructor.

RUB inflation(RUB rub){


return switch(pecent){
!= 0 => RUB.kopek(
rub.value - (rub.value * pecent / 100).ceil(),
),
_ => RUB.kopek(rub.value),
};
}
}

class RUB with Inflation { // или extends Inflation


int _val;
RUB._(this._val, [inflationPecent = 14]){
pecent = inflationPecent;
}

factory RUB.fromStr(String value, [inflationPecent = 14]) {


var rub = (double.parse(value) * 100).toStringAsFixed(0);
return RUB._(int.parse(rub), inflationPecent);
}

RUB.kopek(this._val, [inflationPecent = 14]) {


pecent = inflationPecent;
}

int get value => _val;

@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);

301
return 'RUB($rub)';
}
}

void main() {
var infl = Inflation();
RUB rub = RUB.fromStr('100', 55);
print(rub.inflation(rub)); // RUB(45.00)
print(infl.inflation(rub)); // RUB(86.00)
}

4.11. Generics (Обобщения)


Использование дженериков позволяет уменьшить дублирование кода.
То есть у разработчика появляется возможность использовать единый
интерфейс и реализацию для многих типов. Это часто бывает полезным,
если отличие между создаваемыми классами заключается только в типе
некоторых его переменных, принимаемых на вход типов аргументов
методов или возвращаемых ими значений. Примером самых элементарных
реализаций классов с использованием дженериков являются коллекции:
List<T>, Set<T>, Map<K, V>.
// ex4_58.dart
class MyID {
final int id;
final String idString;

MyID(this.id): idString = id.toString();

@override
String toString() {
return idString;
}
}

class Product<T>{
T id;
final String name;
final double price;

Product(this.id, this.name, this.price);

T getId() => id;


void setId(T idProduct) => id = idProduct;

@override
String toString() {

302
return 'Продукт: $name с id: $id стоит $price тугриков';
}
}

void main(List<String> arguments) {


var product = Product<int>(0, 'Булочка', 33.5);
print(product);
var newProduct = Product<MyID>(MyID(10), 'Пирожок', 50);
print(newProduct);
}
// Продукт: Булочка с id: 0 стоит 33.5 тугриков
// Продукт: Пирожок с id: 10 стоит 50.0 тугриков

Иногда нам нужно задать ограничение при использовании


дженериков. Это делается для того, чтобы не все типы данных могли
указываться программистом для создаваемых экземпляров классов. В
таком случае, после указания дженерика, при объявлении класса,
используйте ключевое слово extends с указанием типа, который будет
выступать в качестве ограничителя. Обычно, в этой роли выступает какой-
то базовый класс:
// ex4_59.dart
class Item{
final String name;
final int value;
Item(this.name, this.value);
}

class Book extends Item{


Book(String name, int pages): super(name, pages);

Box operator +(Book otherBook){


return Box([this, otherBook]);
}
}

class Magazine extends Item{


Magazine(String name, int pages): super(name, pages);
}

class Box<T extends Item> {


List<T> items;
Box(this.items);

void printBooks(){
items.forEach((element) {
print(element.name);
});

303
}

void operator +(T book){


items.add(book);
}
}

void main(List<String> arguments) {


var book1 = Book('Война и Мир т.1', 1234);
var book2 = Book('Тихий Дон т.1', 400);
var box = book1 + book2;
box.printBooks();
print('-' * 30);
box+ Magazine('Огонек', 250);
box.printBooks();
}
/*
Война и Мир т.1
Тихий Дон т.1
------------------------------
Война и Мир т.1
Тихий Дон т.1
Огонек
*/

Также дженерики можно использовать с функциями:


// ex4_60.dart
class MyID {
final int id;
final String idString;

MyID(this.id): idString = id.toString();

@override
String toString() {
return idString;
}
}

T firstElement<T>(List<T> list){
return list[0];
}

void main(List<String> arguments) {


var list = <MyID>[MyID(2), MyID(33)];
print(firstElement(list)); // 2
}

304
4.12. Enum (Перечисления)
Перечисления представляют собой особый вид классов, используемых
для представления фиксированного числа постоянных значений. Для того,
чтобы его создать, достаточно использовать ключевое слово enum. Каждому
значению в перечислении соответствует свой целочисленный индекс.
Например, первое значение имеет индекс 0, второе значение имеет индекс
1 и т. д.
// ex4_61.dart
enum State { none, open, close, lock}

void main(List<String> arguments) {


print(State.none.index == 0); // true
print(State.open.index == 1); // true
// формируем список значений перечисления
var listEnums = State.values;
for (var element in listEnums) {
print('${element.index} => ${element.toString()}');
}
}
/*
0 => State.none
1 => State.open
2 => State.close
3 => State.lock
*/

Чаще всего перечисления используют для создания машины


состояний и выбора потока управления выполнения кода в приложении,
посредством оператора swith-case, либо когда необходимо, чтобы одно из
состояний объекта могло меняться только в четко определенном диапазоне
возможных состояний.
Перечисления в Dart имеют ряд ограничений:
− нельзя создавать подклассы, примеси, объявления и реализации
методов перечисления;
− нельзя создать экземпляр перечисления.
Для демонстрации использования перечисления, а заодно и шаблона
проектирования GoF – State (Состояние), давайте представим, что где-то
установлена кофемашина, которая может готовить 3 вида кофе: капучино,
латте и эспрессо. Исходя из этого, помимо трех режимов приготовления,
она должна поддерживать еще такие режимы работы, как: ожидание, выбор
изготовляемого напитка и выдача сдачи, а также реализовывать механизм
перехода между ними.

305
Начнем с объявления перечисления возможных состояний
кофемашины и интерфейсов, которые будем использовать в ходе
написания классов, реализующих то или иное состояние:
// ex4_62.dart
enum CoffeMachineState {
none,
idle,
choose,
cappuccino,
latte,
espresso,
changeMoney,
}

abstract interface class State {


void insertMoney(ICoffeMachine coffeMachine);
void ejectMoney(ICoffeMachine coffeMachine);
void makeCoffe(ICoffeMachine coffeMachine);
}

abstract interface class ICoffeMachine {


double getWaterValue();
double getMilkValue();
int getOrderMoney();

void setWaterValue(double value);


void setMilkValue(double value);
void setOrderMoney(int money);

void setState(CoffeMachineState state);


CoffeMachineState selectedCoffee();
void returnMoney();
}

Первым реализуем состояние покоя, которое не позволит приготовить


кофе или получить сдачу и является стартовым. То есть с него будет
начинать работать кофемашина и к этому состоянию она должна
переходить после приготовления кофе. А переход из данного состояния в
состояние ожидания будет осуществляться при добавлении денег:
class IdleState implements State {

@override
void ejectMoney(ICoffeMachine coffeMachine) {
print('What the money? Oo');
}

@override

306
void insertMoney(ICoffeMachine coffeMachine) {
print('Go to the choose state');
coffeMachine.setState(CoffeMachineState.choose);
}

@override
void makeCoffe(ICoffeMachine coffeMachine) {
print('Get out of here, rogue');
}
}

Следующим опишем состояние ожидания, из которого можно перейти


к состоянию приготовления одного из выбранных кофе:
class WaitChooseState implements State {

@override
void ejectMoney(ICoffeMachine coffeMachine) {
print('Order or leave your money!');
}

@override
void insertMoney(ICoffeMachine coffeMachine) {
print('Enough funds uploaded to order?');
coffeMachine.setState(CoffeMachineState.choose);
}

@override
void makeCoffe(ICoffeMachine coffeMachine) {
if (coffeMachine.selectedCoffee() ==
CoffeMachineState.none) {
print('Choose the coffee you want to make!');
} else {
coffeMachine.setState(coffeMachine.selectedCoffee());
}
}
}

Теперь опишем состояние выдачи сдачи. После выдачи которой


кофемашина всегда переходит к состоянию покоя:
class ChangeState implements State {
@override
void ejectMoney(ICoffeMachine coffeMachine) {
print('Return ${coffeMachine.getOrderMoney()} parrots');
coffeMachine.setOrderMoney(0);
coffeMachine.setState(CoffeMachineState.idle);
}

307
@override
void insertMoney(ICoffeMachine coffeMachine) {
ejectMoney(coffeMachine);
}

@override
void makeCoffe(ICoffeMachine coffeMachine) {
ejectMoney(coffeMachine);
}
}

Далее приступим к реализации состояний приготовления кофе и


первым на очереди – Капучино. В методе makeCoffe производится расчет
хватает ли внесенных денег и ингредиентов для выполнения заказа. Если
да, то кофе готовится и переходим к состоянию выдачи сдачи. В том случае,
если не хватает внесенных средств – оповещаем об этом клиента и ждем,
пока он добавит недостающую суммы. А при отсутствии ингредиентов –
переходим к состоянию возвращения денег:
class CappuccinoState implements State {
@override
void ejectMoney(ICoffeMachine coffeMachine) {
print('You will not get it!!!');
}

@override
void insertMoney(ICoffeMachine coffeMachine) {
makeCoffe(coffeMachine);
}

@override
void makeCoffe(ICoffeMachine coffeMachine) {
final cost = 32;
final needWater = 0.3;
final needMilk = 0.1;
var waterResidues = coffeMachine.getWaterValue() - needWater;
var milkResidues = coffeMachine.getMilkValue() - needMilk;
var moneyResidues = coffeMachine.getOrderMoney() - cost;
if (moneyResidues >= 0) {
if (waterResidues >= 0 && milkResidues >= 0) {
print('Cooking Cappuccino!');
coffeMachine.setWaterValue(waterResidues);
coffeMachine.setMilkValue(milkResidues);
coffeMachine.setOrderMoney(moneyResidues);
} else {
print('Not enough ingredients!');
}
if (coffeMachine.getOrderMoney() > 0) {

308
coffeMachine.setState(CoffeMachineState.changeMoney);
coffeMachine.returnMoney();
} else {
coffeMachine.setState(CoffeMachineState.idle);
}
} else {
print('Not enough funds!');
}
}
}

Классы состояний приготовления Латте и Эспрессо будут отличаться


от приготовления Капучино только ценой кофе и объемом расходуемых
ингредиентов:
class LatteState implements State {
@override
void ejectMoney(ICoffeMachine coffeMachine) {
print('You will not get it!!!');
}

@override
void insertMoney(ICoffeMachine coffeMachine) {
makeCoffe(coffeMachine);
}

@override
void makeCoffe(ICoffeMachine coffeMachine) {
final cost = 40;
final needWater = 0.3;
final needMilk = 0.2;
var waterResidues = coffeMachine.getWaterValue() - needWater;
var milkResidues = coffeMachine.getMilkValue() - needMilk;
var moneyResidues = coffeMachine.getOrderMoney() - cost;
if (moneyResidues >= 0) {
if (waterResidues >= 0 && milkResidues >= 0) {
print('Cooking Latte!');
coffeMachine.setWaterValue(waterResidues);
coffeMachine.setMilkValue(milkResidues);
coffeMachine.setOrderMoney(moneyResidues);
} else {
print('Not enough ingredients!');
}
if (coffeMachine.getOrderMoney() > 0) {
coffeMachine.setState(CoffeMachineState.changeMoney);
coffeMachine.returnMoney();
} else {
coffeMachine.setState(CoffeMachineState.idle);

309
}
} else {
print('Not enough funds!');
}
}
}

class EspressoState implements State {


@override
void ejectMoney(ICoffeMachine coffeMachine) {
print('You will not get it!!!');
}

@override
void insertMoney(ICoffeMachine coffeMachine) {
makeCoffe(coffeMachine);
}

@override
void makeCoffe(ICoffeMachine coffeMachine) {
final cost = 25;
final needWater = 0.3;
var waterResidues = coffeMachine.getWaterValue() - needWater;
var moneyResidues = coffeMachine.getOrderMoney() - cost;
if (moneyResidues >= 0) {
if (waterResidues >= 0) {
print('Cooking Latte!');
coffeMachine.setWaterValue(waterResidues);
coffeMachine.setOrderMoney(moneyResidues);
} else {
print('Not enough ingredients!');
}
if (coffeMachine.getOrderMoney() > 0) {
coffeMachine.setState(CoffeMachineState.changeMoney);
coffeMachine.returnMoney();
} else {
coffeMachine.setState(CoffeMachineState.idle);
}
} else {
print('Not enough funds!');
}
}
}

Следующий на очереди – класс кофемашины, у которого одно из полей


должно отвечать за ее текущее состояние. В ряде методах класса
произведем вызов методов состояний, с передачей им текущего

310
экземпляра кофемашины, приводимого к интерфейсу ICoffeMachine, тем
самым выполняя сокрытие и не давая возможности состоянию иметь
доступ к методам и полям кофемашины, непредусмотренных
интерфейсом:
class CoffeeMachine implements ICoffeMachine {
double _waterCapacity;
double _milkCapacity;
var _orderMopney = 0;
CoffeMachineState _selectedCoffee = CoffeMachineState.none;
final _allStates = <CoffeMachineState, State>{
CoffeMachineState.idle: IdleState(),
CoffeMachineState.choose: WaitChooseState(),
CoffeMachineState.changeMoney: ChangeState(),
CoffeMachineState.cappuccino: CappuccinoState(),
CoffeMachineState.latte: LatteState(),
CoffeMachineState.espresso: EspressoState()
};
late State _currentState;

CoffeeMachine(this._waterCapacity, this._milkCapacity) {
_currentState = _allStates[CoffeMachineState.idle]!;
}

@override
double getMilkValue() => _milkCapacity;

@override
int getOrderMoney() => _orderMopney;

@override
double getWaterValue() => _waterCapacity;

@override
CoffeMachineState selectedCoffee() => _selectedCoffee;

@override
void setMilkValue(double value) {
_milkCapacity = value;
}

@override
void setOrderMoney(int money) {
_orderMopney = money;
}

@override
void setWaterValue(double value) {

311
_waterCapacity = value;
}

void cappuccino() {
print('Cappuccino preparation selected');
_selectedCoffee = CoffeMachineState.cappuccino;
_currentState.makeCoffe(this);
}

void latte() {
print('Latte preparation selected');
_selectedCoffee = CoffeMachineState.latte;
_currentState.makeCoffe(this);
}

void espresso() {
print('Espresso preparation selected');
_selectedCoffee = CoffeMachineState.espresso;
_currentState.makeCoffe(this);
}

@override
void setState(CoffeMachineState state) {
if (state == CoffeMachineState.idle) {
_selectedCoffee = CoffeMachineState.none;
}
_currentState = _allStates[state]!;
}

@override
void returnMoney() {
_currentState.ejectMoney(this);
}

void insertMoney(int money) {


_orderMopney += money;
print('Inserted $_orderMopney parrots');
_currentState.insertMoney(this);
}

void makeCoffee() {
print('Start preparation of the selected coffee!');
_currentState.makeCoffe(this);
}
}

312
Для проверки работы кофемашины добавьте в функцию main
следующий код и запустите приложение:
void main() {
var coffeeMachine = CoffeeMachine(1.0, 1.0);
coffeeMachine.makeCoffee();
coffeeMachine.insertMoney(10);
coffeeMachine.insertMoney(10);
coffeeMachine.cappuccino();
coffeeMachine.makeCoffee();
coffeeMachine.insertMoney(20);
print('**** When not enough products to make coffee ****');
coffeeMachine = CoffeeMachine(0.1, 0.1);
coffeeMachine.insertMoney(100);
coffeeMachine.makeCoffee();
coffeeMachine.latte();
coffeeMachine.makeCoffee();
}
/* Start preparation of the selected coffee!
Get out of here, rogue
Inserted 10 parrots
Go to the choose state
Inserted 20 parrots
Enough funds uploaded to order?
Cappuccino preparation selected
Start preparation of the selected coffee!
Not enough funds!
Inserted 40 parrots
Cooking Cappuccino!
Return 8 parrots
**** When not enough products to make coffee ****
Inserted 100 parrots
Go to the choose state
Start preparation of the selected coffee!
Choose the coffee you want to make!
Latte preparation selected
Start preparation of the selected coffee!
Not enough ingredients!
Return 100 parrots */

С версии Dart 2.17 появилась возможность задавать расширенные


перечисления в стиле классов – с полями, методами и константными
конструкторами. Если хотите объявить такой enum, то убедитесь, что он
удовлетворяет следующим требованиям:
− все поля (переменные экземпляра) перечисления должны быть final;
− все конструкторы перечисления – константные;

313
фабричные конструкторы могут возвращать только один из

фиксированных, известных экземпляров enum;
− перечисление ни от кого не наследуется (разве что автоматически от
класса Enum);
− нельзя переопределять оператор равенства (==) и свойства (поля)
класса Enum – index и hashCode;
− нельзя объявлять в перечислении поле с именем values;
− все экземпляры перечисления объявляются в начале тела
расширенного перечисления;
− должен быть объявлен хотя бы один экземпляр перечисления.
Для примера реализуем перечисление по странам, добавив поля с
данными по ВВП, госдолгу и популяции населения:
// ex4_63.dart
enum Country {
Russia(GDP: 5327, nationalDebt: 200.5, population: 144713314),
China(GDP: 30327, nationalDebt: 14000.1,
population: 1425887337),
USA(GDP: 25463, nationalDebt: 33400.0, population: 338289857),
India(GDP: 11875, nationalDebt: 600.7, population: 1417173173);

const Country({
required this.GDP,
required this.nationalDebt,
required this.population,
});

final int GDP; // ВВП в млрд. долларов


final double nationalDebt; // млрд. долларов
final int population; // млн. человек

double get GDP2debt => (nationalDebt / GDP);


double get deptOneMan => nationalDebt / population;

@override
String toString() {
return '${this.name}($GDP, $nationalDebt, $population)';
}
}

(String, int) whatCountry(Country country) {


return switch (country) {
Country.Russia => (
'ε(´。•᎑•`)っ ${country.name}',
country.index,
),
Country.China => (

314
' ${country.name}',
country.index,
),
Country.USA => (
'凸( •̀_•́ )凸 ${country.name}',
country.index,
),
Country.India => (
' ${country.name}',
country.index,
),
};
}

void main() {
var russia = Country.Russia;
print(russia); // Russia(5327, 200.5, 144713314)
print(russia.GDP); // 5327
print(russia.nationalDebt); // 200.5
print(russia.population); // 144713314
print(russia.GDP2debt); // 0.03763844565421438
print(russia.deptOneMan); // 0.0000013854979507967042

print(whatCountry(russia)); // (ε(´。•᎑•`)っ Russia, 0)


print(whatCountry(Country.India)); // ( India, 3)
print(whatCountry(Country.China)); // ( China, 1)
print(whatCountry(Country.USA)); // (凸( •̀_•́ )凸 USA, 2)
}

Впервые с такой функциональностью мне довелось столкнуться в Java


и, признаться, с нетерпением ждал, когда она появится в Dart.

4.13. Exceptions (Исключения)


В процессе работы приложения могут происходить непредвиденные
ситуации: обращение по несуществующему индексу списка, деление на
ноль и т. д., способные привести к экстренному завершению программы.
Исключения позволяют в случае ошибки в вычислениях или логике работы
программы сразу же перейти к ее обработке, при этом отменяя все вызовы
функций (методов), которые начались до того, как был совершен вход в
данный обработчик. Поэтому механизм обработки исключений можно
рассматривать как некоторый структурированный «безусловный переход».
Несмотря на то, что исключения можно перехватывать и
обрабатывать, тем самым оставляя программу в рабочем состоянии, в ряде
случаев это делать не стоит. Лучше пусть программа «упадет» и укажет вам

315
на наличие ошибок, чем продолжит работать с некорректными данными.
Особенно, не стоит перехватывать для обработки исключений класс
Exception, так как он является базовым классом для всех исключений, то
есть какое бы не сгенерировалось исключение – оно будет обработано и,
если в блоке обработки не предусмотрена повторная генерация
исключения, чтобы предупредить более верхний уровень приложения, вы
можете и не узнать о наличии ошибки.
Для работы с исключениями в Dart используются такие классы, как
Exception и Error, а также множество их производных классов. Сказать
честно… хватило бы и одного, а наличие двух классов приводит к
неприятным моментам. Но тут уж ничего не поделать и придется страдать
привыкнуть.

4.13.1. Конструкция try…catch…finally

Общий вид данной конструкции можно записать следующим образом:


try{
// блок кода, который может генерировать исключение
}
catch(e){
// блок обработки исключения
}
finally{
// блок кода, который выполнится в любом случае,
// как при отсутствии исключения
// так и после его обработки
}

Та часть кода, в которой может быть сгенерировано исключение,


помещается в блок try. Если в ходе выполнения кода, помещенного в блок
try было сгенерировано исключение, оно может быть перехвачено и
обработано в блоке catch. При этом мы можем перехватывать, как
высокоуровневое исключение (базовое Exception), так и
специализированное, явно задавая тип перехватываемого исключения.
Блок finally выполнится в любом случае, даже если будет сгенерировано
исключение. Обычно в него помещают код, где завершается работа с
файлом, сокетом и прочими объектами. Такой подход гарантирует, что при
отсутствии или наличии исключения мы завершим работу с объектом,
источником информации и т. д.
Давайте представим ситуацию, что в разрабатываемом нами
приложении осуществляется целочисленное деление на значение, которое
ввел пользователь и мы забыли на этапе ввода проверить, чтобы оно не
равнялось нулю. В этом случае, в процессе работы приложения, будет
сгенерировано исключение IntegerDivisionByZeroException и оно

316
прекратит свою работ. Следует отметить, что данное исключение не
рекомендуется использовать, т.к. оно отмечено как deprecated
(устаревшее) и скоро исчезнет из Dart. Поэтому вместо него следует явно
перехватывать ошибку UnsupportedError:
// ex4_64.dart
void main(List<String> arguments) {
var inputValue = 0;
var resultValue = 0;
var scalingValue = 100;

// 1
try{
resultValue = scalingValue ~/ inputValue;
}
catch(e){
// перехват всех исключений и ошибок
print('Произошло деление на ноль!!!');
print(e);
}

// Произошло деление на ноль!!!


// IntegerDivisionByZeroException

//2
try{
resultValue = scalingValue ~/ inputValue;
} on UnsupportedError {
// перехват специализированного исключения
print('Произошло деление на ноль!!!');
}
// Произошло деление на ноль!!!

//3
try{
resultValue = scalingValue ~/ inputValue;
}on Exception catch (e) {
// Перехватывает все исключения
print('Сгенерированное исключение: $e');
} catch (e) {
// Перехватывает вообще все
// и исключения и ошибки
print('Что-то из ряда вон выходящего: $e');
}
//Сгенерированное исключение: IntegerDivisionByZeroException

//4
try{

317
resultValue = scalingValue ~/ inputValue;
}on UnsupportedError {
// перехват специализированного исключения
print('Произошло деление на ноль!!!');
}
finally{
print('Что бы ни произошло, я - великолепен!!!');
}
// Произошло деление на ноль!!!
// Что бы ни произошло, я - великолепен!!!
}

Как видно из примера, мы можем перехватывать одно, несколько или


сразу все исключения (ошибки). В том же случае, если сгенерированное
исключение не соответствует ни одному из перехватываемых, то оно
распространиться после завершения блока finally:
// ex4_65.dart
void main(List<String> arguments) {
var inputValue = 0;
var scalingValue = 100;
try{
var resultValue = scalingValue ~/ inputValue;
}
finally{
print('Что бы ни произошло, я - великолепен!!!');
}
}
/* Что бы ни произошло, я - великолепен!!!
Unhandled exception:
IntegerDivisionByZeroException */

4.13.2. Генерация исключений и ошибок


В тех случаях, когда нам самим необходимо генерировать сообщения
об ошибках или исключения, следует использовать ключевое слово throw:
// ex4_66.dart
void main(List<String> arguments) {
var inputValue = 0;
var resultValue = 0;
var scalingValue = 100;
try{
if (inputValue == 0){
throw UnsupportedError('Oo');
}
resultValue = scalingValue ~/ inputValue;
}on UnsupportedError{
print('Произошло деление на ноль!!!');

318
}
catch(e){
print('Ошибка: $e');
}
// Произошло деление на ноль!!!

try{
if (inputValue == 0){
throw ArgumentError();
}
resultValue = scalingValue ~/ inputValue;
}on UnsupportedError{
print('Произошло деление на ноль!!!');
}
catch(e){
print('Ошибка: $e');
}
//Ошибка: Invalid argument(s)
}

Если генерируемое исключение не может быть обработано на данном


уровне, может быть обработано частично или нам необходимо оповестить
более верхний уровень приложения о сгенерированном исключении, его
можно распространить за текущий блок обработки, используя ключевое
слово rethrow:
// ex4_67.dart
void main(List<String> arguments) {
var inputValue = 0;
var resultValue = 0;
var scalingValue = 100;
try{
resultValue = scalingValue ~/ inputValue;
}on UnsupportedError{
print('Произошло деление на ноль!!!');
rethrow;
} on ArgumentError catch(e){ print('Ошибка: $e'); }
}
/* Что бы ни произошло, я - великолепен!!!
Unhandled exception:
IntegerDivisionByZeroException */

У разработчиков также имеется возможность перехватывать и


обрабатывать, как и в случае с исключениями, ошибки по их типу:
// ex4_68.dart
try{
if (inputValue == 0){
throw ArgumentError();

319
}
}on UnsupportedError {
print('Произошло деление на ноль!!!');
rethrow;
} on ArgumentError catch(e){
print('Ошибка: $e'); //Ошибка: Invalid argument(s)
}

Иногда бывают ситуации, что необходимо создавать экземпляры


абстрактного базового класса с сохранением обязанностей у производных
классов, переопределения не реализованных в нем методов. В этом случае
абстрактный класс становится обычным и методы класса, которые
необходимо реализовать в производном классе, должны генерировать
сообщения об ошибках:
// ex4_69.dart
class Item{
final String name;
final double weight;

Item(this.name, this.weight) ;
}

class StorageSystem {
var itemsList = <Item>[];
final double weightLimit;

StorageSystem(this.weightLimit);

void addItem(Item item) => throw NoSuchMethodError;


Item popItem() => throw NoSuchMethodError;
double systemWeight()=> throw NoSuchMethodError;
}

class Box extends StorageSystem {


Box(double weightLimit):super(weightLimit);

@override
void addItem(Item item) {
var currentSystemWeight = systemWeight();
if((currentSystemWeight+item.weight) < weightLimit){
itemsList.add(item);
print('${item.name} добалнен(о/а) в коробку!');
}
else{
print('${item.name} не помещается в коробку!');
}
}

320
@override
Item popItem() {
return itemsList.removeLast();
}

@override
double systemWeight() {
var sum = 0.0;
for (var element in itemsList) {
sum += element.weight;
}
return sum;
}
// методы, характерные для коробки
}

void main(List<String> arguments) {


var box = Box(18);
StorageSystem? storageSystem = box;
storageSystem.addItem(Item('Книга', 2.6));
storageSystem.addItem(Item('Чайник', 3.9));
storageSystem.addItem(Item('Гантеля', 10));
storageSystem.addItem(Item('Монитор', 4));

print(storageSystem.popItem().name);
print(storageSystem.systemWeight());

StorageSystem? newStorageSystem = StorageSystem(22);


newStorageSystem.addItem(Item('Монитор', 4));
}
/* Книга добалнен(о/а) в коробку!
Чайник добалнен(о/а) в коробку!
Гантеля добалнен(о/а) в коробку!
Монитор не помещается в коробку!
Гантеля
6.5
Unhandled exception:
NoSuchMethodError */

4.13.3. Пользовательские исключения и ошибки


Для того, чтобы реализовать ваше собственное исключение и иметь
возможность его генерировать в процессе работы приложения,
пользовательский класс должен реализовывать интерфейс базового класса
для всех исключений – Exception, а в случае создания пользовательской
ошибки необходимо наследоваться от Error:

321
// ex4_70.dart
class MyException implements Exception {
final String? msg;
const MyException([this.msg]);

@override
String toString() => msg ?? 'MyException';
}

class MyError extends Error {


final String? msg;
MyError([this.msg]);

@override
String toString() => msg ?? 'MyException';
}

class MyNewError extends Error {}

void main(List<String> arguments) {


try{
throw MyException('Пользовательское исключение');
} on MyException catch(e){
print(e); // Пользовательское исключение
}

try{
throw MyError('Пользовательская ошибка');
} on MyError catch(e){
print(e); // Пользовательская ошибка
}

try{
throw MyNewError();
} on MyNewError catch(e){
print(e); // Instance of 'MyNewError'
}
}

4.13.4. Трассировка стека


При перехвате исключения или ошибки с использованием catch() для
него можно указывать один или два аргумента. Первый аргумент –
возникшее исключение, а второй – трассировка стека (объект StackTrace):
// ex4_71.dart
class MyError extends Error {}

322
void exceptionFunc(){
throw MyException('Пользовательское исключение');
}

void errorFunc(){
throw MyError();
}

void main(List<String> arguments) {


try{
exceptionFunc();
} on MyException catch(e, s){
print(e);
print(s);
}

try{
errorFunc();
} on MyError catch(e, s){
print(e);
print(s);
}
// или
// on MyError catch(e){
// print(e);
// print(e.stackTrace);
// }
}
/* Пользовательское исключение
#0 exceptionFunc (file:///C:/code/dart/hello_world/bin/
hello_world.dart:12:3)
#1 main
(file:///C:/code/dart/hello_world/bin/hello_world.dart :21:5)
#2 _delayEntrypointInvocation.<anonymous closure>
(dart:isolate-patch/isolate_patch.dart:295:33)
#3 _RawReceivePort._handleMessage (dart:isolate-
patch/isolate_patch.dart:184:12)

Instance of 'MyError'
#0 errorFunc
(file:///C:/code/dart/hello_world/bin/hello_world.dart:16:3)
#1 main
(file:///C:/code/dart/hello_world/bin/hello_world.dart:28:5)
#2 _delayEntrypointInvocation.<anonymous closure>
(dart:isolate-patch/isolate_patch.dart:295:33)
#3 _RawReceivePort._handleMessage (dart:isolate-
patch/isolate_patch.dart:184:12) */

323
В момент запуска приложения происходит выделение памяти в куче и
стеке. Перед самим выделением памяти срабатывают некоторые события
и, если в написанном коде существуют ошибки, их можно отследить в стеке.
Таким образом, трассировка стека представляет собой список вызовов
методов, которые приложение выполняло при возникновении исключения.

4.13.5 Assert (Утверждение)


Один из способов отладки логики работы приложения в процессе его
разработки - assert (утверждения). Они используются для того, чтобы
прервать выполнение программы, если утверждение ложно и сообщить о
наличии проблемы разработчику. Общая структура утверждения может
быть представлена следующим образом:
assert (условие, опциональноеСообщение);

Для начала посмотрим, как использовать утверждение без


прикрепленного к нему опционального сообщения и что при этом
выводится, когда условие в утверждении возвращает значение false:
// ex4_72.dart
int myFunc(int a, int b){
assert(b != 0);
return a ~/ b;
}

void main(List<String> arguments) {


print(myFunc(6, 0));
}
//Unhandled exception:
// … : Failed assertion: line 2 pos 10: 'b != 0': is not true.

Теперь добавим сообщение, которое внесет дополнительную ясность


в то, с чем связано падение программы:
// ex4_73.dart
int myFunc(int a, int b){
assert(b != 0, 'Деление на ноль');
return a ~/ b;
}

void main(List<String> arguments) {


print(myFunc(6, 0));
}
/*
Unhandled exception:
'…: Failed assertion: line 2 pos 10: 'b != 0': Деление на ноль */

324
Первым аргументом assert может быть любое выражение, которое
возвращает логическое значение (true или false). Если результат
вычисления выражения true, утверждение завершается успешно и
управление переходит к коду, находящемуся за ним. Если false, то
утверждение генерирует ошибку AssertionError.
Утверждения удобно использовать в процессе разработки
программных продуктов. Но необходимо быть внимательным и не
помещать в них в качестве выражений различные функции, возвращающие
значение логического типа данных, не использовать в утверждениях
вызовы методов экземпляров классов и т. д. Это связано с тем, что при
release-сборке проекта все утверждения в коде игнорируются, то есть они
не выполняются.

4.14. Тестирование классов


Тестирование классов принципиально не отличается от
рассмотренного ранее тестирования функций. За тем исключением, что
при написании тестов появляются классы-заглушки, моделирующие
соединение по сети с сервером, базой данных и т.д.
Создайте новый консольный проект «money». Файл из директории
«bin» удалите, он нам не пригодится, а в одноименную библиотеку из
каталога «lib» добавьте следующий код:
abstract class IMoney {
int get value;
IMoney operator +(IMoney other);
IMoney operator -(IMoney other);
}

class MoneyOperationError extends Error {}

class Rub implements IMoney {


late final int _kopek;

Rub._(this._kopek);
Rub.fromInt(int value) : this._(value);

factory Rub(String rub) {


var localRub = (double.parse(rub) * 100).toStringAsFixed(0);
return Rub._(int.parse(localRub));
}

@override
Rub operator +(IMoney other) {
return Rub._(_kopek + other.value);
}

325
@override
int get value => _kopek;

@override
String toString() {
var rub = (_kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}

@override
Rub operator -(IMoney other) {
if (_kopek - other.value < 0) {
throw MoneyOperationError();
}
return Rub._(_kopek - other.value);
}
}

Конечно, правильнее было бы разбить все по отдельным файлам, но


это не полноценный проект, а просто пример. Поэтому не заостряйте на
этом обстоятельстве внимание и добавьте в файл «money_test.dart»
директории «test» приведенный ниже код:
import 'package:money/money.dart';
import 'package:test/test.dart';

void main() {

test('creation', () {
var rub = Rub.kopek(100);
IMoney money = rub;
expect(money.value, 100);
expect(rub.value, 100);
});

test('toString', () {
var rub = Rub.kopek(100);
expect(rub.toString(), 'Rub(1.00)');
});

test('addition', () {
var rub = Rub.kopek(100);
var rub2 = Rub.kopek(200);
expect((rub + rub2).value, 300);
});

group(

326
'subtraction',
() {
late var rub;
late var rub2;
setUp(() {
rub = Rub.kopek(100);
rub2 = Rub.kopek(200);
});

test('error', () {
late dynamic err;
try {
rub -= rub2;
}catch(e){
err = e;
}
expect(err, isA<MoneyOperationError>());
});
test('correct', () {
expect((rub2 - rub).value, 100);
});
}
);
}

Поэкспериментируйте с тестами, изменив поведение класса.

Резюме по разделу
В данной главе мы рассмотрели, что такое объектно-ориентированное
программирование, какие принципы лежат в его основе и как они
реализуются в Dart. Дополнительно затронуто понятие абстракции,
абстрактного базового класса и интерфейс. Интерфейсы рекомендуется
использовать, когда необходимо добавить уровень гибкости в
разрабатываемое приложение, чтобы уменьшить связность более верхнего
(абстрактного) уровня с нижними уровнями, в которых находится
реализация функциональности. А также посредством интерфейсов можно
предоставлять API для доступа к разрабатываемому
модулю/компоненту/плагину и т. д. Поэтому понимание, что представляет
из себя интерфейс и как с ним работать – очень важно для программистов.
Также были рассмотрены модификаторы классов, за что они отвечают
и как при их использовании можно выстрелить себе в ногу. Зачастую это
связано с тем, что в Dart инкапсуляция осуществляется не на уровне класса,
а на уровне библиотеки (файла) из-за чего нужно проявлять особую
осторожность, если не хотите давать пользователям вашего пакета

327
импортировать заранее «испорченный» класс, который не подразумевал
использования в тех случаях, где его применяют.
Дополнительно был рассмотрен такой инструмент, как дженерики
(обобщения), который позволяет уменьшить дублирование кода и
исключения, представляющие собой ошибки, возникающие в ходе
выполнения кода и указывающие на имеющиеся проблемы в логике работы
программы. Они могут генерироваться автоматически, в процессе
выполнения программы или «вручную», самим программистом.
Перехватываемые и обрабатываемые исключения должны
специфицироваться, то есть не представлять собой тип базового
исключений Exception, так как такой подход позволяет точно сказать с
ошибкой какого рода в логике работы столкнулось приложение и корректно
ее обработать. Поэтому, лучше пусть программа «упадет» и укажет вам
на наличие ошибок, чем продолжит работать с некорректными
данными.
P.S. и будьте внимательны, т.к. в Dart существует 2 базовых класса
для оповещения о нештатных ситуациях – Exception и Error.

Вопросы для самопроверки


1. Что такое объект?
2. Как объект связан с классом?
3. Какие характеристики имеются у объекта?
4. Что такое состояние объекта? Как оно реализуется в классе?
5. Что такое поведение объекта? Как оно реализуется в классе?
6. Какие принципы лежат в основе ООП?
7. Как объявить класс?
8. Что такое экземпляр класса?
9. Для чего используется конструктор класса и как он объявляется?
10. Какие типы конструкторов класса существуют в Dart?
11. Что такое перегрузка оператора и для чего она используется?
12. Как объявляются и для чего используются статические переменные и
методы класса?
13. Можно ли в Dart использовать множественное наследование? Какое
ключевое слово используется для наследования от базового класса?
14. Для чего и как переопределяются методы базового класса в
производном?
15. Что такое абстрактный класс и интерфейс? Чем они схожи, а в чем
отличие?
16. Какие модификаторы классов вы знаете? В чем их смысл?
17. Какие ограничения на класс накладывает модификатор abstract?
Приведите примеры.

328
18. Какие ограничения на класс накладывает модификатор base?
Приведите примеры.
19. Какие ограничения на класс накладывает модификатор interface?
Приведите примеры.
20. Какие ограничения на класс накладывает модификатор final?
Приведите примеры.
21. Какие ограничения на класс накладывает модификатор sealed?
Приведите примеры.
22. Какие ограничения на класс накладывает модификатор mixin?
Приведите примеры.
23. Что такое Private field promotion и когда он не работает?
24. Что такое mixins (примеси)? Для чего они используются?
25. Как в Dart реализованы generics (Обобщения)? Для чего они нужны?
26. Перечислите ограничения, которые имеются у обычного и
расширенного enum.
27. Что такое исключение? Для чего они используются?
28. Какие два базовых классов для работы с исключениями существуют?
29. В чем смысл приведения к базовому классу или интерфейсу?
Приведите примеры.
30. Для каких целей и как используется каждый конструкции
try…catch…finally?
31. Какое ключевое слово следует использовать для того, чтобы
сгенерировать исключение? Посредством какого ключевого слова
можно распространить перехваченное исключение дальше?
32. Что такое трассировка стека? Как использовать данный механизм?
33. Для чего используются утверждения? Всегда ли выполняются
утверждения в коде?

Лабораторная работа № 7. Объектно-


ориентированное программирование
Цель работы: познакомиться с основными способами написания
объектно-ориентированного кода в Dart.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Каждое задание должно сопровождаться минимум пятью
тестами и содержать хотя бы одно исключение.

329
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

Задания:
1. Реализуйте класс «Комната общежития». В нем должна храниться
следующая информация: количество проживающих студентов,
максимальный размер комнаты (количество кроватей) и текущий размер
комнаты. При добавлении экземпляра класса «Студент», проверяется, если
количество проживающих студентов превышает максимальный размер
комнаты, должна выводиться ошибка. На коменданта, даже в случае
переполнения комнаты, такие ограничения не должны действовать. Также
реализуйте возможность вывода текущих состояний объектов в терминал.
2. Реализуйте класс «Студент», который должен содержать ФИО
студента, дату поступления и балл GPA. Балл GPA должен быть в диапазоне
от 2.0 до 4.0. Если балл находится вне этого диапазона, то студенту следует
отказать в добавлении в группу (класс «Группа студентов»). Группа должна
предоставлять метод поиска студента по его ФИО и выводить список
студентов ориентируясь на заданный диапазон баллов. Также реализуйте
возможность вывода текущих состояний объектов в терминал.
3. Реализуйте класс «Книжный шкаф», который содержит информацию о
максимальном весе хранимых книг и их количестве. Класс «Книга» должен
содержать поля: название, автор, год издания, вес и стоимость книги. При
добавлении книги в шкаф осуществляется проверка по весу и количеству
книг, которые может вместить шкаф в данный момент. Если один из этих
параметров больше разрешенного, то книга в шкаф не добавляется.
Дополнительно для книжного шкафа реализуйте методы поиска книги по
автору, расчет полной стоимости и веса хранимых книг, получения списка
книг, чья стоимость >= указанной. Также реализуйте возможность вывода
текущих состояний объектов в терминал.
4. Реализуйте класс «Кошелек» куда могут добавляться экземпляры
классов денежных валют (рубль и юань) различного номинала. Кошелек
должен иметь возможность возвращать полную сумму денег всех валют в
задаваемой валюте, количество денег конкретной валюты и уметь
конвертировать с задаваемым коэффициентом деньги из одной валюты в
другую. Требуется реализовать возможность вывода текущих состояний
объектов в терминал.
5. Создайте класс Parking, который может хранить объекты класса Car. В
конструктор Parking следует передать максимальное количество
автомобилей, которые могут быть припаркованы. У класса Car должны быть

330
следующие поля: производитель, модель и гос. номер автомобиля. Класс
Parking иметь следующие методы: добавление автомобиля, удаление
автомобиля, поиск автомобиля по гос. номеру и подсчет количества
автомобилей на парковке. Если парковка заполнена, то автомобиль не
может быть добавлен, о чем предупредите пользователя. Также обеспечьте
вывод текущего состояния объектов в терминал.
6. Реализуйте класс Barbell (Гриф), способный удержать задаваемый
предельный вес и класс Plate (Блин) (10, 15, 20 кг). Экземпляры класса Plate
должны навешиваться на Barbell как с левой, так и с правой стороны. Класс
Barbell должен предоставлять методы для получения общего веса блинов и
разницы между левой и правой стороной, которая не должна превышать 15
кг. Если разница превышает этот вес, то добавление блина на штангу
запрещено и об этом должно быть выведено сообщение. Также обеспечьте
вывод текущего состояния объектов в терминал.
7. Реализуйте класс Product и Order. Order может состоять из любого
количества Products, которые составляют его итоговую стоимость.
Предусмотрите возможность предоставления скидок на определенные
товары в заказе и на общую стоимость заказа. Обеспечьте возможность
получения информации о стоимости товаров определенного типа в заказе
и вывода текущего состояния объектов на терминал.
8. Реализуйте класс Scale (весы) и Product. В конструктор класса Scale
передайте максимальный вес, который они могут выдержать. Также
данный класс должен предоставлять пользователю следующие методы:
добавление и удаление товаров, сортировку товаров по стоимости и весу,
нахождение товара с минимальной стоимостью, товара с максимальным
весом, определение текущего веса всех товаров на весах и общего веса
товаров определенного типа. Если общий вес товаров равен или превышает
максимальный, новые товары не должны добавляться на весы (вывод
оповещения). Также обеспечьте вывод текущего состояния объектов в
терминал.
9. Создайте класс DecimalCounter. В конструкторе этого класса укажите
диапазон счета (например, от 3 до 87). Класс должен увеличивать или
уменьшать свое значение на 1 в указанном диапазоне. Вы можете
инициализировать этот класс значениями по умолчанию или
произвольными значениями. DecimalCounter должен иметь следующие
методы: увеличить значение, уменьшить значение и получить текущее
значение. Если счетчик достигает минимального значения, и вы вызываете
метод уменьшения, то значение счетчика становится равным
максимальному значению и наоборот. Каждый такой переход должен
сопровождаться оповещением пользователя. Также обеспечьте вывод
текущего состояния объектов в терминал.

331
10. Реализуйте класс «Акции», который будет содержать информацию о
компании, эмитенте акций, количестве акций в портфеле, текущей
котировке и курсе заданной валюты, а также класс «Инвестиционный
портфель». Портфель инвестора должен содержать методы для получения
полной суммарной стоимости всех акций в портфеле; по определенной
отрасли; возвращающий список акций компании заданной отрасли. Также
обеспечьте вывод текущего состояния объектов в терминал.
11. Реализуйте класс Library (Библиотека), который содержит
информацию о книгах (Book). Класс Library должен иметь методы для
добавления новой книги, удаления книги, поиска книги по названию и
получения общей стоимости всех книг в библиотеке. Также предусмотрите
возможность предоставления скидки на определенные книги, на общую
стоимость всех книг в библиотеке и обеспечьте вывод текущего состояния
объектов в терминал.
12. Реализуйте класс Delivery (Доставка), который представляет собой
систему доставки товаров. Класс должен иметь методы для добавления
нового заказа (класс Order), отслеживания статуса заказа и расчета общей
стоимости доставки. В системе должны быть предусмотрены различные
виды доставки (например, курьерская доставка, почтовая доставка) с
разными ставками и условиями. Класс Delivery должен также предоставлять
методы для вывода текущего состояния заказов и доставок в терминал.
13. Реализуйте класс JewelryStore (Ювелирный магазин), который
представляет собой систему управления продажей ювелирных изделий.
Класс должен иметь методы для добавления нового изделия, удаления
изделия, поиска изделия по названию,списка изделий где цена >= заданной
и расчета общей стоимости всех изделий в магазине. В системе должны
быть предусмотрены различные типы ювелирных изделий (например,
кольца, ожерелья, серьги) с разными ценами и свойствами. Класс
JewelryStore должен также предоставлять методы для вывода текущего
состояния изделий и информации о продажах в терминал.
14. Реализуйте класс Machine (Машина), который представляет собой
систему управления производством механических устройств. Класс должен
иметь методы для добавления нового устройства, удаления устройства,
поиска устройства по модели, инвертарному номеру и расчета общей
стоимости всех устройств в системе. В системе должны быть
предусмотрены различные типы механических устройств (например,
двигатели, насосы, турбины) с разными параметрами и ценами. Класс
Machine должен также предоставлять методы для вывода текущего
состояния устройств и информации о производстве в терминал.
Предусмотрите случаи, когда может генерироваться исключение,
например, при попытке добавить устройство с некорректными
параметрами или при поиске устройства, которого нет в системе.

332
15. Реализуйте класс TaxiCompany (Таксопарк), который представляет
собой систему управления такси. Класс должен иметь методы для
добавления нового такси, удаления такси, поиска такси по номеру и расчета
общей доходности всего таксопарка. В системе должны быть
предусмотрены различные типы такси (например: легковые автомобили,
минивэны, электромобили) с разными характеристиками и тарифами.
Класс TaxiCompany должен также предоставлять методы для вывода
текущего состояния таксопарка и информации о доходах в терминал.
Предусмотрите случаи, когда может генерироваться исключение,
например, при попытке добавить такси с некорректными
характеристиками или при поиске такси, которого нет в таксопарке.

Таблица 4.2
Варианты работ
№ варианта Номера заданий к варианту
1 1, 8
2 2, 5
3 3, 7
4 4, 10
5 5, 11
6 6, 12
7 7, 14
8 8, 13
9 9, 15
10 3, 11
11 4, 7
12 1, 12
13 2, 9
14 3, 11
15 4, 13
16 5, 8
17 6, 15
18 7, 11
19 8, 15
20 10, 13

Лабораторная работа № 8. Перегрузка операторов


Цель работы: познакомиться с основными способами перегрузки
операторов в Dart.
Требования к формату защиты лабораторной работы:

333
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Каждое задание должно сопровождаться минимум пятью
тестами и содержать хотя бы одно исключение.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

Задания:
1. Создайте класс Matrix, который будет представлять двумерную
матрицу и перегружать операции умножения и сложения для работы с
матрицами любых размеров (например, вы можете написать методы,
которые будут проверять размеры матриц перед выполнением операций).
Также добавите метод для транспонирования матрицы.
2. Создайте класс Date, который будет представлять дату (год, месяц и
день) и перегружать операторы для сравнения дат.
3. Реализуйте класс Time, представляющий время в формате
часы:минуты:секунды. Перегрузите операторы + и - для выполнения
арифметических операций со временем. Например, оператор + должен
позволять складывать два времени и получать новое время как результат.
4. Создайте класс Student с атрибутами name и grade. Перегрузите
операции сравнения, для их использования при сортировке списка
студентов по их оценкам, а если у двух студентов оценки совпадают, то
сортировка должна производиться по алфавиту (по имени студента).
5. Реализуйте класс Color, представляющий цвет в формате RGB.
Перегрузите операторы [] для доступа к компонентам цвета (красный,
зеленый, синий) по индексу. Также перегрузите операторы &, | и ^ для
выполнения побитовых операций над цветами.
6. Создайте класс Fraction, который имеет два атрибута: числитель и
знаменатель. Для класса необходимо перегрузить операции сложения,
вычитания, умножения и деления, чтобы можно было производить
арифметические операции с дробями. При этом методы сложения и
вычитания должны возвращать новый объект класса Fraction, а методы
умножения и деления - результат в виде числителя и знаменателя.
7. Создайте класс Color, имеющий три атрибута: red, green и blue, каждый
из которых должен быть целым числом в диапазоне от 0 до 255. Перегрузите
у класса методы сложения и умножения. Результатом сложения должен

334
быть новый объект класса Color, у которого каждый из атрибутов будет
равен сумме соответствующих атрибутов исходных цветов (если сумма
больше 255, то атрибут равен 255). Аргументом метода умножения является
число в диапазоне от 0 до 1, а его результатом должен быть новый объект
класса Color, у которого каждый из атрибутов будет умножен на данный
аргумент и округлен до целого числа.
8. Реализуйте класс Vector, представляющий вектор в трехмерном
пространстве. Перегрузите операторы +, -, * для выполнения
арифметических операций с векторами. Также перегрузите оператор == для
сравнения векторов. Реализуйте доступ к элементам вектора по индексу.
9. Реализуйте класс MyString, представляющий строку, но хранящий ее
элементы в списке. Перегрузите операторы + для конкатенации строк.
Также перегрузите операторы ==, >, < для лексикографического сравнения
строк. Реализуйте доступ к символам строки по индексу.
10. Реализуйте класс BitArray, представляющий массив битов.
Перегрузите операторы [] для доступа к элементам массива по индексу.
Также перегрузите операторы &, |, ^, >> и << для выполнения побитовых
операций над битами массива.
11. Реализуйте класс Rectangle, представляющий прямоугольник.
Перегрузите оператор * для выполнения операции масштабирования
прямоугольника на коэффициент. Например, умножение прямоугольника
на 2 должно увеличивать его размеры вдвое. Также перегрузите операторы
сравнения, где экземпляры класса будут сравниваться по их площади.
12. Реализуйте класс Point, представляющий точку в двумерном
пространстве. Перегрузите операторы + и - для выполнения
арифметических операций с точками. Например, оператор + должен
позволять складывать две точки и получать новую точку как результат.
Предусмотрите метод, возвращающий расстояние между точками.
13. Реализуйте класс Employee, представляющий сотрудника компании.
Перегрузите операторы + и - для выполнения операций увеличения и
уменьшения зарплаты сотрудника соответственно. Также перегрузите
операторы равенства и неравенства для сравнения сотрудников по
зарплате.
14. Реализуйте класс IPAddress, представляющий IP-адрес. Перегрузите
операторы [] для доступа к октетам IP-адреса по индексу. Также
перегрузите операторы &, | и ^ для выполнения побитовых операций над
IP-адресами.
15. Реализуйте класс TemperatureSensor, представляющий датчик
температуры. Перегрузите операторы [] для доступа к значениям
температуры, полученной от датчика (класс Sensor), по индексу (например,
0 для значения в Цельсиях, 1 для значения в Фаренгейтах и т.д.). Также

335
перегрузите операторы &, | и ^ для выполнения побитовых операций над
значениями датчика температуры.

Таблица 4.3
Варианты работ
№ варианта Номера заданий к варианту
1. 2, 9
2. 3, 11
3. 4, 13
4. 5, 8
5. 6, 15
6. 1, 12
7. 3, 11
8. 4, 7
9. 7, 14
10. 8, 13
11. 9, 15
12. 4, 10
13. 5, 11
14. 6, 12
15. 7, 11
16. 8, 15
17. 10, 13
18. 1, 8
19. 2, 5
20. 3, 7

336
Глава 5. Сборка приложения. Работа с
файлами и директориями
В данной главе будет рассмотрено, какие существуют флаги сборки
приложений и как их применять на практике. Также коснемся момента
конфигурации приложения путем передачи необходимых данных через
терминал в момент его запуска. Отдельно рассмотрим механизмы Dart для
работы с файлами и директориями.
Файлы могут выступать в различном амплуа: от конфигурационных до
хранилищ. Например, в конфигурационные файлы выносится
информация, которую может изменять пользователь в настройках
приложения и влияющая на логику его работы. Это делается для большей
гибкости, так как хранение такой информации в самом коде программного
продукта подразумевает, что при малейшем ее изменении необходимо по
новой осуществлять сборку (компиляцию) программы. А для примера
использования файлов, как хранилищ, реализуем простую базу данных на
основе односвязного списка.

5.1. Сборка приложения


Для сборки приложений используется команда dart compile,
имеющая ряд конфигурационных флагов, отвечающих за режим
компиляции [16]:
Таблица 5.1
Флаги компиляции

Флаг
Результат Описание
компиляции

Автономный исполняемый файл,


компилируемый под целевую
Автономный
exe платформу и содержащий
исполняемый файл
урезанную среду выполнения
Dart.

Файл, компилируемый под


aot-snapshot Модуль AOT целевую платформу, без среды
выполнения Dart.

337
Файл, компилируемый под
целевую платформу, с
промежуточным
jit-snapshot Модуль JIT оптимизированным
представлением исходного кода,
который был получен в ходе
учебного запуска программы.

Файл, содержащий
промежуточное представление
исходного кода в виде
kernel Kernel модуль абстрактного синтаксического
дерева (Kernel AST).
Может запускаться на любых
платформах.

Файл с JavaScript кодом,


js Код на JavaScript сформированный на основе
файла с кодом на Dart.

В таблице, под целевой понимается та платформа, на которой


производится компиляция приложения (Windows, Linux и т.д.). Если вам
нужен полностью автономный файл приложения, то используйте флаг –
exe. Важна скорость и оптимизация – jit-snapshot. Хотите, чтобы
приложение запустилось на любой платформе – kernel.
У каждого из имеющихся флагов есть свои плюсы и минусы. Так
например, exe и aot-snapshot не поддерживают библиотеки dart:mirrors
и dart:developer. Флаг kernel не предоставляет стабильного API, поэтому
нет гарантии, что при сборке приложения на одной версии Dart, оно
запустится на другой. Для запуска файла, скомпилированного с флагом
aot-snapshot требуется утилита dartaotruntime, предоставляющая среду
выполнения Dart. А при желании запустить файлы, скомпилированные с
флагом jit-snapshot или kernel требуется наличие установленного на
платформе Dart SDK.
Далее рассмотрим, как осуществлять сборку приложения и его запуск
(за исключением использования флага js). А начнем с создания нового
консольного приложения «my_app», где не надо ничего добавлять или
удалять.

338
5.1.1. Флаг exe
Откройте терминал «Terminal -> New Terminal» и введите команду
dart compile exe bin\my_app.dart

где после флага сборки указывается путь до компилируемого файла


(будьте внимательны, если у вас Mac или Linux, то используйте другой
формат пути – bin/my_app.dart).
В директории «bin» проекта «my_app» должен появиться
скомпилированный файл с расширением «.exe»:

Рисунок 5.1 – Скомпилированное приложение

Без разницы, какая у вас операционная система, файл будет именно с


таким расширением. Это не значит, что он собран только для Windows! У
каждого флага компиляции свое выходное расширение, в данном случае –
exe.
Если необходимо поменять путь, куда переместить собранное
приложение или имя компилируемого файла, воспользуйтесь
дополнительным параметром -о:
dart compile exe bin\my_app.dart -o bin\new_app
или
dart compile exe bin\my_app.dart -o bin\new_app.exe

Для запуска приложения, скомпилированного таким образом,


достаточно указать до него путь в терминале и нажать Enter:

339
Рисунок 5.2 – Запуск приложения

5.1.2. Флаг aot-snapshot


Удалите скомпилированные ранее файлы из директории «bin», после
чего введите в терминале следующую команду:
dart compile aot-snapshot bin\my_app.dart
Как и с флагом exe, можно использовать дополнительный параметр -
о для указания, куда и с каким именем поместить скомпилированный
файл:
dart compile aot-snapshot bin\my_app.dart -o C:\code\new_app.aot

Для запуска приложения, скомпилированного с таким флагом, нам


понадобится утилита dartaotruntime, предоставляющая среду выполнения
Dart. Так как еще в самом начале, после установки Dart SDK мы
прописывали необходимые пути в переменной окружения path, то не надо
делать никаких телодвижений, а просто начать команду со слова
dartaotruntime:
dartaotruntime bin\my_app.aot

Рисунок 5.3 – Запуск приложения

5.1.3. Флаг jit-snapshot


Данный флаг не поддерживает параметр -о, так как в момент
компиляции осуществляется обучающий запуск приложения и то,
насколько качественно он будет выполнен отразится на итоговой

340
оптимизации приложения. В некоторых случаях удается достичь
значительного прироста по быстродействию.
Удалите скомпилированные ранее файлы из директории «bin» и
введите следующую команду:
dart compile jit-snapshot bin\my_app.dart

Рисунок 5.4 – Обучающий запуск приложения

В директории «bin» проекта «my_app» должен появиться


скомпилированный файл с расширением «.jit»:

Рисунок 5.5 – Скомпилированное приложение

Для запуска приложения, скомпилированного с таким флагом,


понадобится установленный на целевой платформе Dart SDK. В нашем
случае все уже установлено и настроено, поэтому можем сразу перейти к
последнему шагу. Для этого введите в терминале следующую команду:
dart run bin\my_app.jit

Рисунок 5.6 – Запуск приложения

5.1.4. Флаг kernel


Использование этого флага для компиляции ничем не отличается от
флага exe:
dart compile kernel bin\my_app.dart
или
dart compile kernel bin\my_app.dart -o C:\code\new_app

341
После передачи полученного файла третьей стороне, убедитесь, что
там имеется установленный Dart SDK:
dart run bin\my_app.dill

Рисунок 5.7 – Запуск приложения

5.2. Конфигурация запускаемого приложения


Бывают случаи, когда в момент старта приложения, ему необходимо
передать какой-то набор параметров (данных) для последующей
корректной работы. В принципе, нет четкого стандарта, как эти данные
окажутся в программе: считаются с конфигурационного файла, с базы
данных, либо передадутся напрямую с терминала в команде запуска.
Поскольку работе с файлами посвящены следующие разделы главы, то
здесь мы сосредоточимся на последнем варианте и рассмотрим зачем в
главной функции main такой входной аргумент, как List<String>
arguments.
Для начала немного перепишем тело функции main в файле
«my_app.dart», директории «bin»:
import 'package:my_app/my_app.dart' as my_app;

void main(List<String> arguments) {


print(arguments);
print('Hello world: ${my_app.calculate()}!');
}

Теперь скомпилируйте приложение с флагом exe и запустите его


следующим образом:
bin\my_app.exe
bin\my_app.exe -a 34 -b -_- -c hellow world!

342
Рисунок 5.8 – Запуск приложения

Обратите внимание, что все те данные, которые были указаны в


качестве параметров (-имяПараметра) приложения при запуске и
следующие за ними данные были переданы на вход функции main в списке
arguments.
Давайте немного перепишем код и сложим два числа, подающихся при
запуске приложения:
void main(List<String> arguments) {
print(arguments);
var a = int.tryParse(arguments[0]);
var b = int.tryParse(arguments[1]);
if (a == null || b == null) {
print('Invalid input');
return;
}
print('a + b = ${a + b}');
}

Снова скомпилируйте приложение и запустите его, используя


следующие команды:

Рисунок 5.9 – Запуск приложения

Но если на вход приложения не подать ни одного или одно значение,


то оно сразу завершится исключением. Поэтому при работе с
позиционными аргументами нужно учитывать множество факторов.

343
Особенно, когда их огромное количество. Из-за этого и используется флаги
и параметры (свойства – флаг с данными после него), которые позволяют
понять – есть ли в передаваемых на вход данных нужные значения или нет.
Не будем изобретать велосипед и воспользуемся уже готовым
пакетом: args (https://pub.dev/packages/args), позволяющим задать имена
параметров и флагов для их поиска и извлечения данных, а также
использования значений по умолчанию, если их не передали в момент
запуска приложения.
Откройте файл «pubspec.yaml» и добавьте в раздел с зависимостями
пакет args:
# Add regular dependencies here.
dependencies:
args: ^2.4.2
# path: ^1.8.0

Вернемся к файлу «my_app.dart» из директории «bin» и внесем в него


следующие изменения:
import 'package:args/args.dart';

void main(List<String> arguments) {


var parser = ArgParser();

parser.addOption( //добавляем параметр/свойство в парсер


'firts', // по данному ключу будет осуществляться поиск данных
abbr: 'a', // имя свойства при его указании в момент запуска
help: 'First number',
defaultsTo: '1', // значение по умолчанию
);
parser.addOption(
'second',
abbr: 'b',
help: 'Second number',
defaultsTo: '5',
);
parser.addFlag( //добавляем флаг в парсер
'subtract',
abbr: 's',
help: 'Subtract mode',
defaultsTo: false,
);

var args = parser.parse(arguments);


print(arguments);
var a = int.parse(args['firts']);
var b = int.parse(args['second']);
if (args['subtract']){

344
print('a - b = ${a - b}');
}else{
print('a + b = ${a + b}');
}
}

Скомпилируйте приложение и запустите его с различными


параметрами. Обратите внимание на то, что вне зависимости от количества
передаваемых на старте приложения значений, оно нормально работает.
Это связано с тем, что для каждого свойства и флага было указано значение
по умолчанию:

Рисунок 5.10 – Запуск приложения

5.3. Работа с файлами


Для создания, записи, чтения или добавления данных в файл
необходимо использовать класс File, импортируемый из библиотеки
dart:io. В конструктор создаваемого экземпляра класса File передается
путь до файла, с которым будет осуществляться работа:
import 'dart:io';

void main(List<String> arguments) {


var myFile = File('text.txt');
}

Если в конструкторе указывается только имя файла с его


расширением, то подразумевается, что он находится, либо создастся в

345
директории проекта. В случае работы с файлом из другой директории, путь
до него можно прописать следующим образом:
var myFile = File('F:\\code\\text.txt'); // для Windows

Когда файла по указанному пути не существует, при попытке


прочитать из него данные сгенерируется исключение, а при попытке
записать или добавить в него данные, перед началом выполнения операции
произойдет его создание. Сама работа с файлами может осуществляться как
в синхронном, так и в асинхронном режиме. Для работы в синхронном
режиме необходимо явно вызывать методы, которые заканчиваются на
Sync. Дополнительное отличие таких методов заключается в том, что
асинхронные всегда возвращают Future<T>, в то время как синхронные
могут ничего не возвращать (void). Более подробное с асинхронным
программированием познакомимся в следующей главе книги, а в этой
больше сосредоточимся на работе с файлами и директориями в
синхронном режиме.
Для начала давайте создадим файл, запишем в него данные и считаем
их:
import 'dart:io';

void main(List<String> arguments) {


var myFile = File('text.txt');
myFile.writeAsStringSync('Привет! О, этот чудный мир!!');
print(myFile.readAsStringSync()); // чтение всего файла
}
// Привет! О, этот чудный мир!!

Метод writeAsStringSync имеет следующие аргументы:


void writeAsStringSync (
String contents,
{FileMode mode = FileMode.write,
Encoding encoding = utf8,
bool flush = false}
),
где contents – записываемые данные, mode – текущий режим работы с
файлом (по умолчанию - только запись), encoding – кодировка
записываемых данных (по умолчанию – utf8), flush – флаг, отвечающий за
запись всех буферизированных данных в файл, т.е. он производит очистку
буфера вывода.
В приведенном выше коде метод writeAsStringSync вызывается с
параметрами по умолчанию, а это значит, что при повторном запуске
произойдет перезапись, а не добавление данных в файл. Чтобы добавить
данные в существующий файл, необходимо атрибуту mode передать
значение FileMode.append:

346
import 'dart:io';

void main(List<String> arguments) {


var myFile = File('text.txt');
myFile.writeAsStringSync('\nХочу обратно в школу!!!',
mode: FileMode.append,);
print(myFile.readAsStringSync()); // чтение всего файла
}
//Привет! О, этот чудный мир!!
//Хочу обратно в школу!!!

Существует несколько режимов работы с файлами:


− append – режим открытия файла для чтения и записи в конец. Файл
создается, если он еще не существует.
− read – режим открытия файла только для чтения.
− write – режим открытия файла для чтения и записи. Файл будет
перезаписан, если он уже существует. Файл создается, если он еще не
существует.
− writeOnly – режим открытия файла только для записи. Файл будет
перезаписан, если он уже существует. Файл создается, если он еще не
существует.
− writeOnlyAppend – режим открытия файла только для записи в конец
файла. Файл создается, если он еще не существует.
Если необходимо работать с файлом только в определенном режиме
(read, write или append), его следует явно указать в методе open или
openSync, после создания экземпляра класса File. Также можно
использовать методы openRead или openWrite. Все эти методы возвращают
объекты, через которые впоследствии осуществляется работа с файлом в
задаваемом режиме:
import 'dart:io';

void main(List<String> arguments) {


var myFile = File('text.txt');
var sink = myFile.openWrite(mode: FileMode.write);
var stringList = <String>['Ий-хо-хо!', 'И', 'бутылка', 'рома!'];
sink.writeAll(stringList, ' ');
}

В метод writeAll в качестве первого аргумента могут передаваться


объекты, поддерживающие итерацию, а второй аргумент представляет
собой разделитель между записываемыми данными.
Для более гибкого способа чтения файлов или при работе с файлами
большого размера следует использовать поток (Stream). Он предоставляет
данные в виде блоков байтов и позволяет задавать различные

347
преобразователи, чтобы представить содержимое файла в требуемом
формате. То есть класс File может работать не только с текстовыми
представлениями файлов, но и байтовыми:
import 'dart:io';
import 'dart:convert';
import 'dart:async';

void main() async {


final myFile = File('text.txt');
var stringList = <String>[
'Пятнадцать человек на сундук мертвеца.\n',
'Йо-хо-хо, и бутылка рому!\n',
'Пей, и дьявол тебя доведет до конца.\n',
'Йо-хо-хо, и бутылка рому!'
];
for (var element in stringList) {
myFile.writeAsStringSync(element, mode: FileMode.append);
}

Stream<String> lines = myFile


.openRead()
.transform(utf8.decoder) // Декодирование байтов в UTF-8.
.transform(LineSplitter()); // Разделитель данных
try {
await for (var line in lines) {
print('$line: ${line.length}');
}
print('Файл закрыт');
} catch (e) {
print('Ошибка: $e');
}
}
/* Пятнадцать человек на сундук мертвеца.: 38
Йо-хо-хо, и бутылка рому!: 25
Пей, и дьявол тебя доведет до конца.: 36
Йо-хо-хо, и бутылка рому!: 25
Файл закрыт */
Когда стоит задача – перед началом работы с файлом проверить его
наличие в системе, используйте метод existsSync:
import 'dart:io';

void main() async {


final myFile = File('../text.txt');
print(myFile.existsSync()); // false
}

348
В данном случае осуществляется проверка существует ли файл
text.txt на одном уровне выше директории запускаемого приложения или
нет.
Также имеется возможность выводить текущее состояние файла, с
которым будет осуществляться работа:
import 'dart:io';

void main() async {


final myFile = File('text.txt');
print(myFile.statSync());
}
/*FileStat: type file
changed 2023-10-28 11:29:38.000
modified 2023-10-28 13:23:21.000
accessed 2023-10-28 15:49:33.000
mode rw-rw-rw-
size 224 */

Нужно удалить файл? Используйте метод deleteSync или delete:


import 'dart:io';

void main() async {


final myFile = File('text.txt');
print(myFile.existsSync()); // true
myFile.deleteSync();
print(myFile.existsSync()); // false
}

Дополнительно имеется возможность явно создавать файлы, без


записи в них какого-либо значения. Для этого существуют методы
createSync и create, на вход которых атрибуту recursive подается булево
значение (по умолчанию false). Если данный параметр был установлен в
true, при наличии отсутствующих каталогов в пути к файлу, они будут
созданы, после чего создастся сам файл. В противном случае файл создается
только тогда, когда все каталоги на его пути уже существуют. Если таковых
не имеется – сгенерируется исключение FileSystemException:
import 'dart:io';

void main() async {


final myFile = File('data/text.txt');
print(myFile.existsSync()); // false
myFile.createSync(recursive: true);
print(myFile.existsSync()); // true
}

349
Для получения размера файла в байтах используйте метод length и
lengthSync:
import 'dart:io';

void main() async {


final myFile = File('data/text.txt');
myFile.writeAsStringSync('Мама мыла раму');
print(myFile.lengthSync()); // 26
myFile.writeAsStringSync('\nПотом батарею...');
print(myFile.lengthSync()); // 29
}

Время последней модификации файла можно узнать посредством


методов lastModifiedSync и lastModified:
import 'dart:io';

void main() async {


final myFile = File('data/text.txt');
print(myFile.lastModifiedSync()); // 2023-10-28 17:24:49.000
}

Когда нужно скопировать файл, воспользуйтесь методом copy или


copySync, который при копировании возвращает экземпляр класса File
для последующей работы с ним:
import 'dart:io';

void main() async {


final myFile = File('text.txt');
print(File('new.txt').existsSync()); // false
myFile.copySync('new.txt');
print(File('new.txt').existsSync()); // true
}

Переименовать имя не понравившегося файла вам поможет метод


rename или renameSync, возвращающий экземпляр класса File для
последующей работы с переименованным файлом:
import 'dart:io';

void main() async {


final myFile = File('text.txt');
print(File('NewRext.txt').existsSync()); // false
var newFile = myFile.renameSync('NewRext.txt');
print(File('NewRext.txt').existsSync()); // true
print(newFile.existsSync()); // true
}

350
Если хотите более подробного ознакомиться с методами класса File,
то обратитесь к документации Dart.

5.4. Работа с директориями


За работу с директориями в Dart отвечает класс Directory, которому
на вход подается либо абсолютный путь (начиная с указания диска), либо
относительный (расположение относительно места запуска приложения)
до необходимой папки. Как и File, класс Directory предоставляет два
способа работы – в асинхронном и синхронном режиме.
Для начала давайте посмотрим, как проверять – существует ли
директория в системе:
import 'dart:io';

void main() async {


// относительный путь, т.к. запуск производится
// из директории проекта
final myLocalDir1 = Directory('bin');
final myLocalDir2 = Directory('bin2');

print(myLocalDir1.existsSync()); // true
print(myLocalDir2.existsSync()); // false

// абсолютный путь
final myGlobalDir = Directory('C:\\code\\dart');
print(myGlobalDir.existsSync()); // true
}

За создание новой директории отвечают методы create и createSync,


на вход которых атрибуту recursive подается булево значение (по
умолчанию false). Если данный параметр был установлен в true, при
наличии отсутствующих каталогов в пути, они будут созданы:
import 'dart:io';

void main() async {


final myLocalDir = Directory('bin\\data\\input');

print(myLocalDir.existsSync());

// BAD
// myLocalDir.createSync();
// Error: Системе не удается найти указанный путь.

// OK
myLocalDir.createSync(recursive: true);

351
print(myLocalDir.existsSync()); // true
}

За удаление директории отвечают методы delete и deleteSync,


имеющие входной аргумент recursive (по умолчанию false). Если с
данным параметром по умолчанию попытаться удалить директорию,
которая содержит файлы или поддиректории – это приведет к исключению.
Т.е. каталог должен быть пустой в момент его удаления. Передача
аргументу recursive значение true при удалении директории
гарантирует, что все вложенные в нее файлы и поддиректории тоже будут
удалены:
import 'dart:io';

void main() async {


Directory('bin\\data\\input').createSync(recursive: true);
Directory('bin\\data\\input1').createSync();
Directory('bin\\data\\input2').createSync();

var myDir = Directory('bin\\data\\input');


print(myDir.existsSync()); // true
myDir.deleteSync();
print(myDir.existsSync()); // false

myDir = Directory('bin\\data');

// BAD
// myDir.deleteSync();
// OS Error: Папка не пуста.

// OK
myDir.deleteSync(recursive: true);
print(myDir.existsSync()); // false
}

Чтобы переименовать директорию, воспользуйтесь методом rename


или renameSync, возвращающим ссылку на переименованную директорию.
Такие методы можно использовать для переноса файла или директории в
другое место системы:
import 'dart:io';

void main() async {


var myDir = Directory('bin\\data\\input');
myDir.createSync(recursive: true);
print(myDir.existsSync()); // true

var newDir = myDir.renameSync('bin\\data\\inputW');

352
print(newDir.existsSync()); // true
print(myDir.existsSync()); // false

var newDir2 = newDir.renameSync('bin\\in');


print(newDir2.existsSync()); // true
print(newDir.existsSync()); // false
}

Текущую директорию запускаемого приложения, от которой потом


уже отталкиваться для построения пути к файлу, можно узнать следующим
способом:
import 'dart:io';

void main() async {


final rootPath = Directory.current.path;
print(rootPath); // C:\code\dart\my_app
}

А абсолютный путь до директории, которой при создании указали


относительный путь, хранит свойство класса absolute:
import 'dart:io';

void main() async {


final path = Directory('bin');
print(path.absolute.path); // C:\code\my_app\bin
}

Чтобы проникнуть во внутренний мир директории, а точнее узнать ее


содержание, воспользуйтесь методами list и listSync, принимающими на
вход два аргумента: recursive (по умолчанию false) и boolfollowLinks
(по умолчанию true). При передачи аргументу recursive значения true
будет выполняться рекурсивный заход в подкаталоги директории.
Аргумент boolfollowLinks отвечает за работу с ссылками (ярлыки на
другие папки или файлы). При значении false все найденные ссылки
возвращаются как экземпляры класса Link, а не как каталоги или файлы, и
не повторяются. Иначе, ссылки отображаются как каталоги (используются
при рекурсивном обходе директории) или файлы, в зависимости от того, на
что они указывают. Данные методы возвращают список экземпляров
класса FileSystemEntity, который является базовым классом для File,
Directory и Link:
import 'dart:io';

void main() async {


final path = Directory.current;
var myList = path.listSync();

353
for(var item in myList){
if (FileSystemEntity.isFileSync(item.path)){
print('${item.path} - is file');
}
if (FileSystemEntity.isDirectorySync(item.path)){
print('${item.path} - is directory');
}
}

print(path.listSync(recursive: true));
}
/*C:\code\my_app\.dart_tool - is directory
C:\code\my_app\.gitignore - is file
C:\code\my_app\analysis_options.yaml - is file
C:\code\my_app\bin - is directory
C:\code\my_app\CHANGELOG.md - is file
C:\code\my_app\lib - is directory
C:\code\my_app\pubspec.lock - is file
C:\code\my_app\pubspec.yaml - is file
C:\code\my_app\README.md - is file
C:\code\my_app\test - is directory*/

Текущее состояние директории можно узнать, используя метод stat


или statSync:
import 'dart:io';

void main() async {


final dir = Directory.current;
print(dir.statSync());
}
/* FileStat: type directory
changed 2023-10-26 15:44:04.000
modified 2023-10-26 15:44:04.000
accessed 2023-10-27 12:19:11.000
mode rwxrwxrwx
size 4096 */

Порой бывает необходимо в текущей директории создать временный


каталог для размещения там файлов, которые не нужны по завершению
работы приложения и должны бить удалены. Для этих целей класс
Directory предоставляет метод createTemp и createTempSync,
принимающий на вход не обязательный аргумент [String? prefix] и
возвращающий ссылку на экземпляр класса Directory, связанный с
созданной временной директорией. Если prefix задан, то после него
добавляются случайные символы, чтобы гарантировать создание

354
уникальной временной директории, в противном случае в качестве
префикса используется пустая строка:
import 'dart:io';

void main() async {


final dir = Directory('bin');
var tempDir = dir.createTempSync('myTemp');
dir.createTempSync();
for (var item in dir.listSync()) {
print(item);
}
}
/* Directory: 'bin\67c3dd9d'
Directory: 'bin\myTempefd7b43e'
File: 'bin\my_app.dart' */

Временные директории, созданные таким образом, должны удаляться


вручную. Конечно, можно их создавать в временной директории
операционной системы, тем самым возлагая на ОС их удаление, в
соответствии с настроенными политиками. Для получения такого каталога
используйте статическое свойство класса Directory – systemTemp:
import 'dart:io';

void main() async {


final dir = Directory.systemTemp;
print(dir.path); // C:\Users\MADTEA~1\AppData\Local\Temp
}
Обычно для работы с путями используются специализированные
пакеты: path (для чистого Dart) и path_provider (для Flutter). Это связано с
тем, что для каждой операционной системы они могут формироваться
различным образом и держать каждый такой момент в голове – то еще
удовольствие.
Если хотите более подробного ознакомиться с методами класса
Directory, то обратитесь к документации Dart.

5.5. База данных на основе файла и


однонаправленного списка
Создайте консольное приложение «database» со следующей
структурой файлов:

355
Рисунок 5.11 – Структура проекта «database»

Начнем разработку проекта с объявления интерфейса, который


должны будут реализовать записи таблицы:
// i_attribute.dart
abstract interface class IAttribute {
bool check(String attribute, String value);
bool change(String attribute, String value);

@override
String toString();
}

Первый метод интерфейса будет использоваться для проверки


значения по определенному столбцу таблицы базы данных в записи,
реализующей интерфейс, а второй – чтобы по заданному полю записи
изменить значения.
Обязанность реализовать этот интерфейс ложится на класс User из
файла «user.dart». Откройте этот файл, импортируйте интерфейс и
объявите пару перечислений. Первое из них ( AcsessLevel) будет отвечать
за уровень доступа, который имеется у пользователя. А второе
(UserTableFields) позволит не держать в голове, какие поля будут у
таблицы пользователей базы данных, когда мы захотим к ним обратиться
через методы интерфейса:
// user.dart
import 'i_attribute.dart';

enum AcsessLevel {
student('S'),
teacher('T'),
admin('A');

356
final String text;
const AcsessLevel(this.text);

static AcsessLevel fromString(String value) {


return switch (value) {
'T' => AcsessLevel.teacher,
'A' => AcsessLevel.admin,
_ => AcsessLevel.student
};
}

@override
String toString() {
return text;
}
}

enum UserTableFields {
id('id'),
nickname('nickname'),
yearOfBirth('yearOfBirth'),
email('email'),
phone('phone'),
acsessLevel('acsessLevel'),
passwordHash('passwordHash');

final String text;


const UserTableFields(this.text);

@override
String toString() {
return text;
}
}

Далее объявим класс User, его поля (id, псевдоним, год рождения,
электронная почта, номер телефона, уровень доступа и хеш пароля) и
реализуем методы интерфейса IAttribute:
// user.dart
class User implements IAttribute {
final int _id;
String _nickname;
int _yearOfBirth;
String _email;
String _phone;
AcsessLevel _acsessLevel;

357
String _passwordHash;

User({
required int id,
required String nickname,
required int yearOfBirth,
required String email,
required String phone,
required String acsessLevel,
required String passwordHash,
}) : _id = id,
_nickname = nickname,
_yearOfBirth = yearOfBirth,
_email = email,
_phone = phone,
_acsessLevel = AcsessLevel.fromString(
acsessLevel.toUpperCase(),
),
_passwordHash = passwordHash;

int get id => _id;


String get nickname => _nickname;
int get yearOfBirth => _yearOfBirth;
String get email => _email;
String get phone => _phone;
String get acsessLevel => _acsessLevel.text;
String get passwordHash => _passwordHash;

@override
bool change(String attribute, String value) {
switch (attribute) {
case 'nickname':
_nickname = value;
case 'yearOfBirth':
_yearOfBirth = int.parse(value);
case 'email':
_email = value;
case 'phone':
_phone = value;
case 'acsessLevel':
_acsessLevel = AcsessLevel.fromString(value);
case 'passwordHash':
_passwordHash = value;
default:
return false;
}
return true;
}

358
@override
bool check(String attribute, String value) {
return switch (attribute) {
'id' => _id == int.parse(value),
'nickname' => _nickname == value,
'yearOfBirth' => _yearOfBirth == int.parse(value),
'email' => _email == value,
'phone' => _phone == value,
'acsessLevel' => _acsessLevel.text == value,
'passwordHash' => _passwordHash == value,
_ => false
};
}

@override
String toString() {
StringBuffer buffer = StringBuffer();
buffer.write('User(id: $_id, nickname: $_nickname, ');
buffer.write('yearOfBirth: $_yearOfBirth, email: $_email, ');
buffer.write('phone: $_phone, acsessLevel: $_acsessLevel, ');
buffer.write('passwordHash: $_passwordHash)');
return buffer.toString();
}
}

Перейдите в файл «table.dart». Здесь мы объявим класс узла


однонаправленного списка, посредством дженериков указав, что хранимые
в нем данные должны реализовывать интерфейс IAttribute:
// table.dart
import 'i_attribute.dart';
import 'user.dart';

final class _Node<T extends IAttribute> {


T data;
_Node? next;

_Node(this.data, [this.next]);

@override
String toString() {
return data.toString();
}
}

359
Далее объявим класс Table, в который будем добавлять методы работы
с таблицей базы данных. Так как у нас она достаточно простая, то каждая
БД будет содержать только одну таблицу пользователей:
// table.dart
final class Table<T extends IAttribute> {
_Node? _head;
_Node? _tail;

String title;

Table(this.title);

T? get last => _tail?.data as T;


T? get first => _head?.data as T;
}

Поле _head будет хранить ссылку на вершину односвязного списка, а


_tail на его последний элемент. Добавлять всегда будем в конец, обновляя
объект, на который ссылается данное поле.
Теперь расширьте класс, добавив в него метод для добавления записи
в таблицу и проверки на то, существует ли запись с таким идентификатором
в таблице или нет. Обратите внимание, что в методе contains нами
осуществляется обход списка, начиная с его вершины и до тех пор,
следующий за узлом элемент не представляет собой null:
bool contains(T data) {
var temp = _head;
if (data is User) {
while (temp != null) {
if (temp.data.check(
UserTableFields.id.text,
data.id.toString(),
)) {
return true;
}
temp = temp.next;
}
}
return false;
}

bool insert(T data) {


var node = _Node(data);
if (_head == null) {
_head = node;
_tail = node;
return true;

360
} else if (!contains(data)) {
_tail?.next = node;
_tail = node;
return true;
}
return false;
}

За удаление записи из таблицы (по ее идентификатору) будет отвечать


метод remove. В этом случае нам надо хранить ссылку на предыдущий узел
перед удаляемым, чтобы его поле next связать со следующим узлом,
который располагается после удаляемого:
void remove(String id) {
var current = _head;
_Node? prev;

while (current != null &&


!(current.data.check(
UserTableFields.id.text,
id,
))) {
prev = current;
current = current.next;
}

if (current == null) {
return;
}

prev?.next = current.next;
}

Для того, чтобы экземпляры класса Table поддерживали операции


пересечения и объединения, объявите следующие методы:
Table<T> intersect(
String attribute,
String value,
Table<T> table,
) {
var newTable = Table<T>('$title-${table.title}');
var temp = _head;
while (temp != null) {
if (temp.data.check(attribute, value)) {
newTable.insert(temp.data as T);
}
temp = temp.next;
}

361
temp = table._head;
while (temp != null) {
if (temp.data.check(attribute, value)) {
if (!newTable.contains(temp.data as T)) {
newTable.insert(temp.data as T);
}
}
temp = temp.next;
}
return newTable;
}

Table<T> union(Table<T> table) {


var newTable = Table<T>('$title-${table.title}');
var temp = _head;

while (temp != null) {


newTable.insert(temp.data as T);
temp = temp.next;
}

temp = table._head;
while (temp != null) {
if (!newTable.contains(temp.data as T)) {
newTable.insert(temp.data as T);
}
temp = temp.next;
}

return newTable;
}

Для произведения пересечения необходимо передать поле записи, по


которому оно будет производиться и значение, которое должны содержать
в этом поле записи таблиц. У нас довольно простая база данных, поэтому
таблица по результатам пересечения или объединения может не содержать
некоторые записи, т.к. их идентификатор совпадает с имеющейся уже в
новой таблице записью в момент ее добавления (подумайте, как это
исправить а также убрать импорт файла «user.dart» из библиотеки,
описывающей таблицу).
Следующий метод реализует функционал запроса к таблице и
возвращает новую таблицу, записи которой удовлетворяют условию, что в
задаваемом поле хранится необходимое значение:
Table<T> selection(String attribute, String value) {
var newTable = Table<T>('$title-new');

362
var temp = _head;

while (temp != null) {


if (temp.data.check(attribute, value)) {
newTable.insert(temp.data as T);
}
temp = temp.next;
}

return newTable;
}

Последние два метода класса Table не представляют собой ничего


сложного:
void forEach(void Function(T data) action) {
// обход элементов таблицы с передачей их в функцию action
var temp = _head;
while (temp != null) {
action(temp.data as T);
temp = temp.next;
}
}

@override
String toString() {
StringBuffer buffer = StringBuffer();
var temp = _head;
buffer.writeln('${'*' * 10}$title${'*' * 10}');
while (temp != null) {
buffer.writeln(temp.data);
temp = temp.next;
}
return buffer.toString();
}

Теперь перейдите к файлу «database.dart» директории «lib». Здесь


мы опишем интерфейс взаимодействия с базой данных, состоящей из
одной таблицы, а также реализуем функционал записи дынных из БД в
файл и их чтение при запуске приложения. Для некоторого допущения,
давайте считать, что в системе должно быть 2 БД. В одной будут храниться
записи пользователей внутренней системы одного вуза (СПбГУАП – SUAI),
а в другой другого (СПбГЭУ – UNECON). Все эти обязанности мы возложим
на класс Database, но сначала надо настроить экспорт ряда функционала из
библиотеки в основное приложение и импорт в текущий файл,
реализованных ранее классов:

363
// database.dart
export 'src/table.dart';
export 'src/user.dart';

import 'dart:io';

import 'src/table.dart';
import 'src/user.dart';

enum DBType {
suai,
unecon,
}

Далее, в текущем файле, объявите класс Database, содержащий


конструктор и приватный метод для чтения данных таблицы из файла:
class Database {
final String pathToSuaiDB;
final String pathToUneconDB;
late Table<User> _suaiUsers;
late Table<User> _uneconUsers;

Database({
this.pathToSuaiDB = 'suai.txt',
this.pathToUneconDB = 'unecon.txt',
}) {
if (!File(pathToSuaiDB).existsSync()) {
File(pathToSuaiDB).createSync(recursive: true);
_suaiUsers = Table<User>('suai');
} else {
_suaiUsers = _openTable(File(pathToSuaiDB), 'suai');
}

if (!File(pathToUneconDB).existsSync()) {
File(pathToUneconDB).createSync(recursive: true);
_uneconUsers = Table<User>('unecon');
} else {
_uneconUsers = _openTable(File(pathToUneconDB), 'unecon');
}
}

Table<User> _openTable(File file, String tableName) {


var table = Table<User>(tableName);
for (var line in file.readAsLinesSync()) {
var data = line.split(',');
table.insert(User(
id: int.parse(data[0]),

364
nickname: data[1],
yearOfBirth: int.parse(data[2]),
email: data[3],
phone: data[4],
acsessLevel: data[5],
passwordHash: data[6],
));
}

return table;
}
}

Как видно из метода _openTable, в качестве разделителей между


данными записи у нас выступают запятые, а сами записи таблицы хранятся
в файле построчно.
Следующие методы, отвечают за сохранение данных из таблицы в
файл и добавление новой записи в таблицу:
void save(DBType type) {
File file;
Table<User> users;

switch (type) {
case DBType.suai:
file = File(pathToSuaiDB);
users = _suaiUsers;
case DBType.unecon:
file = File(pathToUneconDB);
users = _uneconUsers;
}

var sink = file.openWrite();


users.forEach((user) {
StringBuffer buffer = StringBuffer();
buffer.write('${user.id},${user.nickname},');
buffer.write('${user.yearOfBirth},${user.email},');
buffer.write('${user.phone},${user.acsessLevel},');
buffer.write('${user.passwordHash}\n');
sink.write(buffer.toString());
});
sink.close();
}

bool insert(User user, DBType type) {


File file;
bool isOk = false;
switch (type) {

365
case DBType.suai:
file = File(pathToSuaiDB);
isOk = _suaiUsers.insert(user);
case DBType.unecon:
file = File(pathToUneconDB);
isOk = _uneconUsers.insert(user);
}

if (!isOk) {
return false;
}

StringBuffer buffer = StringBuffer();


buffer.write('${user.id},${user.nickname},');
buffer.write('${user.yearOfBirth},${user.email},');
buffer.write('${user.phone},${user.acsessLevel},');
buffer.write('${user.passwordHash}\n');

file.writeAsStringSync(
buffer.toString(),
mode: FileMode.append,
);
return true;
}

Следующие методы базы данных по своей сути являются обертками


над уже имеющимся функционалом таблицы:
Table<User> selection({
required DBType type,
required String attribute,
required String value,
}) {
return switch (type) {
DBType.suai => _suaiUsers.selection(attribute, value),
DBType.unecon => _uneconUsers.selection(attribute, value),
};
}

Table<User> intersect(String attribute, String value) {


var temp = _suaiUsers.intersect(
attribute,
value,
_uneconUsers,
);
return temp;
}

366
Table<User> union() {
var temp = _suaiUsers.union(_uneconUsers);
return temp;
}

void remove({
required String id,
required DBType type,
}) {
switch (type) {
case DBType.suai:
_suaiUsers.remove(id);
print(_suaiUsers);
case DBType.unecon:
_uneconUsers.remove(id);
print(_uneconUsers);
}
}

void showDB(DBType type) {


switch (type) {
case DBType.suai:
print(_suaiUsers);
case DBType.unecon:
print(_uneconUsers);
}
}

Настало время реализовать консольное меню разрабатываемого


приложения. Перейдите к файлу «menu.dart» директории «bin» и добавьте
в него следующий код:
// menu.dart
import 'dart:io';

import 'package:database/database.dart';

class Menu {
final Database database;
Menu(this.database);

void loop() {
while (true) {
printMenu();
var input = stdin.readLineSync()!;
print('x' * 25);
switch (input) {
case '1':

367
addUser();
case '2':
removeUser();
case '3':
changeUser();
case '4':
showUsers();
case '5':
intersect();
case '6':
union();
case '7':
saveAndExit();
return;
case '8':
return;
}
}
}

void printMenu() {
print('1. Add User');
print('2. Remove User');
print('3. Change User');
print('4. Show Users');
print('5. Intersect Table');
print('6. Union Table');
print('7. Save and Exit');
print('8. Exit');
}
}

Поскольку не хочется расширять объем книги и так понятными


вещами, то в ряде методах не реализованы дополнительные проверки. Это
не критично, если все вводить правильно, до доставит ряд неудобств, если
забыли какой тип данных сейчас должен быть введен. Поэтому рекомендую
эти проверки вам добавить самостоятельно. Например, это касается метода
добавления пользователя:
void addUser() {
try {
stdout.write('Enter id: ');
var id = int.parse(stdin.readLineSync()!);
stdout.write('Enter nickname: ');
var nickname = stdin.readLineSync()!;
stdout.write('Enter year of birth: ');
var yearOfBirth = int.parse(stdin.readLineSync()!);
stdout.write('Enter email: ');

368
var email = stdin.readLineSync()!;
stdout.write('Enter phone: ');
var phone = stdin.readLineSync()!;
stdout.write('Enter password hash: ');
var passwordHash = stdin.readLineSync()!;
stdout.write(
'Acsess level (A - Admin, T - Teacher, S - Student): ',
);
var acsessLevel = stdin.readLineSync()!;
var user = User(
id: id,
nickname: nickname,
yearOfBirth: yearOfBirth,
email: email,
phone: phone,
acsessLevel: acsessLevel,
passwordHash: passwordHash,
);

stdout.write('Add user to DB (S - SUAI, U - Unecon): ');


var type = stdin.readLineSync()!;
switch (type.toUpperCase()) {
case 'S':
database.insert(user, DBType.suai);
database.showDB(DBType.suai);
case 'U':
database.insert(user, DBType.unecon);
database.showDB(DBType.unecon);
default:
print('(︶︿︶)_╭∩╮');
return;
}
} catch (e) {
print('WTF!!!! $e');
}
}

Следующий метод позволит удалять данные из таблицы БД:


void removeUser() {
stdout.write('Add user to DB (S - SUAI, U - Unecon): ');
var type = stdin.readLineSync()!;
late DBType typeDB;
switch (type.toUpperCase()) {
case 'S':
typeDB = DBType.suai;
database.showDB(DBType.suai);
case 'U':

369
typeDB = DBType.unecon;
database.showDB(DBType.unecon);
default:
print('(︶︿︶)_╭∩╮');
return;
}
stdout.write('Enter id: ');
var id = stdin.readLineSync()!;
database.remove(id: id, type: typeDB);
database.showDB(typeDB);
}

Далее реализуем метод, который предоставит пользователю


возможность менять значения полей записи таблицы. Только надо
корректно вводить поле таблицы, по которому должно быть изменено
значение:
void changeUser() {
stdout.write('Select DB (S - SUAI, U - Unecon): ');
var type = stdin.readLineSync()!;
late DBType typeDB;
switch (type.toUpperCase()) {
case 'S':
typeDB = DBType.suai;
database.showDB(DBType.suai);
case 'U':
typeDB = DBType.unecon;
database.showDB(DBType.unecon);
default:
print('(︶︿︶)_╭∩╮');
return;
}
stdout.write('Enter id: ');
var id = stdin.readLineSync()!;
stdout.write('Enter nickname: ');
var user = database
.selection(
type: typeDB,
attribute: 'id',
value: id,
)
.first!;

stdout.write('Enter field: ');


var field = stdin.readLineSync()!;
stdout.write('Enter new value: ');
var newValue = stdin.readLineSync()!;
user.change(field, newValue);

370
database.showDB(typeDB);
}

Последними для меню реализуем методы вывода текущего состояния


таблицы, объединения и пересечение таблиц, а также сохранение данных в
файл, перед закрытием приложения:
void showUsers() {
stdout.write('Select DB (S - SUAI, U - Unecon): ');
var type = stdin.readLineSync()!;
switch (type.toUpperCase()) {
case 'S':
database.showDB(DBType.suai);
case 'U':
database.showDB(DBType.unecon);
default:
print('(︶︿︶)_╭∩╮');
return;
}
}

void intersect() {
stdout.write('Enter field: ');
var field = stdin.readLineSync()!;
stdout.write('Enter intersect value: ');
var value = stdin.readLineSync()!;
print(database.intersect(field, value));
}

void union() {
print(database.union());
}

void saveAndExit() {
database.save(DBType.suai);
database.save(DBType.unecon);
}

Для того, чтобы приложение обрело законченный вид, нам


необходимо объединить реализованный функционал в файле
«database.dart» директории «bin», указать место хранения файлов с
данными таблиц и запустить метод loop меню, предварительно передав в
его конструктом экземпляр класса Database:
import 'package:database/database.dart';

import 'menu.dart';

371
void main(List<String> arguments) {
var db = Database(
pathToSuaiDB: 'bin\\suai.txt',
pathToUneconDB: 'bin\\unecon.txt',
);
var menu = Menu(db);
menu.loop();
}

Если вам лень добавлять пользователей вручную, но хочется


повозиться с функционалом реализованного приложения, можете взять с
репозитория текстовые файлы, поместить их в директорию «bin» и
запустить программу:

Рисунок 5.12 – Запуск приложения

Давайте удалим пользователя из БД. Для этого нам необходимо


выбрать второй пункт меню, базу, с которой будет осуществляться удаление
и id учетной записи:

Рисунок 5.13 – Удаление пользователя

Главное, если хотите сохранить эти изменения, не забудьте


осуществить выход с сохранением! А если не терпится посмотреть, как

372
данные хранятся в файле, то откройте один из них и вас встретит, что-то
такое:

Рисунок 5.14 – Структура файла «suai.txt»

5.6. Работа с JSON-файлами


JSON (JavaScript Object Notation) - довольно простой и
повсеместно используемых формат обмена данными. Его достаточно
удобно читать. Это относится как к человеку, так и к компьютеру.
Дополнительным достоинством JSON является то, что он по сути
представляет собой текстовый формат данных и полностью независим от
языка реализации. То есть сохраненные данные в JSON-формате
средствами одного языка программирования могут быть использованы в
приложении, которое реализовано на другом языке. Именно поэтому он
обрел огромную популярность и широкое распространение.
В основе JSON находятся 2 структуры:
− Коллекция пар «ключ:значение» (объект). На разных языках
программирования концепция такого объекта реализована по-
своему. В Dart, в качестве такой коллекции, используется тип данных
Map<K, V>;
− Упорядоченный список значений (массив, список,
последовательность и т. д.).
Каждый объект представляет из себя неупорядоченную коллекцию
пар «ключ:значение», чье тело заключается в фигурные скобки: {объект}.
Ключом может выступать только строковый тип данных. Каждый ключ
заключается в двойные кавычки ("имяКлюча"), после которых идет
разделительный символ «:» (двоеточие), а пары «ключ:значение»
разделяются между собой запятой.
Значение может представлять собой: строку в двойных кавычках,
число, логический тип данных (true/false), null, объект или массив. То
есть объект может содержать в себе другой объект, массив из объектов или
прочих свойств.
В качестве примера давайте приведем описание объекта Фильм (Film)
со списком актеров, жанром и пользовательскими обзорами в JSON-
формате:

373
{
"name": "Prevozmogun",
"budget": 2000000,
"actors": [
{
"name": "Alexey",
"age": 25,
"filmsAmount": 3,
"aboutActor": "2 academy awards"
},
{
"name": "Max",
"age": 33,
"filmsAmount": 2,
"aboutActor": "Very good actor"
},
{
"name": "Natalya",
"age": 18,
"filmsAmount": 1,
"aboutActor": "Rising star"
}
],
"criticsRating": 7.5,
"audienceRating": 8.5,
"year": 2019,
"country": "Russia",
"genre": [
"Comedy",
"Drama",
"Romance"
],
"reviews": [
{
"name": "Igor",
"text": "I love this movie",
"rating": 10
},
{
"name": "John",
"text": "I hate this movie",
"rating": 1
},
{
"name": "Stas",
"text": "I like this movie",
"rating": 7
}
]
}

374
Прежде чем прейдем к написанию кода, нам предстоит разобраться с
парочкой определений. А именно – сериализация и десериализация
данных. Под сериализацией (кодированием) понимается процесс
преобразования какой-либо структуры данных в формат, позволяющий их
хранение и передачу. Для данного процесса в Dart используется функция
jsonEncode библиотеки dart:convert, которая позволяет осуществлять
перевод значения любого типа в строку (последовательность байт).
Десериализация (декодирование) – процесс обратный сериализации, при
котором из полученных, загруженных или прочитанных данных
производится восстановление структуры данных с необходимыми
параметрами. Для этого процесса в Dart используется функция jsonDecode
библиотеки dart:convert, которая позволяет осуществлять перевод строки
(последовательности байт) в значение любого типа данных.
Если более пристально присмотреться к JSON-представлению фильма,
то можно заметить, что он включает в себя еще 2 класса, а точнее список из
них: Review, Actor. Но мы выделим еще один – Genre, представляющий
собой список жанров.
Создайте новый консольный проект «filmography» со следующей
структурой и наполнением директорий:

Рисунок 5.15 – Структура проекта «filmography»

В файл «film.json» скопируйте текст рассматриваемого ранее


примера и откройте файл «actor.dart». С него то мы и начнем написание
кода. Чтобы предоставить возможность сериализовать экземпляр класса,
необходимо при его описании объявить метод toJson, возвращающий тип
данных Map<String, dynamic>, а для десериализации использовать
фабричный именованный конструктор fromJson:

375
// actor.dart
class Actor{
final String name;
final int age;
final int filmsAmount;
final String aboutActor;

Actor({
required this.name,
required this.age,
required this.filmsAmount,
required this.aboutActor
});

factory Actor.fromJson(Map<String, dynamic> json) {


return Actor(
name: json['name'], // неявное приведение
age: json['age'],
filmsAmount: json['filmsAmount'],
aboutActor: json['aboutActor']
);
}

Map<String, dynamic> toJson() => {


'name': name,
'age': age,
'filmsAmount': filmsAmount,
'aboutActor': aboutActor
};

@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('Actor{name: $name, age: $age, ');
sb.write('filmsAmount: $filmsAmount, ');
sb.write('aboutActor: $aboutActor}');
return sb.toString();
}
}

Следом откройте файл «review.dart» и по лекалу реализованного


ранее Actor, опишите класс, представляющий собой пользовательский
обзор на фильм:
// review.dart
class Review{
final String name;
final String text;

376
final double rating;

Review({
required this.name,
required this.text,
required this.rating
});

factory Review.fromJson(Map<String, dynamic> json) {


return Review(
name: json['name'],
text: json['text'],
rating: json['rating']
);
}

Map<String, dynamic> toJson() => {


'name': name,
'text': text,
'rating': rating
};

@override
String toString() {
return 'Review{name: $name, text: $text, rating: $rating}';
}
}

Далее перейдите к файлу «genre.dart» и добавьте в него следующий


код:
// genre.dart
class Genre {
final List<String> genre;

Genre(this.genre);

factory Genre.fromJson(List<dynamic> json){


return Genre(
json.map((e) => e as String).toList()
);
}

List<dynamic> toJson() => genre;

@override
String toString() {
return 'Genre{genre: $genre}';

377
}
}

Так как поле genre в JSON-файле представляет собой список строк, то


это отразилось на реализации класса Genre, а обязанность при
сериализации экземпляра класса связать эти данные с ключом, будет
возложена на класс Movie:
// movie.dart
import 'actor.dart';
import 'genre.dart';
import 'review.dart';

class Movie {
final String name;
final int budget;
final List<Actor> actors;
final double criticsRating;
final double audienceRating;
final int year;
final String country;
final Genre genre;
final List<Review> reviews;

Movie(
{required this.name,
required this.budget,
required this.actors,
required this.criticsRating,
required this.audienceRating,
required this.year,
required this.country,
required this.genre,
required this.reviews},
);

factory Movie.fromJson(Map<String, dynamic> json) {


return Movie(
name: json['name'],
budget: json['budget'],
actors: List<Actor>.from(
json['actors'].map((x) => Actor.fromJson(x)),
),
criticsRating: json['criticsRating'],
audienceRating: json['audienceRating'],
year: json['year'],
country: json['country'],
genre: Genre.fromJson(json['genre'] as List<dynamic>),

378
reviews: List<Review>.from(
json['reviews'].map((x) => Review.fromJson(x)),
));
}

Map<String, dynamic> toJson() => {


'name': name,
'budget': budget,
'actors': actors,
'criticsRating': criticsRating,
'audienceRating': audienceRating,
'year': year,
'country': country,
'genre': genre,
'reviews': reviews
};

@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('Movie{name: $name, budget: $budget, ');
sb.write('actors: $actors, criticsRating: $criticsRating, ');
sb.write('audienceRating: $audienceRating, year: $year, ');
sb.write(
'country: $country, genre: $genre, reviews: $reviews}'
);
return sb.toString();
}
}

Чтобы экспортировать из библиотеки реализованные классы, добавьте


следующий код в файл «filmography.dart» директории «lib»:
export 'src/actor.dart';
export 'src/genre.dart';
export 'src/movie.dart';
export 'src/review.dart';

Вот мы и подобрались к моменту, когда можем прочитать данные с


JSON-файла, десериализовать их в объект, после чего этот объект
сериализовать и записать получившийся результат в новый файл. Для этого
перейдите к файлу «filmography.dart» в директории «bin»:
// bin - filmography.dart
import 'dart:io';
import 'dart:convert';

import 'package:filmography/filmography.dart';

379
void main(List<String> arguments) {
var myFile = File('bin\\film.json');
var json = jsonDecode(myFile.readAsStringSync());
var movie = Movie.fromJson(json);

print(movie);

var myFile2 = File('bin\\output.json');


myFile2.writeAsStringSync(jsonEncode(movie));
}
/* Movie{name: Prevozmogun, budget: 2000000, actors: [Actor{name:
Alexey, age: 25, filmsAmount: 3, aboutActor: 2 academy awards},
Actor{name: Max, age: 33, filmsAmount: 2, aboutActor: Very good
actor}, Actor{name: Natalya, age: 18, filmsAmount: 1, aboutActor:
Rising star}], criticsRating: 7.5, audienceRating: 8.5, year: 2019,
country: Russia, genre: Genre{genre: [Comedy, Drama, Romance]},
reviews: [Review{name: Igor, text: I love this movie, rating: 10.0},
Review{name: John, text: I hate this movie, rating: 1.0},
Review{name: Stas, text: I like this movie, rating: 7.0}]} */

У такой записи в файл «output.json» имеется небольшая проблема.


Данные в нем будут записаны в одну строку, в связи с чем, осуществлять по
ним какую-либо навигацию – то еще удовольствие. Для исправления этого
недуга воспользуемся классом JsonEncoder:
// bin - filmography.dart
import 'dart:io';
import 'dart:convert';

import 'package:filmography/filmography.dart';

void main(List<String> arguments) {


var myFile = File('bin\\film.json');
var json = jsonDecode(myFile.readAsStringSync());
var movie = Movie.fromJson(json);

print(movie);

var myFile2 = File('bin\\output.json');


var encoder = JsonEncoder.withIndent(' ');
myFile2.writeAsStringSync(encoder.convert(movie));
}

5.6.1. Зачем нужен null?


В ряде случаев, данные могут не храниться на сервере. Поэтому такие
поля в возвращаемом JSON содержат значение null. Либо мы изначально
проектируем систему таким образом, что они не критичны. Так, например,

380
текст отзыва зритель может и не оставить, но выставить рейтинг фильму в
порыве нахлынувших чувств после просмотра – обязан, как и указать свое
имя или никнейм.
Если вы замените текст одного из обзоров в файле «film.json» на null
и запустите приложение, то оно завершится исключением: «_TypeError
(type 'Null' is not a subtype of type 'String')». Чтобы такого больше
происходило, достаточно тип поля text класса Review обозначить как не
null-safety:
// review.dart
class Review{
final String name;
final String? text;
final double rating;
// далее без изменений
}

Но при таком раскладе, в процессе создания экземпляра класса при


десериализации JSON, полю text будет присвоено значение null. Не совсем
приятный момент… ведь чаще всего необходимо предусмотреть: если по
ключу в JSON хранится null, то поле класса должно быть
проинициализировано значением по умолчанию. Для этого придется
внести изменения в фабричный конструктор:
factory Review.fromJson(Map<String, dynamic> json) {
return Review(
name: json['name'],
text: json['text'] ?? '',
rating: json['rating']
);
}

5.6.2. Валидация данных


Наш пример JSON-файла можно представить как коня в сферическом
вакууме. Он слишком идеален! В реальности возвращаемые данные могут
не содержать некоторые связки пар «ключ:значение» или того хуже – не
соответствовать ожидаемому типу данных. Когда у вас нет 146% гарантии,
что принимаемые данные будут иметь фиксированный формат, чтобы
поберечь свои нервы и не менять прожженное антипригарное покрытие
кресла, принимаемые данные следует валидировать. Это делается в
фабричном именованном конструкторе:
factory Review.fromJson(Map<String, dynamic> json) {
final name = json['name'];
if (name is! String) {
throw FormatException(
'Required "name" field of type String in $json',

381
);
}

final text = json['text'];


if (text is! String?) {
throw FormatException(
'Required "text" field of type String? in $json',
);
}

final rating = json['rating'];


if (rating is! double) {
throw FormatException(
'Required "rating" field of type double in $json',
);
}

return Review(
name: name,
text: text ?? '',
rating: rating,
);
}

В данном случае мы осуществили валидацию по ключевым полям и


типам данных их значений. Если искомого ключа не окажется в
Map<String, dynamic>, то вернется null, который не пройдет последующую
проверку. Но это касается только null-safety типов данных.
Для лучшего понимания этого момента давайте представим, что у нас
есть необязательные поля, то есть в принимаемом JSON часть полей
«ключ:значение» может как содержаться, так и отсутствовать:
// ex5_1.dart
class Caffee {
final String name;
final String address;
final int? yearOpened;

Caffee({
required this.name,
required this.address,
this.yearOpened,
});

factory Caffee.fromJson(Map<String, dynamic> json) {


final name = json['name'];
if (name is! String) {
throw FormatException(

382
'Required "name" field of type String in $json',
);
}

final address = json['address'];


if (address is! String) {
throw FormatException(
'Required "address" field of type String in $json',
);
}

final yearOpened = json['yearOpened'] as int?;


// аналогично
// final yearOpened = json['yearOpened'];
// if (yearOpened is! int?) {
// throw FormatException(
// 'Required "address" field of type int? in $json',
// );
// }
return Caffee(
name: name,
address: address,
yearOpened: yearOpened,
);
}

Map<String, dynamic> toJson() => {


'name': name,
'address': address,
'yearOpened': yearOpened,
};

@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('Caffee{name: $name, address: $address, ');
sb.write('yearOpened: $yearOpened}');
return sb.toString();
}
}

void main() {
final json1 = {
'name': 'Rome',
'address': 'Italy, Rome',
'yearOpened': 1500,
};

383
final json2 = {
'name': 'xXx',
'address': 'Mexico, Mexico City',
};

print(Caffee.fromJson(json1));
print(Caffee.fromJson(json2));
}
//Caffee{name: Rome, address: Italy, Rome, yearOpened: 1500}
//Caffee{name:xXx, address:Mexico, Mexico City, yearOpened: null}

В представленных примерах валидация получается слишком


громоздкой, т.к. отдельно проверяется каждое поле. И если при малом их
количестве такая проверка не представляет ничего сложного, то писать
однотипный код для 10, 20 или 40 полей – то еще удовольствие. Исправить
такую несправедливость можно с помощью нововведений третьей версии
Dart. Давайте посмотрим, как это отобразится на именованном фабричном
конструкторе класса Review:
// review.dart
factory Review.fromJson(Map<String, dynamic> json) {
if (json
case {
'name': String name,
'rating': double rating,
}) {
final text = json['text'] as String?;
return Review(
name: name,
text: text ?? '',
rating: rating,
);
} else {
throw FormatException('Invalid JSON: $json');
}
}

А для полного погружения в нирвану, перепишем фабричный


конструктор fromJson класса Movie:
// movie.dart
factory Movie.fromJson(Map<String, dynamic> json) {
if (json
case {
'name': String name,
'budget': int budget,
'actors': List<dynamic> actors,
'criticsRating': double criticsRating,

384
'audienceRating': double audienceRating,
'year': int year,
'country': String country,
'genre': List<dynamic> genre,
'reviews': List<dynamic> reviews,
}) {
return Movie(
name: name,
budget: budget,
actors: actors.map((e) => Actor.fromJson(e)).toList(),
criticsRating: criticsRating,
audienceRating: audienceRating,
year: year,
country: country,
genre: Genre(genre.map((e) => e as String).toList()),
reviews: reviews.map((e) => Review.fromJson(e)).toList(),
);
} else {
throw FormatException('Invalid JSON: $json');
}
}

5.6.3. Пакет json_serializable для десериализации и сериализации


объектов в JSON-формат
Показанный ранее способ для сериализации и десерализации
экземпляров классов хорош только для маленьких проектов, а когда речь
идет о среднем или огромном количестве передаваемых данных и наличии
вложенных объектов, то самостоятельно прописывать под это дело код –
занятие не из приятных. В таких случаях на помощь приходят пакеты,
позволяющие автоматически генерировать код сериализации и
десериализации классов. От разработчика требуется только правильно
осуществить аннотацию классов, для которых этот код будет сгенерирован.
В Dart для генерации кода сериализации в и десериализации из JSON
пользовательских классов рекомендуется использовать пакет
json_serializable (https://pub.dev/packages/json_serializable) и
json_annotation (https://pub.dev/packages/json_annotation). Чтобы их
подключить к проекту, достаточно внести в файл pubspec.yaml следующие
зависимости:
name: filmography
description: A sample command-line application.
version: 1.0.0

environment:
sdk: '>=3.0.0 <5.0.0'

385
dependencies:
json_annotation: ^4.8.1 # последняя версия на
# момент написания книги

dev_dependencies:
lints: ^2.1.0
test: ^1.24.0
build_runner: ^2.4.6
json_serializable: ^6.7.1

Пакет build_runner (https://pub.dev/packages/build_runner)


необходим для запуска процесса кодогенерации и поддерживает
следующие команды:
− build: запускает отдельную сборку и завершает работу;
− watch: запускает постоянный сервер сборки, который следит за
файловой системой на предмет изменений и при необходимости
выполняет перестройку;
− serve: то же самое, что и watch, но также запускает сервер разработки.
По умолчанию он обслуживает веб-каталог и тестовый каталог на
портах 8080 и 8081;
− test: запускает отдельную сборку, создает объединенный выходной
каталог, а затем запускает команду pub run test --precompiled
<merged-output-dir>.
Давайте перепишем с использованием аннотаций код классов проекта
«filmography» и начнем с «actor.dart»:
// actor.dart
import 'package:json_annotation/json_annotation.dart';

part 'actor.g.dart'; // сюда будет сгенерирован


// код сериализации/десериализации

@JsonSerializable()
class Actor {
final String name;
final int age;
// устанавливаем соответствие ключа в JSON полю класса
@JsonKey(name: 'filmsAmount')
final int numberOfFilms;
final String? aboutActor;

Actor({
required this.name,
required this.age,
required this.numberOfFilms,
required this.aboutActor,
});

386
// Подключение генерируемой функции к конструктору fromJson
factory Actor.fromJson(
Map<String, dynamic> json,
) =>
_$ActorFromJson(json);

// Подключение генерируемой функции к методу toJson


Map<String, dynamic> toJson() => _$ActorToJson(this);

@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('Actor{name: $name, age: $age, ');
sb.write('filmsAmount: $numberOfFilms, ');
sb.write('aboutActor: $aboutActor}');
return sb.toString();
}
}

Не переживайте, что часть кода подсвечивается, это нормально и


исправляется запуском build_runner, что проделаем несколько позже.
Также обратите внимание на то, что поле класса filmsAmount было
переименовано в numberOfFilms и чтобы это не привело к печальным
последствиям, его аннотировали @JsonKey(name: 'filmsAmount') явно
указав значение какого ключевого поля JSON присвоить данной
переменной класса.
Т.к. в классе Genre у нас на вход фабричного конструктора fromJson
ожидается List<dynamic>, а не Map<String, dynamic> то в него изменения
вносить не будем. Это связано с тем, что пакет json_serializable при
кодогенерации ориентирован на то, что аннотируемый класс должен
представлять собой отдельный объект, со своими полями. В нашем случае
такой подход приведет к тому, что в JSON-файле появятся 2 вложенных
ключевых поля genre, из-за чего придется вносить правки в сам файл,
чтобы при запуске приложения не произошло исключения
(поэкспериментируйте с этим моментом). Поэтому следующий на очереди
– класс, описывающий пользовательские обзоры:
// review.dart
import 'package:json_annotation/json_annotation.dart';
part 'review.g.dart';

@JsonSerializable()
class Review {
final String name;
final String? text;

387
final double rating;

Review({
required this.name,
required this.text,
required this.rating,
});

factory Review.fromJson(
Map<String, dynamic> json,
) =>
_$ReviewFromJson(json);

Map<String, dynamic> toJson() => _$ReviewToJson(this);

@override
String toString() {
return 'Review{name: $name, text: $text, rating: $rating}';
}
}

Осталось внести изменения в файл «movie.dart» и можно будет


переходить к кодогенерации:
// movie.dart
import 'package:json_annotation/json_annotation.dart';

import 'actor.dart';
import 'genre.dart';
import 'review.dart';

part 'movie.g.dart';

@JsonSerializable()
class Movie {
final String name;
final int budget;
final List<Actor> actors;
final double criticsRating;
final double audienceRating;
final int year;
final String country;
final Genre genre;
final List<Review> reviews;

Movie({
required this.name,
required this.budget,

388
required this.actors,
required this.criticsRating,
required this.audienceRating,
required this.year,
required this.country,
required this.genre,
required this.reviews,
});

factory Movie.fromJson(
Map<String, dynamic> json,
) =>
_$MovieFromJson(json);

Map<String, dynamic> toJson() => _$MovieToJson(this);

@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('Movie{name: $name, budget: $budget, ');
sb.write('actors: $actors, criticsRating: $criticsRating, ');
sb.write('audienceRating: $audienceRating, year: $year, ');
sb.write(
'country: $country, genre: $genre, reviews: $reviews}'
);
return sb.toString();
}
}

Чтобы избавиться от навязчивого красного подчеркивания запустим в


терминале проекта в VS Code следующую команду:
dart run build_runner build

Рисунок 5.16 – Запуск build_runner

После успешной генерации кода в директории появятся 3 новых файла


(см. рис. 5.17): actor.g.dart, review.g.dart и movie.g.dart, которые
будут содержать автоматически сгенерированный код. Его можно

389
модифицировать, но обычно это не рекомендуется делать, так как в случае
какого-либо изменения в файле, по которому этот код генерировался при
повторном запуске команды dart pub run build_runner build все
внесенные изменения затрутся. А теперь представьте, что у вас таких мест
много и во все вы внесли изменения модифицировав код. Чем такой подход
будет лучше написания сериализации и десериализации вручную? Ничем!!!
Можно попросту забыть об этих изменениях, а потом, при очередной
пересборке проекта удивляться почему он так сейчас работает или не
работает вовсе.

Рисунок 5.17 – Текущее состояние директории «lib»

Для проверки корректности работы приложения, удалите из


директории «bin» файл «output.json» и запустите программу.
Такой подход позволяет достаточно удобно и быстро реализовывать
классы, которые предназначены для получения или отправки данных по
сети, хранению в файле и т.д. Да, у него есть ряд ограничений, но куда
проще мириться с ними, чем в сотый раз описывать для модели (класса)
очередной фабричный конструктор fromJson или метод toJson.
Если у вас имеется желание более глубоко погрузиться в возможности
рассматриваемых пакетов, обратитесь к их официальной документации, а
также можете почитать следующую статью из документации по Flutter:
https://docs.flutter.dev/data-and-backend/serialization/json.

5.7. Простая БД по типу «ключ:значение» в формате JSON


В данном разделе главы будет реализовано хранилище по типу
«ключ:значение». По сути, мы с вами напишем аналог (на минималках)
достаточно известного Flutter-пакета – shared_preferences
(https://pub.dev/packages/shared_preferences).

390
Создайте новый консольный проект «json_store» со следующей
структурой директорий и их наполнением:

Рисунок 5.18 – Структура проекта «json_store»

Первым делом откройте «pubspec.yaml» и установите в качестве


зависимости пакет path (https://pub.dev/packages/path), что избавит от
проблем указания путей в различных операционных системах и collection
(https://pub.dev/packages/collection), функционал которого будем
использовать для сравнения двух объектов:
dependencies:
path: ^1.8.3
collection: ^1.18.0

Следом откройте файл «json_file.dart» и добавьте в него следующий


код, отвечающий за создание пути директорий и файла, а также работу с
ним:
// json_file.dart
import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart' as p;

class JSONFile {
final String path;

JSONFile(this.path) {
final dir = Directory(p.dirname(path));
if (!dir.existsSync()) {
dir.createSync(recursive: true);
}
}

Map<String, Object>? read() {

391
final file = File(path);
try {
if (file.existsSync()) {
final data = file.readAsStringSync();
if (data.isNotEmpty) {
final json = jsonDecode(data);
if (json is Map) {
return json.cast<String, Object>();
}
}
}
} on FormatException {
return null;
}
return {};
}

void write(Map<String, Object> json) {


final file = File(path);
if (!file.existsSync()) {
file.createSync(recursive: true);
}
final encoder = const JsonEncoder.withIndent(' ');
return file.writeAsStringSync(
encoder.convert(json),
);
}
}

Так как реализуется простая база данных, которая не подразумевает


хранение сериализованных экземпляров классов, то вместо Map<String,
dynamic> используется Map<String, Object>. Тем самым уменьшается
набор ситуаций, где можем сами себе выстрелить в ногу и узнать об этом
только в процессе работы приложения.
Следующим шагом откройте файл «json_store.dart» директории
«src». В нем будет объявлен класс JSONStore, представляющий собой
высокоуровневую обертку над JSONFile, с методами для добавления,
проверки, замены и извлечения данных из хранилища:
// src - json_store.dart
import 'package:collection/collection.dart';

import 'json_file.dart';

class JSONStore {
final JSONFile _file;
JSONStore(String path) : _file = JSONFile(path);

392
Map<String, Object>? _values;
String get path => _file.path;

bool contains(String key) {


return _getValues().containsKey(key);
}

List<String> get keys {


return List.unmodifiable(_getValues().keys);
}

List<Object> get values {


return List.unmodifiable(
(_values ??= _file.read() ?? {}).values,
);
}

Map<String, Object> _getValues() {


return _values ??= _file.read() ?? {};
}

Object? getValue(String key) => _getValues()[key];

bool valueEquals(Object? a, Object? b) {


return const DeepCollectionEquality().equals(a, b);
}

bool? getBool(String key) => getValue(key) as bool?;


int? getInt(String key) => getValue(key) as int?;
double? getDouble(String key) => getValue(key) as double?;
String? getString(String key) => getValue(key) as String?;
List<Object?>? getList(String key) {
return getValue(key) as List<Object?>?;
}
Map<Object, Object?>? getMap(String key) {
return getValue(key) as Map<Object, Object?>?;
}

void setValue(String key, Object? value) async {


if (value == null) {
resetValue(key);
}

final values = Map.of(_getValues());


final oldValue = values[key];
values[key] = value!;
if (oldValue == null) {
_values = values;

393
_file.write(values);
} else if (!valueEquals(oldValue, value)) {
_values = values;
_file.write(values);
}
}

void resetValue(String key) async {


final values = Map.of(_getValues());
if (values.remove(key) != null) {
_values = values;
_file.write(values);
}
}
}

Теперь перейдите к файлу «json_store.dart» директории «lib» и


добавьте в него одну строку с экспортом:
// lib - json_store.dart
export 'src/json_store.dart';

Демонстрацию работы реализованного хранилища сделаем в


несколько этапов. Сначала запишем данные и проверим их наличие по
ключу, потом изменим пару записей и в конечном счете удалим хотя бы
одну пару «ключ:значение». Для этого откройте файл «json_store.dart»
директории «bin» и замените имеющийся код на следующий:
// bin - json_store.dart
import 'package:json_store/json_store.dart';

void main(List<String> arguments) {


final store = JSONStore('bin/store.json');
store.setValue('strList', <String>['a', 'b', 'c']);
store.setValue('int', 55);
store.setValue('bool', true);
store.setValue('double', 3.14);
store.setValue('map', <String, int>{'a': 1, 'b': 2});
store.setValue('str', '(づ˶•༝•˶)づ♡');

print(store.values);
// [[a, b, c], 55, true, 3.14, {a: 1, b: 2}, (づ˶•༝•˶)づ♡]
print(store.keys);
// [strList, int, bool, double, map, str]

print(store.contains('strList')); // true
print(store.getValue('strList')); // [a, b, c]
print(store.getValue('int')); // 55

394
print(store.getValue('bool')); // true
print(store.getValue('double')); // 3.14
print(store.getValue('map')); // {a: 1, b: 2}
print(store.getValue('str')); // (づ˶•༝•˶)づ♡
}

После запуска приложения, в директории «bin» должен создаться файл


«store.json» со следующим содержимым:
{
"strList": [
"a",
"b",
"c"
],
"int": 55,
"bool": true,
"double": 3.14,
"map": {
"a": 1,
"b": 2
},
"str": "(づ˶•༝•˶)づ♡"
}

Перепишем код функции main, изменив пару значений в хранилище:


// bin - json_store.dart
void main(List<String> arguments) {
final store = JSONStore('bin/store.json');

store.setValue('strList', '-_-');
store.setValue('double', 99);
print(store.getValue('strList')); // -_-
print(store.getValue('double')); // 99
}

После запуска приложения, структура файла «store.json», где


хранятся данные хранилища, преобразится следующим образом:
{
"strList": "-_-",
"int": 55,
"bool": true,
"double": 99,
"map": {
"a": 1,
"b": 2
},

395
"str": "(づ˶•༝•˶)づ♡"
}

Настала пора удалить пару записей:


// bin - json_store.dart
void main(List<String> arguments) {
final store = JSONStore('bin/store.json');

store.resetValue('map');
store.resetValue('str');
print(store.getValue('map')); // null
print(store.getValue('str')); // null
}

// store.json
{
"strList": "-_-",
"int": 55,
"bool": true,
"double": 99
}

Когда вы точно знаете, значение какого типа данных хранится по


ключу, то можно воспользоваться следующим набором методов JSONStore:
bool? getBool(String key) => getValue(key) as bool?;
int? getInt(String key) => getValue(key) as int?;
double? getDouble(String key) => getValue(key) as double?;
String? getString(String key) => getValue(key) as String?;
List<Object?>? getList(String key) {
return getValue(key) as List<Object?>?;
}
Map<Object, Object?>? getMap(String key) {
return getValue(key) as Map<Object, Object?>?;
}

Что касается последних двух методов, то у их возвращаемого значения


необходимо вызвать метод cast и указать к каким типам привести
элементы коллекции:
void main(List<String> arguments) {
final store = JSONStore('bin/store.json');
store.setValue('map', <String, int>{'a': 1, 'b': 2});
store.setValue('strList', <String>['a', 'b', 'c']);
store.setValue('intList', <int>[1, 2, 3]);

var strList = store.getList(


'strList',
)?.cast<String>().toList();

396
var intList = store.getList('intList')?.cast<int>().toList();
var myMap = Map<String, int>.from(
store.getMap('map')?.cast<String, int>() ?? {},
);

print(strList.runtimeType); // List<String>
print(intList.runtimeType); // List<int>
print(myMap.runtimeType); // _Map<String, int>

print(strList); // [a, b, c]
print(intList); // [1, 2, 3]
print(myMap); // {a: 1, b: 2}
}

В противном случае, для обращения к значению по ключу


используется метод getValue, с последующей проверкой к какому типу
данных относится возвращаемое им значение.

Резюме по разделу
В данной главе мы рассмотрели какими способами можно
осуществлять компиляцию разрабатываемого приложения и как его
конфигурировать в момент запуска через терминал. Так же были затронуты
почти все основные возможности Dart по работе с файлами и
директориями. Почему почти все? Потому, что Dart еще может
манипулировать данными в html-файлах, но с появлением Flutter for Web
эти его возможности теряют свою актуальность. Да и давайте признаемся
честно, не так уж и много компаний горит желанием использовать его в
этих целях, тогда как JavaScript и Flutter предоставляет куда больший набор
библиотек и возможностей.
Дополнительно нами был рассмотрен такой формат представления
данных, как JSON и способы работы с ним. Он часто будет встречаться на
вашем пути и чем раньше получится с ним подружиться, тем лучше.
Использование библиотек для генерации кода довольно удобно, но не
спишите их использовать везде, так как они тянут за собой ряд
зависимостей в ваш проект, с которыми в последствии придется считаться.
Особенно, если одну из них прекратит поддерживать ее автор, да и
сообществу до нее не будет никакого дела.

Вопросы для самопроверки


1. Какие флаги для компиляции приложения вы знаете? Приведите их
различия, достоинства и недостатки.

397
2. Какие существуют способы конфигурации приложения при его
запуске средствами терминала?
3. Какой класс в Dart позволяет работать с файлами? Перечислите его
основные методы.
4. Какие режимы работы с файлами существуют? В чем их различия?
5. Какой класс лучше использовать при необходимости
прочитать/загрузить файл большого размера?
6. Как проверить существование файла в системе?
7. Каким образом можно получить путь до директории запускаемого
приложения?
8. Как явным образом создать файл? За что отвечает флаг recursive?
9. Какой класс в Dart позволяет работать с директориями? Перечислите
его основные методы.
10. Что такое JSON (JavaScript Object Notation)? Для чего и где он
используется?
11. Что такое сериализация и десериализация?
12. Для чего можно использовать библиотеку json_serializable? Какие у
нее ограничения?
13. Стоит ли всегда использовать библиотеки для генерации кода?
Почему?

Лабораторная работа № 9. Работа с текстовыми


файлами
Цель работы: познакомиться с основными способами работы с
текстовыми файлами в Dart.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Каждое задание должно сопровождаться минимум двумя
тестами и содержать хотя бы одно исключение.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

398
Задания:
1. Напишите приложение, которое считывает содержимое текстового
файла и выводит в терминал количество слов, а также записывает это
значение в новый файл.
2. Напишите приложение, которое считывает содержимое нескольких
текстовых файлов, объединяет их и записывает результат в новый файл.
3. Напишите приложение, которое считывает содержимое нескольких
текстовых файлов и создает новый файл, в котором объединены все строки
из исходных файлов, отсортированные в алфавитном порядке.
4. Напишите приложение, которое считывает содержимое текстового
файла и подсчитывает количество вхождений каждого слова. Выведите в
терминал и запишите в новый файл слова, отсортированные по количеству
вхождений.
5. Напишите приложение, которое считывает содержимое текстового
файла и выводит в терминал и записывает в новый файл все уникальные
слова.
6. Напишите приложение, которое считывает содержимое двух
текстовых файлов и сравнивает их, записывая в новый файл и выводя в
терминал строки, которые есть только в одном из файлов.
7. Напишите приложение, которое считывает содержимое текстового
файла и создает новый файл, в котором каждое слово записано задом
наперед.
8. Напишите приложение, которое считывает содержимое текстового
файла и подсчитывает количество гласных и согласных букв. Выведите в
терминал полученный результат и запишите в новый файл.
9. Напишите приложение, которое считывает содержимое
многострочного файла с целочисленными значениями (разделенные
пробелом) и выводит в терминал их сумму, а также записывает это
значение в новый файл.
10. Напишите приложение, которое считывает содержимое
многострочного файла с целочисленными значениями (разделенные
пробелом) и выводит в терминал среднее арифметическое, а также
записывает это значение в новый файл.
11. Напишите приложение, которое считывает содержимое
многострочного файла с целочисленными значениями (разделенные
пробелом) и выводит в терминал максимальное и минимальное значение,
а также записывает эти значения в новый файл.
12. Напишите приложение, которое считывает содержимое текстового
файла, записывает в новый файл и выводит в терминал самую часто
встречающуюся гласную и согласную букву.

399
13. Напишите приложение, которое считывает содержимое нескольких
текстовых файлов и создает новый файл, в котором объединены все
уникальные слова из исходных файлов, записываемые на новой строке.
14. Напишите приложение, которое считывает содержимое текстового
файла и записывает в новый файл все уникальные слова в алфавитном
порядке.
15. Напишите приложение, которое считывает содержимое
многострочного файла с целочисленными значениями (разделенные
пробелом) и выводит в терминал сумму только четных чисел, а также
записывает это значение в новый файл.

Таблица 5.2
Варианты работ
№ варианта Номера заданий к варианту
1 1, 2, 12, 14
2 1, 6, 11, 13
3 3, 5, 8, 10
4 6, 12, 13, 14
5 2, 6, 8, 12
6 5, 6, 9, 15
7 6, 7, 11, 15
8 2, 4, 10, 14
9 3, 7, 10, 15
10 3, 6, 7, 15
11 6, 11, 13, 15
12 2, 6, 7, 9
13 1, 3, 12, 13
14 1, 9, 10, 14
15 2, 4, 9, 12
16 4, 6, 7, 11
17 4, 10, 11, 14
18 5, 7, 9, 12
19 7, 9, 10, 14
20 8, 9, 11, 13

Лабораторная работа № 10. Работа с JSON-файлами


Цель работы: познакомиться с основными способами работы с JSON-
файлами средствами Dart.
Требования к формату защиты лабораторной работы:

400
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Каждое задание должно сопровождаться минимум пятью
тестами и содержать хотя бы одно исключение.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

Задание:
Дан файл, содержащий данные в JSON-формате. Напишите
приложение, позволяющее осуществить из него загрузку, изменить
некоторые значения и сохранить текущее состояние экземпляров классов в
новый файл в JSON-формате.

Варианты работ
№ Структура исходного файла
1 {
"pizzeria": {
"name": "Pizza World",
"location": "789 Pine Street",
"menu": [
{
"name": "Margherita",
"price": 10.99,
"ingredients": [
"tomato",
"mozzarella",
"basil"
],
"is_vegetarian": true,
"is_spicy": false,
"special_instructions": "Extra cheese available upon request"
},
{
"name": "Pepperoni",
"price": 12.99,
"ingredients": [

401
"tomato",
"mozzarella",
"pepperoni"
],
"is_vegetarian": false,
"is_spicy": true,
}
]
}
}
2 {
"name": "John Doe",
"age": 30,
"education": [
{
"degree": "Bachelor's",
"major": "Computer Science",
"university": "XYZ University",
"graduationYear": 2010
},
{
"degree": "Master's",
"major": "Data Science",
"university": null,
"graduationYear": 2015
}
],
"certifications": [
"AWS Certified Solutions Architect",
"Google Certified Professional Cloud Architect"
],
"skills": ["Python", "Java", "SQL"],
"experience": [
{
"company": "ABC Inc.",
"position": "Software Engineer",
"startDate": "2016-01-01",
"endDate": "2020-12-31"
},
{
"company": "XYZ Corp.",
"position": "Senior Software Engineer",

402
"startDate": "2021-01-01",
"endDate": null
}
]
}
3 {
"name": "The Foodie's Haven",
"location": {
"address": "123 Main Street",
"city": "Moscow",
"zipcode": "10001"
},
"cuisine": ["Italian", "Mexican", "Asian"],
"menu": [
{
"name": "Spaghetti Carbonara",
"price": 12.99,
"ingredients": ["pasta", "bacon", "eggs", "cheese"]
},
{
"name": "Taco Platter",
"price": 9.99,
"ingredients": ["tortillas", "beef", "lettuce", "salsa"]
}
],
"reviews": [
{
"username": "FoodLover123",
"rating": 4,
"comment": "The pasta was amazing!",
"date": "2021-08-15"
},
{
"username": "GourmetChef",
"rating": 5,
"comment": null,
"date": "2021-08-18"
}
],
"phone": "+7 123-456-7890",
"website": "https://www.myfood.ru"
}

403
4 {
"manufacturer": "XYZ Motors",
"location": {
"address": "789 Oak Avenue",
"city": "SPb",
"zipcode": "48201"
},
"models": [
{
"name": "Sedan",
"year": 2022,
"price": 25000
},
{
"name": "SUV",
"year": 2022,
"price": 35000
}
],
"employees": 5000,
"founder": "John Smith",
"partners": [
"ABC Parts Supplier",
"DEF Electronics"
],
"website": "https://www.xyzmotors.com",
"contact": null
}
5 {
"property": {
"address": "123 Elm Street",
"city": "Kazan",
"zipcode": "94110"
},
"rental": {
"type": "Apartment",
"bedrooms": 2,
"bathrooms": 1.5,
"amenities": ["Swimming Pool", "Gym", "Parking"],
"pets_allowed": true
},
"landlord": {

404
"name": "John Doe",
"phone": "+1 123-456-7890",
"email": "johndoe@example.com"
},
"reviews": [
{
"username": "HappyRenter",
"rating": 5,
"comment": "Great place to live!",
"date": "2021-08-15"
},
{
"username": "SatisfiedTenant",
"rating": 4,
"date": "2021-08-18"
}
],
"lease": null
}
6 {
"library": {
"name": "Central Library",
"location": {
"address": "123 Main Street",
"city": "SPb",
"zipcode": "48201"
},
"books": [
{
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"genre": "Classic",
"available": true
},
{
"title": "To Kill a Mockingbird",
"author": "Harper Lee",
"genre": "Classic",
"available": false
}
],
"librarians": [

405
{
"name": "Ivan Ivanov",
"phone": "+7 123-456-7890",
"email": "vanya@example.com"
},
{
"name": "Maria Pushkina",
"phone": null,
"email": "mpush@example.com"
}
],
"website": "https://www.examplelibrary.com",
"hours": null
}
}
7 {
"project": {
"name": "Residential Complex",
"location": {
"address": "456 Oak Street",
"city": "Spb",
"zipcode": "90001"
},
"contractors": [
{
"name": "ABC Construction",
"phone": "+7 123-456-7890",
"email": "abc@example.com"
},
{
"name": "CDS",
"phone": "+7 987-654-3210",
"email": "cds@example.com"
}
],
"architect": {
"name": "Ivan Pronin",
"phone": "+7 555-555-5555",
"email": "ipron@example.com"
},
"budget": 1000000,
"start_date": "2022-01-01",

406
"end_date": "2023-01-01",
"progress": 70,
"documents": null
}
}
8 {
"sale": {
"customer": {
"name": "Ivan Ivanov",
"email": "ivan@example.com",
"phone": "+7 123-456-7890"
},
"products": [
{
"name": "Laptop",
"brand": "Dell",
"price": 1000,
"specs": {
"processor": "Intel Core i5",
"ram": "8GB",
"storage": "256GB SSD"
}
},
{
"name": "Desktop",
"brand": "HP",
"price": 800,
"specs": {
"processor": "Intel Core i7",
"ram": "16GB",
"storage": "1TB HDD"
}
}
],
"payment_method": "Credit Card",
"shipping_address": {
"street": "123 Main Street",
"city": "Moscow",
"zipcode": "10001"
},
"order_date": "2022-01-01",
"delivery_date": null

407
}
}
9 {
"restaurant": {
"name": "Burger Palace",
"location": {
"address": "123 Main Street",
"city": "Moscow",
"zipcode": "10001"
},
"menu": [
{
"name": "Cheeseburger",
"price": 5.99,
"ingredients": ["Beef Patty", "Cheese", "Lettuce", "Tomato"]
},
{
"name": "Chicken Sandwich",
"price": 6.99,
"ingredients": ["Grilled Chicken", "Lettuce", "Tomato",
"Mayonnaise"]
}
],
"opening_hours": {
"monday": {
"start_time": "09:00",
"end_time": "22:00"
},
"tuesday": {
"start_time": "09:00",
"end_time": "22:00"
},
"wednesday": {
"start_time": "09:00",
"end_time": "22:00"
},
"thursday": {
"start_time": "09:00",
"end_time": "22:00"
},
"friday": {
"start_time": "09:00",

408
"end_time": "23:00"
},
"saturday": {
"start_time": "10:00",
"end_time": "23:00"
},
"sunday": {
"start_time": "10:00",
"end_time": "22:00"
}
},
"drive_thru": true,
"wifi": null
}
}
10 {
"railway_station": {
"name": "Central Station",
"location": {
"address": "123 Main Street",
"city": "Moscow",
"zipcode": "10001"
},
"platforms": [
{
"number": 1,
"capacity": 200,
"trains": [
{
"train_number": "ABC123",
"destination": "City A",
"departure_time": "09:00",
"arrival_time": "12:00",
},
{
"train_number": "DEF456",
"destination": "City B",
"departure_time": "10:00",
"arrival_time": "13:00"
}
]
},

409
{
"number": 2,
"capacity": 150,
"trains": [
{
"train_number": "GHI789",
"destination": "City C",
"departure_time": "11:00",
"arrival_time": "14:00"
}
]
}
],
"ticket_office": true,
"wifi": null
}
}

410
Глава 6. Асинхронное и сетевое
программирование. Isolate
Dart – однопоточный язык программирования, но это совсем не
значит, что у вас нет возможности писать код, который будет выполняться
параллельно. Для этих случаев используются Isolate. Если сравнивать
концепцию Isolate с инструментами для параллельного
программирования в других языках, то ближе всего будет такое понятие,
как процесс. Это связано с тем, что каждый Isolate работает со своей
областью памяти, циклом и очередью событий и может обмениваться с
другими Isolat-ами данными посредством сообщений. Основной поток
выполнения программы на Dart также представляет собой Isolate.
Весь код на Dart выполняется последовательно, то есть за раз
выполняется одна операция. Таким образом, если в основном коде
приложения вызывать выполнение функции, то управление перейдет к ней
и придется дожидаться пока она не вернет результат (выполнит
возлагаемую на нее работу). А если в функции будет выполняться довольно
большой объем вычислений, то с точки зрения пользователя, ваша
программа «зависнет». Такое поведение приложения указывает на
неправильное использование процессорного времени. Что уж говорить, о
негодовании пользователя, когда практически в каждом компьютере
установлены многоядерные процессоры.
Асинхронное программирование позволяет отложить процесс
выполнение функции, не останавливая выполнение основного кода и
задать функцию, которая обработает возвращаемый ей результат. То есть
асинхронная функция будет выполнена конкурентно немного позже, в
освободившееся процессорное время. Но необходимо понимать
следующее: выполнится она все в том же основном потоке приложения.
Поэтому не следует в функциях, объявляемых как асинхронные,
производить сложные вычисления, а тем более прописывать вечные циклы.
Перед тем как перейдем к изучению инструментов асинхронного
программирования в Dart рассмотрим, что такое цикл событий (event loop)
[17]. Это позволит писать более качественный асинхронный
(конкурентный) код, в котором вас будет поджидать куда меньшее
количество сюрпризов, чем в том случае, когда не имеете никакого
представления о Event Loop.

411
6.1. Event Loop архитектура в Dart
6.1.1. Базовая концепция цикла и очереди событий
В каждом приложении с графическим пользовательским интерфейсом
(GUI) реализована концепция цикла событий и очереди событий. Именно
они гарантируют, что любые графические операции и события (движение
или щелчки мышкой, нажатие клавиш и т. д.) обрабатываются по очереди.
То есть каждый попадающий в очередь событий элемент берется оттуда
циклом событий, после чего обрабатывается и так до тех пор, пока в
очереди есть элементы, которые представляют собой операции
ввода/вывода, таймеры и т. д. Рассмотрим очередь событий, которая
содержит события таймера и ввода данных пользователем:
Event Loop
Очередь событий
1. Щелчок мышкой
2. Ввод данных 4. click 3. timer 2. key 1. click
3. Пре рывание таймера
4. Щелчок мышкой

Рисунок 6.1. – Концепция обработки события из очереди циклом событий

В рамках Dart, концепция обработки очереди событий из рис. 6.1,


может быть представлена следующим образом:
Event Loop Main Isolate
Очередь событий

4. click 3. timer 2. key 1. click main()

Обработка щелчка
4. click 3. timer 2. key 1. click
мышки

Обработка нажатия
4. click 3. timer 2. key
клавиш

Обработка
3. timer
прерывания таймера

Рисунок 6.2 – Пример обработки очереди циклом событий в Dart

Из рис. 6.2 видно, что все элементы очереди событий обрабатываются


в главном потоке приложения.

412
6.1.2. Очереди и цикл событий в Dart
Каждое Dart-приложение имеет один цикл событий, который
осуществляет работу с двумя очередями:
− очередь событий. Содержит как события Dart, так и все внешние
события: ввод/вывод, таймеры, сообщения между экземплярами
Isolate и т. д.;
− очередь микрозадач. Используется для очень коротких внутренних
действий, которые необходимо выполнять асинхронно, сразу после
завершения какого-либо события и перед передачей управления
обратно в очередь событий.
В качестве примера элемента для помещения в очередь микрозадач
может выступать задача удаления ресурса (файла и т. д.), после того как он
был закрыт. Так как этот процесс может занимать какое-то время, то его
разумнее всего выполнить в асинхронном режиме. Тем более, что такие
операции отдаются на откуп операционной системы и не выполняются
средствами языка программирования.
Цикл обработки событий начинает свою работу при выходе потока
управления из функции верхнего уровня main. Первым делом он выполняет
любые микрозадачи в порядке их нахождения в очереди микрозадач.
Следом за этим начинается обработка первого элемента в очереди событий,
то есть он извлекается из очереди и обрабатывается. Затем идет повторение
цикла: сначала выполняются все микрозадачи, а после обрабатывается
следующий элемент в очереди событий. Когда обе очереди пусты и больше
не ожидается событий, приложение закрывается.
Ниже представлена структурная схема алгоритма работы цикла
событий:

413
Запуск
приложения

Инициализация очереди
событий и микрозадач

main()

Нет
Запуск первой в очереди
Очередь микрозач
микрозадачи
пустая?

Да

Нет
Обработка первого
Очередь событий события в очереди
пустая?

Да

Завершение
приложения

Рисунок 6.3 – Алгоритм работы цикла событий

Посмотрите внимательно структурную схему алгоритма работы цикла


событий. Из нее следует, что пока цикл событий выполняет задачи из
очереди микрозадач, обработка элементов очереди событий не
производится. То есть приложение не может рисовать графику,
обрабатывать события ввода/вывода и т. д.
И хотя теперь, лучше разбираясь в алгоритме работы цикла событий, у
вас имеется возможность предсказать порядок выполнения задач, нельзя
точно сказать, когда цикл событий будет доставать на обработку задачу из
очереди. Это связано с тем, что сама система обработки событий Dart
основана на однопоточном цикле. То есть при создании очередной
отложенной задачи, событие ставится в очередь, но оно не может быть
обработано до тех пор, пока не будут обработаны все события, находящиеся
перед ним в очереди.
Для добавления очередного элемента в очередь событий, код которого
должен выполниться позже, в Dart используется класс Future. А для
добавления нового элемента в конец очереди микрозадач используется

414
функция верхнего уровня scheduleMicrotask, либо именованный
конструктор Future.microtask.
Обычно рекомендуется при планировании отложенных задач
использовать класс Future, помещая их в очередь событий. Это помогает
сохранить короткую очередь микрозадач, тем самым уменьшая
вероятность того, что из-за нее будет простаивать очередь событий. В тех
же случаях, когда задача обязательно должна завершиться до того, как
будут обработаны какие-либо элементы из очереди событий, немедленно
вызывайте выполнение функции для ее обработки. Если этого сделать не
получается, помещайте в очередь микрозадач.
В качестве примера работы алгоритма цикла событий реализуем
программу, выводящую в терминал строки. Сначала поместим событие
вывода строки в очередь событий, а потом в очередь задач:
// ex6_1.dart
import 'dart:async';

void main(List<String> arguments) {


Future(() => print('1-й элемент очереди событий'));
Future(() => print('2-й элемент очереди событий'));
Future.microtask((){
print('1-й элемент очереди микрозадач');
});
scheduleMicrotask((){
print('2-й элемент очереди микрозадач');
});
}
/* 1-й элемент очереди микрозадач
2-й элемент очереди микрозадач
1-й элемент очереди событий
2-й элемент очереди событий */

6.2. Асинхронное программирование


Асинхронное программирование представляет собой подход, при
котором программа может обрабатывать задачи, не блокируясь на
операциях ввода-вывода или ожидании результата от других задач. Это
позволяет программе конкурентно выполнять другие задачи, пока
ожидается завершение асинхронных операций, улучшая общую
производительность и эффективность использования ресурсов.
Асинхронное программирование особенно полезно в сценариях, где
программа должна выполнять множество небольших задач, которые
зависят от внешних ресурсов, таких как: файлы, сетевые запросы или базы
данных. Оно позволяет избежать ненужных задержек, так как программа
продолжает работать над другими задачами во время ожидания ответа от

415
внешних ресурсов. Дополнительно к этому, асинхронное
программирование может снизить нагрузку на процессор, так как
асинхронные операции, такие как ожидание сетевых запросов, не требуют
активного использования процессорного времени, что особенно критично
в системах с ограниченными ресурсами, таких как встраиваемые
устройства или серверы с высокой нагрузкой.
Для того, чтобы ваш код имел возможность выполняться асинхронно,
в заголовке модуля необходимо импортировать библиотеку dart:async.
После этого вам станут доступны такие классы, как Future и Stream. В Dart
также имеются ключевые слова async и await, которые позволяют писать
асинхронный код, внешне мало чем отличающийся от синхронного.
К наиболее частым задачам, где код должен выполняться асинхронно
можно отнести:
− получение данных по сети;
− запись в базу данных;
− чтение данных из файла.

6.2.1. Future API, async и await


Future API позволяет добавлять задачи как в очередь событий, так и в
очередь микрозадач для их отложенного асинхронного выполнения.
Каждая задача может завершиться успешно, либо в процессе ее работы
сгенерируется исключение, которое следует обработать или дать
распространиться дальше, вплоть до падения приложения.
В Dart существует такое понятие, как future (фьючерс/будущее). Под
ним понимается объект, представляющий собой результат вычисления,
которое, возможно, еще не произошло и его результат может быть известен
когда-нибудь в будущем (о как загнул! ^_^). Говоря простыми словами,
future – экземпляр класса Future<T>, позволяющий нам писать
асинхронный код и предоставляющий доступ к результату вычисления, где
Т – тип возвращаемого результата.
Экземпляр класса Future может быть создан с использованием одного
и следующих конструкторов:
− Future(FutureOr<T> computation()) создает future, содержащий
результат асинхронного вызова computation с помощью Timer.run.
− Future.delayed(Duration duration, [FutureOr<T> computation()])
создает future, вычисление которого выполняется после задержки,
указываемой в duration. Необязательный аргумент конструктора
computation представляет собой ссылку на функцию, которая будет
выполняться после задаваемой задержки.
− Future.error(Object error, [StackTrace? stackTrace]) создает
future, который будет завершен ошибкой error. Это дает достаточно
времени для добавления обработчика ошибки. В том случае, если

416
обработчик не будет добавлен до завершения future, ошибка будет
считаться необработанной.
− Future.microtask(FutureOr<T> computation()) создает future,
который посредством функции scheduleMicrotask помещает функцию
computation в очередь микрозадач (во всех остальных случаях идет работа
с очередью событий), запускает ее асинхронно и возвращает результат.
− Future.sync(FutureOr<T> computation()) создает future,
содержащий результат немедленного вызова computation.
− Future.value([FutureOr<T> value]) создает future, содержащее
значение value.
Почти во всех приведенных конструкторах класса Future тип
возвращаемого значения передаваемой функции в качестве аргумента
computation указывается как FutureOr<T>. Это значит, что передаваемая
функция должна возвращать либо Future<T> либо объект типа T:
import 'dart:async';

int add() => 10 + 15;

void main(List<String> arguments) {


Future<int> future = Future(add);
}

Чтобы получить вычисляемое значение функции add у future


необходимо вызвать метод then, куда передать callback-функцию (функция
обратного вызова) с типом входного аргумента, соответствующему типу
возвращаемого значения. Callback-функция будет вызвана сразу, как
обработается элемент очереди событий, соответствующий future:
// ex6_2.dart
import 'dart:async';

int add() => 10 + 15;

void main(List<String> arguments) {


var firstFuture = Future<int>(add);
Future(()=> print('Oo'));
firstFuture.then((value) => print(value));
print('завершение main');
}
/* завершение main
25
Oo */

Тот же самый результат можно получить следующим способом:


// ex6_3.dart
import 'dart:async';

417
int add() => 10 + 15;
void myPrint(int a) => print(a);

void main(List<String> arguments) {


var firstFuture = Future<int>(add);
var secondFuture = Future(()=> 'Oo');
firstFuture.then(myPrint);
secondFuture.then(print);
print('завершение main');
}
/* завершение main
25
Oo */

Callback-функция способна возвращать объекты различного типа, тем


самым организуя цепочки из методов then, которые будут вызываться друг
за другом:
// ex6_4.dart
import 'dart:async';

void main(List<String> arguments) {


var future = Future<String>(() =>
'Привет! Это событие в очереди под номером: ');

var newfuture = Future<String>(() =>


'Привет! Это еще одно событие в очереди под номером: ');

int a = 10;
future.then((value){
print('$value 1');
return 1;
}).then((value) => print(value + a));

newfuture.then((value){
print('$value 2');
return 2.5;
}).then((value) => print(value + a));

print('завершение main');
}
/* завершение main
Привет! Это событие в очереди под номером: 1
11
Привет! Это еще одно событие в очереди под номером: 2
12.5 */

418
Если в процессе асинхронного выполнения задачи сгенерируется
исключение, то оно вернется как результат выполнения отложенной
операции. Future позволяет перехватывать все возникающие в процессе
отложенного выполнения исключения и ошибки посредством метода
catchError. При отсутствии обработки исключений, либо невозможности
обработать исключение сгенерированного типа, оно будет распространено
дальше, что в итоге приведет к завершению программы. Так, например, в
коде ниже будет производиться обработка всех возможных исключений:
// ex6_5.dart
import 'dart:async';

class MyException implements Exception {


final String? msg;

const MyException([this.msg]);

@override
String toString() => msg ?? 'MyException';
}

int myFunction(){
var sum = 0;
for(var i=0; i<30; i++){
sum += i;
if(sum > 40){
throw MyException();
}
}
return sum;
}

void main(List<String> arguments) {


var future = Future<int>(myFunction);
future.then(print)
.catchError((onError) => print(onError));
Future(()=> print('-_-'));
print('завершение main');
}
/* завершение main
MyException
-_- */

Теперь в метод catchError добавим дополнительную проверку, чтобы


он осуществлял обработку исключений или ошибок только заданного типа.
Для этого используется его второй именованный (необязательный)
аргумент test, которому предается функция, принимающая на вход

419
экземпляр исключения и возвращающая логическое значение. Если
возвращается true, то значит перехватили нужное исключение и будем его
обрабатывать в методе catchError, иначе исключение распространится
дальше.
// ex6_6.dart
void main(List<String> arguments) {
var future = Future<int>(myFunction);
future.then(print)
.catchError((onError) => print(onError),
test: (error) => error is MyException);
Future(()=> print('-_-'));
print('завершение main');
}
/* завершение main
MyException
-_- */

В этом случае исключение обработалось, и программа завершилась в


штатном режиме, но если в функции myFunction будет генерироваться
отличное от MyException исключение (например, просто Exception), то ее
выполнение завершится намного раньше и в терминале будут красоваться
следующие строчки:
завершение main
Unhandled exception:
Exception

Когда же присутствует острая необходимость при успешном


выполнении Future или наличии ошибки выполнить какое-либо действие
(например, закрыть доступ к ресурсу и т. д.), на помощь приходит метод
whenComplete:
// ex6_7.dart
void main(List<String> arguments) {
var future = Future<int>.delayed(
Duration(seconds: 3), // задержка перед выполнением
myFunction
);
future.then(print)
.catchError((onError) => print(onError),
test: (error) => error is MyException)
.whenComplete(() => print('Я все равно лучший!!!'));
Future(()=> print('-_-'));
print('завершение main');
}
/* завершение main
-_-
MyException

420
Я все равно лучший!!! */

Для того, чтобы какой-то функционал приложения вызывался с


определенной периодичностью следует использовать класс Timer, а не
Future:
// ex6_8.dart
import ‘dart:async’;
import ‘dart:io’;

void main(List<String> arguments) async {


var count = 0;
Timer.periodic(
Duration(milliseconds: 500),
(timer) {
// вызов функции, запрос на бэк и т.д.
stdout.write(‘*’);
count++;
if (count >= 10) {
timer.cancel();
}
},
);
}
// **********

Если вам необходимо выполнить функцию, передаваемую в


создаваемый экземпляр класса Future, а уже после использовать ее
результат в процессе работы приложения, то присмотритесь к
именованному конструктору Future<Т>.sync:
// ex6_9.dart
import 'dart:async';

void main(List<String> arguments) {


var future = Future<String>.sync(() {
print('Запуск Future');
return '^_^';
});
future.then((value){
print('($value) - завершение Future');
});
print('завершение main');
}
/* Запуск Future
завершение main
(^_^) - завершение Future */

421
Для аналогичных целей можно использовать конструктор
Future<T>.value, которому на вход передается значение:
// ex6_10.dart
import 'dart:async';

void main(List<String> arguments) {


var future = Future<String>.value('^_^');
future.then((value){
print('($value) - завершение Future');
});
print('завершение main');
}
// завершение main
// (^_^) - завершение Future

За более низкоуровневый способ создания объектов типа Future


отвечает класс Completer. Он позволяет создавать Future и завершать их
позже с заданным значением или состоянием ошибки. Для примера
давайте напишем небольшую функцию, моделирующую скачивание JSON и
его десериализацию:

// ex6_11.dart
import 'dart:convert';
import 'dart:async';

String downloadData() {
final jsonString = '''
[
{
"id": 1,
"title": "Изучаем Python",
"author": "Гаспарян Эрик",
"urlImage": "https://6008409614.jpg"
},
{
"id": 2,
"title": "Программирование на C++",
"author": "Петров А.Н.",
"urlImage": "https://6053518495.jpg"
},
{
"id": 3,
"title": "Программирование на Java",
"author": "Иванов А.Н.",
"urlImage": "https://6053518383.jpg"
}
]

422
''';
return jsonString;
}

class Book {
final int id;
final String title;
final String author;
final String urlImage;

Book({
required this.id,
required this.title,
required this.author,
required this.urlImage,
});

factory Book.fromJson(Map<String, dynamic> json) {


return Book(
id: json['id'],
title: json['title'],
author: json['author'],
urlImage: json['urlImage'],
);
}

@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('Book(id: $id, title: $title, ');
sb.write('author: $author, urlImage: $urlImage)');
return sb.toString();
}
}

Future<List<Book>> getBooks() {
var completer = Completer<List<Book>>();
try {
final jsonString = downloadData();
final List<dynamic> jsonList = jsonDecode(jsonString);
final List<Book> books = jsonList
.map(
(json) => Book.fromJson(json),
)
.toList();
completer.complete(books);
} catch (e) {
print(e);

423
completer.completeError(e);
}
return completer.future;
}

void main() {
getBooks().then((books) {
for (var it in books) {
print(it);
}
}).catchError((onError) {
print(onError);
});
}
/* Book(id: 1, title: Изучаем Python, author: Гаспарян Эрик,
urlImage: https://6008409614.jpg)
Book(id: 2, title: Программирование на C++, author: Петров А.Н.,
urlImage: https://6053518495.jpg)
Book(id: 3, title: Программирование на Java, author: Иванов А.Н.,
urlImage: https://6053518383.jpg) */

Измените любое поле JSON таким образом, чтобы это привело к


ошибке при десериализации и посмотрите, как себя поведет приложение.
Для упрощения написания функций, которые должны выполняться
асинхронно, существуют ключевые слова async и await. Эти два слова
довольно тесно связаны друг с другом, поскольку ключевое слово await
можно использовать только в теле тех функций, которые помечены, как
async. К тому же тип возвращаемого результата функции должен быть
обернут в Future:
// ex6_12.dart
String getBigData() {
return 'Гигатонны информации))';
}

Future<void> makeRequestData() async {


print('Запрос данных');
var data = await getBigData();
print(data);
print('Данные получены');
}

void main(List<String> arguments) {


print('Запуск main');
makeRequestData();
print('Завершение main');
}
/* Запуск main

424
Запрос данных
Завершение main
Гигатонны информации))
Данные получены */

Код в асинхронной функции makeRequestData выполняется синхронно


(просто примите это ^_^) вплоть до первого вызова await, которое
представляет собой асинхронную операцию. То есть она не блокирует
выполнение других операций, позволяя им выполняться до своего
завершения. Таким образом, когда поток управления в функции
makeRequestData встречается с ключевым словом await, он
останавливается до тех пор, пока вызываемая функция getBigData не
вернет свой результат. После чего продолжается выполнение функции
makeRequestData.
В результате выполнения await всегда возвращается объект Future. А
в том случае, когда вызываемая функция возвращает значение отличного
от Future типа данных, Dart автоматически оборачивает его в Future. При
этом, возвращаемое значение вызываемой через await функции может
быть получено путем обычного присваивания.
Если в функции getBigData сгенерируется исключение, его следует
обработать посредством конструкции try…catch…finally:
// ex6_13.dart
Future<String> getBigData() async{
throw Exception('Прервалось соединение!!!');
}

Future<void> makeRequestData() async {


print('Запрос данных');
try {
print(await getBigData());
print('Данные получены');
} catch (e) {
print('Что-то пошло не так: $e');
}
}

void main() {
print('Запуск main');
makeRequestData();
print('Завершение main');
}
/* Запуск main
Запрос данных
Завершение main
Что-то пошло не так: Exception: Прервалось соединение!!! */

425
Количество вызовов функций с использованием ключевого слова
await в теле функции, помеченной как async, не ограничено. Необходимо
помнить только то, что вызываться они будут последовательно:
// ex6_14.dart
String getBigData() {
return 'Гигатонны информации))';
}

String changeData(String data) {


return data.toUpperCase();
}

Future<void> makeRequestData() async {


print('Запрос данных');
var data = await getBigData();
print(data);
print('Данные получены');
print('Приступаем к изменению данных');
print(await changeData(data));
print('Данные изменены');
}

void main(List<String> arguments) {


print('Запуск main');
makeRequestData();
print('Завершение main');
}
/* Запуск main
Запрос данных
Завершение main
Гигатонны информации))
Данные получены
Приступаем к изменению данных
ГИГАТОННЫ ИНФОРМАЦИИ))
Данные изменены */

Для большего постижения дзен асинхронности, давайте перепишем


один из предыдущих примеров, где моделировали скачивание JSON и его
десериализацию таким образом, чтобы каждый объект десериализовался в
отдельном Future, в конечном счете образуя список из книг:
// ex6_15.dart
import 'dart:convert';
import 'dart:async';

Future<String> downloadData() async{


final jsonString = '''

426
[
// без изменения
]
''';
return jsonString;
}

class Book {
// без изменения
}

Future<Book> decode(Map<String, dynamic> json) async{


var book = Book.fromJson(json);
print('Future decoded book with id: ${book.id}');
return book;
}

Future<List<Book>> getBooks() async{


var completer = Completer<List<Book>>();
try {
final jsonString = await downloadData();
final List<dynamic> jsonList = jsonDecode(jsonString);
final List<Book> books = await Future.wait(
[
for (var json in jsonList) decode(json),
]
);
completer.complete(books);
} catch (e) {
print(e);
completer.completeError(e);
}
return completer.future;
}

void main() {
print('Запуск main');
getBooks().then((books) {
for (var it in books) {
print(it);
}
}).catchError((onError) {
print(onError);
});
print('Завершение main');
}
/* Запуск main
Завершение main

427
Future decoded book with id: 1
Future decoded book with id: 2
Future decoded book with id: 3
Book(id: 1, title: Изучаем Python, author: Гаспарян Эрик,
urlImage: https://6008409614.jpg)
Book(id: 2, title: Программирование на C++, author: Петров А.Н.,
urlImage: https://6053518495.jpg)
Book(id: 3, title: Программирование на Java, author: Иванов А.Н.,
urlImage: https://6053518383.jpg)*/

Статический метод класса Future – wait позволяет подождать


завершение нескольких фьючерсов и собирать их результаты в список. А
результатом могут быть как нормальные данные, так и ошибки. Для
примера, изменим у одной из книг в JSON представлении id на строковый
тип данных и модифицируем функцию decode и getBooks:
// ex6_16.dart
import 'dart:convert';
import 'dart:async';

Future<String> downloadData() async {


final jsonString = '''
[
{
"id": "1",
"title": "Изучаем Python",
"author": "Гаспарян Эрик",
"urlImage": "https://6008409614.jpg"
},
// далее без изменений
]
''';
return jsonString;
}

class Book {
// без изменений
}

Future<Book> decode(Map<String, dynamic> json) async {


var completer = Completer<Book>();
try {
var book = Book.fromJson(json);
print('Future decoded book with id: ${book.id}');
completer.complete(book);
} catch (e) {
completer.completeError(e);
}

428
return completer.future;
}

Future<List<Book>> getBooks() async {


var completer = Completer<List<Book>>();
var books = <Book>[];
try {
final jsonString = await downloadData();
final List<dynamic> jsonList = jsonDecode(jsonString);
await Future.wait([
for (var json in jsonList) decode(json)
],
cleanUp: (Book value) {
books.add(value);
});
} catch (e) {
print(e);
// такое себе решение, но ради примера - можно
} finally {
completer.complete(books);
}
return completer.future;
}

void main() {
print('Запуск main');
getBooks().then((books) {
for (var it in books) {
print(it);
}
}).catchError((onError) {
print(onError);
});
print('Завершение main');
}
/* Запуск main
Завершение main
Future decoded book with id: 2
Future decoded book with id: 3
type 'String' is not a subtype of type 'int'
Book(id: 2, title: Программирование на C++, author: Петров А.Н.,
urlImage: https://6053518495.jpg)
Book(id: 3, title: Программирование на Java, author: Иванов А.Н.,
urlImage: https://6053518383.jpg)*/

Аргумент cleanUp статической функции wait класса Future позволяет


поэлементно обработать данные успешно завершивших свою работу

429
Future. Такая обработка делается для закрытия соединения или
освобождения какого-то ресурса перед завершением работы функции
(метода) и передачи ошибки на более высокий уровень. В нашем случае она
перехватывается блоком catch и не распространяется дальше. Это сделано
только в познавательных целях!!! Тащить такое в проект крайне не
рекомендуется… если только вы не любитель, когда вам делают больно.
Когда важно обработать результат первого завершившего свою работу
Future в коллекции и отбросить все остальные, следует использовать
статический метод any:
// ex6_17.dart
void main() {
print('Запуск main');
Future.any<int>([
Future.delayed(Duration(seconds: 3), () => 1),
Future.delayed(Duration(seconds: 2), () => 2),
Future.delayed(Duration(seconds: 1), () => 3),
]).then((value) {
print(value);
});
print('Завершение main');
}
// Запуск main
// Завершение main
// 3

А если перед вами стоит задача выполнить операцию асинхронно


определенное количество раз, на помощь придет статический метод
doWhile. Анонимная функция, подаваемая ему на вход, должна возвращать
булевское значение. Если оно равно true – то функция снова добавится в
очередь событий и обработается, иначе, текущая Future завершит свою
работу:
// ex6_18.dart
void main() async{
print('Запуск main');
var count = 0;
Future.doWhile(() async {
print('count = $count');
count++;
await Future.delayed(Duration(milliseconds: 500));
return count <= 4;
});
// либо
// Future.doWhile((){
// print('count = $count');
// count++;
// await Future.delayed(Duration(milliseconds: 500));

430
// if (count > 4) {
// return false;
// };
// return true;
// });
print('Завершение main');
}
/* Запуск main
count = 0
Завершение main
count = 1
count = 2
count = 3
count = 4 */

Еще один метод класса Future – timeout, принимающий на вход время


и необязательную анонимную функцию onTimeout, которая должна
возвращать тот же тип данных, ожидаемый от Future. Метод timeout
позволяет задать временной лимит на выполнение задачи. То есть, если
задача выполнится за отведенное время, то все хорошо, иначе будет
выполнена функция, переданная аргументу onTimeout. А когда ему ничего
не передается, по истечении указанного времени сгенерируется
исключение TimeoutException:
// ex6_19.dart
import 'dart:async';

void main(List<String> arguments) {


var future1 = Future.delayed(
Duration(seconds: 2),
() => 'Future 1',
);
var future2 = Future.delayed(
Duration(seconds: 4),
() => 'Future 2',
);
future1
.timeout(
Duration(seconds: 3),
onTimeout: () => 'Timeout for Future 1',
)
.then((value) => print(value));

future2
.timeout(
Duration(seconds: 3),
onTimeout: () => 'Timeout for Future 2',
)

431
.then((value) => print(value));

future2
.timeout(
Duration(seconds: 3),
)
.then((value) => print(value))
.catchError(
(e) => print(e),
);
}
// Future 1
// Timeout for Future 2
// TimeoutException after 0:00:03.000000: Future not completed

6.2.2. Stream (Поток)


Поток (Stream) в Dart – это последовательность асинхронных событий,
которые подразделяются на три типа:
− событие данных (элемент потока);
− событие ошибки (что-то пошло не так);
− событие "done", оповещающее всех слушателей (тех, кто подписался
на поток) о его завершении.
Основное преимущество от использования потоков заключается в
том, что код остается слабосвязанным, т.к. классу, в котором создается
экземпляр потока, отвечающий за выдачу готовых данных, не нужно
ничего знать о том, кто подписался (слушает) на получение событий и
почему. Аналогичная ситуация обстоит и с потребителями данных. Они
должны только придерживаться интерфейса потока, в то время как
источник данных от них полностью скрыт.
Для управления потоками в Dart [18] используются следующие классы:
− Stream. Представляет асинхронный поток данных. Слушатели могут
подписаться на получение уведомлений о появлении новых событий
данных.
− EventSink. Обратный поток, добавление событий данных в который
направляет эти данные в подключенный поток.
− StreamController. Упрощает управление потоками, автоматически
создавая поток и приемник, а также предоставляя методы для
управления поведением потока.
− StreamSubscription. Экземпляры этого класса могут сохранять ссылку
на подписку, что позволяет им приостанавливать, возобновлять или
отменять поток данных.
В большинстве случаев нет необходимости напрямую создавать
экземпляры классов Stream и EventSink. Это связано с тем, что при

432
создании экземпляра класса StreamController, автоматически создается
поток и приемник.
Примером потока может выступать изменение положения курсора
мыши, список простых чисел, получаемые по сети данные и т. д. На каждый
поток имеется возможность подписаться (прослушать поток), путем
задания одной или нескольких callback-функций, которые будут
вызываться при добавлении в него новых данных. Самый простой пример
использования потоков – написание асинхронной генераторной функции:
// ex6_20.dart
Stream<int> myGenerator(int last) async* {
for (var i = 0; i <= last; i++) {
yield i;
}
}

void createGenerator(int lastValue) async {


var stream = myGenerator(lastValue);
// слушаем поток и выводим получаемые данные в терминал
stream.listen((s) => print(s));
}

void main(List<String> arguments) {


print('Запуск main');
createGenerator(20);
print('Завершение main');
}
/* Запуск main
Завершение main
0
...
20 */

Также потоки предоставляют возможность асинхронно итерироваться


по существующим последовательностям. Для этого используется
именованный конструктор Stream.fromIterable:
// ex6_21.dart
void iterableStream(List<int> list) {
var stream = Stream.fromIterable(list);
print('Начало работы потока');
stream.listen(
(s) => print(s),
);
print('Завершение работы потока');
}

void main(List<String> arguments) {

433
iterableStream([1, 2, 3, 4, 5]);
}
/* Начало работы потока
Завершение работы потока
1

5 */

Обратите внимание на вывод в терминал в предыдущем примере.


Итерация по списку происходила в асинхронном режиме. Порой может
возникнуть ситуация, что основной код функции, обрабатывающей
события потока, должен выполнять в синхронном режиме. Для этого
следует использовать конструкцию await for:
// ex6_22.dart
void iterableStream(List<int> list) async {
var stream = Stream.fromIterable(list);
print('Начало работы потока');
await for (var num in stream) {
print(num);
}
print('Завершение работы потока');
}

void main(List<String> arguments) {


iterableStream([1, 5]);
}
/* Начало работы потока
1
5
Завершение работы потока */

При использовании await for для обработки исключения следует


использовать конструкцию try…catch…finally. А в том случае, когда
функция обработки задается через метод listen экземпляра класса Stream,
для этих целей применяется анонимная функция, передаваемая
именованному аргументу onError:
// ex6_23.dart
Stream<int> myGenerator(int last) async* {
for (var i = 0; i <= last; i++) {
if (i >= 2) {
throw Exception('Ошибка!!!');
}
yield i;
}
}

434
void createGenerator(int lastValue) async {
var stream = myGenerator(lastValue);
// слушаем поток и выводим получаемые данные в терминал
stream.listen((s) => print(s),
onError: (e) => print(e));
}

void main(List<String> arguments) {


print('Запуск main');
createGenerator(20);
print('Завершение main');
}
/* Запуск main
Завершение main
0
1
Exception: Ошибка!!! */

Теперь давайте разберемся, как осуществляется работа с экземпляром


класса StreamController:
// ex6_24.dart
import 'dart:async';

void main(List<String> arguments) {


final controller = StreamController<String>();

final subscription = controller.stream.listen((String data) {


print('Listening: $data');
});

controller.add('Привет!!!');
controller.add('И еще раз, Привет!!!');
}
// Listening: Привет!!!
// Listening: И еще раз, Привет!!!

Экземпляр класса StreamController предоставляет доступ к потоку


для прослушивания и реагирования на события посредством метода listen
экземпляра класса Stream, которому задается функция обратного вызова,
обрабатывающая поступающие данные через метод add в поток. Сам же
метод потока listen возвращает экземпляр StreamSubscription,
позволяющий управлять подпиской на поток.
Давайте представим, что у нас имеется кофемашина состоящая из
монетоприемника и блока приготовления кофе. Блок приготовления
подписывается на события поступления денег в монетоприемник и после

435
того, как накапливается пороговая сумма, начинается приготовление
капучино:
// ex6_25.dart
import 'dart:async';

class CoinAcceptor{
final _addCoin = StreamController<int>();
Stream<int> get dataStream => _addCoin.stream;

void addCoin(int coin) => _addCoin.add(coin);


}

class CoffeMachine{
int valueCoins = 0;

CoffeMachine(Stream<int> stream){
stream.listen(addCoin);
}

void addCoin(int coin){


valueCoins += coin;
if(valueCoins >=30){
print('Готовим капучино!');
}
print('Общее кол-во монет: $valueCoins');
}
}

void main(List<String> arguments) {


print('Запуск main');
var coinAcceptor = CoinAcceptor();
var coffeMachine = CoffeMachine(coinAcceptor.dataStream);
coinAcceptor.addCoin(25);
coinAcceptor.addCoin(4);
coinAcceptor.addCoin(3);
print('Завершение main');
}
/* Запуск main
Завершение main
Общее кол-во монет: 25
Общее кол-во монет: 29
Готовим капучино!
Общее кол-во монет: 32 */

Класс CoffeMachine может быть реализован и более компактным


образом:

436
class CoffeMachine{
int valueCoins = 0;

CoffeMachine(Stream<int> stream){
stream.listen((coin){
valueCoins += coin;
if(valueCoins >=30){
print('Готовим капучино!');
}
print('Общее кол-во монет: $valueCoins');
});
}
}

В приведенном примере работы кофемашины, у монетоприемника


может быть только один подписчик на события. Когда же имеется
необходимость разрешить несколько слушателей потока, следует создать
широковещательный поток используя именованный конструктор
Stream<T>.broadcast:
final _addCoin = StreamController<int>.broadcast ();

Потоки поддерживают возможность передавать значения не только


встроенных типов данных, но и пользовательских:
// ex6_26.dart
import 'dart:async';

class Coin{
final int value;
Coin(this.value);
}

class CoinAcceptor{
final _addCoin = StreamController<Coin>();
Stream<Coin> get dataStream => _addCoin.stream;

void addCoin(Coin coin) => _addCoin.add(coin);


}

class CoffeMachine{
int valueCoins = 0;

CoffeMachine(Stream<Coin> stream){
stream.listen((coin){
valueCoins += coin.value;
if(valueCoins >= 30){
print('Готовим капучино!');
}

437
print('Общее кол-во монет: $valueCoins');
});
}
}

void main(List<String> arguments) {


print('Запуск main');
var coinAcceptor = CoinAcceptor();
var coffeMachine = CoffeMachine(coinAcceptor.dataStream);
coinAcceptor.addCoin(Coin(35));
print('Завершение main');
}
/* Запуск main
Завершение main
Готовим капучино!
Общее кол-во монет: 35 */

Чтобы продемонстрировать то, как можно самостоятельно закрыть


поток, перепишем немного последний пример:
// ex6_27.dart
import 'dart:async';

class Coin{
final int value;
Coin(this.value);
}

class CoinAcceptor{
final _addCoin = StreamController<Coin>();
Stream<Coin> get dataStream => _addCoin.stream;

Future<void> addCoin(Coin coin) async{


if (!_addCoin.isClosed){
_addCoin.add(coin);
}
}
Future<void> dispose() async => await _addCoin.close();
}

class CoffeMachine{
int valueCoins = 0;

CoffeMachine(CoinAcceptor coinAcceptor){
coinAcceptor.dataStream.listen((coin) async{
valueCoins += coin.value;
if(valueCoins >= 30){
print('Готовим капучино!');

438
}
if (valueCoins >= 60){
await coinAcceptor.dispose();
}
print('Общее кол-во монет: $valueCoins');
}, onDone: (){
print('Завершение работы');
});
}
}

void main(List<String> arguments) async{


print('Запуск main');
var coinAcceptor = CoinAcceptor();
var coffeMachine = CoffeMachine(coinAcceptor);
await coinAcceptor.addCoin(Coin(35));
await coinAcceptor.addCoin(Coin(5));
await coinAcceptor.addCoin(Coin(20));
await coinAcceptor.addCoin(Coin(63));
print('Завершение main');
}
/* Запуск main
Готовим капучино!
Общее кол-во монет: 35
Готовим капучино!
Общее кол-во монет: 40
Готовим капучино!
Завершение работы
Завершение main
Общее кол-во монет: 60 */

Именованный аргумент onDone метода listen позволяет отследить


завершение потока, чтобы освободить занятые ресурсы. Это может быть
открытый ранее файл, сетевое соединение и т.д.
В качестве еще одного примера рассмотрим применение Stream для
реализации такого паттерна проектирования, как Наблюдатель (Observer).
За основу возьмем пиццерию с новым бариста, который не знает ни одного
постоянного посетителя. Чтобы не спутать, кому какой кофе предназначен,
каждому заказу назначается номер. Клиент, после того как сделал заказ,
ожидает, когда бариста произнесет его номер, и забирает свой кофе. Далее
он уже может не слушать выкрики этого сотрудника, а наслаждаться сочной
пиццей, попивая свой великолепный кофе.
Начнем с объявления перечисления, класса заказа и абстрактного
класса BaseBarista, который будет предоставлять метод для подписки
клиентов на оповещение о номере приготовленного заказа:

439
// ex6_28.dart
import 'dart:async';
import 'dart:math';

final _random = Random();

enum OrderType { cappuccino, latte, espresso }

class Order {
static int _orderId = 1;
late final int orderId;
final OrderType orderType;

Order(this.orderType) {
orderId = _orderId++;
}

@override
String toString() {
return 'order ($orderId): ${orderType.name}';
}
}

abstract class BaseBarista {


final _update = StreamController<int>.broadcast();

StreamSubscription<int> subscribe(void Function(int) update);


void takeOrder(Order order);
Order getOrder(int orderId);
}

Далее напишем класс Barista, реализующий функционал подписки на


оповещение о событии, а также передающий в Stream номер
приготовленного заказа кофе для оповещения всех клиентов, чтобы
клиент, который делал заказ с обозначенным идентификатором, забрал его
и отписался от оповещений:
class Barista extends BaseBarista {
final _orders = <Order>[];
final _finishOrder = <Order>[];

@override
void takeOrder(Order order) {
print('Barista accepted $order');
_orders.add(order);
}

@override

440
Order getOrder(int orderId) {
Order? clientOrder;
for (var order in _finishOrder) {
if (order.orderId == orderId) {
clientOrder = order;
break;
}
}
if (clientOrder is Order) {
_finishOrder.remove(clientOrder);
return clientOrder;
} else {
throw ArgumentError('Oo');
}
}

void processingOrder() {
if (_orders.isNotEmpty) {
// выбираем случайный заказ
var order = _orders[_random.nextInt(_orders.length)];
_orders.remove(order);
_finishOrder.add(order);
print('Barista has completed $order');
_update.sink.add(order.orderId);
} else {
// если нет заказов, бариста натирает кофемашину
print('Barista rubs the coffee machine');
}
}

@override
StreamSubscription<int> subscribe(void Function(int p1) update) {
return _update.stream.listen(update);
}
}

Далее реализуем класс клиента, который будет подписываться на


оповещение от бармена при формировании заказа и отписываться от него,
когда забрал свое кофе:
class Client {
final String name;
final BaseBarista _barista;
Order? _order;
StreamSubscription<int>? _subscription;

Client(this.name, this._barista);

441
void createOrder() {
var orderType = OrderType.values[_random.nextInt(
OrderType.values.length,
)];
_order = Order(orderType);
print('Client $name made $_order');
_barista.takeOrder(_order!);
_subscription = _barista.subscribe(update);
}

void update(int orderId) async {


if (_order is Order) {
if (orderId == _order!.orderId) {
print('Client $name took ${_barista.getOrder(orderId)}');
await _subscription?.cancel();
}
}
}
}

Для проверки работы реализованного шаблона напишем следующую


функцию main:
void main() {
var names = <String>[
'Alexander',
'George',
'Maksim',
'Hermann',
'Oleg',
'Alexey',
'Stanislav'
];

var barista = Barista();


var clients = <Client>[
for (var name in names) Client(name, barista)
];

for (var client in clients) {


print('*' * 30);
client.createOrder();
}
print('*' * 30);
print('*' * 4 + 'Barista starts to fill orders' + '*' * 4);

for (var it = 0; it < 10; it++) {


print('*' * 30);

442
barista.processingOrder();
}
}
/* ******************************
Client Alexander made order (1): latte
Barista accepted order (1): latte
******************************
Client George made order (2): espresso
******************************
Client Alexander made order (1): latte
Barista accepted order (1): latte
******************************
... ... ... ... ... ... ... ...
******************************
****Barista starts to fill orders****
******************************
Barista has completed order (5): espresso
******************************
Barista has completed order (3): espresso
******************************
... ... ... ... ... ... ... ...
******************************
Barista has completed order (7): cappuccino
******************************
Barista rubs the coffee machine
******************************
... ... ... ... ... ... ... ...
******************************
Barista rubs the coffee machine
******************************
Client Oleg took order (5): espresso
Client Maksim took order (3): espresso
Client Alexey took order (6): cappuccino
Client George took order (2): espresso
Client Alexander took order (1): latte
Client Hermann took order (4): espresso
Client Stanislav took order (7): cappuccino */

6.3. Isolate (Изоляты)


Несмотря на то, что Dart запускает приложение в одном изоляте, при
необходимости имеется возможность создавать пользовательские изоляты,
имеющие свою собственную память и единственный поток выполнения,
который запускает цикл обработки событий. То есть каждый новый изолят
получает свой собственный цикл событий и свою собственную память, к

443
которой другие изоляты не имеют доступа. Единственный способ,
благодаря которому изоляты могут работать вместе – обмен сообщениями.
Несмотря на имеющиеся у такого подхода недостатки, у него также
есть ряд преимуществ [19]:
− Выделение памяти и сборка мусора в изолированном объекте не
требуют блокировки.
− Есть только один поток и если он не занят, то память не изменяется.
В качестве первого примера, по классике, реализуем эхо-изолят и
разберем принцип его работы, а уже после погрузимся во все тяжкие
«Уличной магии» :
// ex6_29.dart
import 'dart:isolate';

class IsolatesMessage<T> {
final SendPort sender;
final T message;

IsolatesMessage({
required this.sender,
required this.message,
});
}

late SendPort isolateSendPort;


late Isolate isolate;

Future<void> createIsolate() async {


var receivePort = ReceivePort();
isolate = await Isolate.spawn(
echoCallbackFunction,
receivePort.sendPort,
);
isolateSendPort = await receivePort.first;
}

Future<String> sendReceive(String send) async{


var port = ReceivePort();
isolateSendPort.send(
IsolatesMessage<String>(
sender: port.sendPort,
message: send,
)
);
return await port.first;
}

444
void echoCallbackFunction(SendPort sendPort){
var receivePort = ReceivePort();
// возвращаем ссылку на порт для отправки данных
// в главный изолят
sendPort.send(receivePort.sendPort);
receivePort.listen((message) {
// обработчик принимаемых сообщений изолятом
var isolateMessage = message as IsolatesMessage<String>;
print('Isolate: ${isolateMessage.message}');
isolateMessage.sender.send(isolateMessage.message);
});
}

void main()async{
await createIsolate();
print('Main: ${await sendReceive('Старт!')}');
print('Main: ${await sendReceive('1')}');
print('Main: ${await sendReceive('2')}');
print('Main: ${await sendReceive('3')}');
isolate.kill();
}
/* Isolate: Старт!
Main: Старт!
Isolate: 1
Main: 1
Isolate: 2
Main: 2
Isolate: 3
Main: 3 */

Для создания экземпляра изолята используется статический метод


Isolate.spawn<T>, где первым аргументов выступает ссылка на функцию,
которая будет выполняться в изоляте и принимает в качестве входного
аргумента сообщение типа данных T, а вторым – ссылка на экземпляр
сообщения, которое поступает в изолят сразу при его создании. В нашем
случае это ссылка на порт, через который вернется ссылка на порт из
создаваемого изолята для последующего обмена сообщениями с ним.
Класс IsolatesMessage используется для обмена сообщениями между
главным изолятом и созданным. Его поле sender хранит ссылку на
SendPort, через который будет отправляться сообщение из изолята
главному приложению, а в поле message передаются данные, работа с
которыми будет производиться в изоляте.
ReceivePort создается каждый раз при обращении к изоляту, поэтому
функция createIsolate отвечает за создание изолята и его
инициализацию, то есть получения от него ссылки на порт, через который
в последующем будет осуществляться пересылка сообщения в изолят.

445
Посредством метода sendReceive производится вся остальная работа с
созданным изолятом: передается сообщение из основного приложения,
ожидается ответ, после чего он возвращается в основное приложение.
Такой пример работы с изолятом можно часто встретить на просторах
интернета, но его проблема в том, что осуществляется дюже много лишних
действий. Так, например, await port.first закрывает ReceivePort из-за
чего при отправке сообщения изоляту, приходится каждый раз создавать
новый экземпляр класса ReceivePort и его SendPort передавать в изолят,
чтобы получить из него ответ.
Прежде чем использовать изолят, постарайтесь ответить на вопрос:
«Зачем он вам нужен?». Если для единичного выполнения расчетов или
разового получения большого объема данных по сети с их
десериализацией, то для его создания лучше подойдет статический метод
run класса Isolate. А для часто повторяющихся ресурсоемких задач,
приоритетнее поднять изолят один раз, используя Isolate.spawn.
Представим ситуацию, что нам необходимо получать с сервера
большой JSON, который не желательно десериализовать в главном изоляте,
т.к. это скажется на пользовательском интерфейсе (будет тормозить). В
качестве сервера используем сервис, предоставляющий фейковый Rest API
- https://reqres.in/ и его end-point: https://reqres.in/api/users/{ID}, который
возвращает JSON:
{
"data": {
"id": 2,
"email": "janet.weaver@reqres.in",
"first_name": "Janet",
"last_name": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg"
},
"support": {
"url": "https://reqres.in/#support-heading",
"text": "To keep ReqRes free, contributions towards server
costs are appreciated!"
}
}

Для текущего примера будем считать, что данные пользователя


необходимо получить один раз, поэтому при создании изолята используем
статический метод run. Начнем с импортирования необходимых
библиотек, а также описания класса User (и его составляющих) с
конструктором fromJson:
// ex6_30.dart
import 'dart:convert';
import 'dart:io';

446
import 'dart:isolate';

class User {
final UserData data;
final Support support;

User({
required this.data,
required this.support,
});

factory User.fromJson(Map<String, dynamic> json) {


return User(
data: UserData.fromJson(json['data']),
support: Support.fromJson(json['support']),
);
}

Map<String, dynamic> toJson() {


return {
'data': data.toJson(),
'support': support.toJson(),
};
}

@override
String toString() {
return JsonEncoder.withIndent(' ').convert(this);
}
}

class UserData {
final int id;
final String email;
final String firstName;
final String lastName;
final String avatar;

UserData({
required this.id,
required this.email,
required this.firstName,
required this.lastName,
required this.avatar,
});

factory UserData.fromJson(Map<String, dynamic> json) {


return UserData(

447
id: json['id'],
email: json['email'],
firstName: json['first_name'],
lastName: json['last_name'],
avatar: json['avatar'],
);
}

Map<String, dynamic> toJson() {


return {
'id': id,
'email': email,
'first_name': firstName,
'last_name': lastName,
'avatar': avatar,
};
}
}

class Support {
final String url;
final String text;

Support({
required this.url,
required this.text,
});

factory Support.fromJson(Map<String, dynamic> json) {


return Support(
url: json['url'],
text: json['text'],
);
}

Map<String, dynamic> toJson() {


return {
'url': url,
'text': text,
};
}
}

Следующим шагом добавим функцию fetchUser и


fetchUserWithoutId для получения данных о конкретном пользователе и
их десериализации. Они нам понадобятся, чтобы продемонстрировать, как

448
использовать при создании изолята требующие передачу на их вход
значений аргументов функции, так и не требующие:
Future<User?> fetchUser(int id) async {
User? user;
var httpClient = HttpClient();
try {
var request = await httpClient.getUrl(
Uri.parse('https://reqres.in/api/users/$id'),
); // запрос по адресу, чтобы получить данные о пользователе
// с конкретным id
var response = await request.close();
if (response.statusCode == HttpStatus.ok) {
var responseBody = await response
.transform(
utf8.decoder,
)
.join();
user = User.fromJson(jsonDecode(responseBody));
}
} catch (e) {
print('An error occurred during the API call: $e');
} finally {
httpClient.close();
}
return user;
}

Future<User?> fetchUserWithoutId() async{


return await fetchUser(1);
}

Теперь перейдем к функции main:


void main() async {
print('Запуск main');
// запуск изолята с передачей функции, требующей указание
// аргумента и которая будет выполняться в новом изоляте
Isolate.run<User?>(() => fetchUser(2)).then(
(value) => print(value),
);
// или
// var user = await Isolate.run<User?>(
// () => fetchUser(2),
// );

// запуск изолята с указанием функции без входных аргументов,


// которая будет выполняться в новом изоляте
Isolate.run<User?>(fetchUserWithoutId).then(

449
(value) => print(value),
);
print('завершение main');
}
/* Запуск main
завершение main
{
"data": {
"id": 2,
"email": "janet.weaver@reqres.in",
"first_name": "Janet",
"last_name": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg"
},
"support": {
"url": "https://reqres.in/#support-heading",
"text": "To keep ReqRes free, contributions towards server costs
are appreciated!"
}
}
{
"data": {
"id": 1,
"email": "george.bluth@reqres.in",
"first_name": "George",
"last_name": "Bluth",
"avatar": "https://reqres.in/img/faces/1-image.jpg"
},
"support": {
"url": "https://reqres.in/#support-heading",
"text": "To keep ReqRes free, contributions towards server costs
are appreciated!"
}
} */

Для демонстрации долгоиграющей работы с изолятом, воспользуемся


другим методом его создания– Isolate.spawn. Нам потребуется внести
небольшие изменения в код предыдущего примера, а точнее – переписать
функцию main, добавить пару классов и функцию. А приступим к этому делу
с объявления классов сообщений, которые будут использоваться для
взаимодействия между главным и создаваемым изолятом:
// ex6_31.dart
/// сообщения между главным и создаваемым изолятом
sealed class Message {}

class StartMessage extends Message {


final SendPort sender;

450
StartMessage(this.sender);
}

class StopMessage extends Message {}

class UserRequestMessage extends Message {


final int id;
UserRequestMessage(this.id);
}

class UserResponseMessage extends Message {


final User? user;
UserResponseMessage(this.user);
}

Следом добавим функцию isolateFetchUser, принимающую на вход


стартовое сообщение, хранящее в себе порт для установления
взаимодействия между создаваемым и главным изолятом. До тех пор, пока
из приложения не придет сообщение об остановке, изолят будет ждать
сообщение с идентификатором пользователя, данные по которому
необходимо скачать и вернуть в главный изолят:
void isolateFetchUser(StartMessage message) async {
var receivePort = ReceivePort();
var sendPort = message.sender;
sendPort.send(
StartMessage(receivePort.sendPort),
);

receivePort.listen((message) async {
switch (message) {
case StopMessage():
sendPort.send(StopMessage());
receivePort.close();
Isolate.current.kill();
case UserRequestMessage(id: var id):
var user = await fetchUser(id);
sendPort.send(UserResponseMessage(user));
}
});
}

Настала очередь преобразить функцию main, добавив в нее


возможность ввода id пользователя с клавиатуры:
void main() async {
var receivePort = ReceivePort();
await Isolate.spawn(
isolateFetchUser,

451
StartMessage(receivePort.sendPort),
);

SendPort? sendPort;
// слушаем порт изолята
receivePort.listen((message) {
switch (message) {
case StartMessage(sender: var port):
sendPort = port;
case StopMessage():
print('Isolate stopped');
receivePort.close();
case UserResponseMessage(user: var user):
print(user);
}
});

await Future.delayed(Duration(seconds: 1));

while (true) {
if (sendPort == null) {
print('Isolate not started');
break;
}
print('Enter user id');
var input = stdin.readLineSync()!;
var id = int.tryParse(input);
if (id is int) {
sendPort?.send(UserRequestMessage(id));
} else if (input == 'exit') {
sendPort?.send(StopMessage());
break;
} else {
print('Invalid user id');
}
await Future.delayed(Duration(seconds: 1));
}
}

452
Рисунок 6.4 – Запуск приложения

Когда экземпляр класса Isolate становится более не нужным, его


работу рекомендуется завершить посредством метода kill, который
принимает на вход один из следующих параметров:
− Isolate.immediate. Изолят завершает свою работу как можно скорее.
Так как управляющие сообщения обрабатываются по порядку, то все
ранее отправленные управляющие события из этого изолята будут
обработаны. Завершение работы изолята должно произойти не
позднее, чем при вызове метода с параметром
Isolate.beforeNextEvent. То есть оно может произойти раньше, если
у системы есть способ полностью завершить работу в более раннее
время, даже во время выполнения другого события.
− Isolate.beforeNextEvent (используется по умолчанию). Завершение
работы планируется до следующего возврата управления в цикл
событий принимающего изолятора.
Помимо закрытия изолята не забывайте про порты, т.к. если их
оставить открытыми, это может приводить к утечкам памяти или к тому,
что ваше приложение не завершит работу в штатном режиме. Для примера
второго случая, закомментируйте строку receivePort.close() в функции
main, запустите приложение и попробуйте его завершить, введя exit.
Чтобы не ждать маны небесной (когда приложение закроется само), для
экстренного закрытия приложения, в терминале используйте связку
клавиш «Ctrl+C». А к каким «развлечениям» приводит попытка запуска
огромного количества изолятов, можно посмотреть в моем докладе с

453
CrossConf 2023: «100 изолятов – не предел, или Dart в мультиагентных
системах» (https://www.youtube.com/watch?v=VSO5S8RwlWw).
Начиная с версии 2.15 изоляты группируются в изолирующие группы
и используют одну и ту же кучу (Heap) для хранения объектов, создаваемых
в изоляте, которая управляется общим сборщиком мусора. Это ускорило
как запуск самих изолятов, так и положительно сказалось на
быстродействии Dart. Но даже это обстоятельство не дает изолятам
возможности обмениваться их состоянием или объектами напрямую.
Взаимодействие между ними должно осуществляться только посредством
сообщений через порты. С другой стороны, появление изоляционных групп
сделало возможным передачу, в качестве сообщения, неизменяемых
объектов пользовательского типа данных между изолятами одной группы
без их копирования.
Рассмотренные ранее способы создают изоляты в одной
изоляционной группе, а для создания новой такой группы, следует
использовать статический метод spawnUri класса Isolate. Его отличие от
двух предыдущих, заключается в том, что на вход подается путь до
библиотеки (файла) Dart, содержащей функцию верхнего уровня main с
одной из следующих сигнатур [20]:
main() // 1
main(args) // 2
main(args, message) // 3

А сигнатура метода spawnUri представлена ниже:


Future<Isolate> spawnUri(
Uri uri,
List<String> args,
dynamic message,
{bool paused = false,
SendPort? onExit,
SendPort? onError,
bool errorsAreFatal = true,
bool? checked,
Map<String, String>? environment,
Uri? packageConfig,
bool automaticPackageResolution = false,
@Since("2.3") String? debugName}
)

Аргумент uri должен содержать путь до запускаемой в новой


изоляционной группе библиотеки. Список аргументов args задается только
при использовании 2 или 3-го типа сигнатуры main и должен содержать
данные, необходимые для конфигурации запускаемой библиотеки в
изоляте, либо быть пустым. Третий аргумент – message, передается только

454
в случае использования 3-го типа сигнатуры main (обычно это порт для
взаимодействия между изолятами).
Изолят стартует сразу при его создании (аргумент по умолчанию
paused = false), но его можно и остановить, используя следующий метод:
isolate.pause(isolate.pauseCapability);

а для запуска остановленного изолята, необходимо использовать:


isolate.resume(isolate.pauseCapability);

Если изоляту на его старте не задали порты onExit или onError, то


перед их заданием, его необходимо остановить, после чего использовать
методы addOnExitListener и addErrorListener. Посредством аргумента
environment в изолят можно передать настройки среды приложения, а
debugName позволяет задавать имя изолята, которое будет использоваться
при отладке и логировании. Что касается остальных необязательных
аргументов метода spawnUri, если для вас это критично – обратитесь к
официальной документации [20].
В зависимости от того, взаимодействие между изолятами
осуществляется в рамках одной изоляционной группы или нет,
накладывается ряд ограничений на типы передаваемых объектов. Так,
например, при передаче данных из одной изоляционной группы в другую,
передаваемое через SendPort сообщение должно представлять собой
объект одного из следующих типов [21]:
− null;
− true и false;
− Экземпляры int, double, String;
− Экземпляры, созданные с помощью литералов list, map и set;
− Экземпляры, созданные конструкторами:
• List, Map, LinkedHashMap, Set и LinkedHashSet;
• TransferableTypedData;
• Capability.
− Экземпляр SendPort, полученный при обращении к полю sendPort
экземпляра класса ReceivePort или RawReceivePort;
− Экземпляры Type одного из типов, упомянутых выше, Object, dynamic,
void и Never, а также не null-safety варианты всех этих типов. Для
составных типов данных, все их части должны быть отправляемыми,
иначе объект этого типа данных не может быть передан.
В случае передачи сообщений между изолятами одной изоляционной
группы, через SendPort может быть отправлен объект любого типа,
кроме:
− Объекты с собственными ресурсами. Например: Socket;
− ReceivePort;
− DynamicLibrary;

455
− Finalizable;
− Finalizer;
− NativeFinalizer;
− Pointer;
− UserTag;
− MirrorReference.
Для демонстрации создания новой изоляционной группы и как с ней
работать, давайте создадим новый консольный проект
«isolate_spawn_uri» со следующей структурой директории «bin»:

Рисунок 6.5 – Структура директории «bin»

Первым делом переместите классы User, UserData и Support в файл


«user.dart». Следом за этим откройте файл «message.dart». В отличие от
работы в рамках одной изоляционной группы, сейчас мы не можем
передавать экземпляры классов сообщений StartMessage, StopMessage,
UserRequestMessage и UserResponseMessage, поэтому им предстоит
добавить именованный фабричный конструктор fromJson и метод toJson.
Такая декомпозиция объекта в тип Map<String, dynamic> позволит нам
разложить объект на примитивы, которые можно использовать при
передаче данных через SendPort из одной изоляционной группы в другую
с их последующим восстановлением:
// isolate_spawn_uri - message.dart
import 'dart:isolate';
import 'user.dart';

enum MessageType {
start,
stop,
userRequest,
userResponse;

static MessageType fromString(String value) {


return switch (value) {

456
'start' => MessageType.start,
'userRequest' => MessageType.userRequest,
'userResponse' => MessageType.userResponse,
'stop' => MessageType.stop,
_ => throw Exception('Unknown message type: $value'),
};
}
}

sealed class Message {


final MessageType type;
Message({required this.type});

factory Message.fromJson(Map<String, dynamic> json) {


if (json case {'type': var type}) {
var msType = MessageType.fromString(type);
return switch (msType) {
MessageType.start => StartMessage.fromJson(json),
MessageType.stop => StopMessage.fromJson(json),
MessageType.userRequest => UserRequestMessage.fromJson(
json,
),
MessageType.userResponse => UserResponseMessage.fromJson(
json,
),
};
}

throw Exception('Unknown message: $json');


}

Map<String, dynamic> toJson();


}

class StartMessage extends Message {


final SendPort sender;
StartMessage(
this.sender, {
super.type = MessageType.start,
});

factory StartMessage.fromJson(Map<String, dynamic> json) {


return StartMessage(json['sender']);
}

@override
Map<String, dynamic> toJson() {
return {'type': type.name, 'sender': sender};

457
}
}

class StopMessage extends Message {


StopMessage({super.type = MessageType.stop});

factory StopMessage.fromJson(Map<String, dynamic> json) {


return StopMessage();
}

@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
};
}
}

class UserRequestMessage extends Message {


final int id;
UserRequestMessage(
this.id, {
super.type = MessageType.userRequest,
});

factory UserRequestMessage.fromJson(Map<String, dynamic> json) {


return UserRequestMessage(json['userId']);
}

@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'userId': id,
};
}
}

class UserResponseMessage extends Message {


final User? user;
UserResponseMessage(
this.user, {
super.type = MessageType.userResponse,
});

factory UserResponseMessage.fromJson(Map<String, dynamic> json) {


if (json case {'type': 'userResponse', 'user': var user}) {
if (user is Map<String, dynamic>) {

458
return UserResponseMessage(User.fromJson(user));
}
}
return UserResponseMessage(null);
}

@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'user': user?.toJson(),
};
}
}

Далее реализуем функционал, который будет запускаться в новой


изоляционной группе. Для этого откройте файл «uri_isolate.dart» и
добавьте в него следующий код, начав с объявления импортов и функции
верхнего уровня – main:
// isolate_spawn_uri - uri_isolate.dart
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

import 'message.dart';
import 'user.dart';

void main(List<String> arguments, SendPort sendPort) async {


int? startUserId;
if (arguments.isNotEmpty) {
startUserId = int.tryParse(arguments[0]);
return;
}

var receivePort = ReceivePort();


// отправка сообщения в другую изоляционную группу
sendPort.send(
StartMessage(receivePort.sendPort).toJson(),
);

if (startUserId is int) {
var user = await fetchUser(startUserId);
sendPort.send(
UserResponseMessage(user).toJson(),
);
}

459
receivePort.listen((message) async {
var mes = Message.fromJson(message);
switch (mes) {
case StopMessage():
sendPort.send(
StopMessage().toJson(),
);
receivePort.close();
Isolate.current.kill();
case UserRequestMessage(id: var id):
var user = await fetchUser(id);
sendPort.send(
UserResponseMessage(user).toJson(),
);
case StartMessage() || UserResponseMessage():
print('Message is not supported');
}
});
}

Future<User?> fetchUser(int id) async {


User? user;
var httpClient = HttpClient();
try {
var request = await httpClient.getUrl(
Uri.parse('https://reqres.in/api/users/$id'),
); // запрос по адресу, чтобы получить данные о пользователе
// с конкретным id
var response = await request.close();
if (response.statusCode == HttpStatus.ok) {
var responseBody = await response
.transform(
utf8.decoder,
)
.join();
user = User.fromJson(jsonDecode(responseBody));
}
} catch (e) {
print('An error occurred during the API call: $e');
} finally {
httpClient.close();
}
return user;
}

Обратите внимание, что у всех экземпляров классов сообщений, перед


их отправкой, вызывается метод toJson. Если этого не сделать, то

460
приложение упадет со следующей ошибкой: ArgumentError (Invalid
argument: is a regular instance reachable via : Instance of
'StartMessage').
Теперь откройте файл «isolate_spawn_uri.dart». В него мы добавим
код для создания изолята в новой изоляционной группе и ввода
идентификатора пользователя с клавиатуры, данные по которому мы
хотим получить:
// isolate_spawn_uri - isolate_spawn_uri.dart
import 'dart:io';
import 'dart:isolate';

import 'message.dart';

void main() async {


var receivePort = ReceivePort();
await Isolate.spawnUri(
Uri.parse('uri_isolate.dart'),
[],
receivePort.sendPort,
);

SendPort? sendPort;
// слушаем порт изолята
receivePort.listen((message) {
var mes = Message.fromJson(message);
switch (mes) {
case StartMessage(sender: var port):
sendPort = port;
case StopMessage():
print('Isolate stopped');
receivePort.close();
case UserResponseMessage(user: var user):
print(user);
case UserRequestMessage():
print('Message is not supported');
}
});

await Future.delayed(Duration(seconds: 1));

while (true) {
if (sendPort == null) {
print('Isolate not started');
break;
}

print('Enter user id');

461
var input = stdin.readLineSync()!;
var id = int.tryParse(input);
if (id is int) {
sendPort?.send(
UserRequestMessage(id).toJson(),
);
} else if (input == 'exit') {
sendPort?.send(
StopMessage().toJson(),
);
break;
} else {
print('Invalid user id');
}
await Future.delayed(Duration(seconds: 1));
}
}

Рисунок 6.6 – Запуск приложения

Надеюсь, теперь вас не так сильно будет пугать необходимость


использования изолятов и появилась хотя бы крупица понимания, что
такое изоляционные группы, а также чем отличается передача сообщений
между изолятами одной и разных изоляционных групп. Если хотите еще
глубже погрузиться в изоляты – обратитесь к официальной документации
[22].

462
6.4 Async или Isolate?
Вот несколько советов, которые помогут вам определиться, что
использовать – асинхронное программирование или изоляты:
− Если части кода не должны быть прерваны, используйте обычный
синхронный процесс (один метод или несколько методов, которые
вызывают друг друга);
− Если фрагменты кода могут работать независимо, без влияния на
плавность работы приложения (отсутствие зависаний), используйте
Future;
− Если работа может занять некоторое время и потенциально вызывать
задержки в работе графического пользовательского интерфейса
приложения, используйте Isolate.
Также при выборе того, использовать Future или Isolate можно
ориентироваться на среднее время, необходимое для выполнения кода:
− Future, если выполнение метода занимает пару миллисекунд.
− Isolate, если время работы метода может занимать несколько сотен и
более миллисекунд.

6.5. Зоны (Zones)


Зона представляет собой среду для выполнения кода, которая остается
стабильной при выполнении асинхронных вызовов [23]. Говоря другими
словами, зона представляет собой глобальный try…catch или виртуальную
изолированную область, исключения в которой не будет влиять на другие
зоны. Это позволяет перехватить и обработать неожиданные ошибки в
процессе синхронного и асинхронного выполнения кода, которые не
удалось предусмотреть на этапе проектирования приложения.
Даже если вы не используете этот механизм, ваш код все равно
работает в контексте зоны (по умолчанию – Zone.root):
import 'dart:async';

void main(){
print(Zone.current); // Instance of '_RootZone'
}

Для запуска кода в другой зоне используется функция runZoned или


runZonedGuarded библиотеки dart:async. Второй вариант более удобен, т.к.
на верхний уровень вынесен аргумент, принимающий другую функцию, где
прописывается обработка не перехваченных в теле зоны исключений. А в
случае с runZoned это предстоит прописывать вручную, используя класс
ZoneSpecification, благодаря которому разработчики могут при создании
новой зоны переопределить некоторые функциональные возможности

463
существующей. Это может быть замена или изменение поведения print,
таймеров, микрозадач или способа обработки не перехваченных ошибок.
Давайте начнем с чего-нибудь простого. А именно, объявим
пользовательскую зону, в которой будут перехватываться и
модифицироваться все вызовы функции ptrint:
// ex6_32.dart
import 'dart:async';

void main(){
runZoned((){
print('Hello Zone: ${Zone.current}');
print('(⊙ _ ⊙ )');
}, zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line){
// self - зона, на которую был зарегистрирован обработчик
// parent - родительская зона self
// zone - текущая зона, для которой родительской выступает self
// line - строка, которая была передана в Zone.print
parent.print(zone, '${DateTime.now()} [$self] $line');
}
));
}
/* 2023-11-10 17:32:32.529321 [Instance of '_CustomZone'] Hello
Zone: Instance of '_CustomZone'
2023-11-10 17:32:32.533321 [Instance of '_CustomZone'] (⊙ _ ⊙ ) */

Функция runZoned принимает на вход следующие параметры:


R runZoned<R>(
R body(),
{Map<Object?, Object?>? zoneValues,
ZoneSpecification? zoneSpecification,
)

Как видно из сигнатуры runZoned, она может возвращать тот же тип


значения, что и функция body, в которой указывается выполняемый в
рамках создаваемой зоны код (часть кода) приложения. Если не
используются необязательные аргументы zoneValues и zoneSpecification,
то их значения берутся из родительской зоны. Таким образом, у зон могут
быть общие значения, хранимые в zoneValues, если этот аргумент не
переопределили при создании новой зоны и общие функциональные
возможности.
Представьте, что пользователь ввел пароль и у нас где-то в коде он
отправился на вывод в терминал… не приятная, с точки зрения
безопасности ситуация, но ее достаточно просто исправить:

464
// ex6_33.dart
import 'dart:async';

final censored = '凸( •̀_•́ )凸';

void main() async {


await runZoned(
() async {
await Future.delayed(
Duration(milliseconds: 300),
() {
print('Hello Zone: ${Zone.current}');
print('qwerty');
},
);
},
zoneValues: {#_secret: 'qwerty'}, // тип ключа - symbol
zoneSpecification: ZoneSpecification(print: (
Zone self,
ZoneDelegate parent,
Zone zone,
String line,
) {
if (line.contains(Zone.current[#_secret] as String)) {
line = censored;
}
parent.print(zone, '${DateTime.now()} $line');
}));

print(Zone.current[#_secret]);
}
// 2023-11-10 18:02:12.163632 Hello Zone: Instance of '_CustomZone'
// 2023-11-10 18:02:12.169635 凸( •̀_•́ )凸
// null

А теперь расширим пример еще одной вложенной зоной:


// ex6_34.dart
import 'dart:async';

final censored = '凸( •̀_•́ )凸';

Future<void> callPrint() async{


await Future.delayed(
Duration(milliseconds: 300),
() {
print('Hello Zone: ${Zone.current}');
print('qwerty');
},

465
);
}

void main() async {


await runZoned(
() async {
await callPrint();

await runZoned(
() async {
await callPrint();
},
zoneSpecification: ZoneSpecification(print: (
Zone self,
ZoneDelegate parent,
Zone zone,
String line,
) {
if (line.contains(Zone.current[#_secret] as String)) {
line = censored;
}
parent.print(zone, '${DateTime.now()} $line');
}),
);
},
zoneValues: {#_secret: 'qwerty'}, // тип ключа - symbol
);

print(Zone.current[#_secret]);
}
// Hello Zone: Instance of '_CustomZone'
// qwerty
// 2023-11-10 18:26:20.128392 Hello Zone: Instance of '_CustomZone'
// 2023-11-10 18:26:20.134393 凸( •̀_•́ )凸
// null

Давайте разберемся, как можно обработать не перехваченное


исключение или ошибку во время асинхронной работы приложения,
используя параметр handleUncaughtError конструктора класса
ZoneSpecification. Почему только во время асинхронной работы? А все
дело в том, что если выбрасывается не перехватываемое исключение при
синхронном выполнении кода, то после его перехвата, приложение
экстренно завершится:
// ex6_35.dart
import 'dart:async';

final censored = '凸( •̀_•́ )凸';

466
Future<void> callPrint() async {
await Future.delayed(
Duration(milliseconds: 300),
() {
print('Hello Zone: ${Zone.current}');
},
);
await Future.error(censored);
}

void main() async {


await runZoned(
() async {
await callPrint();
},
zoneSpecification: ZoneSpecification(
handleUncaughtError: (
Zone self,
ZoneDelegate parent,
Zone zone,
Object error,
StackTrace stackTrace,
) {
try {
print('error: $error, stackTrace: $stackTrace');
} catch (e, s) {
if (identical(e, error)) {
parent.handleUncaughtError(zone, error, stackTrace);
} else {
parent.handleUncaughtError(zone, e, s);
}
}
},
),
);
print('Завершение программы');
}
// Hello Zone: Instance of '_CustomZone'
// error: 凸( •̀_•́ )凸, stackTrace:

Такие ошибки необходимо перехватывать в теле зоны, используя


конструкцию try…catch…finally:
// ex6_36.dart
void main() async {
await runZoned(
() async {

467
try {
await callPrint();
} catch (e) {
print(e);
}
},
zoneSpecification: ZoneSpecification(
handleUncaughtError: (
Zone self,
ZoneDelegate parent,
Zone zone,
Object error,
StackTrace stackTrace,
) {
try {
print('error: $error, stackTrace: $stackTrace');
} catch (e, s) {
if (identical(e, error)) {
parent.handleUncaughtError(zone, error, stackTrace);
} else {
parent.handleUncaughtError(zone, e, s);
}
}
},
),
);
print('Завершение программы');
}
// Hello Zone: Instance of '_CustomZone'
// 凸( •̀_•́ )凸
// Завершение программы

А сейчас перепишем код в теле объявляемой зоны, для генерации


асинхронной ошибки, используя Future.error:
// ex6_37.dart
void main() async {
await runZoned(
() async {
Future.error(censored);
},
zoneSpecification: ZoneSpecification(
handleUncaughtError: (
Zone self,
ZoneDelegate parent,
Zone zone,
Object error,
StackTrace stackTrace,

468
) {
try {
print('error: $error, stackTrace: $stackTrace');
} catch (e, s) {
if (identical(e, error)) {
parent.handleUncaughtError(zone, error, stackTrace);
} else {
parent.handleUncaughtError(zone, e, s);
}
}
},
),
);
print('Завершение программы');
}
//error: 凸( •̀_•́ )凸, stackTrace:
//Завершение программы

Более лаконично приведенный выше код, но работающий по той же


схеме, можно написать, используя функцию runZonedGuarded для создания
новой зоны:
// ex6_38.dart
import 'dart:async';

final censored = '凸( •̀_•́ )凸';

void main() async {


await runZonedGuarded(
() async {
Future.error(censored);
// или
// Future.delayed(Duration(milliseconds: 300), () {
// throw ArgumentError(censored);
// });
},
(Object error, StackTrace stackTrace) {
print('error: $error, stackTrace: $stackTrace');
}
);
print('Завершение программы');
}
//error: 凸( •̀_•́ )凸, stackTrace:
//Завершение программы

Сигнатура этой функции отличается от runZoned всего лишь одним


аргументом – onError, который переопределяет необязательный аргумент
handleUncaughtError класса ZoneSpecification:

469
R? runZonedGuarded<R>(
R body(),
void onError(
Object error,
StackTrace stack
),
{Map<Object?, Object?>? zoneValues,
ZoneSpecification? zoneSpecification}
)

Под конец текущего раздела давайте немного пошалим и изменим


поведение создаваемых в зоне таймеров. Для обычного таймера увеличим
время срабатывания, а у периодического поменяем его тело выполнения,
ограничив тремя срабатываниями:
// ex6_39.dart
import 'dart:async';

void main() async {


await runZonedGuarded(
() async {
Timer(Duration(milliseconds: 1), () {
print('Запуск одноразового таймера');
});
Timer.periodic(Duration(milliseconds: 500), (timer) {
print('Запуск периодического таймера');
});
},
(Object error, StackTrace stackTrace) {
print('error: $error, stackTrace: $stackTrace');
},
zoneSpecification: ZoneSpecification(
createTimer: (
Zone self,
ZoneDelegate parent,
Zone zone,
Duration duration,
void Function() f,
) {
var newDuration = duration + Duration(seconds: 2);
return parent.createTimer(zone, newDuration, f);
},
createPeriodicTimer: (
Zone self,
ZoneDelegate parent,
Zone zone,
Duration period,
void Function(Timer timer) f,

470
) {
return parent.createPeriodicTimer(
zone,
period,
(Timer timer) {
if (timer.tick > 3) {
timer.cancel();
return;
}
print('Hello Zone: ${Zone.current}');
},
);
},
),
);
print('Завершение программы');
}
// Завершение программы
// Hello Zone: Instance of '_CustomZone'
// Hello Zone: Instance of '_CustomZone'
// Hello Zone: Instance of '_CustomZone'
// Запуск одноразового таймера

Благодаря зонам у разработчика имеется достаточно гибкий механизм


переопределения части поведения функциональных возможностей
родительской зоны, возможность обработки не перехватываемых ошибок и
исключений, возникающих при асинхронном выполнении кода и т.д.
Приведенных примеров должно хватить на основные сценарии
использования зон. Просто поэкспериментируйте с ними: верните из зоны
значение, замените какое-то поведение, сделайте так, чтобы print сразу
писал данные в файл. Если же вы хотите прослыть монсеньором, который
знает толк в извращениях, то перед погружением в документацию, github и
поиск по достаточно узкой теме, рекомендую разжиться каким-нибудь
крепким напитком

6.6. Сетевое программирование


В настоящее время трудно представить приложение, которое не
осуществляет работу по сети. Очень часто нам приходится
взаимодействовать с различными сервисами для отправки им или
получения данных, их последующей обработки и визуализации. Либо
самим организовывать клиент-серверную архитектуру у разрабатываемого
программного продукта.
Dart предоставляет довольно обширные возможности для
организации такой работы посредством библиотеки dart:io. Вследствие

471
чего довольно просто написать собственный TCP, UDP или HTTP сервер, а
также клиентские приложения для работы с ним.
Способы взаимодействия между программами по сети можно разбить
на две категории: с установлением и без установления соединения.
Посредством интерфейса сокетов и стека протоколов TCP/IP каждый из них
может работать, используя различные транспортные механизмы. К
сожалению, семиуровневая модель обмена данными по сети OSI (Open
Systems Interconnection), представленная в стандарте ISO (International
Standards Organization), является излишне сложной для большинства
реальных применений. В связи с этим в стеке протоколов ТСР/IP
используется ее сокращенная, четырехуровневая версия.
В этом случае разрабатываемые приложения реализуют прикладной
уровень. Он позволяет обмениваться сообщениями двум процессам,
которые выполняются либо локально, либо на разных компьютерах. Далее
с прикладного уровня сообщение передается транспортному, где
транспортный протокол разбивает длинные сообщения на сегменты и
передает их на сетевой уровень. Здесь сегменты разбиваются на
датаграммы (порции данных) и осуществляется их маршрутизация. В свою
очередь, сетевой уровень использует канальный для передачи датаграмм
между отдельными системами на пути от источника сообщения к пункту
его назначения.
Принцип работы протоколов, которые ориентированы на
установление соединения, можно описать на примере обычного
телефонного разговора. Отправной точкой возьмем начало самого
процесса — запрос соединения с конкретным абонентом (узлом/точкой
сети). У абонента на другой стороне имеется выбор: ответить или не
ответить на звонок. Если он отвечает на звонок, то разговор может
состояться различным количеством способов: в виде монолога, диалога,
постоянного перебивания друг друга и так далее. В этом случае вы можете
быть уверены, что никакая часть передаваемой информации не теряется и
перед окончанием разговора вы попрощаетесь с собеседником, после чего
оба завершите звонок. Также имеется возможность понять, что в процессе
разговора произошли неполадки: собеседника не слышно или резко
оборвался звонок. Основной транспортный протокол, который
ориентирован на установление соединения — ТСР (Transmission Control
Protocol — протокол управления передачей).
Принцип работы протоколов, которые не ориентированы на
установление соединения, можно описать на примере почтовой рассылки
(не по e-mail, а старой и доброй «Почтой России»). В большинстве случаев
письма дойдут до адресатов, но на это нет гарантии. И если что-то пошло
не так, необходимо самостоятельно найти выход из сложившейся ситуации.

472
В этом случае основным транспортным протоколом является UDP (User
Datagram Protocol — протокол пользовательских датаграмм).
UDP и ТСР работают поверх сетевого IР-протокола. Эта комбинация
протоколов, а также ряд протоколов прикладного уровня, которые
выполняются поверх них, известны под общим названием «стек
протоколов TCP/IP».
Существует две версии сетевого протокола IP: IPv4 и IPv6. В первом
случае адреса сокетов представляют собой пары (address, port), где address
— адрес IPv4, а port — номер порта в диапазоне 1—65 535. Адреса сокетов
IPv6 представляют собой кортежи из четырех элементов вида (address, port,
flowinfo, scopeid). Следует запомнить, что взаимодействие по сети всегда
предполагает обмен байтовыми строками (октетами), и если вам
необходимо передать текст, то сначала его надо кодировать в байты,
которые впоследствии декодирует получатель.
В большинстве случаев сетевой обмен осуществляется с
использованием клиент-серверной архитектуры, закладываемой в
разрабатываемые приложения. Сервер прослушивает входящий трафик по
своему сетевому адресу на определенном порту. Пока запросов от клиентов
не поступает, он будет пребывать в состоянии ожидания, то есть не
предпринимать каких-либо действий. В зависимости от выбранного
протокола (с установлением или без установления соединения) связь
сервера с клиентом будет осуществляться по-разному.
В первом случае клиент должен инициировать начальный обмен
данными с сервером, который в конечном счете устанавливает соединение
посредством сетевого канала связи между двумя процессами. Данное
соединение будет держаться и позволять процессам обмениваться
сообщениями до тех пор, пока оба не изъявят желание завершить сеанс
связи. В данном случае сервер для обслуживания запросов должен
реализовывать асинхронный, многопоточный или многопроцессорный
подход для обработки каждого входящего соединения. Это связано с тем,
что вызовы методов сокетов являются блокирующими, и сервер не сможет
обрабатывать новые входящие соединения, пока не завершит работу с
предыдущими.
Во втором случае запросы поступают на сервер в случайном порядке и
немедленно обрабатываются. То есть ответ от сервера посылается клиенту
без задержки. При этом каждое сообщение обрабатывается независимо от
других, что делает UDP хорошо приспособленным для кратковременного
взаимодействия без сохранения состояния (служба DNS) или процесса
начальной загрузки сети.

473
6.6.1. Разработка пакета «protocol»
Под протоколом передачи данных обычно понимают некоторый
своеобразный язык, используемый по договоренности двумя сторонами
(компьютерными программами или джентльменами, разрабатывающими
составные части приложения) для общения между собой. Все межсетевое
взаимодействие осуществляется посредством интерфейса сокетов, которые
могут реализовывать различные транспортные механизмы.
Прежде чем приступим к написанию клиентского и серверного
приложения, необходимо создать пакет «protocol». Он будет
использоваться во всех следующих примерах, предоставляя классы
сообщений, для обмена данными между клиентом и сервером. Удалите из
директории пакета файлы с примером и тестом (они нам не понадобятся) и
измените структуру каталога «lib», как представлено на рисунке ниже:

Рисунок 6.7 – Структура пакета «protocol»

Сервер будет у себя хранить файл с пользователями, к операциям над


которыми должен быть доступ у клиентского приложения (удалить,
добавить, изменить, получить весь список пользователей или конкретного
по его id). Чтобы при использовании конструкции switch у нас была
гарантия покрытия всех вариантов объявленных сообщений, они будут
наследоваться от одного базового класса с модификатором sealed.
Начнем с класса пользователя, его сериализации и десериализации.
Для этого откройте файл «user.dart» и добавьте в него следующий код:
// user.dart
class User{
final int id;

474
final String name;
final int age;
final String education;

User({
required this.id,
required this.name,
required this.age,
required this.education
});

factory User.fromJson(Map<String, dynamic> json){


return User(
id: json['id'],
name: json['name'],
age: json['age'],
education: json['education']
);
}

Map<String, dynamic> toJson() => {


'id': id,
'name': name,
'age': age,
'education': education
};

@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('User{id: $id, name: $name, ');
sb.write('age: $age, education: $education}');
return sb.toString();
}
}

Далее откройте файл «request_message.dart». Он будет содержать


классы запросов, которые клиент может отправить на сервер. Начнем его
наполнение с объявления перечисления, описывающего тип запросов:
// request_message.dart
import 'user.dart';

////////////RequestType//////////
enum RequestType {
all,
add,
update,

475
delete,
get;

static RequestType fromString(String value) {


try {
return RequestType.values.firstWhere(
(element) => element.toString().split('.').last == value,
);
} catch (_) {
throw FormatException('Invalid enum value: $value');
}
}
}

Первым объявим базовый класс с фабричным конструктором, что


позволит создавать экземпляры его производных классов и сразу
приводить их к базовому:
////////////RequestMessage//////////
sealed class RequestMessage {
final RequestType type;

RequestMessage({required this.type});

factory RequestMessage.fromJson(Map<String, dynamic> json) {


switch (RequestType.fromString(json['type'])) {
case RequestType.all:
return GetAllUsersRequest();
case RequestType.add:
return AddUserRequest.fromJson(json);
case RequestType.update:
return UpdateUserRequest.fromJson(json);
case RequestType.delete:
return DeleteUserRequest.fromJson(json);
case RequestType.get:
return GetUserRequest.fromJson(json);
}
}

Map<String, dynamic> toJson();


}

Первым реализуем класс запроса на получение списка всех


пользователей, данные которых хранятся на сервере:
class GetAllUsersRequest extends RequestMessage {
GetAllUsersRequest({super.type = RequestType.all});

476
@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
};
}
}

Следом пойдут классы сообщений на добавление и обновление


данных пользователя:
class AddUserRequest extends RequestMessage {
final User user;

AddUserRequest(this.user, {super.type = RequestType.add});

factory AddUserRequest.fromJson(
Map<String, dynamic> json,
) {
return AddUserRequest(User.fromJson(json['user']));
}

@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'user': user.toJson(),
};
}
}

class UpdateUserRequest extends RequestMessage {


final User user;

UpdateUserRequest(
this.user, {
super.type = RequestType.update,
});

factory UpdateUserRequest.fromJson(
Map<String, dynamic> json,
) {
return UpdateUserRequest(
User.fromJson(json['user']),
);
}

477
@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'user': user.toJson(),
};
}
}

Последние 2 типа сообщений, которые клиент может направить на


сервер – удаление пользователя или получение данных о пользователе по
его идентификатору:
class DeleteUserRequest extends RequestMessage {
final int id;

DeleteUserRequest(
this.id, {
super.type = RequestType.delete,
});

factory DeleteUserRequest.fromJson(
Map<String, dynamic> json,
) {
return DeleteUserRequest(json['id']);
}

@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'id': id,
};
}
}

class GetUserRequest extends RequestMessage {


final int id;
GetUserRequest(
this.id, {
super.type = RequestType.get,
});

factory GetUserRequest.fromJson(
Map<String, dynamic> json,
) {
return GetUserRequest(json['id']);
}

478
@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'id': id,
};
}
}

Теперь перейдем к файлу «response_message.dart». Главное отличие


будет в том, что по результатам выполнение операции добавления,
удаления или обновления, на клиент будет отправляться не 3 сообщения, а
одно – OperationResponse:
// response_message.dart
import 'user.dart';

////////////ResponseType//////////
enum ResponseType {
all,
success,
get;

static ResponseType fromString(String value) {


try {
return ResponseType.values.firstWhere(
(element) => element.toString().split('.').last == value,
);
} catch (_) {
throw FormatException('Invalid enum value: $value');
}
}
}

////////////ResponseMessage//////////
sealed class ResponseMessage {
final ResponseType type;

ResponseMessage({required this.type});
factory ResponseMessage.fromJson(Map<String, dynamic> json) {
switch (ResponseType.fromString(json['type'])) {
case ResponseType.all:
return GetAllUsersResponse.fromJson(json);
case ResponseType.get:
return GetUserResponse.fromJson(json);
case ResponseType.success:
return OperationResponse.fromJson(json);

479
}
}

Map<String, dynamic> toJson();


}

class GetAllUsersResponse extends ResponseMessage {


final List<User> users;

GetAllUsersResponse(
this.users, {
super.type = ResponseType.all,
});

factory GetAllUsersResponse.fromJson(
Map<String, dynamic> json,
) {
final usersResponse = json['users'] as List<dynamic>;
if (usersResponse.isNotEmpty) {
return GetAllUsersResponse(
usersResponse.map((e) => User.fromJson(e)).toList(),
);
}
return GetAllUsersResponse([]);
}

@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'users': users.map((e) => e.toJson()).toList(),
};
}
}

class GetUserResponse extends ResponseMessage {


final User? user;

GetUserResponse(
this.user, {
super.type = ResponseType.get,
});

factory GetUserResponse.fromJson(
Map<String, dynamic> json,
) {
if (json['user'] != null) {
return GetUserResponse(User.fromJson(json['user']));

480
}
return GetUserResponse(null);
}

@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'user': user?.toJson(),
};
}
}

class OperationResponse extends ResponseMessage {


final bool success;
OperationResponse(
this.success, {
super.type = ResponseType.success,
});

factory OperationResponse.fromJson(
Map<String, dynamic> json,
) {
return OperationResponse(json['success']);
}

@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'success': success,
};
}
}

Осталось только прописать экспорт функционала реализованных


библиотек в файле «protocol.dart»:
// protocol.dart
library;

export 'src/request_message.dart';
export 'src/response_message.dart';
export 'src/user.dart';

481
6.6.2. Клиент- серверное приложение на основе TCP
Создайте новое консольное приложение «tcp_server» в той же
директории, где располагается пакет «protocol» и добавьте в него файлы и
директории в соответствии с представленной ниже структурой проекта:

Рисунок 6.8 – Структура пакета «tcp_server»

Прежде чем переходить к заполнению файла с пользователями или


реализации функционала сервера, откройте «pubspec.yaml» и добавьте
разработанный ранее пакет в виде зависимости текущего проекта:
# pubspec.yaml
name: tcp_server
description: A sample command-line application.
version: 1.0.0

environment:
sdk: '>=3.0.0 <5.0.0'

dependencies:
protocol:
# Настройки пакета
path: ../protocol

dev_dependencies:
lints: ^2.1.0
test: ^1.24.0

Далее заполним файл «users.json» данными о пользователях,


которые будут запрашиваться клиентом у сервера:

482
[
{
"id": 2,
"name": "Petr Petrov",
"age": 25,
"education": "Bachelor"
},
{
"id": 3,
"name": "Sidor Sidorov",
"age": 18,
"education": "Secondary"
},
{
"id": 5,
"name": "Stasko",
"age": 34,
"education": "PhD"
},
{
"id": 6,
"name": "Ivan Ivanov",
"age": 25,
"education": "Master"
}
]

В файле «user_db.dart» объявим класс UserDB, в котором реализуем


функционал хранилища для добавления, удаления или модификации
хранимых о пользователе данных. Начнем с импорта необходимых
зависимостей, описания исключения, которое может быть сгенерировано в
процессе работы с мини-БД и заготовки класса UserDB:
// user_db.dart
import 'dart:async';
import 'dart:io';
import 'dart:convert';

import 'package:protocol/protocol.dart';

class DBException implements Exception {


final String? msg;
const DBException([this.msg]);

@override
String toString() => msg ?? 'DBException';
}

483
class UserDB {
var _users = <User>[];
late File _file;
final String patToDB;

UserDB(this.patToDB);

Future<void> init() async {


_file = File(patToDB);
if (await _file.exists()) {
try {
var data = jsonDecode(
await _file.readAsString(),
) as List<dynamic>;
_users = data.map((e) => User.fromJson(e),).toList();
} catch (e) {
throw DBException('Ошибка десериализации: $e');
}
} else {
throw DBException('Отсутствует файл с данными');
}
}

Следующие методы класса UserDB отвечают за сохранение данных в


файл, добавление нового пользователя и изменение существующего:
Future<void> save() async {
final encoder = JsonEncoder.withIndent(' ');
try {
var data = _users.map((e) => e.toJson()).toList();
await _file.writeAsString(encoder.convert(data));
} catch (e) {
throw DBException('Ошибка сериализации: $e');
}
}

Future<void> add(User user) async {


var index = _users.indexWhere(
(element) => element.id == user.id,
);
if (index == -1) {
_users.add(user);
await save();
} else {
throw DBException(
'Пользователь с таким id уже существует',

484
);
}
}

Future<void> update(User user) async {


var index = _users.indexWhere(
(element) => element.id == user.id,
);
if (index != -1) {
_users[index] = user;
await save();
} else {
throw DBException(
'Пользователь с таким id не существует',
);
}
}

Ниже приведены еще 3 метода класса UserDB – удаление, получение


списка всех пользователей или конкретного пользователя по его
идентификатору:
Future<void> delete(int id) async {
_users.removeWhere((element) => element.id == id);
await save();
}

Future<List<User>> getAll() async {


return _users;
}

Future<User?> getById(int id) async {


return _users.firstWhere(
(element) => element.id == id,
);
}

Для более удобного импортирования класса UserDB откройте файл


«tcp_server.dart» директории «lib» и пропишите в нем строчку с
экспортом:
export 'src/user_db.dart';

Теперь перейдите в файл «tcp_server.dart» директории «bin». Здесь


будет располагаться основной код реализуемого tcp-сервера, а начнем с
импорта необходимых библиотек и объявления функций для обработки
данных от клиента:

485
// bin – tcp_server.dart
import 'dart:convert';
import 'dart:io';

import 'package:protocol/protocol.dart';
import 'package:tcp_server/tcp_server.dart';

Future<GetAllUsersResponse> getAllUsers(UserDB db) async {


return GetAllUsersResponse(await db.getAll());
}

Future<OperationResponse> addUser(UserDB db, User user) async {


try {
await db.add(user);
return OperationResponse(true);
} catch (e) {
return OperationResponse(false);
}
}

Future<OperationResponse> updateUser(UserDB db, User user) async {


try {
await db.update(user);
return OperationResponse(true);
} catch (e) {
return OperationResponse(false);
}
}

Future<OperationResponse> deleteUser(UserDB db, int id) async {


try {
await db.delete(id);
return OperationResponse(true);
} catch (e) {
return OperationResponse(false);
}
}

Future<GetUserResponse> getUser(UserDB db, int id) async {


try {
return GetUserResponse(await db.getById(id));
} catch (e) {
return GetUserResponse(null);
}
}

486
Для создания TCP-сервера, в функции «main», используется класс
ServerSocket и его статический метод bind, в который передается IP и
номер порта сервера (хоста), после чего указывается анонимная функция
для асинхронной обработки каждого нового соединения к серверу:
// bin – tcp_server.dart
void main(List<String> arguments) async {
final encoder = JsonEncoder.withIndent(' ');
var userDB = UserDB('bin\\users.json');
await userDB.init();

ServerSocket? tcpServer;
print('Запуск main');
ServerSocket.bind('127.0.0.1', 8084).then((serverSocket) {
tcpServer = serverSocket;
serverSocket.listen((socket) {
// обрабатываем соединение очередного клиента
// с сервером
socket
.cast<List<int>>()
.transform(
utf8.decoder,
)
.listen((rawData) async {
var json = jsonDecode(rawData);
var clientRequest = RequestMessage.fromJson(json);
ResponseMessage message = switch (clientRequest) {
GetAllUsersRequest() => await getAllUsers(userDB),
AddUserRequest() => await addUser(
userDB,
clientRequest.user,
),
UpdateUserRequest() => await updateUser(
userDB,
clientRequest.user,
),
DeleteUserRequest() => await deleteUser(
userDB,
clientRequest.id,
),
GetUserRequest() => await getUser(
userDB,
clientRequest.id,
),
};
socket.write(encoder.convert(message));
});
});

487
});

// обрабатываем асинхронный ввод с клавиатуры


stdin.transform(utf8.decoder).listen((data) {
if (data == 'exit') {
tcpServer?.close();
exit(0);
}
});
print('Завершение main');
}

Каждое принимаемое от пользователя сообщение десериализуется и


на основе полученного типа экземпляра класса запроса выполняется
необходимое действие с отправкой ответа на клиент.
Настала пора приступить к написанию клиентской части. Создайте
новое консольное приложение «tcp_client» в той же директории, где
располагается пакет «protocol», указав его в качестве зависимости
разрабатываемого проекта. Никаких новых файлов или директорий
создавать не нужно. Откройте файл «tcp_client.dart» в каталоге «bin»,
добавив для начала импорт библиотек и пару функций, реализующих меню
взаимодействия с сервером:
// bin – tcp_client.dart
import 'dart:convert';
import 'dart:io';

import 'package:protocol/protocol.dart';

Future<void> menu(Socket socket) async {


while (true) {
print('*' * 20);
print('1. GetAllUsers');
print('2. AddUser');
print('3. UpdateUser');
print('4. DeleteUser');
print('5. GetUser');
print('6. Exit');
print('*' * 20);
var input = stdin.readLineSync();
if (input is String) {
switch (input) {
case '1':
socket.write(jsonEncode(GetAllUsersRequest()));
case '2':
var user = addUser();
socket.write(jsonEncode(AddUserRequest(user)));

488
case '3':
var user = addUser();
socket.write(jsonEncode(UpdateUserRequest(user)));
case '4':
var id = getId();
socket.write(jsonEncode(DeleteUserRequest(id)));
case '5':
var id = getId();
socket.write(jsonEncode(GetUserRequest(id)));
case '6':
// закрываем соединение и освобождаем ресурсы
socket.destroy();
return;
default:
print('Некорректный ввод');
}
}
await Future.delayed(const Duration(seconds: 2));
}
}

int getId() {
print('Введите введите id');
var id = int.parse(stdin.readLineSync()!);
return id;
}

User addUser() {
print('Введите введите id');
var id = int.parse(stdin.readLineSync()!);
print('Введите введите имя');
var name = stdin.readLineSync()!;
print('Введите введите возраст');
var age = int.parse(stdin.readLineSync()!);
print('Введите введите образование');
var education = stdin.readLineSync()!;
return User(
id: id,
name: name,
age: age,
education: education,
);
}

Для написания клиентской части нам понадобится класс Socket и его


статический метод connect, который создает новое соединение с

489
задаваемым хостом и портом и возвращает Future, завершающийся одним
из перечисленных образов:
− После подключения к серверу возвращается экземпляр класса Socket.
− Из-за того, что не удалось найти заданный хост и порт, генерируется
исключение.
// bin – tcp_client.dart
void main(List<String> arguments) async {
// соединяемся с сервером
var socket = await Socket.connect('127.0.0.1', 8084);

socket.cast<List<int>>().transform(utf8.decoder).listen((rawData)
{
var json = jsonDecode(rawData);
var clientRequest = ResponseMessage.fromJson(json);
switch (clientRequest) {
case GetAllUsersResponse() || GetUserResponse():
print(rawData);
case OperationResponse(success: bool success):
if (success) {
print('Success');
} else {
print('Failed');
}
}
});
menu(socket);
}

Первым необходимо запустить проект с серверной частью


приложения и только после этого стартовать клиента:

490
Рисунок 6.9 – Пример работы клиент-серверного приложения

Чтобы завершить работу с клиентской частью достаточно выбрать


нужный пункт меню. А для сервера – ввести в терминал «exit» и нажать
Enter, либо экстренно завершить приложение сочетанием клавиш
«Ctrl+C».
Поэкспериментируйте с различными запросами на сервер, добавьте
логгер, попробуйте предусмотреть ситуацию, что размер пакета передачи
данных ограничен и что делать, если сообщение не помещается в один
пакет.

6.6.3. Передача данных между сервером и клиентом по протоколу


UDP

Код организации соединения, что на стороне сервера, что на стороне


клиента будет не очень-то и отличаться. В обоих случаях используется класс
RawDatagramSocket с его статическим методом bind, который осуществляет

491
связывание с задаваемым хостом и портом и возвращает
Future<RawDatagramSocket>.
Создайте два новых консольных приложения «udp_server» и
«udp_client», подключив к ним пакет «protocol». Что касается структуры
проекта сервера, то она аналогична «tcp_server», за исключением
наполнения функции «main» исполняемого файла «udp_server.dart»
директории «bin»:
// bin – udp_server.dart
import 'dart:convert';
import 'dart:io';

import 'package:protocol/protocol.dart';
import 'package:udp_server/udp_server.dart';

Future<GetAllUsersResponse> getAllUsers(
UserDB db,
) async {
return GetAllUsersResponse(await db.getAll());
}

Future<OperationResponse> addUser(
UserDB db,
User user,
) async {
try {
await db.add(user);
return OperationResponse(true);
} catch (e) {
return OperationResponse(false);
}
}

Future<OperationResponse> updateUser(
UserDB db,
User user,
) async {
try {
await db.update(user);
return OperationResponse(true);
} catch (e) {
return OperationResponse(false);
}
}

Future<OperationResponse> deleteUser(
UserDB db,
int id,

492
) async {
try {
await db.delete(id);
return OperationResponse(true);
} catch (e) {
return OperationResponse(false);
}
}

Future<GetUserResponse> getUser(
UserDB db,
int id,
) async {
try {
return GetUserResponse(await db.getById(id));
} catch (e) {
return GetUserResponse(null);
}
}

void main(List<String> arguments) async {


final encoder = JsonEncoder.withIndent(' ');
var userDB = UserDB('bin\\users.json');
await userDB.init();

RawDatagramSocket? udpServer;
print('Запуск main');
await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 8083)
.then((serverSocket) {
udpServer = serverSocket;
serverSocket.listen((event) async {
if (event == RawSocketEvent.read) {
var datagram = serverSocket.receive();
var rawData = utf8.decode(datagram!.data);
var json = jsonDecode(rawData);
var clientRequest = RequestMessage.fromJson(json);
ResponseMessage message = switch (clientRequest) {
GetAllUsersRequest() => await getAllUsers(userDB),
AddUserRequest() => await addUser(
userDB,
clientRequest.user,
),
UpdateUserRequest() => await updateUser(
userDB,
clientRequest.user,
),
DeleteUserRequest() => await deleteUser(
userDB,

493
clientRequest.id,
),
GetUserRequest() => await getUser(
userDB,
clientRequest.id,
),
};
serverSocket.send(
utf8.encode(encoder.convert(message)),
serverSocket.address,
8084,
);
}
});
});

stdin.transform(utf8.decoder).listen((data) {
if (data == 'exit') {
udpServer?.close();
}
exit(0);
});
print('Завершение main');
}

Теперь откройте клиентское приложение и файл «udp_client.dart»


директории «bin» добавьте следующий код:
// bin – udp_client.dart
import 'dart:convert';
import 'dart:io';

import 'package:protocol/protocol.dart';

Future<void> menu(
(
RawDatagramSocket rawDgramSocket,
String ip,
int serverPort,
) connection) async {
while (true) {
print('*' * 20);
print('1. GetAllUsers');
print('2. AddUser');
print('3. UpdateUser');
print('4. DeleteUser');
print('5. GetUser');
print('6. Exit');

494
print('*' * 20);
var input = stdin.readLineSync();
var (socket, ip, port) = connection;
if (input is String) {
switch (input) {
case '1':
socket.send(
utf8.encode(jsonEncode(GetAllUsersRequest())),
InternetAddress(ip),
port,
);
case '2':
var user = addUser();
socket.send(
utf8.encode(jsonEncode(AddUserRequest(user))),
InternetAddress(ip),
port,
);
case '3':
var user = addUser();
socket.send(
utf8.encode(jsonEncode(UpdateUserRequest(user))),
InternetAddress(ip),
port,
);
case '4':
var id = getId();
socket.send(
utf8.encode(jsonEncode(DeleteUserRequest(id))),
InternetAddress(ip),
port,
);
case '5':
var id = getId();
print(jsonEncode(GetUserRequest(id)));
socket.send(
utf8.encode(jsonEncode(GetUserRequest(id))),
InternetAddress(ip),
port,
);
case '6':
// закрываем соединение и освобождаем ресурсы
socket.close();
return;
default:
print('Некорректный ввод');
}
}

495
await Future.delayed(const Duration(seconds: 2));
}
}

int getId() {
print('Введите введите id');
var id = int.parse(stdin.readLineSync()!);
return id;
}

User addUser() {
print('Введите введите id');
var id = int.parse(stdin.readLineSync()!);
print('Введите введите имя');
var name = stdin.readLineSync()!;
print('Введите введите возраст');
var age = int.parse(stdin.readLineSync()!);
print('Введите введите образование');
var education = stdin.readLineSync()!;
return User(
id: id,
name: name,
age: age,
education: education,
);
}

void main(List<String> arguments) async {


// соединяемся с сервером
var rawDgramSocket = await RawDatagramSocket.bind(
'127.0.0.1',
8084,
);
rawDgramSocket.listen((event) {
if (event == RawSocketEvent.read) {
var rawData = rawDgramSocket.receive();
var json = jsonDecode(utf8.decode(rawData!.data));
var clientRequest = ResponseMessage.fromJson(json);
switch (clientRequest) {
case GetAllUsersResponse() || GetUserResponse():
print(utf8.decode(rawData.data));
case OperationResponse(success: bool success):
if (success) {
print('Success');
} else {
print('Failed');
}
}

496
}
});
menu((rawDgramSocket, '127.0.0.1', 8083));
}

Как и в предыдущем случае, сначала стартуем серверную часть, а


потом клиентскую:

Рисунок 6.10 – Пример работы клиент-серверного приложения

6.6.4. Структура HTTP-сообщений и REST URLs


HTTP-сообщения, которые используются для взаимодействия с http-
серсисами, состоят из следующих элементов [24, 25]:
− Header (Заголовок);
− Body (Тело).
Заголовок содержит в себе такие метаданные как: информация о
кодировке, методах HTTP и т. д. Структура заголовка напоминает собой
словарь из пар «ключ:значение». Ключи могут нести как уточняющие
данные для сервиса или клиента, так и ограничивающие. Заголовки делятся
на 2 типа: заголовок запроса и ответа. Каждый из которых состоит из 3-х
подгрупп. В случае заголовка запроса это:
1. Основные заголовки (General headers). Такие заголовки относятся ко
всему сообщению. Например, Via (en-US).

497
2. Заголовки запроса (Request headers). Они позволяют задать
уточняющие параметры запроса (как, например, Accept-Language),
придающие контекст (как Referer), или накладывающие ограничения
на условия (like If-None).
3. Заголовки сущности (Entity headers). Например, Content-Length,
относящиеся к телу сообщения. Данные заголовки могут
отсутствовать, если у запроса нет тела.
Для заголовка ответа:
1. Основные заголовки (General headers).
2. Заголовки ответа (Response headers). Они позволяют сообщить
дополнительную информацию о сервере, которая не уместилась в
строку состояния (статуса).
3. Заголовки сущности (Entity headers).
При взаимодействии клиента и сервера заголовки запросов и ответов
отличаются не только
Body (тело) представляет собой данные в любом формате для передачи
по сети. Формат используемых данных указывается в поле заголовка
Content-Type. Например, Content- Type: application/json. Оно бывает не у всех
запросов и ответов. Такие запросы, как GET и DELETE в нем обычно не
нуждаются. У ответов с кодом состояния (статуса), например, 201 или 204,
оно тоже обычно отсутствует.
На каждый запрос клиента приходит ответ от сервера, где первой
строкой идет строка состояния (например: HTTP/1.1 404 Not Found.),
содержащая в себе следующую информацию:
1. Версия протокола (HTTP/1.1);
2. Числовой код статуса (состояния) ответа о том прошел запрос успешно
или нет;
3. Пояснение (текстовое описание кода состояния).
Из самых распространенных числовых кодов состояний ответа можно
выделить:
− 200 (OK) – запрос выполнен успешно.
− 201 (Created) – запрос выполнен успешно и ресурс на стороне сервера
создан. Этот код ответа используется для подтверждения успеха
запроса PUT или POST.
− 400 (Bad Request) – запрос был неправильно сформирован.
− 404 (Not Found) – требуемый ресурс не найден.
− 401 (Unauthorized) – необходимо выполнить аутентификацию перед
доступом к ресурсу.
− 405 (Method Not Allowed) – используемый метод HTTP не
поддерживается для данного ресурса.
− 409 (Conflict) – произошел конфликт.

498
− 500 (Internal Server Error) – внутренняя ошибка сервера (обработка
запроса не удалась из-за непредвиденных обстоятельств на стороне
сервера).

REST URLs
Основной абстракцией в REST является ресурс, то есть любая
информация, которой можно присвоить имя: документ, сущность,
изображение и т. д. Он может быть одноэлементным (project) или
коллекцией (projects). Доступ к ресурсам REST API сервиса осуществляется
посредством обращения к ним через Uniform Resource Identifier
(унифицированный идентификатор ресурсов, URI), который на сервере
представлен в виде URL-адреса верхнего уровня.
Предположим, у вас есть REST API, который предоставляет
информацию о проектах и списке дел, которые необходимо выполнить в
рамках проекта. Если вы хотите получить информацию о задаче, используя
ее идентификатор, URL-адрес будет выглядеть следующим образом:
http://www.domain.com/api/v1/todo/projects/3/tasks/1
где www.yourdomain.com – домен сервера, api – указывает на то, что
работа будет осуществляться с предоставляемым сервером API для доступа
к его ресурсам, v1 – версия API, todo – сервис запущенный на сервере,
projects – коллекция проектов, 3 – идентификатор проекта из коллекции,
tasks – коллекция задач проекта с идентификатором 3, 1 - идентификатор
задачи из коллекции.
Также при доступе к ресурсам в конец URL при обращении от клиента
можно добавлять строку запроса:
http://www.domain.com/api/v1/todo/projects/3/tasks?id=1
По сути, приведенные выше два примера идентичны и отличаются
только способами обращения к сервису для получения ресурса. В первом
случае идентификаторы ресурсов указываются в формируемом пути:
projects/{proj-id}/tasks/{task-id}, а во втором используется комбинация
первого и строки запроса (query string), добавляемой к заголовку
отправляемого HTTP-сообщения: projects/{proj-id}/tasks?id={task-id}. Можно
даже сформировать доступ к задаче, перенеся идентификатор проекта в
строку запроса:
http://www.domain.com/api/v1/todo/projects/tasks?projID=3&taskID=1
Строка запроса может использоваться даже для замены тела (body)
отправляемого сообщения, так как по сути представляет собой связку пар
«ключ=значение», начинающуюся с символа вопросительного знака, где
символ «&» выступает в качестве разделителя между парами
«ключ=значение».
Хорошим правилом при формировании REST URLs является то, что
ресурс должен быть представлен существительным, а не глаголом. Да и
вообще наличие глаголов в пути к ресурсу не желательно (но иногда имеет

499
место быть). Это связано с тем, что существительные обладают свойствами,
тогда как глагол описывает действия. В связи с этим для того, чтобы на
стороне сервера понять, какие действия необходимо выполнить над
ресурсом, анализируется каким из HTTP-запросов было выполнено
обращение по URL: GET, PUT, POST или DELETE.
Также используйте версионирование API, поскольку оно позволяет
избежать проблем, когда не все клиенты способны моментально перейти на
новую версию, спокойно осуществлять работу над новой версией и ее
тестирование. Часто бывают моменты, что требуемый функционал новой
версии API работает не совсем корректно, либо обратно не совместим с
предыдущей и наличие старой версии с уже проверенной годами
функциональностью будет являться спасательным кругом. Конечно, со
временем старые версии будут удаляться, но главное, что это происходит
не слишком резко!
Для более глубокого погружения в принципы и лучшие практики
формирования REST URLs обратитесь к следующим ресурсам [26, 27].

6.6.4. HTTP-сервер и клиент


Создайте два новых консольных приложения «http_server» и
«http_client», подключив к ним пакет «protocol». Что касается структуры
проекта сервера, то она аналогична «tcp_server», за исключением кода
исполняемого файла «http_server.dart» директории «bin».
Начнем с объявления импортов и части функций, которые не
претерпели изменений, относительно реализации tcp или udp-сервера:
// bin – http_server.dart
import 'dart:convert';
import 'dart:io';

import 'package:protocol/protocol.dart';
import 'package:http_server/http_server.dart';

final _encoder = JsonEncoder.withIndent(' ');

Future<GetAllUsersResponse> getAllUsers(UserDB db) async {


return GetAllUsersResponse(await db.getAll());
}

Future<OperationResponse> addUser(UserDB db, User user) async {


try {
await db.add(user);
return OperationResponse(true);
} catch (e) {
return OperationResponse(false);
}

500
}

Future<OperationResponse> updateUser(UserDB db, User user) async {


try {
await db.update(user);
return OperationResponse(true);
} catch (e) {
return OperationResponse(false);
}
}

Future<OperationResponse> deleteUser(UserDB db, int id) async {


try {
await db.delete(id);
return OperationResponse(true);
} catch (e) {
return OperationResponse(false);
}
}

Future<GetUserResponse> getUser(UserDB db, int id) async {


try {
return GetUserResponse(await db.getById(id));
} catch (e) {
return GetUserResponse(null);
}
}

Теперь реализуем функцию для обработки GET-запроса на получение


списка всех пользователей (http://127.0.0.1:8080/allUsers) и
конкретного пользователя по его идентификатору
(http://127.0.0.1:8080/user?id=5):
void getHandlers(HttpRequest request, UserDB userDB) async {
var response = request.response;
if (request.uri.path.startsWith('/user') &&
request.uri.query != '') {
var id = int.parse(request.uri.queryParameters['id']!);
response
..headers.contentType = ContentType(
'application',
'json',
)
..write(_encoder.convert(await getUser(userDB, id)))
..close();
return;
}
if (request.uri.path.startsWith('/allUsers')) {

501
response
..headers.contentType = ContentType(
'application',
'json',
)
..write(_encoder.convert(await getAllUsers(userDB)))
..close();
return;
}
}

Далее добавим функцию для обработки POST-запроса на добавление


нового пользователя (http://127.0.0.1:8080/addUser) или изменение
данных существующего (http://127.0.0.1:8080/updateUser):
void postHandlers(HttpRequest request, UserDB userDB) async {
final response = request.response;
try {
final body = await utf8.decoder.bind(request).join();
if (request.uri.path.startsWith('/addUser')) {
final message = AddUserRequest.fromJson(jsonDecode(body));
await addUser(userDB, message.user);
} else if (request.uri.path.startsWith('/updateUser')) {
final message = UpdateUserRequest.fromJson(jsonDecode(body));
await updateUser(userDB, message.user);
} else {
throw ArgumentError('User add or update error');
}
response
..headers.contentType = ContentType(
'application',
'json',
)
..write(_encoder.convert(OperationResponse(true)))
..close();
} catch (e) {
response
..headers.contentType = ContentType(
'application',
'json',
)
..statusCode = HttpStatus.badRequest
..write(_encoder.convert(OperationResponse(false)))
..close();
}
}

502
Функция для удаления пользователя тоже обрабатывает POST-запрос,
но несколько иного вида (http://127.0.0.1:8080/deleteUser/{id}). То
есть нам предстоит изъять id пользователя из последнего сегмента пути:
void postPositionHandlers(HttpRequest request, UserDB userDB) async
{
final response = request.response;
final part = request.requestedUri.pathSegments;
try {
if (part[0] == 'deleteUser') {
var id = int.tryParse(part.last);
await deleteUser(userDB, id!);
response
..headers.contentType = ContentType(
'application',
'json',
)
..write(_encoder.convert(OperationResponse(true)))
..close();
} else {
throw ArgumentError('User delete error');
}
} catch (e) {
print(e);
response
..headers.contentType = ContentType(
'application',
'json',
)
..statusCode = HttpStatus.badRequest
..write(_encoder.convert(OperationResponse(false)))
..close();
}
}

Для написания функционала, который позволит получать данные с


использованием протокола HTTP через REST API будем использовать класс
HttpServer. Он представляет собой поток, предоставляющий объекты
HttpRequest, где каждый такой объект связан с объектом HttpResponse.
Таким образом, в ходе обработки запроса, сервер передает ответы на
сторону клиента, используя объект HttpResponse экземпляра класса
обрабатываемого запроса:
void main(List<String> arguments) async {
var userDB = UserDB('bin\\users.json');
await userDB.init();
HttpServer? httpServer;
print('Запуск main');

503
HttpServer.bind(InternetAddress.loopbackIPv4,
8080).then((server) {
httpServer = server;
server.listen((HttpRequest request) async {
try {
ContentType? contentType = request.headers.contentType;
switch ((request.method, contentType?.mimeType)) {
case ('GET', _):
getHandlers(request, userDB);
case ('POST', 'application/json'):
postHandlers(request, userDB);
case ('POST', _):
postPositionHandlers(request, userDB);
default:
request.response
..statusCode = HttpStatus.methodNotAllowed
..write('Unsupported request: ${request.method}.')
..close();
}
} catch (e) {
print('Exception in handleRequest: $e');
}
});
});

stdin.transform(utf8.decoder).listen((data) {
if (data == 'exit') {
httpServer?.close();
}
exit(0);
});
print('Завершение main');
}

Несмотря на то, что клиентское приложение еще не реализовано, мы


можем запустить сервер, открыть браузер и сделать несколько GET-
запросов:

504
Рисунок 6.11 – Получение данных пользователя с id=3

Рисунок 6.12 – Получение списка всех пользователей

Теперь откройте клиентское приложение и файл «http_client.dart»


директории «bin» добавьте следующий код:
// bin – http_client.dart
import 'dart:convert';

505
import 'dart:io';

import 'package:protocol/protocol.dart';

int getId() {
print('Введите введите id');
var id = int.parse(stdin.readLineSync()!);
return id;
}

User addUser() {
print('Введите введите id');
var id = int.parse(stdin.readLineSync()!);
print('Введите введите имя');
var name = stdin.readLineSync()!;
print('Введите введите возраст');
var age = int.parse(stdin.readLineSync()!);
print('Введите введите образование');
var education = stdin.readLineSync()!;
return User(
id: id,
name: name,
age: age,
education: education,
);
}

Future<void> getAllUsers(HttpClient httpClient) async {


try {
var request = await httpClient.getUrl(
Uri.parse('http://127.0.0.1:8080/allUsers'),
);
var response = await request.close();
if (response.statusCode == HttpStatus.ok) {
var responseBody = await response
.transform(
utf8.decoder,
)
.join();
print(responseBody);
}
} catch (e) {
print('An error occurred during the API call: $e');
}
}

Future<void> getUser(HttpClient httpClient, int id) async {


try {

506
var request = await httpClient.getUrl(
Uri.parse('http://127.0.0.1:8080/user?id=$id'),
);
var response = await request.close();
if (response.statusCode == HttpStatus.ok ||
response.statusCode == HttpStatus.badRequest) {
var responseBody = await response
.transform(
utf8.decoder,
)
.join();
print(responseBody);
}
} catch (e) {
print('An error occurred during the API call: $e');
}
}

Future<void> deleteUser(HttpClient httpClient, int id) async {


try {
var request = await httpClient.postUrl(
Uri.parse('http://127.0.0.1:8080/deleteUser/$id'),
);
var response = await request.close();
if (response.statusCode == HttpStatus.ok ||
response.statusCode == HttpStatus.badRequest) {
var responseBody = await response
.transform(
utf8.decoder,
)
.join();
print(responseBody);
}
} catch (e) {
print('An error occurred during the API call: $e');
}
}

Future<void> addOrUpdateUser(
HttpClient httpClient,
User user, [
String endpoint = 'updateUser',
]) async {
try {
var request = await httpClient.postUrl(
Uri.parse('http://127.0.0.1:8080/$endpoint'),
);
RequestMessage? requestMessage;

507
if (endpoint == 'updateUser') {
requestMessage = UpdateUserRequest(user);
} else {
requestMessage = AddUserRequest(user);
}
request
..headers.contentType = ContentType(
'application',
'json',
)
..write(jsonEncode(requestMessage));
final response = await request.close();
if (response.statusCode == HttpStatus.ok ||
response.statusCode == HttpStatus.badRequest) {
var responseBody = await response
.transform(
utf8.decoder,
)
.join();
print(responseBody);
}
} catch (e) {
print('An error occurred during the API call: $e');
}
}

void main(List<String> arguments) async {


while (true) {
print('*' * 20);
print('1. GetAllUsers');
print('2. AddUser');
print('3. UpdateUser');
print('4. DeleteUser');
print('5. GetUser');
print('6. Exit');
print('*' * 20);
final httpClient = HttpClient();
var input = stdin.readLineSync();
if (input is String) {
switch (input) {
case '1':
await getAllUsers(httpClient);
case '2':
var user = addUser();
await addOrUpdateUser(httpClient, user, 'addUser');
case '3':
var user = addUser();
await addOrUpdateUser(httpClient, user);

508
case '4':
var id = getId();
await deleteUser(httpClient, id);
case '5':
var id = getId();
await getUser(httpClient, id);
case '6':
// закрываем соединение и освобождаем ресурсы
httpClient.close();
return;
default:
print('Некорректный ввод');
}
}
httpClient.close();
await Future.delayed(const Duration(seconds: 2));
}
}

Если сервер не останавливали, то сразу стартуем клиентское


приложение, иначе сначала запустите http-сервер:

Рисунок 6.13 – Удаление пользователя

509
Резюме по разделу
В текущей главе были рассмотрены возможности асинхронного и
параллельного программирования в Dart, что такое цикл событий (Event
Loop) и принципы его работы, а также базовые механизмы для организации
межсетевого взаимодействия.
Future API, а также ключевые слова async и await предоставляют
довольно большой набор возможностей при написании асинхронного кода,
задачи по выполнению которого помещаются в очередь событий и при
необходимости могут быть добавлены в очередь микрозадач. Первым
делом цикл событий выполняет любые микрозадачи в порядке их
нахождения в очереди микрозадач. После того, как очередь микрозадач
становится пустой, начинается обработка первого элемента в очереди
событий. Затем идет повторение этого цикла. Когда же обе очереди
становятся пустыми и больше не ожидается событий, приложение
закрывается.
Isolate позволяет писать параллельный код и так как весь код,
который запускается в изолятах работает со своей областью памяти, то для
обмена данными между ними используются сообщения. В случае
изоляционных групп ими могут выступать экземпляры пользовательских
классов (с некоторыми ограничениями), а когда необходимо переслать
сообщение между разными изоляционными группами, его необходимо
декомпозировать до уровня примитивов, поддерживаемых SendPort.
Если выполнения задачи не занимает больше нескольких микросекунд
– используйте асинхронное программирование, в противном случае
задумайтесь о возможности использования изолятов.
При организации межсетевого взаимодействия не забывайте
закрывать сокеты, если они больше не нужны и корректно выходить из
приложения. Так как при экстренной остановке приложения могут
оставаться открытые соединения, из-за чего не получится его
перезапустить с исходными параметрами портов.

Вопросы для самопроверки


1. Что такое Event Loop и каковы принципы его работы в Dart?
2. В чем отличие очереди микрозадач от очереди событий?
3. Для чего используется очередь событий?
4. Когда при использовании Future API задачи добавляются в очередь
событий или микрозадач?
5. Для чего используется метод then экземпляра класса Future?
6. Как обрабатываются исключения при использовании Future API и
ключевых слов async и await? В чем разница?

510
7. Что такое Stream (Поток) в Dart и для чего он используется?
8. Какие классы используются для управления потоками в Dart? В чем их
отличия?
9. Что такое Isolate? Опишите принципы работы с изолятами.
10. В чем различие между передачей сообщений между изолятами одной
и разной группы? Приведите ограничения SendPort.
11. Опишите случаи, когда лучше использовать асинхронное
программирование и изоляты.
12. В чем разница между ТСР и UDP?
13. Что такое REST? А REST API?
14. Какие типы HTTP-запросов (сообщений) чаще всего используются для
организации взаимодействия клиента и сервера?
15. Какая структура у HTTP-запросов (сообщений)?
16. На какие типы делятся заголовки HTTP-запросов (сообщений)?
17. Что такое код состояния (статуса) запроса? Для чего он используется?
Приведите пример.
18. Перечислите самые распространенные числовые коды состояний
ответа сервера.
19. Что такое REST URLs? По каким правилам строятся URL-адреса адреса
REST API сервиса?

Лабораторная работа № 11. Асинхронное


программирование и изоляты
Цель работы: познакомиться с основными способами написания
асинхронных программ и работы с изолятами.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Каждое задание должно сопровождаться минимум тремя
тестами и содержать хотя бы одно исключение;
• Каждая задача должна иметь 2 решения: Future API или
Stream и Isolate.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

511
Задания:
1. Напишите приложение, которое осуществляет чтение данных из
файла и запись этих данных в другой файл. Названия файлов не должны
совпадать.
2. Напишите приложение, которое заполняет 100 списков (минимум 15
элементов) случайными значениями. После заполнения для каждого списка
необходимо рассчитать медиану и записать полученные значения в
текстовый файл.
3. Напишите приложение, которое осуществляет чтение данных из
файла в главном изоляте и передает каждую строку одному из трех
изолятов или Future, которые осуществляют запись в файл. У каждого
изолята свой файл для записи, содержимое которых отличается –
прочитанная строка из исходного файла может быть единожды записана в
один из трех файлов.
4. Сформируйте каталог из 10 – 15 файлов, состоящих из двух строк. В
первой находится символ операции (+, *, /), а во второй – два числа с
плавающей точкой, разделенные пробелом. Напишите приложение,
которое выполнит чтение файлов и требуемые действия над числами, после
чего запишет сумму результатов их вычислений в файл «result.txt».
5. Сформируйте каталог из 10 – 15 файлов, состоящих из двух строк. В
первой находится число (1 - арифметическое среднее, 2 – сумма элементов,
3- произведение элементов), а во второй – числа с плавающей точкой,
разделенные пробелом (минимум 12). Напишите приложение, которое
выполнит чтение файлов и требуемые действия над числами, после чего
запишет сумму результатов их вычислений в файл «result.txt».
6. Разработайте приложение, которые осуществляют поиск файлов в
заданной корневой директории (обходя вложенные) на основе заданных
критериев (размер, расширение файла, дата последнего изменения и т.д.),
записывая пути до файлов, удовлетворяющих условию в отдельный файл.
7. Напишите приложение для чтения и подсчета количества строк в
наборе текстовых файлов (минимум 30 файлов). После того, как все файлы
обработаны – выведите в терминал общее количество строк всех файлов.
8. Напишите приложение для подсчета количества вхождений слов в
наборе текстовых файлов (минимум 8 файлов). На каждый текстовый файл
выделяется свой изолят или Future (Stream), после чего результаты их
работы объединяются и сохраняются в файл «result.txt».
9. Напишите приложение для мониторинга изменений (добавление,
удаление или изменение файлов) в нескольких каталогах. Раз в сутки
результаты всех изолятов собираются и записываются в файл «result-[дата
и время записи].txt».
10. Напишите приложение, позволяющее производить архивирование
файлов в заданной директории.

512
11. Напишите приложение, которое читает содержимое нескольких
файлов и выводит в терминал только уникальные строки.
12. Напишите приложение, которое раз в сутки удаляет все файлы в
целевой директории, созданные более чем N дней назад.
13. Напишите приложение, которое раз в сутки копирует все файлы новые
файлы из одной директории в другую.
14. Напишите приложение, которое совершает одновременный запрос к
нескольким API: https://reqres.in/api/unknown и https://reqres.in/api/users,
десериализуя полученные данные в объекты и записывая их в один файл.

Лабораторная работа № 12. Клиент-серверное


приложение
Цель работы: познакомиться с основными способами разработки
клиент-серверных приложений на Dart.
Требования к формату защиты лабораторной работы:
• Отчет (титульный лист, текст задания с кодом по его
выполнению);
• Готовность внести исправления, в присутствии преподавателя,
в код любого из выполненных заданий лабораторной работы и
ответить на вопросы;
• Каждое задание должно сопровождаться минимум шестью
тестами и содержать хотя бы одно исключение.
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.

Задания:
1. Разработайте TCP клиент-серверное приложение, которое способно
передавать файлы с сервера клиенту и наоборот. Продумайте API для
клиент-серверного взаимодействия и консольный интерфейс клиентской
части. Сервер должен поддерживать работу с несколькими клиентами.
2. Разработайте UDP клиент-серверное приложение, которое способно
передавать файлы с сервера клиенту и наоборот. Продумайте API для
клиент-серверного взаимодействия и консольный интерфейс клиентской
части. Сервер должен поддерживать работу с несколькими клиентами.
3. Разработайте HTTP клиент-серверное приложение, которое способно
передавать файлы с сервера клиенту и наоборот. Продумайте API для
клиент-серверного взаимодействия и консольный интерфейс клиентской
части. Сервер должен поддерживать работу с несколькими клиентами.

513
4. Разработайте приложение (клиентскую и серверную часть), для
общения пользователей внутри локальной сети с использованием
протокола TCP. Продумайте API для клиент-серверного взаимодействия и
консольный интерфейс клиентской части.
5. Разработайте приложение (клиентскую и серверную часть), для
общения пользователей внутри локальной сети с использованием
протокола UDP. Продумайте API для клиент-серверного взаимодействия и
консольный интерфейс клиентской части.
6. Разработайте приложение (клиентскую и серверную часть), для
общения пользователей внутри локальной сети с использованием
протокола HTTP. Продумайте API для клиент-серверного взаимодействия и
консольный интерфейс клиентской части.
7. Разработайте TCP-сетевой калькулятор, где на клиентской стороне
пользователь вводит выражение, а само его вычисление производится на
серверной части, после чего ответ возвращается клиенту. Сервер должен
поддерживать работу с несколькими клиентами.
8. Разработайте приложение (клиентскую и серверную часть) с
использованием протокола UDP для перевода значений в различную
систему счисления (десятичная, восьмеричная, двоичная,
шестнадцатеричная). С клиента передаются следующие данные: значение,
его текущая система счисления, в какую систему счисления необходимо
перевести. После того, как на сервере осуществится перевод значения,
верните его клиенту. Сервер должен поддерживать работу с несколькими
клиентами.
9. Разработайте HTTP клиент-серверное приложение, которое позволит
пользователям добавлять, просматривать и удалять записи в консольной
записной книжке. Сервер должен осуществлять хранение записей
пользователей и обрабатывать запросы клиентов.
10. Разработайте TCP клиент-серверное приложение, которое позволит
пользователям конвертировать значения между различными единицами
измерения. Клиенты будут отправлять запросы на сервер с указанием
исходной и целевой единицы измерения, а сервер должен возвращать
результаты конвертации.
11. Разработайте UDP клиент-серверное приложение, которое позволит
пользователям создавать резервные копии файлов и папок на удаленном
сервере через консольный интерфейс. Клиенты могут отправлять запросы
на сервер для создания, восстановления и удаления резервных копий.

514
Планы на следующую книгу по Dart
Далее в планах, помимо Flutter, написание книги по более
продвинутому уровню Dart, в которой скорей всего будет:
1. «Разработка backend на Dart» с использованием Serverpod;
2. глава с разбором коллекций и разработки на Dart классических
структур данных. Возможно, зацепим еще алгоритмы поиска и
сортировки;
3. более глубокое погружение в асинхронное программирование и
изоляты, с примером разработки небольшой мультиагентной системы
(возможно включу в третье переиздание «Основ Dart»);
4. глава по работе с базами данных (sqlite);
5. DartFFI;
6. работа с gRPC (под вопросом).

515
Курсы автора на Stepik

Паттерны проектирования GoF на Dart


Курс дает всеобъемлющий обзор паттернов (шаблонов)
проектирования из книги "банды четырех" на языке программировании
Dart. Помимо классических реализаций паттернов проектирования, где это
только возможно, рассматривается их реализация теми средствами, что
предоставляет Dart.
Ссылка для ознакомления с курсом: https://stepik.org/a/105448
Ссылка на приобретение курса со скидкой в 25%:
https://stepik.org/a/105448/pay?promo=5d8296aaa4866abf

Грокаем Python через разработку проекта


Курс представляет собой последовательные шаги по написанию
системы для автоматизированной проверки заданий с использованием
telegram-бота по таким предметам, как: программирование на Python;
анализ данных; ML. Рассматриваются различные аспекты работы с Docker,
базой данных и ORM SQLAlchemy.
Ссылка для ознакомления с курсом: https://stepik.org/a/138391
Ссылка на приобретение курса со скидкой в 25%:
https://stepik.org/a/138391/pay?promo=afbdb2458da53878

Python в мультиагентных системах


Курс представляет собой последовательные шаги по написанию
мультиагентной системы поиска таких позиций шахматных фигур на доске,
где отсутствуют ситуации, что какая-либо фигура находится под атакой.
Ссылка для ознакомления с курсом: https://stepik.org/a/178349
Ссылка на приобретение курса со скидкой в 25%:
https://stepik.org/a/178349/pay?promo=87eb32a55e412504

516
Как отблагодарить автора и зачем это
делать?
Написание любой учебной литературы – труд, требующий огромных
усилий не только в обобщении существующего материала и как его подать
читателю, но и в придумывании различных примеров и заданий на
закрепление пройденных разделов, а также многое другое.
Если у вас имеется желание поддержать мои начинания, то это можно
сделать различными способами, которые приведены ниже. Все деньги,
полученные таким образом, идут на поддержку моей образовательной
деятельности, покупку различного оборудования для докторской
диссертации, оплату публикаций статей в научных журналах и приближают
момент, когда еще раз возьмусь за «перо» для написания новой книги или
актуализации текущей.

https://www.tinkoff.ru/rm/chernyshev.stanislav20/FUUfY1048

ЮMoney: https://yoomoney.ru/to/410011696202148

517
Адрес EVM кошелька Bybit Wallet:
0x3ff35d9325f8c4cbabd6f14ba5e170459420faa8

Адрес SUI кошелька Bybit Wallet:


0x9300ecb7a65ab4564a4c81ef045f0ef8d175a13fe3cfc7acdd25b8afa
0b00225

Оформить подписку или задонатить на бусти:


https://boosty.to/madteacher

518
Где посмотреть актуальную версию книги?
Самую свежую версию книги можно скачать на Boosty:
https://boosty.to/madteacher. А по подписке или разовой плате доступны ее
промежуточные варианты перед публикацией.

519
Список используемых источников

1. Dart. URL: https://dart.dev


2. Sound null safety. URL: https://dart.dev/null-safety
3. Flutter. URL: https://flutter.dev
4. Effective Dart: Documentation. URL: https://dart.dev/guides/language/
effective-dart/documentation
5. String class. URL: https://api.dart.dev/stable/3.1.3/dart-core/String-
class.html
6. List<E> class. URL: https://api.dart.dev/stable/3.1.3/dart-core/List-
class.html
7. Set<E> class. URL: https://api.dart.dev/stable/3.1.3/dart-core/Set-
class.html
8. Map<K, V> class. URL: https://api.dart.dev/stable/3.1.3/dart-core/Map-
class.html
9. Ожегов С. И. Толковый словарь русского языка : около 100 000 слов,
терминов и фразеологических выражений // С. И. Ожегов ; под ред. Л.
И. Скворцова. - 26-е изд., испр. и доп. - М. : Оникс [и др.], 2009. - 1359
c.
10. Горский Д.П. Вопросы абстракции и образование понятий // М.: Изд-во
Академии наук СССР, 1961. 353 с.
11. ГОСТ 28397-89 (ИСО 2382-15-85) Языки программирования. Термины
и определения : Межгосударственный стандарт : дата введения 1991-
01-01 / Федеральное агентство по техническому регулированию. – Изд.
официальное. – Москва : Стандартинформ, 2010. – 8 с.
12. Typedefs. URL: https://dart.dev/language/typedefs
13. PREFER inline function types over typedefs. URL:
https://dart.dev/effective-dart/design#prefer-inline-function-types-over-
typedefs
14. Creating packages. URL: https://dart.dev/guides/libraries/create-library-
packages
15. Dart testing. URL: https://dart.dev/guides/testing
16. dart compile. URL: https://dart.dev/tools/dart-compile
17. Concurrency in Dart. URL: https://dart.dev/language/concurrency
18. Streams and Sinks in Dart and Flutter. URL: https://dart.academy/streams-
and-sinks-in-dart-and-flutter/
19. Dart asynchronous programming: Isolates and event loops. URL:
https://medium.com/dartlang/dart-asynchronous-programming-isolates-
and-event-loops-bffc3e296a6a

520
20. spawnUri static method. URL: https://api.dart.dev/stable/3.1.4/dart-
isolate/Isolate/spawnUri.html
21. send abstract method. URL: https://api.dart.dev/stable/3.1.4/dart-
isolate/SendPort/send.html
22. Isolate class. URL: https://api.dart.dev/stable/3.1.4/dart-isolate/Isolate-
class.html
23. Zone class. URL: https://api.dart.dev/stable/3.1.4/dart-async/Zone-
class.html
24. HTTP Messages. — URL: https://developer.mozilla.org/en-
US/docs/Web/HTTP/Messages
25. Wei-Meng Lee Go Programming Language For Dummies // Published by:
John Wiley & Sons, Inc., ISBN: 978-1-119-78619-1, 336 Pages
26. REST API Tutorial. — URL: https://restfulapi.net
27. REST API conventions. — URL: https://www.ibm.com/docs/en/urbancode-
release/6.1.1?topic=reference-rest-api-conventions

521
522

Вам также может понравиться