Академический Документы
Профессиональный Документы
Культура Документы
ОСНОВЫ
DART
2-е издание, переработанное и
дополненное
2024
ПУБЛИЧНАЯ ЛИЦЕНЗИЯ
Учебник Чернышева Станислава Андреевича «Основы Dart»,
называемое далее «Произведением», защищено действующим
российским и международным авторско-правовым
законодательством. Все права на Произведение, предусмотренные
законом, как имущественные, так и неимущественные, принадлежат
его автору. Настоящая Лицензия устанавливает способы
использования электронной версии Произведения, право на которые
предоставлено автором и правообладателем неограниченному кругу
лиц, при условии безоговорочного принятия этими лицами всех
условий данной Лицензии. Любое использование Произведения, не
соответствующее условиям данной Лиценции, а равно и
использование Произведения лицами, не согласными с условиями
Лицензии, возможно только при наличии письменного разрешения
автора и правообладателя, а при отсутствии такого разрешения
является противозаконным и преследуется в рамках гражданского,
административного и уголовного права. Автор и правообладатель
настоящим разрешает следующие виды использования данного
файла, являющегося электронным представлением Произведения,
без уведомления правообладателя и без выплаты авторского
вознаграждения:
1) воспроизведение Произведения (полностью или частично) на
бумаге путем распечатки с помощью принтера в одном экземпляре
для удовлетворения личных бытовых или учебных потребностей, без
права передачи воспроизведенного экземпляра другим лицам;
2) копирование и распространение данного файла в
электронном виде, в том числе путем записи на физические носители
и путем передачи по компьютерным сетям, с соблюдением
следующих условий:
➢ все воспроизведенные и передаваемые любым лицам
экземпляры файла являются точными копиями оригинального
файла в формате PDF или EPUB, при копировании не производится
никаких изъятий, сокращений, дополнений, искажений и любых
других изменений, включая изменение формата представления
файла;
➢ распространение и передача копий другим лицам
производится исключительно бесплатно, то есть при передаче
не взимается никакое вознаграждение ни в какой форме, в том
числе в форме просмотра рекламы, в форме платы за носитель или за
сам акт копирования и передачи, даже если такая плата оказывается
значительно меньше фактической стоимости или себестоимости
носителя, акта копирования и т. п.
Любые другие способы распространения данного файла при
отсутствии письменного разрешения автора запрещены. В
частности, запрещается:
➢ внесение каких-либо изменений в данный файл, создание и
распространение искаженных экземпляров, в том числе
экземпляров, содержащих какую-либо часть произведения;
➢ распространение данного файла в Сети Интернет через веб-
сайты, оказывающие платные услуги, через сайты коммерческих
компаний и индивидуальных предпринимателей (включая
файлообменные и любые другие сервисы, организованные в Сети
Интернет коммерческими компаниями, в том числе бесплатные), а
также через сайты, содержащие рекламу любого рода;
➢ продажа и обмен физических носителей, содержащих данный
файл, даже если вознаграждение значительно меньше себестоимости
носителя;
➢ включение данного файла в состав каких-либо
информационных и иных продуктов;
➢ распространение данного файла в составе какой-либо
платной услуги или в дополнение к такой услуге.
С другой стороны, разрешается дарение (бесплатная передача)
носителей, содержащих данный файл, бесплатная запись данного
файла на носители, принадлежащие другим пользователям,
распространение данного файла через бесплатные
децентрализованные файлообменные P2P-сети и т. п. Ссылки на
экземпляр файла, расположенные на официальной странице Boosty,
телеграм-канале или группы VK автора, разрешены без ограничений.
С. А. Чернышев запрещает Российскому авторскому
обществу и любым другим организациям производить любого
рода лицензирование этого произведения и осуществлять в
интересах автора какую бы то ни было иную связанную с
авторскими правами деятельность без его письменного
разрешения.
ОСНОВЫ
DART
2 издание, переработанное и дополненное
2024
Чернышев, Станислав Андреевич
Основы Dart – 2-е изд., перераб. и доп. / С.А. Чернышев. ‒ 2024. ‒ 521 с.,
ил.
Предисловие ............................................................................................. 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
Предисловие
11
https://www.tinkoff.ru/rm/chernyshev.stanislav20/FUUfY1048
12
Все деньги, полученные таким образом, идут на
поддержку моей образовательной деятельности, покупку
различного оборудования для докторской диссертации,
оплату публикаций статей в научных журналах и приближают
момент, когда еще раз возьмусь за «перо» для написания
новой книги или актуализации текущей.
Что касается исходных кодов рассматриваемых в книге
примеров, их можно скачать с моего github-репозитория:
https://github.com/MADTeacher/dart_basics
madteacher@bk.ru MADTeacher
MADTeacher
https://vk.com/madteacher
13
Глава 1. Краткая история и встроенные
типы данных
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.
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
После того, как архив с Dart SDK загрузился, распакуйте его в удобную
для вас директорию. Обычно этой директорией выступает корневой
каталог диска (C, D, F и т. д.). Теперь необходимо прописать путь до
распакованного Dart SDK в переменных средах в переменной «Path», как
показано на рисунке ниже:
17
Рисунок 1.3 – Указываем путь до Flutter SDK в переменной «Path»
18
После перезапуска VS Code создадим новый проект для Dart. Для этого
используем английскую связку клавиш «Ctrl+Shift+P» и введем в
появившейся командной строке «Dart: New Project». Данная команда может
появиться в списке команд до того, как введете ее полностью. В этом случае
просто выбираем ее из списка:
19
Рисунок 1.7 – Добавление создаваемого проекта в Workspace Trust
20
Рисунок 1.9 – Структура директории проекта
на
void main(List<String> arguments) {
print('Hello world!');
}
21
пользоваться автодополнением кода, когда у вас нет соединения с
интернетом. Чаще всего, такая ситуация, будет сопровождаться следующей
плашкой уведомлений:
22
1.3. Правила именования
При написании кода на Dart лучше придерживаться следующих
рекомендаций при объявлении переменных, функций, классов и их
методов:
1. При объявлении переменных, функций и методов классов
используется верблюжий стиль, а само название начинается с
маленькой буквы (lowerCamelCase). Для логического разделения слов
в объявляемой переменной необходимо использовать символ в
верхнем регистре: myCatName. Имя же объявляемого класса начинается
с большой буквы (UpperCamelCase): DailySchedule;
2. Нельзя использовать в начале объявляемого имени числовые
значения;
3. Регистр символов имеет значение. Так, например, var CHECK = 10; и
var check = 10; две совершенно разные переменные;
4. Не используйте в качестве имен переменных ключевые слова Dart;
5. Если имя переменной, функции и т.д. начинается с символа «_», то она
является приватной (для импортирующего код модуля).
23
случае используется «//», после чего идет комментарий, который не
переносится на следующую строку:
// комментарий
var a = 10; // еще один комментарий
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 b = 2.2;
b = 3;
25
вещественных значений из-за округления. Поэтому их, рекомендуют
сравнивать посредством >=, <= или метода compareTo, который возвращает:
− отрицательное число, если значение, с которым происходит
сравнение – больше;
− ноль, если они равны;
− положительное число, если меньше.
print(4.compareTo(5)); // -1
print(5.compareTo(4)); // 1
print(4.compareTo(4)); // 0
26
1.4.2. Строки (String)
Строки в Dart представляют собой последовательность символов в
кодировке UTF-16. Для их объявления (создания) могут использоваться как
одинарные, так и двойные кавычки:
String s1 = 'Мама мыла раму';
var s2 = "Мама мыла две рамы";
var s3 = '''Многострочная
строка''';
27
print(s2); // Мама мыла две рамы
var s3 = s2.toUpperCase();
print(s3); // МАМА МЫЛА ДВЕ РАМЫ
28
var s1 = 'Вот те на!';
print(s1.contains('е')); // true
print(s1.contains('на')); // true
print(s1.contains('-_-')); // false
29
Чтобы разбить строку на несколько частей, воспользуйтесь методом
split:
var s1 = "Мама мыла рамы";
print(s1.split(' ')); // [Мама, мыла, рамы]
print(s1.split('л')); // [Мама мы, а рамы]
print(s1.split('мыла')); // [Мама , рамы]
print(myStr.isEmpty); // false
print(myStr.isNotEmpty); // true
myStr = '';
print(myStr.isEmpty); // true
print(myStr.isNotEmpty); // false
30
несколькими способами. Отдав вывод типа объектов, с которыми работает
список на откуп Dart, либо задать явно:
var myList1 = [ 1, 2, 3];
List<int> myList2 = [1, 2, 3];
var myList1 = <int>[]; // пустой список
// добавление в список
31
print(myList2); // [1, 4, 1, 3, 5, 4, 5, 6]
// удаление из списка
32
элемента списка кратно двум, этот элемент удаляется из списка. Более
подробно данный вид функций мы разберем в 3-й главе.
Теперь рассмотрим случай, когда список уже сформирован и нам
необходимо перезаписать значения его элементов:
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
myStr = myList.join();
print(myStr); // PelloO world?
print(myList.isEmpty); // false
print(myList.isNotEmpty); // true
myList = [];
print(myList.isEmpty); // true
print(myList.isNotEmpty); // false
34
print(myList.sublist(2, 4)); // [2, 5]
print(myList.sublist(2, 2)); // [ ], если start == end
35
print(myList.lastWhere((element) => element % 3 == 0)); // 6
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); // [ , , М, а, а, а, а, л, м, м, м, р, у, ы]
37
(лямбда) функция, состоящая из двух аргументов: аккумулирующее
значение, хранящее результат предыдущих вычислений и текущий
итерируемый элемент коллекции. Для работы этого метода необходимо
наличие хотя бы одного элемента в коллекции:
List<int> numbers = [1, 2, 3, 4, 5];
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]
}
39
var newList = List.from(myList);
newList[0].add(77);
print('Элементы myList: $myList');
// Элементы myList: [[10, 3, 4, 1, 77]]
40
В данном случае мы объявили запись с неименованными
позиционными полями, поэтому обращение к ним производится через
геттер $1 и $2. Такой подход не всегда удобен, т.к. приходится держать в
голове через какой геттер обращаться к необходимым данным, что
обходится использованием именованных полей:
var myRecord = (cost: 10, smile: '-_-');
// или ({int cost, String smile}) myRecord = (cost: 10,
// smile: '-_-');
print(myRecord); // (cost: 10, smile: -_-)
// 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})'
// Bad
(int, String) myRecord2 = ('-_-', 10);
// Error: A value of type '(String, int)' can't be
// assigned to a variable of type '(int, String)'.
print(myRecord.runtimeType); // (String)
print(myRecord); // (-_-)
42
Еще одним свойством записей является то, что их можно проверять на
равенство. Единственное, что нужно учитывать – тип проверяемых записей
должен совпадать, иначе неровен час допустить ошибку:
var myRecord1 = (10, '-_-');
var myRecord2 = (10, '-_-');
print(myRecord1 == myRecord2); // true
43
print(mySet); // {1, 2, 5, 6, 7, 8}
// Добавление
mySet.add(10);
print(mySet); // {1, 2, 5, 6, 7, 8, 10}
// Удаление
44
mySet.remove(1); // удаляем один элемент
print(mySet); // {2, 5, 6, 7, 8, 10, 11, 12, 13}
mySet.clear();
print(mySet); // {}
print(mySet.first); // 1
print(mySet.last); // 10
// Внимание, множество не упорядочено!
// Это синтетический пример!!!
print(mySet.length); // 8
print(mySet.isEmpty); // false
print(mySet.isNotEmpty); // true
45
print(mySet.where((element) => element % 3 == 0).toSet());
// {6, 9}
46
1.4.7. Таблицы (Map)
Так как не принято переводить название этого типа данных, то будем
придерживаться этой традиции. Map представляет из себя объект, который
связывает ключи и значения. Как ключи, так и значения могут быть
объектами любого типа данных. Ключ по своей сути уникален и не может
встречаться несколько раз, в то время как значение может использоваться
сколько угодно раз и связываться с различными ключами. Говоря простыми
словами, Map представляет собой серию пар «ключ:значение» (MapEntry <K,
V>).
Ниже приведен пример объявления этого типа данных:
var myMap = <String, String>{
//ключ //значение
'first': 'Мама',
'second': 'мыла',
'fifth': 'раму'
};
print(myMap); // {first: Мама, second: мыла, fifth: раму}
myMap[1] = 'Бабушка';
// добавляем новую пару «ключ:значение»
47
print(myMap);
// {1: Бабушка, 2: мыла, 3: раму, 10: по утрам!}
var a = myMap[2];
print(a); // null
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]
// очистка мапы
myMap.clear();
print(myMap); // {}
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}
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.
51
должны быть проинициализированы до их использования. Иначе в
процессе выполнения приложение выбросит исключение:
late String name;
// final int variable; // ошибка
52
неистово мяукать, пытаясь надавить на жалость, чтобы ее покормили
колбасой. Закодировать эту ситуацию можно написать следующим
образом:
class Cat {
void helloMaster(){
print("Мяу-у-у-у!!!");
}
}
53
Так как теперь у нас кошка может то присутствовать, то отсутствовать
в квартире, то в метод openFridge необходимо передавать аргумент типа
«Cat?» и учитывать, ссылается передаваемый аргумент на null или на
экземпляр класса. Для этого Dart предоставляет несколько возможностей в
виде операторов: «?.», «??» и «!.». Оператор «?.» вызовет метод
экземпляра класса, если переменная не ссылается на null, иначе никакой
метод вызываться не будет:
void openFridge(Cat? cat){
cat?.helloMaster();
}
54
openFridge(newCat); // Мяу-у-у-у!!!
}
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
56
print("Мяу-у-у-у!!!");
}
}
57
− при использовании Object? или Object используйте is проверки для
определения типа. Это позволит убедиться, что у объекта имеется
необходимый метод, прежде чем к нему обращаться;
− тип dynamic разрешает все операции (они не отслеживаются на этапе
компиляции), но такое поведение может привести к экстренному
завершению приложения;
Несмотря на наличие такого гибкого механизма, как dynamic, его не
рекомендуется использовать повсеместно, так как это может повлечь за
собой трудно отлавливаемые ошибки не только в самом коде, но и в логике
разрабатываемого приложения. Поэтому вместо dynamic лучше
предпочитать работу с Object. Основное исключением из этого правила
является работа с существующими API, которые используют dynamic.
Например, Map<String, dynamic> используется для представления JSON-
объекта.
58
Несмотря на то, что при инициализации строковой переменной или в
функции print мы можем использовать кириллицу, ее ввод с терминала
при работающем приложении не поддерживается. Поэтому вооружаемся
словариком и постигаем дзен английского языка
Для ввода данных с клавиатуры через терминал нам потребуется
импортировать библиотеку dart:io, которая позволяет работать с
операциями ввода-вывода, файлами, сокетами, HTTP и т.д. Мы немного
упростим код и будем считать, что пользователь всегда вводит корректное
значение:
import 'dart:io';
void main() {
print('Введите целочисленное значение');
String? input = stdin.readLineSync(); // синхронный ввод данных
print(input?.runtimeType); // String
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}');
}
void main() {
print('Введите список ключей: ');
String? keysInput = stdin.readLineSync();
void main() {
print('Введите числа через пробел: ');
String? input = stdin.readLineSync();
60
List<String> inputValues = input!.split(' ');
Резюме по главе
В первой главе помимо краткой истории языка программирования
Dart, были рассмотрены его основные особенности, встроенные типы
данных и способы работы с ними. Приведенные способы работы со
списками, множествами и т. д., постоянно будут встречаться в ходе работы
над реальными проектами, так что лучше их изучить в самом начале пути
и не заглядывать каждый раз в справочник для уточнения по методам и
способам работы с ними.
Отдельно стоит отметить концепцию null-safety, что позволяет не
беспокоиться о наличии значения null в переменных и предоставляет
разработчикам механизм введения в код разрабатываемого приложения не
null-safety типов данных и операторы для работы с ними.
Также нами были рассмотрены правила наименования переменных.
Что касается того, с заглавной или строчной буквы будет начинаться имя
переменной, функции, класса и т.д., эти правила носят рекомендательный
характер. Это совершенно не значит, что вы обязаны их придерживаться,
но само следование этим правилам является «хорошим тоном» при
написании приложений. Такое положение дел связано с тем, что его
придерживается огромное количество программистов, в соответствии с
чем, код становится более читаемый. Согласитесь, куда приятнее вникать в
то, что написано в коде, когда все следуют одному и тому же соглашению
по наименованию.
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. Какие правила при наименовании переменных существуют?
62
Выберете вариант, соответствующий вашему порядковому номеру в
журнале группы. В том случае, если ваш порядковый номер больше
последнего номера варианта, используйте следующую формулу: N = n % f +
1, где n – ваш порядковый номер, f – номер последнего варианта, N –
вариант для выполнения.
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
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
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
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 и
управляющие конструкции
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
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
Таблица 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
Таблица 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;
}
void helloMaster(){
print("Мяу-у-у-у!!!");
}
}
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
}
Далее рассмотрим коллекции и объекты, к которым можно применять
данную операцию.
77
необходимо извлечь конкретные элементы и записать их значения в
переменные? Давайте с этим разбираться.
Количество переменных, на которое распаковывается список должно
соответствовать его количеству элементов:
final myList = [1, 2];
final [a, ] = myList; // Bad state: Pattern matching error
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.
79
// ex2_8.dart
var myRecord2 = (3.14, cost: 10, smile: '-_-', 22);
var (b, c) = (myRecord2.$1, myRecord2.smile);
print('$b, $c'); // 3.14 -_-
80
};
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
81
экземпляр которого будет деструктурирован, за которым в круглых скобках
указываются его имена полей, которые будут распакованы в переменные:
// ex2_13.dart
class Employee {
final String name;
final int age;
final int 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(:age,) = employee;
print("Age: $age"); // Age: 19
82
2.3. Управление потоком выполнения кода
Для управления потоком выполнения кода в Dart используются
следующие виды операторов:
− Условный оператор if, if-case;
− Тернарный оператор;
− Оператор ??;
− Операторы циклов (for, for-in, while и do-while);
− Операторы потока выполнения (break, continue, return);
− Оператор выбора потока выполнения (switch-case) и switch-
выражения.
83
} else if (a > c){
print('a > c'); // a > c
}
else{
print('Ни то и ни другое');
}
}
if (a > b) {
c = a;
} else if (a < b) {
c = b;
}else{
c = b;
}
if (a > b) {
c = a;
} else{
c = b;
}
84
if (a > b) {
c = a;
}
print('Max: $c'); // Max: 20
}
Таблица 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
85
} else {
print('Значение не входит в промежуток');
}
}
86
блок 2
}
…
else if (значение case шаблон){
блок (n-1)
}
else{
блок (n)
}
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
}
88
class Cat {
final String name;
final int age;
Cat(this.name, this.age);
}
void main() {
dynamic obj = Employee('John', 30, 1000);
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');
}
print('Max is $c'); // 10
90
print('Max is $max'); // 25
2.3.4. Оператор ??
Общий вид записи данного оператора можно представить следующим
способом:
expr1 ?? expr2
void main() {
var c = calculate() ?? calculate(10);
// попробуйте переписать для оператора ?:
print(c); // 70
91
} else{
c = calculate(10);
}
print(c); // 70
if (calculate(3) != null){
c = calculate(3);
} else{
c = calculate(10);
}
print(c); // 21
}
92
}
print(str); // 01234
Если использовать цикл 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
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 => раму
Cat(this.name, this.age);
}
void main() {
var catList = <Cat>[for (var i = 0; i<= 3; i++)
Cat('Tommy$i', i+1)];
94
Для того, чтобы пройти по всем элементам строки используйте
следующий вид записи цикла:
// ex2_35.dart
var myStr = 'Hi!';
for(var i = 0; i <myStr.length; i++){
print(myStr[i]); // H i !
}
do{
// блок кода
}
while (условие выхода из цикла);
95
print(myStr[i]); // H i !
i++;
}
i = 0;
do{
print(i); // 0 1 2
i++;
}while(i < 3);
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
97
помощью шаблонов (Pattern Matching), указываемых после ключевого
слова case.
В общем виде конструкцию switch-case можно записать следующим
образом:
switch(объект):
case шаблон1:
блок1
case шаблон2:
блок2
case шаблон3:
блок3
...
default:
блок (n) # действие по умолчанию
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');
}
}
99
};
print(b); // 0
Такой способ работает только при замене case на =>, где в левой части
указывается шаблон для сравнения, а в правой возвращаемый результат.
Шаблоны могут быть различного вида и состоять из логических операций
«И», «ИЛИ» (логический шаблон) и операций сравнения (реляционный
шаблон):
// ex2_48.dart
void main() {
var myList = [1, 4, 5, 2, 33, 45, 90];
// или
void main() {
var myList = [1, 4, 5, 2, 33, 45, 90];
var newList = <int>[];
100
}
print(myStr); // 0 1 3 !
}
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
}
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],
];
print(myStr); // 5 5 5 5
}
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
}
104
// если есть элементы с такими данными
print('Hi, John!');
default:
print('No match');
}
}
}
// Full match
// No match
// (╯'□')╯︵ ┻━┻
// Hi, John!
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
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: -_-)
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]
@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),
];
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}
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} */
110
print('Что ты тут забыл? : $element');
}
}
// Что ты тут забыл? : Employee{Max, 22, Tranee, 2000}
// Что ты тут забыл? : Employee{Alex, 30, Manager, 30000}
// Элитальный сотрудник: Employee{Anna, 27, Team Leader, 29000}
// Что ты тут забыл? : Employee{John, 22, Junior, 4000}
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),
];
112
print('Salary match: $element');
}
}
// Name match: Employee{Alexandr, 30, Manager, 30000}
// Salary match: Employee{Anna, 27, Team Leader, 29000}
113
Резюме по разделу
В данном разделе мы рассмотрели существующие операторы и
базовые синтаксические конструкции языка программирования Dart:
условный оператор, циклы и т.д. Они будут постоянно встречаться вам в
процессе написания приложений, поэтому понимание их принципов
работы снизит вероятность ошибок в логике разрабатываемого
программного продукта.
Также были затронуты шаблоны, как с ними работать при организации
потока выполнения программы и принципы их деструктурирования. Это
достаточно новый механизм, появившийся только в третьей версии Dart, но
уже зарекомендовавший себя, т.к. позволяет писать более лаконичный,
читаемый (когда привыкнешь к синтаксису) и элегантный код.
114
18. Что такое Guard clause и с какими операторами используется?
19. Какие способы для деструктурирования списков вы знаете? Приведите
примеры.
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
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 "Ленив",
"Студент",
"Постоянно жалуется на жизнь"
]
}
118
2,
5
]
}
119
[1, 3, 4, 5, 6, -2, 7, -12, 22]
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
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
Таблица 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
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. Выражение (может состоять из констант, функций, переменных
и т. д., всегда возвращает значение).
Все вышеперечисленные составляющие тесно переплетены между
собой и позволяют нам управлять ходом выполнения программы. А так как
чаще всего в основе языков программирования лежит английский алфавит,
представьте, что они – это упрощенная версия английского языка. Конечно,
используя такой упрощенный язык не получится свободно пообщаться со
своим лучшим другом на отвлеченные темы, вспоминая «сына маминой
подруги», но это совершенно не значит, что вы не сможете понять друг
друга. Каждый день сотни тысяч программистов общаются между собой с
его использованием! Только это происходит путем чтения и написания
кода. Именно поэтому очень важным аспектом является его читаемость,
которая позволяет понять изначально закладываемый смысл
разработчиком того или иного кусочка кода.
133
раз приходилось думать программисту, то чем бы такая работа отличалась
от каторги!!!
134
часть текста из книги мы упрощаем читателю жизнь. Теперь он не бросит
ее в мусорное ведро, а сразу сможет найти необходимый для прочтения
материал. Все дело в том, что название раздела представляет собой
обобщение входящего в него текста. Таким образом вводится новый
уровень абстракции.
Примером раздела при написании книги в программировании
является функция или процедура. Их отличие заключается в том, что
функция возвращает значение, а процедура нет. Даже когда язык
программирования не имеет ключевого слова для объявления процедуры,
функция, которая не возвращает значение (void), может спокойно
рассматриваться как процедура. Далее по тексту мы будет оперировать
функциями. Чаще всего вам попадалось определение, что функция служит
для того, чтобы упростить повторное использование кода. Но только ли на
этом заканчиваются ее возможности? Функция также служит для вывода
процесса написания программы на новый уровень абстракции!
Представим, что в предыдущем примере кода мы использовали функции
только для того, чтобы убрать дублирование кода:
void main(){
объявление переменных
функция
200 строк кода
функция
3000 строк кода
функция
100 строк кода
функция
n-строк кода
строка вывода результата в терминал
}
135
функция для n-строк кода
строка вывода результата в терминал
}
Смотрите, код стал более читаемым. Имя каждой функции говорит для
разработчика за что она отвечает и ему нет необходимости погружаться в
детали ее реализации, если на то нет веских причин. Мы вывели наше
приложение на новый уровень абстракции! Вместо того, чтобы разбираться
в огромном количестве кода, перед нашими глазами только функции,
которые своими именами абстрагируют нас от того кода, что в них был
перенесен. А если надо что-то изменить? Это становится куда проще, чем
кажется на первый взгляд. Для этого достаточно внести изменения в код
функции. По ее имени можно понять та эта функция или нет, после чего
разобраться в ее коде и найти место, где его модифицировать. Именно
поэтому в книгах и статьях часто рекомендуют, чтобы тела функций были
достаточно короткими, ведь в таком случае разработчику куда удобнее в
них ориентироваться. К сожалению, рекомендации и реальная жизнь две
разные вещи. В связи с этим готовьтесь, что в легаси проектах придется
разбираться с функциями или классами очень большой длины.
Книга поделена на разделы, по ним мы можем найти интересующий
нас текст, но их слишком много и не прослеживается структура, которая
могла бы как-то объединить разделы по их смысловому назначению. Чтобы
решить эту проблему давайте вспомним, что разделы могут быть
вложенными, где раздел, находящийся на более высоком уровне, имеет
обобщенное название для входящих в его состав вложенных разделов:
136
объявление переменных
функция, объединяющая первую четверть программы
функция, объединяющая вторую четверть программы
функция, объединяющая третью четверть программы
функция, объединяющая четвертую четверть программы
строка вывода результата в терминал
}
Теперь те функции, которые вошли в более высокоуровневые будут
считаться деталями их реализации. И так можно продолжать до
бесконечности: путем введения более высокого уровня, содержащего более
обобщенное название для существующих ранее функций и строк кода мы
упрощаем чтение, поддержку и процесс изменения кодовой базы проекта.
func main(){
объявление переменных
функция, объединяющая первую четверть программы
функция, объединяющая вторую четверть программы
функция, объединяющая третью четверть программы
функция, объединяющая четвертую четверть программы
строка вывода результата в терминал
}
137
Рис. 3.2 – Глава, как новый уровень абстракции
func main(){
объявление переменных
функция, объединяющая первую четверть программы
функция, объединяющая вторую четверть программы
функция, объединяющая третью четверть программы
функция, объединяющая четвертую четверть программы
строка вывода результата в терминал
}
138
При этом по названию модулей он должен иметь возможность понять
какие обязанности на них возлагаются. Это позволяет сократить время
поиска участка кода, который необходимо модифицировать или исправить
логику его работы, а также упрощает чтение и поддержку кода самого
проекта.
Но это еще не все. В некоторых случаях книги состоят из частей,
названия которых представляют собой обобщение всех глав, входящих в их
состав, а сами части книги можно рассматривать как более высокий уровень
абстракции по отношению к ним. Аналогом частей книг в
программировании могут выступать пакеты, в состав которых может
входить различное количество модулей (библиотек). Обычно, когда
доходит дело до пакетов, предполагается, что код в них работает исправно
и покрыт тестами, чтобы тем разработчикам, которые его будут
использовать, не приходилось самостоятельно проверять правильность
работы, что освобождает их время и упрощает процесс разработки.
Заметьте, каждый раз повышая уровень абстракции мы упрощали себе
жизнь, поскольку пропадала необходимость запоминать огромный массив
информации, скрываемый за этим новым уровнем абстракции. Вводя
новую функцию, ее имя брало на себя обязанность описания того, для чего
она предназначена, снимая с нас нагрузку помнить, что за код в ней
написан. Теперь для разработчика большую роль играет что нужно подать
этой функции на вход и значение какого типа данных она вернет, чем сам
код тела функции.
Обратите внимание на то, что если в начале код программы смотрелся
следующим образом:
void main(){
объявление переменных
Хреналион строк действий для решения поставленной задачи
строка вывода результата в терминал
}
void main(){
объявление переменных
функция, объединяющая первую четверть программы
функция, объединяющая вторую четверть программы
функция, объединяющая третью четверть программы
функция, объединяющая четвертую четверть программы
139
строка вывода результата в терминал
}
140
Для передачи аргументов (параметров) в функцию необходимо после
объявления ее имени в круглых скобках указать тип и имя ее входного
аргумента, который свяжется с подаваемой при вызове функции на ее вход
переменной:
void myFunction(String name){
print('Привет, $name!');
}
141
print(myHelloString); // Привет, Александр!
print(myHelloString.runtimeType); // String
}
142
print('$name заработал $salary рублей в месяц.');
print('$name работает в должности $workPosition.');
}
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 'Не установлено имя новорожденного!';
}
144
return '$name родился $date $monthName!';
}
return 'Не установлено имя новорожденного!';
}
// BAD
// Error: Required named parameter 'name' must be provided.
print(myFunction(
date: 10,
monthName: 'сентября',
));
}
145
// ex3_7.dart
class Employee {
String name;
int age;
int salary;
@override
String toString() {
// чтобы печатать состояние объекта в терминале
return 'Employee{$name, $age, $salary}';
}
}
void updateMapValue(
Map<String, int> funcMap, {
required String key,
required int value,
}) {
funcMap[key] = value;
}
146
var myMap = {
'a': 1,
'b': 2,
};
updateMapValue(myMap, key: 'b', value: 3);
print(myMap); // {'a': 1, 'b': 3}
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!';
}
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();
}
149
}
print(secondLevel); // error: Undefined name 'secondLevel'.
}
void main(List<String> arguments) {
topLevelFunction();
}
150
3.3.5. Функция как входной аргумент другой функции
Теперь используем написанные в предыдущем разделе функции в
качестве входного аргумента другой функции. Для этого на понадобится в
качестве типа аргумента указать сигнатуру принимаемой на вход функции
(возвращаемый тип данных Function(тип1, … типN)):
// ex3_14.dart
int add(int a, int b) {
return a + b;
}
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);
}
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
}
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();
}
153
Для начала дадим типу Map<String, Map<(int, List<int>), int>>
псевдоним OMyMap. В этом нам поможет ключевое слово typedef:
// ex3_17.dart
typedef OMyMap = Map<String, Map<(int, List<int>), int>>;
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);
}
155
функции»), то есть функции без имени. Их структуру записи можно
представить следующим образом:
([[Type] arg1[, …]]) {
// блок кода
};
int sub(
int c,
int a,
int b,
int Function(int, int) func,
) =>
c - func(a, b);
156
Анонимные или стрелочные функции могут присваиваться
переменной, точно также как и обычные функции:
// ex3_21.dart
int add(int a, int b) => a + b;
3.3.8. Замыкания
Замыкания представляют собой довольно мощный инструмент, в
основе которого лежит возможность функций запоминать значения
переменных из объемлющих областей видимости. То есть из тех областей
видимости, где данная функция была объявлена. В основе идеи замыкания
лежит то, что функция может возвращать функцию, которая в свою очередь
может на вход принимать совершенно отличные значения от тех, что
подаются функции верхнего уровня, но использует в своей работе данные,
определенные в функции верхнего уровня. Звучит немного запутанно, не
прав да ли?
Давайте представим, что в качестве функции верхнего уровня
выступает завод, производящий технику. Ему каждый день поставляют
оборудование и забирают изготовленные микроволновые печи, которые
развозят по магазинам и, в конечном счете, одна из них оказалась в доме
покупателя. Каждый из нас часто пользуется этим устройством, чтобы
разогреть или приготовить еду и даже не задумывается, а из каких деталей
она состоит, как они взаимодействуют друг с другом и т. д. Так вот, эта
микроволновая печь и есть возвращаемая вложенная функция. В нее мы
ставим тарелку с супом, задаем ей режим работы, а она уже выполняет
нагрев с использованием той аппаратной начинки, что использовалось на
производственной линии завода:
// ex3_22.dart
int indexMicrowave = 0;
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;
};
}
158
print(calculation(7)); // 5764801
}
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)); }
}
160
}
// [10, 20, 30, 5, 3, 2]
// [20, 30, 5, 3, 2]
// [30, 5, 3, 2]
// [5, 3, 2]
// [3, 2]
// [2]
// 70
161
прекращают свою работку, как обычные функции. В момент приостановки
выполнения генераторной функции, сохраняется информация о ее
состоянии, куда входят данные о местоположении точки выхода из
функции и локальной области видимости. Возобновляется работа
генераторной функции с оператора yield, то есть с той точки, в которой была
выполнена остановка ее выполнения.
В качестве примера давайте сгенерируем последовательность
значений от 0 до 5 и запишем ее в список:
// ex3_27.dart
Iterable<int> myGenerator() sync* {
var k = 0;
while (k < 5) {
yield k++;
}
}
162
она продолжила свою работу с точки предыдущего выхода из нее, то есть с
«yield 0;». После того, как последовательность операторов yield в теле
функции закончилась, прекратился и цикл for.
Теперь давайте реализуем генераторную функцию, которая будет
возвращать только значения, которые нацело делятся на 4, а диапазон по
какое значение должна генерироваться последовательность будет
задаваться пользователем. В этом нам поможет библиотека «dart:io»:
// ex3_29.dart
import 'dart:io';
for(var it in myGenerator(n)){
result.add(it);
}
print(result); // [0, 4, 8, 12, 16]
}
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]
}
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).
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();
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
}
168
Как видим из рисунка 3.5, доступ к приватным функциям
импортируемых библиотек из основного кода приложения закрыт. А
попытка их вызова приведет к ошибке:
import 'src/my_calculator.dart' as calculator;
import 'src/short_calculator.dart';
169
Если необходимо импортировать несколько частей библиотеки, то
перечислите их через запятую:
import 'src/my_calculator.dart' as calculator show mul, add;
170
загрузки импортируемой библиотеки используется ключевое слово
deferred в связке с префиксом as <name>.
Дополнительным условием использования данного механизма
является оборачивание отложенной загрузки библиотеки в асинхронную
функцию, возвращающую Future. Это связано с тем, что используемый
метод для самой ленивой загрузки – loadLibrary возвращает этот тип
данных.
В качестве примера выполним отложенную загрузку библиотеки и
вызовем функцию деления:
import 'src/my_calculator.dart' deferred as calculator;
//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';
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';
173
Текущий проект «my_new_lib» можно рассматривать как уже готовый
пакет, готовый для импортирования в другой проект, т.к. на его верхнем
уровне имеется файл «pubspec.yaml», с информацией о самом пакете и его
зависимостях от других пакетов.
174
операционной системы или в браузере. Он состоит из двух частей:
самого приложения и тестового приложения для его проверки.
Для написания тестов в Dart принято использовать пакет (библиотеку)
test, а когда необходимо использовать объекты заглушки (фиктивные
объекты) – mockito.
Перед тем, как перейдем к рассмотрению функциональности пакета
тестирования, как его устанавливать и организовывать тестовое окружение
кода приложения, создайте новый консольный проект с названием
«conquest_functions».
// my_str.dart
List<String> splitString(String line, String splitter){
return line.split(splitter);
}
175
String stringToUpperCase(String line){
return line.toUpperCase();
}
import 'package:conquest_functions/my_math.dart';
import 'package:conquest_functions/my_str.dart';
void main(){
test('Проверка сложения', (){
expect(add(3, 7), equals(10));
});
176
Нажмите на него и дождитесь завершения теста. Если он завершился
успешно, то рисунок виджета сменится на зеленую галочку:
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));
});
});
177
expect(deleteSurroundingSpaces(line), equals('oO'));
});
test('Перевод в нижний регистр', () {
var line = 'ПроВерка';
expect(stringToLowerCase(line), equals('проверка'));
});
});
}
178
});
test('Проверка умножения', () {
expect(mul(a, b), equals(a*b));
});
});
179
Рисунок 3.15 – Тест не пройден
180
Рисунок 3.17 – Тест функции сложения не пройден
void main() {
// код тестов
}
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));
});
});
Теперь при запуске тестов проекта часть из них будет отмечена как
пропущенные:
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));
});
});
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));
});
});
184
Если нужно запускать только те тесты, которые не отмечены тегом,
используйте для этого флаг --exclude-tags или -x: dart test -x
"windows"
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
void main() {
test("main is checked", () async {
186
Прежде чем подать данные на вход запущенного процесса,
сконфигурируем коллекцию для получения результата работы
приложения:
var stdoutSplitter = StreamSplitter(
process.stdout
.transform(
utf8.decoder,
)
.transform(
const LineSplitter(),
),
);
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));
188
структуре директорий. У пакета вместо каталога «bin» - «example»,
хранящий примеры использования его функционала.
189
Удалите файлы из каталога «example», «test» и «src». Нам они сейчас
не понадобятся.
Теперь откройте реализованный ранее проект «my_new_lib» и
скопируйте файлы из его директории «src» каталога «lib» в аналогичную
папку текущего проекта:
export 'src/my_add.dart';
export 'src/my_mul.dart';
export 'src/my_pow_n.dart';
export 'src/my_sub.dart';
environment:
sdk: ^3.1.2
dev_dependencies:
lints: ^2.0.0
test: ^1.21.0
190
Не беря в расчет отсутствие примеров и тестов, вас можно поздравить
с первым созданным пакетом! Далее мы разберем различные способы его
подключения к разрабатываемым проектам.
environment:
sdk: ^3.1.2
dev_dependencies:
lints: ^2.0.0
test: ^1.21.0
191
Если имеется необходимость установить пакет, который находится в
менеджере пакетов pub, то достаточно в зависимостях (dependencies:)
файла «pubspec.yaml» прописать имя этого пакета и его версию.
Резюме по разделу
В данной главе мы рассмотрели, как объявлять и использовать
функции при написании кода. Какие способы передачи аргументов в
функции существуют и чем отличаются. Что такое замыкания и как они
реализованы в Dart. Поговорили про абстракцию, что она привносит в
процесс разработки, для чего нужна и как функции ее поддерживают.
Также рассмотрели как можно импортировать функции, переменные
и т.д., написанные в других модулях (файлах с расширением «.dart»), писать
свои библиотеки и пакеты.
Отдельно стоит отметить такой механизм, как генераторные функции,
которые используются для ленивой генерации последовательности
значений по запросу. Они представляют собой альтернативу спискам, так
как при их использовании в памяти не хранится массив значений и объект,
с необходимым нам текущим значением элемента последовательности,
создается только в момент обращения к генераторному объекту.
С особой осторожностью обращайтесь с такими типами передаваемых
аргументов в функцию, как: List, Set, Mat, пользовательский тип данных
и т.д. Это связано с тем, что такие аргументы передаются в функцию по
ссылке, а не значению и какое-либо их изменение в теле функции приведет
к тому, что они останутся и после завершения функции. Поэтому будьте
192
предельно осторожны и не меняйте значения этих объектов в блоке кода
функции, если эти изменения не требует логика работы приложения.
Дополнительно, в главе задели такую тему, как тестирование. Писать
тесты или не писать, решение за вами (или вашим руководителем). В любом
случаев от их наличия выиграют все, так как большинство ошибок будут
отлавливаться на стадии разработки, что скажется на удобстве
использования разрабатываемого приложения.
Если же у вас появилась мысль по поводу написания собственного
пакета и загрузки его в pub, то одним из обязательных условий является
наличие у него тестов.
193
23. Какой набор базовых библиотек входит в Dart?
24. Какая базовая библиотека автоматически подключается к dart-
файлам?
25. Какое ключевое слово позволяет подключать к вашему модулю код из
других модулей (файлов с расширением «.dart»)?
26. Для чего используется файл «pubspec.yaml»?
27. Как создать в проекте библиотеку?
28. Как организовать API для доступа к предоставляемой библиотекой
функциональности?
29. Для чего используются ключевые слова show и hide?
30. Как организовать отложенную загрузку подключаемой
библиотеки/модуля?
31. Как подключить пакет к проекту?
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
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 и какие механизмы для этого используются, давайте, как и в случае с
функциями, сначала поговорим об абстракции и ее месте в объектно-
ориентированном программировании.
203
4.1.1. Основа абстракции в ООП – класс
Класс изначально является высокоуровневой абстракцией, в связи с
чем может рассматриваться как одновременное написание главы книги
вместе с ее разделами, где после объявление главы идет небольшой текст с
пояснениями для чего она предназначена, то есть описываются состояния
класса. Простые типы данных и операции над ними так и остаются на
первом уровне абстракции и используются в процессе написания
состояний классов, а также операций над ними в методах описываемого
класса. Каждый новый класс описывается в новом файле, имя которого
должно совпадать с именем класса, которое отвечает на вопросы: «Кто?»,
«Что?», то есть имя класса – существительное, а имена его методов –
глаголы. Иногда в одном файле может содержаться по несколько классов.
Как и в предыдущем случае, когда мы рассматривали разделы, они
могут быть вложенными. На верхнем уровне находятся те, по которым
читатель может спокойно ориентироваться (публичные методы, т. е.
интерфейс класса), в то время как разделы на более низких уровнях
абстракции сокрыты от его глаз и считаются деталями реализации класса
(приватные методы).
204
изменения своих состояний. Все начинается с маленьких запчастей,
которые входят в состав более сложных, становясь деталями их реализации,
то есть переменными класса более высокого уровня. Доступ к изменению
состояний данных запчастей можно получить только через методы детали,
в состав которой входит эта запчасть. Так как двигатель довольно сложная
и закрытая система, то предоставление доступа напрямую к каким-то
запчастям может привести к фатальным последствиям.
А сами по себе приведенные выше узлы без кузова ни на что не
способны, т. к. он является связующим звеном, позволяющим закрепить их
и детали, необходимые для нормальной работы автомобиля.
И несмотря на то, что автомобиль состоит из огромного количества
составных частей, водителю нет необходимости знать все его устройство.
Достаточно просто выучить дорожные правила, уметь крутить руль, а также
различать педаль газа и тормоза. Автомобиль как класс, является
абстракцией очень высокого уровня, в состав которой входит множество
других классов. Он представляет собой обобщение, любой метод которого
приводит в движение огромное количество изменений состояний его
составных частей.
Наша задача, как разработчиков, научиться писать программы на
высоком уровне абстракции! Чем раньше мы выкинем из головы как
осуществляется работа с классом поршень и возложим обязанность по его
контролю на класс двигатель, чем раньше мы перестанем вручную считать
набираемую автомобилем скорость путем подсчета скорости вращения
колес, а возложим эту обязанность на класс спидометр, тем быстрее в
процессе написания кода выйдем на более высокий уровень абстракции и
будем способны разрабатывать системы различной сложности.
205
4.1.4. Три столпа абстракции в ООП: интерфейс, полиморфизм и
приведение
Полиморфизм очень тесно связан с наследованием и позволяет
менять (переопределять) поведение базовых классов в производных.
Допустим у нас имеется базовый класс машина и у которого прописали
метод для набора скорости в зависимости от того, как сильно водитель
зажимает педаль газа. Если наследоваться от данного класса и не
переопределять этот метод, то экземпляр производного класса будет
работать с реализацией метода, определенной в базовом классе. Но ведь у
нас существует множество различных марок машин, и они по-разному
разгоняются до 100 км/ч. Здесь на помощь и приходит полиморфизм,
позволяя переопределить реализацию метода базового класса, после чего
при создании экземпляра производного класса при вызове метода набора
скорости уже будет вызываться не реализация базового класса, а
переопределенная в производном.
Для лучшего понимания, почему так происходит давайте разберем это
при помощи рисунков. Как уже говорилось ранее производный класс
наследует от базового его поведение и состояния. Помимо этого, на
производный класс накладывается обязанность по созданию экземпляра
базового и его начальной инициализации. Говоря другими словами, при
создании экземпляра производного класса у нас создается не один, а два
экземпляра класса (производный и базовый), тесно связанные между
собой. Сначала разберем случай без переопределения метода базового
класса в производном:
Базовый класс Производный класс
206
Обращаясь к таким переменным, как «а», «б», «в» производного класса,
разработчик на самом деле взаимодействует с переменными базового
класса, т. е. имена перечисленных переменных в производном классе
являются ссылками на переменные базового. Тоже касается и метода «а».
Переменная же «г» и метод «б» связаны только с производным классом. При
переопределении метода «а» в производном классе, приведенная на
рисунке связь разрушится и всегда, при работе с экземпляром
производного класса будет вызываться переопределенная в нем
реализация метода «а»:
Базовый класс Производный класс
207
встречали такие строчки кода, где переменной типа базового класса
присваивается создаваемый экземпляр производного:
Базовый_класс имя_переменной = Производный_класс();
208
Базовый класс Производный класс
209
двигатель, потом доставили на завод, собирающий автомобили и
установили его. Так как он реализует необходимый интерфейс, то в момент
его установки (вызова конструктора с передачей ему объектов)
выполняется приведение к интерфейсу базового класса (либо просто
интерфейсу), после чего вся работа с ним будет осуществляться только в
рамках этого интерфейса.
В момент установки двигателя нам надо определиться кто будет
отвечать за его жизненный цикл: сам автомобиль или кто-то еще? То есть
использовать композицию или агрегацию. Это два довольно близких по
значению понятия, которые нам говорят, что можно использовать объекты
одного класса в другом (в виде состояний). Отличие заключается только в
том, что в случае агрегации класс, которому в конструкторе, либо
специальном методе передается объект, с которым он будет
взаимодействовать и использовать на протяжении всего своего жизненного
цикла не управляет жизненным циклом передаваемого ему объекта. В
момент удаления класса не удалится сам объект, который он использовал.
В качестве примера представим, что мы разбили машину, но двигатель
остался цел и перед ее утилизацией его вытащили, отправив на склад. При
использовании композиции класс, которому передается объект, берет на
себя управление его жизненным циклом (двигатель утилизируют с
автомобилем).
Если вы создаете класс под конкретную задачу, где нет необходимости
закладывать вариативность, производить сокрытие других методов его
внутренних объектов, то нет смысла прибегать к использованию более
абстрактных типов данных. Передавайте в его конструктор или методы для
установки объектов взаимодействия объекты только менее абстрактных
пользовательских типов данных.
Приведение к более абстрактным типам данных для последующего
взаимодействия с объектом через интерфейс позволяет разделять
приложение на слои, где сразу закладывается интерфейс сопряжения
между ними, после чего на классы ложится обязанность реализовать
соответствующее поведение. А связывание слоев производится путем
передачи объекта в конструктор создаваемого класса с его последующим
приведением к необходимому интерфейсу. При этом уменьшается
связность между слоями системы, что позволяет их делать заменяемыми.
По отдельности интерфейс, полиморфизм и приведение не
представляют ничего ультимативного, но, когда разработчик овладевает их
совместным использованием, у него в руках появляется очень гибкий
инструмент, открывающий дорогу в удивительный мир разработки
программного обеспечения, либо в ад, где уже для каждого программиста,
который бездумно его использует приготовлен отдельный котел. Как сказал
210
один из моих учеников: «Вот я и научился прятать говнокод за
интерфейсом. Теперь можно поднимать грейд до мидла» =)
211
реализовывать не только методы интерфейсного класса, но и
переопределять публичные значения переменных такого класса.
Таким образом, если в случае с наследованием мы наследуем состояние и
поведение, то в случае с интерфейсом – объявляем контракт, которому
должен следовать класс реализующий интерфейс.
212
осуществляется проверка на корректность входных данных перед их
присваиванием. И снова имеется вероятность установить значение, из-за
которого данные экземпляра класса потеряют инвариантность!
Так вот, инкапсуляция помимо объединения данных и методов,
должна обеспечивать гарантию того, что данные не потеряют свою
инвариантность. Это достигается путем сокрытия служебных методов,
полей класса и введению дополнительных проверок в методы, которые в
случае чего приведут к падению приложения с оповещением об ошибке или
выбрасыванию исключения, но не позволят нарушить инвариантность
данных.
Наверное, из-за того, что инкапсуляция использует механизм
сокрытия для достижения своих целей, некоторыми разработчиками и
принято считать их тождественными определениями, но все куда сложнее,
чем кажется на первый взгляд.
В С++, Java и других языках программирования приватные
переменные или методы не видны за пределами методов класса, в котором
они определяются. То есть к ним нельзя обратиться через экземпляр класса.
Наличие модификаторов доступа позволяет использовать сокрытие при
проектировании программы, а сами приватные переменные и методы при
этом повышают уровень безопасности и надежности за счет ограничения
доступа к важным или критичным частям реализации объекта.
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';
cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
cat.sleepState = true;
cat.currentState(); // Кот спит
cat.sleep(); // Сон во сне... мммм...
}
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';
/*
// ошибка
var cat = Cat()
..age=3
..name='Тимоха'
.._sleepState=true;
*/
cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
cat.sleep(); // Кот засыпает: Хр-р-р-р-р...
cat.sleep(); // Сон во сне... мммм...
cat.wakeUp(); // Лениво потягиваясь, открывает глаза...
}
216
..age=3
..name='Тимоха'
.._sleepState=false;
cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
cat._sleepState = true;
cat.currentState(); // Кот спит
cat.sleep(); // Сон во сне... мммм...
cat._sleepState = false;
cat.currentState(); // Кот бодрствует
}
// ex4_4 - main.dart
import 'cat.dart';
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 - main.dart
import 'cat.dart';
218
..name = 'Тимоха';
cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
}
// ex4_6 - main.dart
import 'cat.dart';
219
cat.helloMaster(); // Мя-я-я-я-у!!!
cat.currentState(); // Кот бодрствует
}
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';
print(cat.name); // Тимоха
print(cat.age); // 3
print(cat.address); // Москва
cat.helloMaster();
cat.currentState(); // Кот спит
}
Cat(
this.name,
221
this._address, [
this.age = 4,
this._sleepState = true,
]);
// остальные методы не изменялись
}
// ex4_8 - main.dart
import 'cat.dart';
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';
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';
cat = Cat.onlyName('Тимоха');
catProcessing(cat);
print('*' * 20);
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,
);
// ex4_11 - main.dart
import 'cat.dart';
225
);
cat.helloMaster();
cat.currentState();
}
cat = Cat.onlyName('Твикс');
catProcessing(cat);
print('*' * 20);
cat = Cat.defaultCat();
catProcessing(cat);
print('*' * 20);
226
4.3.2. Константный конструктор
Такой вид конструктора необходимо использовать в тех случаях, когда
значения объектов и переменных класса не меняются, то есть все они
объявлены через final. Для использования такого объявления
конструктора, перед ним следует добавить ключевое слово const:
// ex4_12 - cat.dart
class ImmutableCat {
final String name;
final int age;
void helloMaster(){
print('Мя-я-я-я-у!!!');
}
}
// ex4_12 - main.dart
import 'cat.dart';
227
var barsik = const ImmutableCat('Барсик', 2);
SingleCat.fromName(this._name, this.age);
// ex4_14 - main.dart
import 'singlecat.dart';
228
void main(List<String> arguments) {
var cat = SingleCat('Тимоха', 2);
var newCat = SingleCat('Барсик', 4);
print(newCat.name);
}
/* Создаем экземпляр класса кота
Экземпляр класса кота был создан ранее!
Тимоха */
// ex4_15 - main.dart
import 'singlecat.dart';
229
var newCat2 = SingleCat();
print(newCat2.name); // Тимоха
Book.fromSettings(this.name, this.pages);
230
4.4. Статические переменные и методы класса
В предыдущих примерах использовались статические переменные
класса. Их отличие от обычных заключается в том, что они будут хранить
одно и то же значение вне зависимости от того, с каким экземпляром класса
сейчас производится работа. Также к ним можно обращаться только через
имя самого класса (без создания экземпляра класса), либо прописав
сеттеры и геттеры. Отдельно стоит обратить внимание на то, что
статическая переменная должна быть инициализирована до момента ее
использования (обращения к ней):
// ex4_17.dart
class Book{
static var bookPages = 10;
231
4.5. Перегрузка операторов
Dart предоставляет возможность программисту перегружать
стандартные операторы (см. главу 2), тем самым создавая более гибкие
классы. Иногда перегрузку путают с переопределением, так вот, с
академической колокольни – операторы могут только перегружаться, а не
переопределяться. Так уж исторически повелось, поскольку они не
привязаны только к какому-то конкретному типу данных.
Ряд перегрузок операторов мы с вами рассмотрим, взяв за основу
рубли, где значения будут храниться в копейках. Для боевого проекта такой
код не очень подойдет, но даст полноценное представление о том, как
реализуются перегрузки операторов.
Rub._(this.kopek);
// перегрузка сложения
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);
}
// переопределение
@override
String toString() {
var rub = (kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}
}
233
// ex4_20.dart
class Rub {
late final int kopek;
Rub._(this.kopek);
// перегрузка сложения
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)';
}
}
234
rub1 += 10;
print(rub1); // Rub(20.00)
rub1 += Rub('2');
print(rub1); // Rub(22.00)
rub1 += 3.4;
print(rub1); // Rub(25.40)
}
Rub._(this.kopek);
@override
int get hashCode => kopek.hashCode;
// Переопределение hashCode необходимо для правильной
// проверки равенства объектов
235
bool operator >(Rub other) {
return kopek > other.kopek;
}
// переопределение
@override
String toString() {
var rub = (kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}
}
236
4.5.3. Перегрузка битовых операторов
Цифровой век диктует свои законы, поэтому даешь битовые операции
для цифрового рубля!
// ex4_22.dart
class Rub {
late final int kopek;
Rub._(this.kopek);
// перегрузка побитового И
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 ~() {
return Rub._(~kopek);
}
237
}
// переопределение
@override
String toString() {
var rub = (kopek / 100).toStringAsFixed(2);
return 'Rub($rub)';
}
}
Book(this.name, this.pages);
238
return Box([this, otherBook]);
}
@override
String toString() {
return 'Book($name, $pages)';
}
}
class Box {
final List<Book> _items;
Box(this._items);
String _printBooks() {
var str = '[';
for (var element in _items) {
str += ('$element, ');
}
str += ']';
return str;
}
@override
239
String toString() {
return _printBooks();
}
}
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), ]
*/
240
одиноки в этих вопросах, но та щепотка магии, о которой далее пойдет
речь, сгладит острые углы неприятия сложившейся ситуации.
Как гласит народная мудрость: «Не можешь бороться – возглавь!».
Если переложить ее на IT-сленг, то звучать она, скорее всего, будет
следующим образом: «Что-то не нравится – напиши свой
велосипедокостыль!» Как раз такой функционал и предоставляют
разработчику методы расширения, обобщенный вид объявления которых
можно представить следующим образом:
extension <ИмяРасширения>? on <тип> {
(<добавляемые методы>)*
}
241
// [h, t, t, p, s, :, /, /, d, a, r, t, ., d, e, v]
print(url.isUrl()); // true
url = '-_-';
print(url.isUrl()); // false
}
class Box {
// без изменений
}
242
extension TrueBox on Box {
void add(Object book){
this + book;
}
}
Plumber(
this.name,
this._age,
this.id,
this._salary,
this._yearsExperience,
);
Plumber.withMinSalary(
this.name,
243
this._age,
this.id,
this._yearsExperience,
) : _salary = 1000;
void ageIncrease() {
_age++;
}
void yearsExperienceIncrease() {
_yearsExperience++;
}
@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;
void ageIncrease() {
_age++;
}
void yearsExperienceIncrease() {
_yearsExperience++;
}
@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,
);
void ageIncrease() {
_age++;
}
void yearsExperienceIncrease() {
_yearsExperience++;
}
@override
String toString() {
return 'Employee($name, $age, $id, $_salary)';
}
}
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)';
}
}
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);
@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)';
}
}
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);
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);
// остальные методы не изменялись
}
250
print(plumber); // Plumber(Max, 22, 4, 1000)
if (employee is Builder){
// появляется доступ к полям и методам Builder
print(employee.category); // 1
}
251
}
}
// Builder(Alex, 22, 1, 2200, 2)
// Plumber(John, 27, 4, 8100)
// Builder(Max, 33, 2, 13200, 4)
// Plumber(Kate, 23, 4, 8100)
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,
),
];
253
Если у вас уже есть опыт разработки и использования шаблонов
проектирования GoF, то наверняка заметили, что мы с вами реализовали
фабричный метод в «Dart-стиле».
Что касается сокрытия свойств и методов производного класса при
приведении к базовому, это можно посмотреть следующим образом.
Переместите код с объявлением классов в отдельный файл, импортируйте
его, создайте экземпляр класса Builder, приведите к Employee и
попробуйте отыскать доступ к его свойству category (дисклеймер – его не
будет видно):
254
которым осуществляется работа, привести к необходимому производному
классу.
Методы, которые объявлены, но не имеют реализацию, называются
чисто виртуальными методами. А класс, который содержит хоть один
виртуальный метод – абстрактный класс. Отличие абстрактного класса от
обычного заключается в том, что экземпляр абстрактного класса не может
быть создан (исключение – фабричный конструктор). Но к таким классам
мы можем приводить производные от них классы. То есть что базовый, что
абстрактный класс может выступать в роли публичного интерфейса к
экземплярам производных от них классов.
Давайте перепишем класс Employee таким образом, чтобы он стал
абстрактным базовым классом, передав на откуп производным классам
расчет премии для сотрудников:
// ex4_33.dart
abstract class Employee {
final String name;
final int id;
int _age;
int _salary;
int _yearsExperience;
@override
int calculateBonus() {
return ((_salary / 100) * _yearsExperience).toInt();
}
@override
int calculateBonusWithParam(int percent) {
255
return ((_salary / 100) * percent).toInt();
}
}
@override
int calculateBonus() {
return calculateBonusWithParam(50);
}
@override
int calculateBonusWithParam(int percent) {
return ((_salary / 100) * percent).toInt();
}
}
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
257
хранения. Мы можем добавлять в них вещи, забирать последнюю
добавленную вещи и считать общую сумму веса тех вещей, которые в них
хранятся. Так как это объекты из различной предметной области, то они не
будут наследоваться от базового абстрактного класса СистемаХранения, но
будут реализовывать его интерфейс:
// ex4_34.dart
class Item{
final String name;
final double weight;
Item(this.name, this.weight) ;
}
Item popItem();
double systemWeight();
}
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;
}
// методы, характерные для коробки
}
@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;
}
// методы, характерные для шкафа
}
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 */
@override
String get _name => 'Алан';
@override
260
String greet(Person person) {
return 'Привет, ${person._name}!!! Меня зовут $_name.';
}
@override
int howMuchOlder(Person person) {
return age*2 - person.age;
}
}
@override
String get _name => '';
@override
String greet(Person person) {
return 'Вот ты и попался, ${person._name}!!!';
}
@override
int howMuchOlder(Person person) {
return -1;
}
}
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
}
}
}
void someMethod1() {
if (_privateField != null) {
int i = _privateField; // OK
}
}
void someMethod2() {
if (_privateField is int) {
int i = _privateField; // OK
}
}
}
262
У механизма продвижения приватных полей имеется ряд исключений.
Например, он не будет работать в следующих случаях (тут придется по
старинке):
− поле не обозначено как final, т.к. такие поля могут менять свое
значение в процессе работы приложения;
− поле переопределено геттером или не final полем;
− поле не является приватным, т.к. такие поля могут быть
переопределены в другом месте приложения;
− у поля такое же имя, как и у геттера или не final поля в другом
несвязанном классе библиотеки;
− в библиотеке есть любой класс, интерфейс которого содержит
объявление геттера с тем же именем, но у него нет его реализации.
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
264
класса для создания примесей. Также его нельзя использовать для передачи
в конструкцию switch-case, т.е. такие классы не обладают
исчерпываимостью. Но это сложно назвать ограничением.
Когда такой класс импортируется из библиотеки (одного файла в
другой), при наследовании нет доступа к приватным полям и методам
базового класса. Для начала давайте рассмотрим пример, когда базовый и
производный класс размещены в одной библиотеке:
// ex4_37.dart
class User{
final String name;
int _age;
final int id;
String _privateHello(){
return 'Private Hello!';
}
String publicHello(){
return 'Public Hello!';
}
}
@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);
265
print(user._age); // 22
}
@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());
}
266
// ex4_39 - lib_a.dart
class User{
// без изменений
}
// ex4_39 - main.dart
import 'lib_a.dart';
@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);
267
Как и при отсутствии модификатора, такие классы запрещено
использовать для создания примесей, и они не обладают
исчерпываимостью.
На абстрактный класс также распространяются правила,
рассмотренные ранее, в одном файле имеется доступ к приватным полям и
методам абстрактного базового класса, при импортировании – нет. Ну, и
обходятся эти ограничения аналогичным образом…
Используйте данный модификатор, если хотите отдать на откуп
производным классам реализацию ряда методов базового класса.
User(this.name, this.id){
print('User created');
}
String publicHello(){
return 'Public Hello!';
}
@override
String toString() {
268
return 'User($name, $id)';
}
}
@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();
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)
}
// 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.
// ex4_43 - main.dart
import 'lib_a.dart';
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
@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 */
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
273
библиотеку в другой файл и реализуем наследование от класса, который
наследуется от интерфейса:
// ex4_46 - lib_a.dart
interface class Money {
late final int _val;
Money._(this._val);
Money.fromString(String value)
: this._(
(double.parse(value) * 100).toInt(),
);
double value() {
return _val / 100;
}
@override
String toString() {
var money = (_val / 100).toStringAsFixed(2);
return 'Money($money)';
}
}
@override
Money operator +(Money other) {
return Money._(_val + other._val);
}
274
}
// ex4_46 - main.dart
import 'lib_a.dart';
@override
String toString() {
return 'MyMoney(${value()})';
}
}
void main() {
var money = Money.fromInt(100);
print(money); // Money(1.00)
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';
Rub._(this._kopek);
@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)
Rub._(this._kopek);
Rub.fromInt(int value) : this._(value);
@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();
}
}
USD._(this._cent);
USD.fromInt(int value) : this._(value);
@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);
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.allConverToRub()); // Rub(13143.00)
print(wallet.allConverToUsd()); // USD(131.43)
}
280
производный класс к базовому или интерфейсу при передаче в конструктор
или метод какого-нибудь объекта. Если вы уже читали про SOLID, то вот вам
ответ на то, на каких свойствах строится принцип подстановки Барбары-
Лисков .
Что касается модификатора класса abstract interface, то он
позволяет, используя виртуальные методы, более лаконичным образом
описывать интерфейс и запрещает создавать экземпляры интерфейсного
класса (ну, разве что не через фабричный конструктор):
abstract class IMoney {
int get value;
IMoney operator +(IMoney other);
IMoney operator -(IMoney other);
}
281
Сначала мы рассмотрим простые варианты реализации sealed-
классов, а только потом перейдем к примерам с щепоткой «уличной
магии». Давайте вернемся к концепции кошелька и первым делом объявим
sealed-класс Money и его производные классы:
sealed class Money {}
class RUB extends Money {}
class USD extends Money {}
class EUR extends Money {}
282
print('EUR');
}
}
Money(this._val);
@override
RUB operator +(Money other) {
return RUB(value + other.value);
}
@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
return 'RUB($rub)';
}
}
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)';
}
}
@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('Разные валюты');
}
}
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('Неподдерживаемая валюта');
}
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'),
)); // ╭∩╮( •̀_•́ )╭∩╮
}
287
Создайте новый консольный проект «sealed_example» со следующей
структурой директорий и файлов:
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
}
@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)';
}
}
@override
RUB operator +(Money other) {
289
return RUB(value + other.value);
}
@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
return 'RUB($rub)';
}
}
@override
EUR operator +(Money other) {
return EUR(value + other.value);
}
@override
String toString() {
var eur = (value / 100).toStringAsFixed(2);
return 'EUR($eur)';
}
}
290
print(money.$1 + money.$2);
case (RUB(value: > 50000), USD()):
print(money.$2 + money.$1);
case _:
print('╭∩╮( •̀_•́ )╭∩╮');
}
}
// paper_rub.dart
part of 'money.dart';
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';
@override
DigitalRUB operator +(Money other) {
return DigitalRUB._(value + other.value);
}
@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
return 'DigitalRUB($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(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('Неподдерживаемая валюта');
}
293
Если сейчас попробуете запустить приложение, то не увидите никаких
изменений, т.к. в терминале увидите предыдущий результат. Что нам дает
объявление класса RUB как sealed? Мы можем его производные классы
использовать в switch-case как с классами USD и EUR, так и отдельно от них.
Начнем с совместного использования. Откройте файл
«sealed_example.dart» директории «bin» и добавьте в него следующий код:
import 'package:sealed_example/sealed_example.dart';
294
}
295
),
_ => RUB.kopek(rub.value),
};
}
}
RUB.kopek(this._val);
@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)
}
296
}
void main() {
RUB rub = RUB.fromStr('100', 55);
print(rub.inflation(rub)); // RUB(45.00)
}
297
}
@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');
}
}
void main() {
var rub = RUB();
rub.inflation(); // GlobalInflation Inflation
RUB.kopek(this._val);
@override
String toString() {
var rub = (value / 100).toStringAsFixed(2);
299
return 'RUB($rub)';
}
}
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.
@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)
}
@override
String toString() {
return idString;
}
}
class Product<T>{
T id;
final String name;
final double price;
@override
String toString() {
302
return 'Продукт: $name с id: $id стоит $price тугриков';
}
}
void printBooks(){
items.forEach((element) {
print(element.name);
});
303
}
@override
String toString() {
return idString;
}
}
T firstElement<T>(List<T> list){
return list[0];
}
304
4.12. Enum (Перечисления)
Перечисления представляют собой особый вид классов, используемых
для представления фиксированного числа постоянных значений. Для того,
чтобы его создать, достаточно использовать ключевое слово enum. Каждому
значению в перечислении соответствует свой целочисленный индекс.
Например, первое значение имеет индекс 0, второе значение имеет индекс
1 и т. д.
// ex4_61.dart
enum State { none, open, close, lock}
305
Начнем с объявления перечисления возможных состояний
кофемашины и интерфейсов, которые будем использовать в ходе
написания классов, реализующих то или иное состояние:
// ex4_62.dart
enum CoffeMachineState {
none,
idle,
choose,
cappuccino,
latte,
espresso,
changeMoney,
}
@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');
}
}
@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());
}
}
}
307
@override
void insertMoney(ICoffeMachine coffeMachine) {
ejectMoney(coffeMachine);
}
@override
void makeCoffe(ICoffeMachine coffeMachine) {
ejectMoney(coffeMachine);
}
}
@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!');
}
}
}
@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!');
}
}
}
@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 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 */
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,
});
@override
String toString() {
return '${this.name}($GDP, $nationalDebt, $population)';
}
}
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
315
на наличие ошибок, чем продолжит работать с некорректными данными.
Особенно, не стоит перехватывать для обработки исключений класс
Exception, так как он является базовым классом для всех исключений, то
есть какое бы не сгенерировалось исключение – оно будет обработано и,
если в блоке обработки не предусмотрена повторная генерация
исключения, чтобы предупредить более верхний уровень приложения, вы
можете и не узнать о наличии ошибки.
Для работы с исключениями в Dart используются такие классы, как
Exception и Error, а также множество их производных классов. Сказать
честно… хватило бы и одного, а наличие двух классов приводит к
неприятным моментам. Но тут уж ничего не поделать и придется страдать
привыкнуть.
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);
}
//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('Что бы ни произошло, я - великолепен!!!');
}
// Произошло деление на ноль!!!
// Что бы ни произошло, я - великолепен!!!
}
318
}
catch(e){
print('Ошибка: $e');
}
// Произошло деление на ноль!!!
try{
if (inputValue == 0){
throw ArgumentError();
}
resultValue = scalingValue ~/ inputValue;
}on UnsupportedError{
print('Произошло деление на ноль!!!');
}
catch(e){
print('Ошибка: $e');
}
//Ошибка: Invalid argument(s)
}
319
}
}on UnsupportedError {
print('Произошло деление на ноль!!!');
rethrow;
} on ArgumentError catch(e){
print('Ошибка: $e'); //Ошибка: Invalid argument(s)
}
Item(this.name, this.weight) ;
}
class StorageSystem {
var itemsList = <Item>[];
final double weightLimit;
StorageSystem(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} не помещается в коробку!');
}
}
320
@override
Item popItem() {
return itemsList.removeLast();
}
@override
double systemWeight() {
var sum = 0.0;
for (var element in itemsList) {
sum += element.weight;
}
return sum;
}
// методы, характерные для коробки
}
print(storageSystem.popItem().name);
print(storageSystem.systemWeight());
321
// ex4_70.dart
class MyException implements Exception {
final String? msg;
const MyException([this.msg]);
@override
String toString() => msg ?? 'MyException';
}
@override
String toString() => msg ?? 'MyException';
}
try{
throw MyError('Пользовательская ошибка');
} on MyError catch(e){
print(e); // Пользовательская ошибка
}
try{
throw MyNewError();
} on MyNewError catch(e){
print(e); // Instance of 'MyNewError'
}
}
322
void exceptionFunc(){
throw MyException('Пользовательское исключение');
}
void errorFunc(){
throw MyError();
}
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
В момент запуска приложения происходит выделение памяти в куче и
стеке. Перед самим выделением памяти срабатывают некоторые события
и, если в написанном коде существуют ошибки, их можно отследить в стеке.
Таким образом, трассировка стека представляет собой список вызовов
методов, которые приложение выполняло при возникновении исключения.
324
Первым аргументом assert может быть любое выражение, которое
возвращает логическое значение (true или false). Если результат
вычисления выражения true, утверждение завершается успешно и
управление переходит к коду, находящемуся за ним. Если false, то
утверждение генерирует ошибку AssertionError.
Утверждения удобно использовать в процессе разработки
программных продуктов. Но необходимо быть внимательным и не
помещать в них в качестве выражений различные функции, возвращающие
значение логического типа данных, не использовать в утверждениях
вызовы методов экземпляров классов и т. д. Это связано с тем, что при
release-сборке проекта все утверждения в коде игнорируются, то есть они
не выполняются.
Rub._(this._kopek);
Rub.fromInt(int value) : this._(value);
@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);
}
}
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.
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. Для чего используются утверждения? Всегда ли выполняются
утверждения в коде?
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
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 для
работы с файлами и директориями.
Файлы могут выступать в различном амплуа: от конфигурационных до
хранилищ. Например, в конфигурационные файлы выносится
информация, которую может изменять пользователь в настройках
приложения и влияющая на логику его работы. Это делается для большей
гибкости, так как хранение такой информации в самом коде программного
продукта подразумевает, что при малейшем ее изменении необходимо по
новой осуществлять сборку (компиляцию) программы. А для примера
использования файлов, как хранилищ, реализуем простую базу данных на
основе односвязного списка.
Флаг
Результат Описание
компиляции
337
Файл, компилируемый под
целевую платформу, с
промежуточным
jit-snapshot Модуль JIT оптимизированным
представлением исходного кода,
который был получен в ходе
учебного запуска программы.
Файл, содержащий
промежуточное представление
исходного кода в виде
kernel Kernel модуль абстрактного синтаксического
дерева (Kernel AST).
Может запускаться на любых
платформах.
338
5.1.1. Флаг exe
Откройте терминал «Terminal -> New Terminal» и введите команду
dart compile exe bin\my_app.dart
339
Рисунок 5.2 – Запуск приложения
340
оптимизации приложения. В некоторых случаях удается достичь
значительного прироста по быстродействию.
Удалите скомпилированные ранее файлы из директории «bin» и
введите следующую команду:
dart compile jit-snapshot bin\my_app.dart
341
После передачи полученного файла третьей стороне, убедитесь, что
там имеется установленный Dart SDK:
dart run bin\my_app.dill
342
Рисунок 5.8 – Запуск приложения
343
Особенно, когда их огромное количество. Из-за этого и используется флаги
и параметры (свойства – флаг с данными после него), которые позволяют
понять – есть ли в передаваемых на вход данных нужные значения или нет.
Не будем изобретать велосипед и воспользуемся уже готовым
пакетом: args (https://pub.dev/packages/args), позволяющим задать имена
параметров и флагов для их поиска и извлечения данных, а также
использования значений по умолчанию, если их не передали в момент
запуска приложения.
Откройте файл «pubspec.yaml» и добавьте в раздел с зависимостями
пакет args:
# Add regular dependencies here.
dependencies:
args: ^2.4.2
# path: ^1.8.0
344
print('a - b = ${a - b}');
}else{
print('a + b = ${a + b}');
}
}
345
директории проекта. В случае работы с файлом из другой директории, путь
до него можно прописать следующим образом:
var myFile = File('F:\\code\\text.txt'); // для Windows
346
import 'dart:io';
347
преобразователи, чтобы представить содержимое файла в требуемом
формате. То есть класс File может работать не только с текстовыми
представлениями файлов, но и байтовыми:
import 'dart:io';
import 'dart:convert';
import 'dart:async';
348
В данном случае осуществляется проверка существует ли файл
text.txt на одном уровне выше директории запускаемого приложения или
нет.
Также имеется возможность выводить текущее состояние файла, с
которым будет осуществляться работа:
import 'dart:io';
349
Для получения размера файла в байтах используйте метод length и
lengthSync:
import 'dart:io';
350
Если хотите более подробного ознакомиться с методами класса File,
то обратитесь к документации Dart.
print(myLocalDir1.existsSync()); // true
print(myLocalDir2.existsSync()); // false
// абсолютный путь
final myGlobalDir = Directory('C:\\code\\dart');
print(myGlobalDir.existsSync()); // true
}
print(myLocalDir.existsSync());
// BAD
// myLocalDir.createSync();
// Error: Системе не удается найти указанный путь.
// OK
myLocalDir.createSync(recursive: true);
351
print(myLocalDir.existsSync()); // true
}
myDir = Directory('bin\\data');
// BAD
// myDir.deleteSync();
// OS Error: Папка не пуста.
// OK
myDir.deleteSync(recursive: true);
print(myDir.existsSync()); // false
}
352
print(newDir.existsSync()); // true
print(myDir.existsSync()); // false
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*/
354
уникальной временной директории, в противном случае в качестве
префикса используется пустая строка:
import 'dart:io';
355
Рисунок 5.11 – Структура проекта «database»
@override
String toString();
}
enum AcsessLevel {
student('S'),
teacher('T'),
admin('A');
356
final String text;
const AcsessLevel(this.text);
@override
String toString() {
return text;
}
}
enum UserTableFields {
id('id'),
nickname('nickname'),
yearOfBirth('yearOfBirth'),
email('email'),
phone('phone'),
acsessLevel('acsessLevel'),
passwordHash('passwordHash');
@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;
@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();
}
}
_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);
360
} else if (!contains(data)) {
_tail?.next = node;
_tail = node;
return true;
}
return false;
}
if (current == null) {
return;
}
prev?.next = current.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;
}
temp = table._head;
while (temp != null) {
if (!newTable.contains(temp.data as T)) {
newTable.insert(temp.data as T);
}
temp = temp.next;
}
return newTable;
}
362
var temp = _head;
return newTable;
}
@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();
}
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({
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');
}
}
364
nickname: data[1],
yearOfBirth: int.parse(data[2]),
email: data[3],
phone: data[4],
acsessLevel: data[5],
passwordHash: data[6],
));
}
return table;
}
}
switch (type) {
case DBType.suai:
file = File(pathToSuaiDB);
users = _suaiUsers;
case DBType.unecon:
file = File(pathToUneconDB);
users = _uneconUsers;
}
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;
}
file.writeAsStringSync(
buffer.toString(),
mode: FileMode.append,
);
return true;
}
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);
}
}
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');
}
}
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,
);
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);
}
370
database.showDB(typeDB);
}
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);
}
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();
}
372
данные хранятся в файле, то откройте один из них и вас встретит, что-то
такое:
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» со следующей
структурой и наполнением директорий:
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
});
@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('Actor{name: $name, age: $age, ');
sb.write('filmsAmount: $filmsAmount, ');
sb.write('aboutActor: $aboutActor}');
return sb.toString();
}
}
376
final double rating;
Review({
required this.name,
required this.text,
required this.rating
});
@override
String toString() {
return 'Review{name: $name, text: $text, rating: $rating}';
}
}
Genre(this.genre);
@override
String toString() {
return 'Genre{genre: $genre}';
377
}
}
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},
);
378
reviews: List<Review>.from(
json['reviews'].map((x) => Review.fromJson(x)),
));
}
@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();
}
}
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);
import 'package:filmography/filmography.dart';
print(movie);
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;
// далее без изменений
}
381
);
}
return Review(
name: name,
text: text ?? '',
rating: rating,
);
}
Caffee({
required this.name,
required this.address,
this.yearOpened,
});
382
'Required "name" field of type String in $json',
);
}
@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}
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');
}
}
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
@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);
@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('Actor{name: $name, age: $age, ');
sb.write('filmsAmount: $numberOfFilms, ');
sb.write('aboutActor: $aboutActor}');
return sb.toString();
}
}
@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);
@override
String toString() {
return 'Review{name: $name, text: $text, rating: $rating}';
}
}
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);
@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();
}
}
389
модифицировать, но обычно это не рекомендуется делать, так как в случае
какого-либо изменения в файле, по которому этот код генерировался при
повторном запуске команды dart pub run build_runner build все
внесенные изменения затрутся. А теперь представьте, что у вас таких мест
много и во все вы внесли изменения модифицировав код. Чем такой подход
будет лучше написания сериализации и десериализации вручную? Ничем!!!
Можно попросту забыть об этих изменениях, а потом, при очередной
пересборке проекта удивляться почему он так сейчас работает или не
работает вовсе.
390
Создайте новый консольный проект «json_store» со следующей
структурой директорий и их наполнением:
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);
}
}
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 {};
}
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;
393
_file.write(values);
} else if (!valueEquals(oldValue, value)) {
_values = values;
_file.write(values);
}
}
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')); // (づ˶•༝•˶)づ♡
}
store.setValue('strList', '-_-');
store.setValue('double', 99);
print(store.getValue('strList')); // -_-
print(store.getValue('double')); // 99
}
395
"str": "(づ˶•༝•˶)づ♡"
}
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
}
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}
}
Резюме по разделу
В данной главе мы рассмотрели какими способами можно
осуществлять компиляцию разрабатываемого приложения и как его
конфигурировать в момент запуска через терминал. Так же были затронуты
почти все основные возможности Dart по работе с файлами и
директориями. Почему почти все? Потому, что Dart еще может
манипулировать данными в html-файлах, но с появлением Flutter for Web
эти его возможности теряют свою актуальность. Да и давайте признаемся
честно, не так уж и много компаний горит желанием использовать его в
этих целях, тогда как JavaScript и Flutter предоставляет куда больший набор
библиотек и возможностей.
Дополнительно нами был рассмотрен такой формат представления
данных, как JSON и способы работы с ним. Он часто будет встречаться на
вашем пути и чем раньше получится с ним подружиться, тем лучше.
Использование библиотек для генерации кода довольно удобно, но не
спишите их использовать везде, так как они тянут за собой ряд
зависимостей в ваш проект, с которыми в последствии придется считаться.
Особенно, если одну из них прекратит поддерживать ее автор, да и
сообществу до нее не будет никакого дела.
397
2. Какие существуют способы конфигурации приложения при его
запуске средствами терминала?
3. Какой класс в Dart позволяет работать с файлами? Перечислите его
основные методы.
4. Какие режимы работы с файлами существуют? В чем их различия?
5. Какой класс лучше использовать при необходимости
прочитать/загрузить файл большого размера?
6. Как проверить существование файла в системе?
7. Каким образом можно получить путь до директории запускаемого
приложения?
8. Как явным образом создать файл? За что отвечает флаг recursive?
9. Какой класс в Dart позволяет работать с директориями? Перечислите
его основные методы.
10. Что такое JSON (JavaScript Object Notation)? Для чего и где он
используется?
11. Что такое сериализация и десериализация?
12. Для чего можно использовать библиотеку json_serializable? Какие у
нее ограничения?
13. Стоит ли всегда использовать библиотеки для генерации кода?
Почему?
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
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. Щелчок мышкой
Обработка щелчка
4. click 3. timer 2. key 1. click
мышки
Обработка нажатия
4. click 3. timer 2. key
клавиш
Обработка
3. timer
прерывания таймера
412
6.1.2. Очереди и цикл событий в Dart
Каждое Dart-приложение имеет один цикл событий, который
осуществляет работу с двумя очередями:
− очередь событий. Содержит как события Dart, так и все внешние
события: ввод/вывод, таймеры, сообщения между экземплярами
Isolate и т. д.;
− очередь микрозадач. Используется для очень коротких внутренних
действий, которые необходимо выполнять асинхронно, сразу после
завершения какого-либо события и перед передачей управления
обратно в очередь событий.
В качестве примера элемента для помещения в очередь микрозадач
может выступать задача удаления ресурса (файла и т. д.), после того как он
был закрыт. Так как этот процесс может занимать какое-то время, то его
разумнее всего выполнить в асинхронном режиме. Тем более, что такие
операции отдаются на откуп операционной системы и не выполняются
средствами языка программирования.
Цикл обработки событий начинает свою работу при выходе потока
управления из функции верхнего уровня main. Первым делом он выполняет
любые микрозадачи в порядке их нахождения в очереди микрозадач.
Следом за этим начинается обработка первого элемента в очереди событий,
то есть он извлекается из очереди и обрабатывается. Затем идет повторение
цикла: сначала выполняются все микрозадачи, а после обрабатывается
следующий элемент в очереди событий. Когда обе очереди пусты и больше
не ожидается событий, приложение закрывается.
Ниже представлена структурная схема алгоритма работы цикла
событий:
413
Запуск
приложения
Инициализация очереди
событий и микрозадач
main()
Нет
Запуск первой в очереди
Очередь микрозач
микрозадачи
пустая?
Да
Нет
Обработка первого
Очередь событий события в очереди
пустая?
Да
Завершение
приложения
414
функция верхнего уровня scheduleMicrotask, либо именованный
конструктор Future.microtask.
Обычно рекомендуется при планировании отложенных задач
использовать класс Future, помещая их в очередь событий. Это помогает
сохранить короткую очередь микрозадач, тем самым уменьшая
вероятность того, что из-за нее будет простаивать очередь событий. В тех
же случаях, когда задача обязательно должна завершиться до того, как
будут обработаны какие-либо элементы из очереди событий, немедленно
вызывайте выполнение функции для ее обработки. Если этого сделать не
получается, помещайте в очередь микрозадач.
В качестве примера работы алгоритма цикла событий реализуем
программу, выводящую в терминал строки. Сначала поместим событие
вывода строки в очередь событий, а потом в очередь задач:
// ex6_1.dart
import 'dart:async';
415
внешних ресурсов. Дополнительно к этому, асинхронное
программирование может снизить нагрузку на процессор, так как
асинхронные операции, такие как ожидание сетевых запросов, не требуют
активного использования процессорного времени, что особенно критично
в системах с ограниченными ресурсами, таких как встраиваемые
устройства или серверы с высокой нагрузкой.
Для того, чтобы ваш код имел возможность выполняться асинхронно,
в заголовке модуля необходимо импортировать библиотеку dart:async.
После этого вам станут доступны такие классы, как Future и Stream. В Dart
также имеются ключевые слова async и await, которые позволяют писать
асинхронный код, внешне мало чем отличающийся от синхронного.
К наиболее частым задачам, где код должен выполняться асинхронно
можно отнести:
− получение данных по сети;
− запись в базу данных;
− чтение данных из файла.
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';
417
int add() => 10 + 15;
void myPrint(int a) => print(a);
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';
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;
}
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
-_- */
420
Я все равно лучший!!! */
421
Для аналогичных целей можно использовать конструктор
Future<T>.value, которому на вход передается значение:
// ex6_10.dart
import 'dart:async';
// 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,
});
@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) */
424
Запрос данных
Завершение main
Гигатонны информации))
Данные получены */
void main() {
print('Запуск main');
makeRequestData();
print('Завершение main');
}
/* Запуск main
Запрос данных
Завершение main
Что-то пошло не так: Exception: Прервалось соединение!!! */
425
Количество вызовов функций с использованием ключевого слова
await в теле функции, помеченной как async, не ограничено. Необходимо
помнить только то, что вызываться они будут последовательно:
// ex6_14.dart
String getBigData() {
return 'Гигатонны информации))';
}
426
[
// без изменения
]
''';
return jsonString;
}
class Book {
// без изменения
}
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)*/
class Book {
// без изменений
}
428
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)*/
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
430
// if (count > 4) {
// return false;
// };
// return true;
// });
print('Завершение main');
}
/* Запуск main
count = 0
Завершение main
count = 1
count = 2
count = 3
count = 4 */
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
432
создании экземпляра класса StreamController, автоматически создается
поток и приемник.
Примером потока может выступать изменение положения курсора
мыши, список простых чисел, получаемые по сети данные и т. д. На каждый
поток имеется возможность подписаться (прослушать поток), путем
задания одной или нескольких callback-функций, которые будут
вызываться при добавлении в него новых данных. Самый простой пример
использования потоков – написание асинхронной генераторной функции:
// ex6_20.dart
Stream<int> myGenerator(int last) async* {
for (var i = 0; i <= last; i++) {
yield i;
}
}
433
iterableStream([1, 2, 3, 4, 5]);
}
/* Начало работы потока
Завершение работы потока
1
…
5 */
434
void createGenerator(int lastValue) async {
var stream = myGenerator(lastValue);
// слушаем поток и выводим получаемые данные в терминал
stream.listen((s) => print(s),
onError: (e) => print(e));
}
controller.add('Привет!!!');
controller.add('И еще раз, Привет!!!');
}
// Listening: Привет!!!
// Listening: И еще раз, Привет!!!
435
того, как накапливается пороговая сумма, начинается приготовление
капучино:
// ex6_25.dart
import 'dart:async';
class CoinAcceptor{
final _addCoin = StreamController<int>();
Stream<int> get dataStream => _addCoin.stream;
class CoffeMachine{
int valueCoins = 0;
CoffeMachine(Stream<int> stream){
stream.listen(addCoin);
}
436
class CoffeMachine{
int valueCoins = 0;
CoffeMachine(Stream<int> stream){
stream.listen((coin){
valueCoins += coin;
if(valueCoins >=30){
print('Готовим капучино!');
}
print('Общее кол-во монет: $valueCoins');
});
}
}
class Coin{
final int value;
Coin(this.value);
}
class CoinAcceptor{
final _addCoin = StreamController<Coin>();
Stream<Coin> get dataStream => _addCoin.stream;
class CoffeMachine{
int valueCoins = 0;
CoffeMachine(Stream<Coin> stream){
stream.listen((coin){
valueCoins += coin.value;
if(valueCoins >= 30){
print('Готовим капучино!');
}
437
print('Общее кол-во монет: $valueCoins');
});
}
}
class Coin{
final int value;
Coin(this.value);
}
class CoinAcceptor{
final _addCoin = StreamController<Coin>();
Stream<Coin> get dataStream => _addCoin.stream;
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('Завершение работы');
});
}
}
439
// ex6_28.dart
import 'dart:async';
import 'dart:math';
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}';
}
}
@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);
}
}
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);
}
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 */
443
которой другие изоляты не имеют доступа. Единственный способ,
благодаря которому изоляты могут работать вместе – обмен сообщениями.
Несмотря на имеющиеся у такого подхода недостатки, у него также
есть ряд преимуществ [19]:
− Выделение памяти и сборка мусора в изолированном объекте не
требуют блокировки.
− Есть только один поток и если он не занят, то память не изменяется.
В качестве первого примера, по классике, реализуем эхо-изолят и
разберем принцип его работы, а уже после погрузимся во все тяжкие
«Уличной магии» :
// ex6_29.dart
import 'dart:isolate';
class IsolatesMessage<T> {
final SendPort sender;
final T message;
IsolatesMessage({
required this.sender,
required this.message,
});
}
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 */
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!"
}
}
446
import 'dart:isolate';
class User {
final UserData data;
final Support support;
User({
required this.data,
required this.support,
});
@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,
});
447
id: json['id'],
email: json['email'],
firstName: json['first_name'],
lastName: json['last_name'],
avatar: json['avatar'],
);
}
class Support {
final String url;
final String text;
Support({
required this.url,
required this.text,
});
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;
}
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!"
}
} */
450
StartMessage(this.sender);
}
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));
}
});
}
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);
}
});
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 – Запуск приложения
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
454
в случае использования 3-го типа сигнатуры main (обычно это порт для
взаимодействия между изолятами).
Изолят стартует сразу при его создании (аргумент по умолчанию
paused = false), но его можно и остановить, используя следующий метод:
isolate.pause(isolate.pauseCapability);
455
− Finalizable;
− Finalizer;
− NativeFinalizer;
− Pointer;
− UserTag;
− MirrorReference.
Для демонстрации создания новой изоляционной группы и как с ней
работать, давайте создадим новый консольный проект
«isolate_spawn_uri» со следующей структурой директории «bin»:
enum MessageType {
start,
stop,
userRequest,
userResponse;
456
'start' => MessageType.start,
'userRequest' => MessageType.userRequest,
'userResponse' => MessageType.userResponse,
'stop' => MessageType.stop,
_ => throw Exception('Unknown message type: $value'),
};
}
}
@override
Map<String, dynamic> toJson() {
return {'type': type.name, 'sender': sender};
457
}
}
@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
};
}
}
@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'userId': id,
};
}
}
458
return UserResponseMessage(User.fromJson(user));
}
}
return UserResponseMessage(null);
}
@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'user': user?.toJson(),
};
}
}
import 'message.dart';
import 'user.dart';
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');
}
});
}
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';
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');
}
});
while (true) {
if (sendPort == null) {
print('Isolate not started');
break;
}
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));
}
}
462
6.4 Async или Isolate?
Вот несколько советов, которые помогут вам определиться, что
использовать – асинхронное программирование или изоляты:
− Если части кода не должны быть прерваны, используйте обычный
синхронный процесс (один метод или несколько методов, которые
вызывают друг друга);
− Если фрагменты кода могут работать независимо, без влияния на
плавность работы приложения (отсутствие зависаний), используйте
Future;
− Если работа может занять некоторое время и потенциально вызывать
задержки в работе графического пользовательского интерфейса
приложения, используйте Isolate.
Также при выборе того, использовать Future или Isolate можно
ориентироваться на среднее время, необходимое для выполнения кода:
− Future, если выполнение метода занимает пару миллисекунд.
− Isolate, если время работы метода может занимать несколько сотен и
более миллисекунд.
void main(){
print(Zone.current); // Instance of '_RootZone'
}
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'] (⊙ _ ⊙ ) */
464
// ex6_33.dart
import 'dart:async';
print(Zone.current[#_secret]);
}
// 2023-11-10 18:02:12.163632 Hello Zone: Instance of '_CustomZone'
// 2023-11-10 18:02:12.169635 凸( •̀_•́ )凸
// null
465
);
}
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
466
Future<void> callPrint() async {
await Future.delayed(
Duration(milliseconds: 300),
() {
print('Hello Zone: ${Zone.current}');
},
);
await Future.error(censored);
}
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'
// 凸( •̀_•́ )凸
// Завершение программы
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:
//Завершение программы
469
R? runZonedGuarded<R>(
R body(),
void onError(
Object error,
StackTrace stack
),
{Map<Object?, Object?>? zoneValues,
ZoneSpecification? zoneSpecification}
)
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'
// Запуск одноразового таймера
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», как представлено на рисунке ниже:
474
final String name;
final int age;
final String education;
User({
required this.id,
required this.name,
required this.age,
required this.education
});
@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write('User{id: $id, name: $name, ');
sb.write('age: $age, education: $education}');
return sb.toString();
}
}
////////////RequestType//////////
enum RequestType {
all,
add,
update,
475
delete,
get;
RequestMessage({required this.type});
476
@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
};
}
}
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(),
};
}
}
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(),
};
}
}
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,
};
}
}
factory GetUserRequest.fromJson(
Map<String, dynamic> json,
) {
return GetUserRequest(json['id']);
}
478
@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'id': id,
};
}
}
////////////ResponseType//////////
enum ResponseType {
all,
success,
get;
////////////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
}
}
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(),
};
}
}
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(),
};
}
}
factory OperationResponse.fromJson(
Map<String, dynamic> json,
) {
return OperationResponse(json['success']);
}
@override
Map<String, dynamic> toJson() {
return {
'type': type.name,
'success': success,
};
}
}
export 'src/request_message.dart';
export 'src/response_message.dart';
export 'src/user.dart';
481
6.6.2. Клиент- серверное приложение на основе TCP
Создайте новое консольное приложение «tcp_server» в той же
директории, где располагается пакет «protocol» и добавьте в него файлы и
директории в соответствии с представленной ниже структурой проекта:
environment:
sdk: '>=3.0.0 <5.0.0'
dependencies:
protocol:
# Настройки пакета
path: ../protocol
dev_dependencies:
lints: ^2.1.0
test: ^1.24.0
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"
}
]
import 'package:protocol/protocol.dart';
@override
String toString() => msg ?? 'DBException';
}
483
class UserDB {
var _users = <User>[];
late File _file;
final String patToDB;
UserDB(this.patToDB);
484
);
}
}
485
// bin – tcp_server.dart
import 'dart:convert';
import 'dart:io';
import 'package:protocol/protocol.dart';
import 'package:tcp_server/tcp_server.dart';
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
});
import 'package:protocol/protocol.dart';
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,
);
}
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 – Пример работы клиент-серверного приложения
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);
}
}
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');
}
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,
);
}
496
}
});
menu((rawDgramSocket, '127.0.0.1', 8083));
}
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].
import 'package:protocol/protocol.dart';
import 'package:http_server/http_server.dart';
500
}
501
response
..headers.contentType = ContentType(
'application',
'json',
)
..write(_encoder.convert(await getAllUsers(userDB)))
..close();
return;
}
}
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();
}
}
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');
}
504
Рисунок 6.11 – Получение данных пользователя с id=3
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,
);
}
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> 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');
}
}
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));
}
}
509
Резюме по разделу
В текущей главе были рассмотрены возможности асинхронного и
параллельного программирования в Dart, что такое цикл событий (Event
Loop) и принципы его работы, а также базовые механизмы для организации
межсетевого взаимодействия.
Future API, а также ключевые слова async и await предоставляют
довольно большой набор возможностей при написании асинхронного кода,
задачи по выполнению которого помещаются в очередь событий и при
необходимости могут быть добавлены в очередь микрозадач. Первым
делом цикл событий выполняет любые микрозадачи в порядке их
нахождения в очереди микрозадач. После того, как очередь микрозадач
становится пустой, начинается обработка первого элемента в очереди
событий. Затем идет повторение этого цикла. Когда же обе очереди
становятся пустыми и больше не ожидается событий, приложение
закрывается.
Isolate позволяет писать параллельный код и так как весь код,
который запускается в изолятах работает со своей областью памяти, то для
обмена данными между ними используются сообщения. В случае
изоляционных групп ими могут выступать экземпляры пользовательских
классов (с некоторыми ограничениями), а когда необходимо переслать
сообщение между разными изоляционными группами, его необходимо
декомпозировать до уровня примитивов, поддерживаемых SendPort.
Если выполнения задачи не занимает больше нескольких микросекунд
– используйте асинхронное программирование, в противном случае
задумайтесь о возможности использования изолятов.
При организации межсетевого взаимодействия не забывайте
закрывать сокеты, если они больше не нужны и корректно выходить из
приложения. Так как при экстренной остановке приложения могут
оставаться открытые соединения, из-за чего не получится его
перезапустить с исходными параметрами портов.
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 сервиса?
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,
десериализуя полученные данные в объекты и записывая их в один файл.
Задания:
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
516
Как отблагодарить автора и зачем это
делать?
Написание любой учебной литературы – труд, требующий огромных
усилий не только в обобщении существующего материала и как его подать
читателю, но и в придумывании различных примеров и заданий на
закрепление пройденных разделов, а также многое другое.
Если у вас имеется желание поддержать мои начинания, то это можно
сделать различными способами, которые приведены ниже. Все деньги,
полученные таким образом, идут на поддержку моей образовательной
деятельности, покупку различного оборудования для докторской
диссертации, оплату публикаций статей в научных журналах и приближают
момент, когда еще раз возьмусь за «перо» для написания новой книги или
актуализации текущей.
https://www.tinkoff.ru/rm/chernyshev.stanislav20/FUUfY1048
ЮMoney: https://yoomoney.ru/to/410011696202148
517
Адрес EVM кошелька Bybit Wallet:
0x3ff35d9325f8c4cbabd6f14ba5e170459420faa8
518
Где посмотреть актуальную версию книги?
Самую свежую версию книги можно скачать на Boosty:
https://boosty.to/madteacher. А по подписке или разовой плате доступны ее
промежуточные варианты перед публикацией.
519
Список используемых источников
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