Авторские права Link to heading

  1. Если код или любое программное обеспечение, полученное в результате, окажутся дефектными, пользователь берет на себя расходы на все необходимые услуги, ремонт или исправление.

  2. В НИКАКОМ СЛУЧАЕ MINDVIEW LLC ИЛИ ЕГО ИЗДАТЕЛЬ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПЕРЕД ЛЮБОЙ СТОРОНОЙ ПО ЛЮБОЙ ЮРИДИЧЕСКОЙ ТЕОРИИ ЗА ПРЯМЫЕ, КОСВЕННЫЕ, ОСОБЫЕ, СЛУЧАЙНЫЕ ИЛИ ПОСЛЕДУЮЩИЕ УБЫТКИ, ВКЛЮЧАЯ УПУЩЕННУЮ ПРИБЫЛЬ, ПРЕРВАНИЕ БИЗНЕСА, УБЫТКИ БИЗНЕС-ИНФОРМАЦИИ ИЛИ ЛЮБЫЕ ДРУГИЕ ДЕНЕЖНЫЕ УБЫТКИ, ИЛИ ЗА ЛИЧНЫЕ ТРАВМЫ, ВОЗНИКАЮЩИЕ В РЕЗУЛЬТАТЕ ИСПОЛЬЗОВАНИЯ ЭТОГО ИСТОЧНИКА КОДА И ЕГО ДОКУМЕНТАЦИИ, ИЛИ В РЕЗУЛЬТАТЕ НЕВОЗМОЖНОСТИ ИСПОЛЬЗОВАТЬ ЛЮБУЮ ПРОГРАММУ, ПОЛУЧЕННУЮ В РЕЗУЛЬТАТЕ, ДАЖЕ ЕСЛИ MINDVIEW LLC ИЛИ ЕГО ИЗДАТЕЛЬ БЫЛИ УВЕДОМЛЕНЫ О ВОЗМОЖНОСТИ ТАКИХ УБЫТКОВ. MINDVIEW LLC ОТКАЗЫВАЕТСЯ ОТ ЛЮБЫХ ГАРАНТИЙ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ, ПОДРАЗУМЕВАЕМЫМИ ГАРАНТИЯМИ ТОРГОВОЙ ПРИГОДНОСТИ И ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ. ИСТОЧНИК КОДА И ДОКУМЕНТАЦИЯ, ПРЕДОСТАВЛЕННЫЕ В НАСТОЯЩЕМ ДОКУМЕНТЕ, ПРЕДОСТАВЛЯЮТСЯ “КАК ЕСТЬ”, БЕЗ ЛЮБЫХ СООПРЕДЕЛЯЮЩИХ УСЛУГ ОТ MINDVIEW LLC, И MINDVIEW LLC НЕ ИМЕЕТ ОБЯЗАТЕЛЬСТВ ПО ПРЕДОСТАВЛЕНИЮ ОБСЛУЖИВАНИЯ, ПОДДЕРЖКИ, ОБНОВЛЕНИЙ, УЛУЧШЕНИЙ ИЛИ МОДИФИКАЦИЙ.

Обратите внимание, что MindView LLC поддерживает веб-сайт, который является единственной точкой распространения электронных копий Исходного кода, где он доступен бесплатно на условиях, указанных выше:
https://github.com/BruceEckel/AtomicKotlinExamples

Если вы думаете, что нашли ошибку в Исходном коде, пожалуйста, отправьте исправление по адресу:
https://github.com/BruceEckel/AtomicKotlinExamples/issues

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

AtomicKotlin (www.AtomicKotlin.com) от Bruce Eckel и Светланы Исаковой, ©2021 MindView LLC

Раздел I: Программирование Link to heading

Основы Link to heading

В программировании есть что-то удивительно притягательное — Винт Серф
Этот раздел предназначен для читателей, которые учатся программировать. Если вы опытный программист, переходите к Резюме 1 и Резюме 2.

Введение Link to heading

Эта книга предназначена как для новичков, так и для опытных программистов. Вы новичок, если у вас нет предварительных знаний в программировании, но “предназначена” потому, что мы даем вам достаточно информации, чтобы вы могли разобраться самостоятельно. Когда вы закончите, у вас будет прочная основа в программировании и в Kotlin. Если вы опытный программист, переходите к Резюме 1 и Резюме 2, а затем продолжайте оттуда. Часть названия книги “Атомные” относится к атомам как к наименьшим неделимым единицам. В этой книге мы стараемся вводить только одну концепцию за главу, поэтому главы не могут быть далее подразделены — таким образом, мы называем их атомами.

Концепции Link to heading

Все языки программирования состоят из функций. Вы применяете эти функции для получения результатов. Kotlin мощный — у него не только богатый набор функций, но вы также можете обычно выразить эти функции множеством способов. Если все это будет представлено слишком быстро, вы можете уйти с мыслью, что Kotlin “слишком сложен”.

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

  1. Маленькие шаги и небольшие победы. Мы сбрасываем тиранию главы. Вместо этого мы представляем каждый маленький шаг как атомарную концепцию или просто атом, который выглядит как маленькая глава. Мы стараемся представить только одну новую концепцию на атом. Типичный атом содержит один или несколько небольших, исполняемых фрагментов кода и вывод, который он производит.
  2. Без ссылок вперед. Насколько это возможно, мы избегаем фраз вроде: “Эти функции объясняются в более позднем атоме.”
  3. Без ссылок на другие языки программирования. Мы делаем это только когда это необходимо. Аналогия с функцией в языке, который вы не понимаете, не полезна.
  4. Показывайте, а не рассказывайте. Вместо того чтобы устно описывать функцию, мы предпочитаем примеры и вывод. Лучше увидеть функцию в коде.
  5. Практика перед теорией. Мы стараемся сначала показать механику языка, а затем объяснить, почему эти функции существуют. Это противоположно “традиционному” обучению, но часто кажется, что это работает лучше.

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

Где индекс? Link to heading

Эта книга написана в Markdown и издана с помощью Leanpub. К сожалению, ни Markdown, ни Leanpub не поддерживают индексы. Однако, создавая самые маленькие возможные главы (атомы), состоящие из одной темы в каждом атоме, оглавление выполняет функцию своего рода индекса. Кроме того, версии электронной книги позволяют осуществлять поиск по всему тексту.

Ссылки на другие разделы Link to heading

Ссылка на атом в книге выглядит так: Введение, что в данном случае относится к текущему атому. В различных форматах электронных книг это создает гиперссылку на этот атом.

Форматирование Link to heading

В этой книге: • Курсив вводит новый термин или концепцию, а иногда подчеркивает идею.
Atomic Kotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Введение 8
• Шрифт фиксированной ширины указывает на ключевые слова программы, идентификаторы и имена файлов.
Примеры кода также представлены в этом шрифте и окрашены в цвет в электронных версиях книги.
• В прозе мы следуем за именем функции пустыми скобками, как в func(). Это напоминает читателю, что он смотрит на функцию.
• Чтобы сделать электронную книгу удобной для чтения на всех устройствах и позволить пользователю увеличивать размер шрифта, мы ограничиваем ширину наших кодовых списков 47 символами. Иногда это требует компромисса, но мы считаем, что результаты того стоят. Для достижения этих ширин мы можем убрать пробелы, которые в противном случае могли бы быть включены во многие стили форматирования — в частности, мы используем отступы в два пробела вместо стандартных четырех пробелов.

“Пауза” Link to heading

Иногда вы будете видеть: • - Это указывает на паузу или своего рода небольшой сброс. В этой книге это часто встречается перед кратким резюме текущего подраздела, но там, где заголовок “Резюме” был бы избыточным. Некоторые книги используют подобный механизм, чтобы указать, что идея завершена, и мы начинаем что-то новое, но это все еще в рамках одной темы и недостаточно велико, чтобы оправдать подраздел или новый раздел. Markdown в Leanpub довольно ограничен, и использование одной или нескольких точек (моя первоначальная попытка) невозможно. Использование двух дефисов в markdown приводит к появлению точки и дефиса. Возможно, есть лучший способ сделать это, но я его не нашел, поэтому остановился на этом.

Пример книги Link to heading

Мы предоставляем бесплатный образец электронной книги на AtomicKotlin.com. Образец включает в себя первые два раздела целиком, а также несколько последующих атомов. Таким образом, вы можете попробовать книгу и решить, подходит ли она вам.

Полная книга доступна для покупки как в печатном виде, так и в формате eBook. Если вам понравилось то, что мы сделали в бесплатном образце, пожалуйста, поддержите нас и помогите продолжить нашу работу над AtomicKotlin (www.AtomicKotlin.com) авторов Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC, оплатив то, что вы используете. Мы надеемся, что книга будет полезна, и мы ценим вашу поддержку.

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

Упражнения и решения Link to heading

Большинство атомов в Atomic Kotlin сопровождаются небольшим количеством упражнений. Чтобы улучшить ваше понимание, мы рекомендуем решать упражнения сразу после прочтения атома. Большинство упражнений проверяются автоматически с помощью интегрированной среды разработки (IDE) JetBrains IntelliJ IDEA, так что вы можете видеть свой прогресс и получать подсказки, если застрянете.

Вы можете найти следующие ссылки на http://AtomicKotlin.com/exercises/4. Чтобы решить упражнения, установите IntelliJ IDEA с плагином Edu Tools, следуя этим инструкциям:

  1. Установите IntelliJ IDEA и плагин Edu Tools5.
  2. Откройте курс Atomic Kotlin и решите упражнения6.

В курсе вы найдете решения для всех упражнений. Если вы застряли на каком-то упражнении, проверьте подсказки или попробуйте заглянуть в решение. Мы все же рекомендуем реализовать его самостоятельно.

Если у вас возникли проблемы с настройкой и запуском курса, пожалуйста, прочитайте Руководство по устранению неполадок7. Если это не решит вашу проблему, пожалуйста, свяжитесь с командой поддержки, как указано в руководстве.

4 http://AtomicKotlin.com/exercises/
5 https://www.jetbrains.com/help/education/install-edutools-plugin.html
6 https://www.jetbrains.com/help/education/learner-start-guide.html?section=Atomic%20Kotlin
7 https://www.jetbrains.com/help/education/troubleshooting-guide.html

Atomic Kotlin (www.AtomicKotlin.com) Бруса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Введение 10

Если вы обнаружите ошибку в содержании курса (например, тест для задания выдает неправильный результат), пожалуйста, используйте наш трекер проблем, чтобы сообщить о проблеме с этой предзаполненной формой8. Обратите внимание, что вам нужно будет войти в YouTrack. Мы ценим ваше время, помогая улучшить курс!

Семинары Link to heading

Вы можете найти информацию о живых семинарах и других учебных инструментах на сайте AtomicKotlin.com.

Конференции Link to heading

Брюс организует конференции Open-Spaces, такие как Зимний Технический Форум. Присоединяйтесь к рассылке на AtomicKotlin.com, чтобы быть в курсе наших мероприятий и мест, где мы выступаем.

Поддержите нас Link to heading

Это был большой проект. На создание этой книги и сопутствующих материалов потребовалось время и усилия. Если вам понравилась эта книга и вы хотите увидеть больше подобных материалов, пожалуйста, поддержите нас: • Пишите в блогах, твитах и т.д. и расскажите своим друзьям. Это grassroots-кампания, поэтому все, что вы сделаете, поможет. • Приобретите электронную или печатную версию этой книги на AtomicKotlin.com. • Посетите AtomicKotlin.com для получения информации о других поддерживающих продуктах или мероприятиях.

О нас Link to heading

Брюс Эккель — автор многократно награждаемой книги Thinking in Java и Thinking in C++, а также ряда других книг по программированию, включая Atomic Kotlin (www.AtomicKotlin.com) в соавторстве с Светланой Исаковой, ©2021 MindView LLC, и Atomic Scala. Он провел сотни презентаций по всему миру и организует альтернативные конференции и мероприятия, такие как Winter Tech Forum и ретриты для разработчиков. Брюс имеет степень бакалавра в области прикладной физики и магистра в области компьютерной инженерии. Его блог доступен по адресу www.BruceEckel.com, а его консалтинговый, обучающий и конференционный бизнес называется Mindview LLC.

Светлана Исакова начала свою карьеру в команде разработчиков компилятора Kotlin и сейчас является адвокатом разработчиков в JetBrains. Она преподает Kotlin и выступает на конференциях по всему миру, а также является соавтором книги Kotlin in Action.

Благодарности Link to heading

• Команда по разработке языка Kotlin и contributors.
• Разработчики Leanpub, которые значительно упростили процесс публикации этой книги.
• Джеймс Уорд за конвертацию сборки Gradle в Kotlin и за его общую крутость.

Посвящения Link to heading

Моему любимому отцу, Э. Уэйну Эккелю. 1 апреля 1924 года — 23 ноября 2016 года. Вы первыми научили меня машинам, инструментам и дизайну.
Моему отцу, Сергею Львовичу Исакову, который ушел из жизни так рано и которого мы всегда будем помнить.

О обложке Link to heading

Дэниел Уилл-Харрис 14 разработал обложку на основе логотипа Kotlin.
10 http://www.atomicscala.com/
11 http://www.WinterTechForum.com
12 http://www.BruceEckel.com
13 https://www.mindviewllc.com/
14 http://www.will-harris.com
Atomic Kotlin (www.AtomicKotlin.com) от Брюса Эккела и Светланы Исаковой, ©2021 MindView LLC

Почему Kotlin? Link to heading

Программы должны быть написаны так, чтобы их могли читать люди, и лишь случайно — для выполнения машинами. — Гарольд Абельсон, соавтор книги “Структура и интерпретация компьютерных программ”.

Этот раздел является обзором исторического развития языков программирования, чтобы вы могли понять, где находится Kotlin и почему вам может быть интересно его изучение. Мы вводим некоторые темы, которые, если вы новичок, могут показаться слишком сложными на данный момент. Не стесняйтесь пропустить этот раздел и вернуться к нему после того, как прочитаете больше книги.

Проектирование языков программирования — это эволюционный путь от удовлетворения потребностей машины к удовлетворению потребностей программиста.

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

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

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

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

Почему Kotlin?

Первоначальные программисты работали непосредственно с числами, представляющими машинные инструкции процессора. Этот подход приводил к многочисленным ошибкам, и был создан ассемблерный язык, чтобы заменить числа мнемоническими опкодами — словами, которые программисты могли легче запомнить и читать, наряду с другими полезными инструментами. Однако все еще существовало соответствие один к одному между инструкциями ассемблера и машинными инструкциями, и программистам приходилось писать каждую строку ассемблерного кода. Кроме того, каждый процессор использовал свой собственный уникальный ассемблерный язык.

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

Компиляторы и Интерпретаторы Link to heading

Инструкции интерпретируемого языка выполняются непосредственно программой, называемой интерпретатором. Kotlin компилируется, а не интерпретируется. Исходный код компилируемого языка преобразуется в другое представление, которое выполняется как собственная программа, либо непосредственно на аппаратном процессоре, либо на виртуальной машине, которая эмулирует процессор:
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Почему Kotlin? 14
Языки, такие как C, C++, Go и Rust, компилируются в машинный код, который выполняется непосредственно на базовом аппаратном центральном процессоре (ЦП). Языки, такие как Java и Kotlin, компилируются в байт-код, который является промежуточным уровнем формата, который не выполняется непосредственно на аппаратном ЦП, а вместо этого на виртуальной машине, которая является программой, выполняющей инструкции байт-кода. Программы, созданные с помощью версии Kotlin для JVM, выполняются на Java Virtual Machine (JVM).

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

На этапе компиляции код проверяется компилятором на наличие ошибок компиляции. (IntelliJ IDEA и другие среды разработки подчеркивают эти ошибки, когда вы вводите код, так что вы можете быстро обнаружить и исправить любые проблемы). Если ошибок компиляции нет, исходный код будет скомпилирован в байт-код.

Ошибка времени выполнения не может быть обнаружена на этапе компиляции, поэтому она возникает только при запуске программы. Обычно ошибки времени выполнения труднее обнаружить и их исправление обходится дороже. Языки с статической типизацией, такие как Kotlin, обнаруживают как можно больше ошибок на этапе компиляции, в то время как динамические языки выполняют свои проверки безопасности во время выполнения (некоторые динамические языки не выполняют столько проверок безопасности, сколько могли бы).

Языки, оказавшие влияние на Kotlin Link to heading

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

FORTRAN: FORmula TRANslation (1957) Link to heading

Созданный для использования учеными и инженерами, цель Fortran заключалась в том, чтобы упростить кодирование уравнений. Тщательно настроенные и протестированные библиотеки Fortran все еще используются сегодня, но они обычно «обернуты», чтобы сделать их вызываемыми из других языков.

LISP: LISt Processor (1958) Link to heading

Вместо того чтобы быть специфичным для приложений, LISP воплотил в себе основные концепции программирования; это был язык компьютерных ученых и первый язык функционального программирования (вы узнаете о функциональном программировании в этой книге). Компромиссом за его мощь и гибкость была эффективность: LISP обычно был слишком дорог для работы на ранних машинах, и только в последние десятилетия машины стали достаточно быстрыми, чтобы возродить использование LISP. Например, редактор GNU Emacs полностью написан на LISP и может быть расширен с помощью LISP.

ALGOL: ALGOrithmic Language (1958) Link to heading

Можно с уверенностью сказать, что это самый влиятельный язык 1950-х годов, так как он ввел синтаксис, который сохранился во многих последующих языках. Например, C и его производные являются “языками, подобными ALGOL”.

COBOL: Общий язык, ориентированный на бизнес (1959) Link to heading

Разработан для обработки бизнес-, финансовых и административных данных. Он имеет синтаксис, похожий на английский, и был предназначен для самодокументирования и высокой читаемости. Хотя эта цель в целом не была достигнута — COBOL известен ошибками, вызванными неправильно поставленной точкой — Министерство обороны США заставило широко внедрить его на мейнфреймах, и системы по-прежнему работают (и требуют обслуживания) сегодня.

BASIC: Универсальный Символический Язык Инструкций для Начинающих Link to heading

Код (1964) Link to heading

BASIC был одной из ранних попыток сделать программирование доступным. Хотя он имел большой успех, его возможности и синтаксис были ограничены, поэтому он был лишь частично полезен для людей, которым нужно было изучать более сложные языки. Это в основном интерпретируемый язык, что означает, что для его выполнения вам нужен оригинальный код программы. Несмотря на это, многие полезные программы были написаны на BASIC, в частности, как AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC.
Почему Kotlin? 16
как скриптовый язык для продуктов “Office” от Microsoft. BASIC можно даже считать первым “открытым” языком программирования, так как люди создавали множество его вариаций.

Simula 67, оригинальный объектно-ориентированный язык Link to heading

(1967) Link to heading

Симуляция обычно включает в себя множество «объектов», взаимодействующих друг с другом. Разные объекты имеют разные характеристики и поведения. Языки, существовавшие в то время, были неудобны для использования в симуляциях, поэтому был разработан язык Simula (еще один «язык, похожий на ALGOL»), чтобы обеспечить прямую поддержку для создания симуляционных объектов. Оказалось, что эти идеи также полезны для общего программирования, и это стало началом языков, ориентированных на объекты (ОО).

Pascal (1970) Link to heading

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

Реализация Pascal, Turbo Pascal от Borland, изначально работала на машинах CP/M, а затем перешла на ранние MS-DOS (предшественник Windows), позже эволюционировав в язык Delphi для Windows. Помещая все в память, Turbo Pascal компилировал с молниеносной скоростью на очень слабых машинах, значительно улучшая опыт программирования. Его создатель, Андерс Хейлсберг, позже разработал как C#, так и TypeScript.

Никлаус Вирт, изобретатель Pascal, создал последующие языки: Modula, Modula-2 и Oberon. Как следует из названия, Modula сосредоточилась на разделении программ на модули для лучшей организации и более быстрой компиляции. Большинство современных языков поддерживают отдельную компиляцию и какую-то форму модульной системы.

Atomic Kotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Почему Kotlin? 17

C (1972) Link to heading

Несмотря на растущее количество языков более высокого уровня, программисты все еще писали на ассемблере. Это часто называют системным программированием, поскольку оно выполняется на уровне операционной системы, но также включает в себя встроенное программирование для специализированных физических устройств. Это не только трудоемко и дорого (Брюс начал свою карьеру, пиша на ассемблере для встроенных систем), но и не переносимо — ассемблерный код может выполняться только на процессоре, для которого он написан. C был разработан как «язык высокого уровня, близкий к ассемблеру», который все же достаточно близок к аппаратному обеспечению, чтобы вам редко приходилось писать на ассемблере. Более того, программа на C может выполняться на любом процессоре с компилятором C. C отделил программу от процессора, что решило огромную и дорогостоящую проблему. В результате бывшие программисты на ассемблере стали значительно более продуктивными на C. C оказался настолько эффективным, что современные языки (в частности, Go и Rust) все еще пытаются его вытеснить в области системного программирования.

Smalltalk (1972) Link to heading

Созданный с самого начала как чисто объектно-ориентированный, Smalltalk значительно продвинул теорию объектно-ориентированного программирования и языков, став платформой для экспериментов и демонстрации быстрого развития приложений. Однако он был создан в то время, когда языки все еще были собственническими, и стоимость системы Smalltalk могла достигать тысяч долларов. Он был интерпретируемым, поэтому для запуска программ требовалась среда Smalltalk. Реализации Smalltalk с открытым исходным кодом не появились до того, как мир программирования продвинулся дальше. Программисты Smalltalk внесли значительный вклад в понимание, которое принесло пользу более поздним объектно-ориентированным языкам, таким как C++ и Java.

C++: Лучший C с объектами (1983) Link to heading

Бьёрн Страуструп создал C++, потому что хотел улучшить C и добавить поддержку объектно-ориентированных конструкций, с которыми он столкнулся, используя Simula-67. Брюс был членом Комитета по стандартам C++ в течение первых восьми лет и написал три книги по C++, включая “Мысли на C++”.

Обратная совместимость с C была основополагающим принципом дизайна C++, поэтому код на C может компилироваться в C++ практически без изменений. Это обеспечивало легкий путь миграции — программисты могли продолжать писать на C, получать преимущества C++ и постепенно экспериментировать с его возможностями, оставаясь при этом продуктивными. Большинство критики C++ можно отследить до ограничения обратной совместимости с C.

Одной из проблем C была проблема управления памятью. Программист сначала должен выделить память, затем выполнить операцию с использованием этой памяти, а затем освободить память. Забывание освободить память называется утечкой памяти и может привести к исчерпанию доступной памяти и сбою процесса. Начальная версия C++ внесла некоторые новшества в этой области, включая конструкторы для обеспечения правильной инициализации. Поздние версии языка значительно улучшили управление памятью.

Python: Дружелюбный и Гибкий (1990) Link to heading

Дизайнер Python, Гвидо ван Россум, создал язык, основываясь на своем вдохновении “программирования для всех”. Его забота о сообществе Python сделала его известным как самое дружелюбное и поддерживающее сообщество в мире программирования. Python был одним из первых языков с открытым исходным кодом, что привело к его реализации практически на каждой платформе, включая встроенные системы и машинное обучение. Его динамичность и простота в использовании делают его идеальным для автоматизации небольших, повторяющихся задач, но его возможности также поддерживают создание больших, сложных программ. Python — это настоящий “язык с низов”; у него никогда не было компании, продвигающей его, а отношение его поклонников заключалось в том, чтобы никогда не навязывать язык, а просто помогать всем, кто хочет его изучить. Язык продолжает постоянно улучшаться, и в последние годы его популярность стремительно возросла.

Python, возможно, был первым мейнстримным языком, который объединил функциональное и объектно-ориентированное программирование. Он предшествовал Java с автоматическим управлением памятью с использованием сборки мусора (вам обычно не нужно самостоятельно выделять или освобождать память) и возможностью запускать программы на нескольких платформах.

Haskell: Чистое функциональное программирование (1990) Link to heading

Вдохновленный Miranda (1985), проприетарным языком, Haskell был создан как открытый стандарт для исследований в области чистого функционального программирования, хотя он также использовался для продуктов. Синтаксис и идеи из Haskell оказали влияние на ряд последующих языков, включая Kotlin.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Почему Kotlin? 19

Java: Виртуальные машины и сборка мусора (1995) Link to heading

Джеймс Гослинг и его команда получили задачу написать код для телевизионной приставки. Они решили, что им не нравится C++, и вместо создания приставки создали язык Java. Компания Sun Microsystems сделала огромный маркетинговый рывок в поддержку этого бесплатного языка (что на тот момент было новой идеей), чтобы попытаться доминировать на развивающемся интернет-ландшафте.

Это воспринимаемое окно возможностей для доминирования в Интернете оказало большое давление на дизайн языка Java, что привело к значительному количеству недостатков (книга “Thinking in Java” освещает эти недостатки, чтобы читатели были готовы с ними справляться). Брайан Гоетц из Oracle, текущий ведущий разработчик Java, сделал замечательные и удивительные улучшения в Java, несмотря на унаследованные ограничения. Хотя Java была невероятно успешной, важной целью дизайна Kotlin является исправление недостатков Java, чтобы программисты могли быть более продуктивными.

Успех Java был обусловлен двумя инновационными функциями: виртуальной машиной и сборкой мусора. Эти функции были доступны в других языках — например, LISP, Smalltalk и Python имеют сборку мусора, а UCSD Pascal работал на виртуальной машине — но никогда не считались практичными для мейнстримовых языков. Java изменила это, и, делая это, сделала программистов значительно более продуктивными.

Виртуальная машина является промежуточным слоем между языком и аппаратным обеспечением. Языку не нужно генерировать машинный код для конкретного процессора; ему нужно только генерировать промежуточный язык (байт-код), который работает на виртуальной машине. Виртуальные машины требуют вычислительной мощности и, до появления Java, считались непрактичными. Java Virtual Machine (JVM) дала начало слогану Java “напиши один раз, запускай везде”. Кроме того, другие языки могут быть более легко разработаны с нацеливанием на JVM; примеры включают Groovy, язык сценариев, похожий на Java, и Clojure, версию LISP.

Сборка мусора решает проблему забывания о высвобождении памяти или когда трудно понять, когда участок памяти больше не используется. Проекты значительно задерживались или даже отменялись из-за утечек памяти. Хотя сборка мусора присутствует в некоторых предыдущих языках, считалось, что она создает неприемлемое количество накладных расходов, пока Java не продемонстрировала ее практичность.

AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Почему Kotlin? 20

JavaScript: Java только в названии (1995) Link to heading

Оригинальный веб-браузер просто копировал и отображал страницы с веб-сервера. Веб-браузеры начали proliferировать, становясь новой программной платформой, которая нуждалась в поддержке языка. Java хотела стать этим языком, но была слишком неудобной для этой задачи. JavaScript начинался как LiveScript и был встроен в NetScape Navigator, один из первых веб-браузеров. Переименование его в JavaScript было маркетинговым ходом от NetScape, так как язык имел лишь смутное сходство с Java.

С ростом популярности веба JavaScript стал чрезвычайно важным. Однако поведение JavaScript было настолько непредсказуемым, что Дуглас Крокфорд написал книгу с ироничным названием “JavaScript: Хорошие части”, в которой он продемонстрировал все проблемы языка, чтобы программисты могли их избежать. Последующие улучшения, внесенные комитетом ECMAScript, сделали JavaScript неузнаваемым для оригинального программиста JavaScript. Теперь его считают стабильным и зрелым языком.

WebAssembly (WASM) был разработан из JavaScript как своего рода байт-код для веб-браузеров. Он часто работает гораздо быстрее, чем JavaScript, и может быть сгенерирован другими языками. На момент написания команда Kotlin работает над добавлением WASM в качестве целевой платформы.

C#: Java для .NET (2000) Link to heading

C# был разработан для предоставления некоторых важных возможностей Java на платформе .NET (Windows), при этом освобождая разработчиков от необходимости следовать языку Java. Результатом стали многочисленные улучшения по сравнению с Java. Например, C# разработал концепцию расширяющих функций, которые активно используются в Kotlin. C# также стал значительно более функциональным, чем Java. Многие функции C# явно повлияли на дизайн Kotlin.

Scala: SCALAble (2003) Link to heading

Мартин Одерски создал Scala для работы на виртуальной машине Java: чтобы использовать уже проделанную работу над JVM, взаимодействовать с Java-программами и, возможно, с идеей о том, что она может вытеснить Java. Как исследователь, Одерски и его команда использовали Scala как платформу для экспериментов с языковыми особенностями, особенно с теми, которые не были включены в Java.
AtomicKotlin (www.AtomicKotlin.com) Бруса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Почему Kotlin? 21
Эти эксперименты были познавательными, и ряд из них нашел свое отражение в Kotlin, обычно в модифицированной форме. Например, возможность переопределять операторы, такие как +, для использования в специальных случаях называется перегрузкой операторов. Это было включено в C++, но не в Java. Scala добавила перегрузку операторов, но также позволяет изобретать новые операторы, комбинируя любые последовательности символов. Это часто приводит к запутанному коду. Ограниченная форма перегрузки операторов включена в Kotlin, но вы можете перегружать только те операторы, которые уже существуют.
Scala также является объектно-функциональным гибридом, как Python, но с акцентом на чистые функции и строгие объекты. Это вдохновило выбор Kotlin также быть объектно-функциональным гибридом.
Как и Scala, Kotlin работает на JVM, но взаимодействует с Java гораздо проще, чем Scala (см. Приложение B). Кроме того, Kotlin нацелен на JavaScript, операционную систему Android и генерирует нативный код для других платформ.
AtomicKotlin развился из идей и материалов в AtomicScala15.

Groovy: Динамический язык JVM (2007) Link to heading

Динамические языки привлекательны, потому что они более интерактивны и лаконичны, чем статические языки. Было сделано множество попыток создать более динамичный опыт программирования на JVM, включая Jython (Python) и Clojure (диалект Lisp). Groovy стал первым, кто достиг широкого признания.

На первый взгляд, Groovy кажется упрощенной версией Java, обеспечивая более приятный опыт программирования. Большинство Java-кода будет работать без изменений в Groovy, поэтому программисты на Java могут быстро стать продуктивными, а затем изучить более сложные функции, которые обеспечивают заметные улучшения в программировании по сравнению с Java.

Операторы Kotlin ?. и ?:, которые решают проблему пустоты, впервые появились в Groovy.

Существует множество функций Groovy, которые можно узнать в Kotlin. Некоторые из этих функций также встречаются в других языках, что, вероятно, способствовало их включению в Kotlin.
15 http://www.AtomicScala.com
AtomicKotlin (www.AtomicKotlin.com) Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Почему Kotlin? 22

Почему Kotlin? (Введён в 2011 году, Версия 1.0: Link to heading

2016) Link to heading

Точно так же, как C++ изначально задумывался как “лучший C”, Kotlin изначально был ориентирован на то, чтобы стать “лучшим Java”. С тех пор он значительно вышел за рамки этой цели. Kotlin прагматично выбирает только самые успешные и полезные функции из других языков программирования — после того, как эти функции были протестированы в реальных условиях и доказали свою ценность.

Таким образом, если вы приходите из другого языка, вы можете узнать некоторые функции этого языка в Kotlin. Это сделано намеренно: Kotlin максимизирует продуктивность, используя проверенные концепции.

Читаемость Link to heading

Читаемость является основной целью в дизайне языка. Синтаксис Kotlin лаконичен — он не требует церемоний в большинстве сценариев, но при этом может выражать сложные идеи.

Инструменты Link to heading

Kotlin разработан компанией JetBrains, которая специализируется на инструментах для разработчиков. Он имеет первоклассную поддержку инструментов, и многие языковые функции были разработаны с учетом инструментов.

Мультипарадигменный Link to heading

Kotlin поддерживает несколько парадигм программирования, которые мягко вводятся в этой книге: • Императивное программирование • Функциональное программирование • Объектно-ориентированное программирование
AtomicKotlin (www.AtomicKotlin.com) Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Почему Kotlin? 23

Мультиплатформенность Link to heading

Исходный код на Kotlin может быть скомпилирован для различных целевых платформ: • JVM. Исходный код компилируется в байт-код JVM (файлы .class), который затем может быть выполнен на любой Java Virtual Machine (JVM). • Android. У Android есть собственная среда выполнения, называемая ART (предшественник назывался Dalvik). Исходный код Kotlin компилируется в формат Dalvik Executable (файлы .dex). • JavaScript, для выполнения в веб-браузере. • Нативные бинарные файлы путем генерации машинного кода для конкретных платформ и ЦПУ.

Эта книга сосредоточена на самом языке, используя JVM в качестве единственной целевой платформы. Как только вы освоите язык, вы сможете применять Kotlin для различных приложений и целевых платформ.

Две особенности Kotlin Link to heading

Этот атом не предполагает, что вы программист, что затрудняет объяснение большинства преимуществ Kotlin по сравнению с альтернативами. Тем не менее, есть две темы, которые имеют большое значение и могут быть объяснены на этом раннем этапе: совместимость с Java и вопрос указания «отсутствия значения».

Легкая совместимость с Java Link to heading

Чтобы стать «лучшей C», C++ должен быть обратно совместим с синтаксисом C, но Kotlin не обязан быть обратно совместим с синтаксисом Java — ему нужно просто работать с JVM. Это освобождает дизайнеров Kotlin для создания гораздо более чистого и мощного синтаксиса, без визуального шума и усложнений, которые загромождают Java.

Чтобы Kotlin стал «лучшей Java», опыт его использования должен быть приятным и без трений, поэтому Kotlin обеспечивает легкую интеграцию с существующими Java-проектами. Вы можете написать небольшой фрагмент функциональности на Kotlin и вставить его среди вашего существующего Java-кода. Java-код даже не знает, что Kotlin-код там присутствует — он просто выглядит как еще один Java-код.

Компании часто исследуют новый язык, создавая отдельную программу на этом языке. В идеале эта программа должна быть полезной, но не критически важной, чтобы в случае неудачи проекта его можно было завершить с минимальными потерями. Не каждая компания хочет тратить ресурсы на такого рода эксперименты. Поскольку Kotlin бесшовно интегрируется в существующую Java-систему (и получает выгоду от тестов этой системы), его становится очень дешево, а иногда и бесплатно, попробовать, чтобы увидеть, подходит ли он.

Кроме того, JetBrains, компания, создающая Kotlin, предоставляет IntelliJ IDEA в «Community» (бесплатной) версии, которая включает поддержку как Java, так и Kotlin, а также возможность легко интегрировать их. У него даже есть инструмент, который берет Java-код и (в основном) переписывает его на Kotlin.

Приложение B охватывает совместимость с Java.

Представление пустоты Link to heading

Особенно полезной особенностью Kotlin является его решение сложной проблемы программирования.
Что вы делаете, когда кто-то передает вам словарь и просит найти слово, которого не существует? Вы можете гарантировать результаты, придумав определения для неизвестных слов. Более полезный подход — просто сказать: «Для этого слова нет определения». Это демонстрирует значительную проблему в программировании: как указать «нет значения» для участка памяти, который не инициализирован, или для результата операции?
Ссылка null была изобретена в 1965 году для ALGOL Тони Хоаром, который позже назвал это «моей миллиардной ошибкой». Одна из проблем заключалась в том, что это было слишком просто — иногда сказать, что комната пуста, недостаточно. Вам может понадобиться знать, например, почему она пуста. Это приводит ко второй проблеме: реализации. Ради эффективности это обычно было просто специальное значение, которое могло поместиться в небольшом объеме памяти, и что может быть лучше, чем память, уже выделенная для этой информации?
Исходный язык C не инициализировал память автоматически, что вызывало множество проблем. C++ улучшил ситуацию, устанавливая вновь выделенную память в нули. Таким образом, если числовое значение не инициализировано, оно просто является числовым нулем.
Atomic Kotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Почему Kotlin? 25
Это не казалось таким уж плохим, но это позволяло неинициализированным значениям тихо ускользать из-под контроля (новые компиляторы C и C++ часто предупреждают вас об этом). Хуже того, если участок памяти был указателем — используемым для указания («указывать на») другой участок памяти — нулевой указатель указывал бы на нулевую позицию в памяти, что почти наверняка не то, что вам нужно.
Java предотвращает доступ к неинициализированным значениям, сообщая о таких ошибках во время выполнения. Хотя это и обнаруживает неинициализированные значения, это не решает проблему, потому что единственный способ проверить, что ваша программа не упадет, — это запустить ее. В коде Java существует множество таких ошибок, и программисты тратят огромное количество времени на их поиск.
Kotlin решает эту проблему, предотвращая операции, которые могут вызвать ошибки null на этапе компиляции, до того как программа сможет запуститься. Это самая отмечаемая особенность программистов Java, переходящих на Kotlin. Эта одна функция может минимизировать или устранить ошибки null в Java, сэкономив вашему проекту значительное количество времени и денег.

Изобилие преимуществ Link to heading

Две функции, которые мы смогли объяснить здесь (без необходимости в дополнительных знаниях программирования), существенно влияют на то, являетесь ли вы программистом на Java или нет. Если Kotlin — ваш первый язык, и вы попадаете в проект, который требует больше программистов, гораздо проще привлечь одного из многих существующих программистов на Java к работе с Kotlin.

У Kotlin есть много других преимуществ, которые мы не можем объяснить, пока вы не узнаете больше о программировании. Для этого и предназначена остальная часть книги.
• -
Языки часто выбираются по увлечению, а не по разуму… Я пытаюсь сделать так, чтобы Kotlin был языком, который любят по причине. — Андрей Бреслав, ведущий дизайнер языка Kotlin.
Atomic Kotlin (www.AtomicKotlin.com) Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Привет, мир! Link to heading

“Привет, мир!” — это программа, которая обычно используется для демонстрации базового синтаксиса языков программирования. Мы разрабатываем эту программу в несколько этапов, чтобы вы поняли ее части.

Сначала давайте рассмотрим пустую программу, которая ничего не делает: // HelloWorld/EmptyProgram.kt fun main() { // Код программы здесь … }

Пример начинается с комментария, который является пояснительным текстом и игнорируется Kotlin. // (двойной косой слэш) начинает комментарий, который продолжается до конца текущей строки: // Однострочный комментарий

Kotlin игнорирует // и все, что после него, до конца строки. На следующей строке он снова начинает обращать внимание. Первая строка каждого примера в этой книге — это комментарий, начинающийся с названия подкаталога, содержащего файл исходного кода (в данном случае HelloWorld), за которым следует имя файла: EmptyProgram.kt. Пример подкаталога для каждого атома соответствует имени этого атома.

Ключевые слова зарезервированы языком и имеют специальное значение. Ключевое слово fun является сокращением от function. Функция — это коллекция кода, которую можно выполнить, используя имя этой функции (мы уделяем много времени функциям на протяжении всей книги). Имя функции следует за ключевым словом fun, так что в данном случае это main() (в тексте мы следуем за именем функции с помощью скобок).

main() на самом деле является специальным именем для функции; оно указывает на “точку входа” для программы на Kotlin. Программа на Kotlin может иметь много функций с разными именами, но main() — это та, которая автоматически вызывается при выполнении программы.

Список параметров следует за именем функции и заключен в скобки. Здесь мы не передаем ничего в main(), поэтому список параметров пуст.

Тело функции появляется после списка параметров. Оно начинается с открывающей фигурной скобки ({) и заканчивается закрывающей фигурной скобкой (}). Тело функции содержит операторы и выражения. Оператор производит эффект, а выражение дает результат.

EmptyProgram.kt не содержит операторов или выражений в теле, только комментарий.

Давайте сделаем так, чтобы программа выводила “Привет, мир!”, добавив строку в тело main(): // HelloWorld/HelloWorld.kt fun main() { println(“Привет, мир!”) } /* Вывод: Привет, мир! */

Строка, которая выводит приветствие, начинается с println(). Как и main(), println() — это функция. Эта строка вызывает функцию, которая выполняет свое тело. Вы указываете имя функции, за которым следуют скобки, содержащие один или несколько параметров. В этой книге, когда мы ссылаемся на функцию в тексте, мы добавляем скобки после имени в качестве напоминания о том, что это функция. Здесь мы говорим println().

println() принимает один параметр, который является строкой. Вы определяете строку, помещая символы в кавычки.

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

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

Для некоторых примеров в книге мы показываем вывод в конце списка, внутри многострочного комментария. Многострочный комментарий начинается с /* (косая черта, за которой следует звездочка) и продолжается — включая переносы строк (которые мы называем новыми строками) — до тех пор, пока / (звездочка, за которой следует косая черта) не завершит комментарий: / Многострочный комментарий Не заботится о новых строках */

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

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

Упражнения и решения можно найти на www.AtomicKotlin.com.

var & val Link to heading

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

  • var, сокращение от variable, что означает, что вы можете переназначить его содержимое.
  • val, сокращение от value, что означает, что вы можете только инициализировать его; вы не можете переназначить его.

Вы определяете var следующим образом: var идентификатор = инициализация

Ключевое слово var следует за идентификатором, знаком равенства и затем значением инициализации. Идентификатор начинается с буквы или символа подчеркивания, за которым следуют буквы, цифры и символы подчеркивания. Регистр имеет значение (то есть thisValue и thisvalue — это разные идентификаторы).

Вот несколько определений var: // VarAndVal/Vars.kt fun main() { var whole = 11 // [1] var fractional = 1.4 // [2] var words = “Twas Brillig” // [3] println(whole) println(fractional) println(words) } /* Вывод: 11 1.4 Twas Brillig */

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

  • [1] Создайте var с именем whole и сохраните в нем 11.
  • [2] Сохраните “дробное число” 1.4 в var fractional.
  • [3] Сохраните некоторый текст (строку) в var words.

Обратите внимание, что println() может принимать любое одно значение в качестве аргумента. Как подразумевает название variable, var может изменяться. То есть вы можете изменить данные, хранящиеся в var. Мы говорим, что var изменяемый (mutable):

// VarAndVal/AVarIsMutable.kt fun main() { var sum = 1 sum = sum + 2 sum += 3 println(sum) } /* Вывод: 6 */

Присваивание sum = sum + 2 берет текущее значение sum, добавляет два и присваивает результат обратно в sum. Присваивание sum += 3 означает то же самое, что и sum = sum + 3. Оператор += берет предыдущее значение, хранящееся в sum, и увеличивает его на 3, затем присваивает этот новый результат обратно в sum.

Изменение значения, хранящегося в var, — это полезный способ выразить изменения. Однако, когда сложность программы увеличивается, ваш код становится более понятным, безопасным и легким для понимания, если значения, представленные вашими идентификаторами, не могут изменяться — то есть они не могут быть переназначены. Мы указываем неизменяемый идентификатор, используя ключевое слово val вместо var. val может быть назначен только один раз, когда он создается: val идентификатор = инициализация

Ключевое слово val происходит от value, указывая на что-то, что не может изменяться — оно неизменяемо (immutable). Выбирайте val вместо var всякий раз, когда это возможно. Пример из Vars.kt в начале этого атома можно переписать, используя val:

// VarAndVal/Vals.kt fun main() { val whole = 11 // whole = 15 // Ошибка // [1] val fractional = 1.4 val words = “Twas Brillig” println(whole) println(fractional) println(words) } /* Вывод: 11 1.4 Twas Brillig */

  • [1] Как только вы инициализируете val, вы не можете его переназначить. Если мы попытаемся переназначить whole на другое число, Kotlin выдаст ошибку, сказав: “Val cannot be reassigned.”

Выбор описательных имен для ваших идентификаторов делает ваш код более понятным и часто уменьшает необходимость в комментариях. В Vals.kt вы не имеете представления о том, что представляет собой whole. Если ваша программа хранит число 11, чтобы обозначить время дня, когда вы получаете кофе, будет более очевидно для других, если вы назовете его coffeetime, и легче читать, если это coffeeTime (согласно стилю Kotlin, мы делаем первую букву строчной).

var полезны, когда данные должны изменяться во время выполнения программы. Это звучит как общая необходимость, но на практике оказывается, что это можно избежать. В общем, ваши программы легче расширять и поддерживать, если вы используете val. Однако в редких случаях слишком сложно решить проблему, используя только val. По этой причине Kotlin предоставляет вам гибкость var. Однако, проводя больше времени с val, вы обнаружите, что вам почти никогда не нужны var, и что ваши программы более безопасны и надежны без них.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Типы данных Link to heading

Данные могут иметь разные типы. Чтобы решить математическую задачу, вы пишете выражение: 5.9 + 6
Вы знаете, что сложение этих чисел дает другое число. Kotlin тоже это знает. Вы знаете, что одно из них — это дробное число (5.9), которое Kotlin называет Double, а другое — целое число (6), которое Kotlin называет Int. Вы знаете, что результат — это дробное число.
Тип (также называемый типом данных) говорит Kotlin, как вы намерены использовать эти данные. Тип определяет множество значений, которые выражение этого типа может производить. Тип также определяет операции, которые могут быть выполнены над данными, значение данных и то, как значения этого типа могут быть сохранены.
Kotlin использует типы, чтобы проверить, что ваши выражения корректны. В приведенном выше выражении Kotlin создает новое значение типа Double для хранения результата.
Kotlin старается адаптироваться к вашим потребностям. Если вы попросите его сделать что-то, что нарушает правила типов, он выдаст сообщение об ошибке. Например, попробуйте сложить строку и число: // DataTypes/StringPlusNumber.kt fun main() {
println(“Sally” + 5.9)
}
/* Вывод:
Sally5.9
*/
Типы говорят Kotlin, как правильно их использовать. В этом случае правила типов говорят Kotlin, как сложить число и строку: путем объединения двух значений и создания строки для хранения результата.
Теперь попробуйте умножить строку и Double, изменив + в StringPlusNumber.kt на :
DataTypes 33
“Sally” * 5.9
Комбинирование типов таким образом не имеет смысла для Kotlin, поэтому он выдает вам ошибку.
В var и val мы хранили несколько типов. Kotlin сам определил типы для нас, исходя из того, как мы их использовали. Это называется выводом типа.
Мы можем быть более подробными и указать тип:
val идентификатор: Тип = инициализация
Вы начинаете с ключевого слова val или var, за которым следует идентификатор, двоеточие, тип, знак = и значение инициализации. Таким образом, вместо того чтобы говорить:
val n = 1
var p = 1.2
Вы можете сказать:
val n: Int = 1
var p: Double = 1.2
Мы сказали Kotlin, что n — это Int, а p — это Double, вместо того чтобы позволить ему вывести тип.
Вот некоторые из базовых типов Kotlin:
// DataTypes/Types.kt
fun main() {
val whole: Int = 11 // [1]
val fractional: Double = 1.4 // [2]
val trueOrFalse: Boolean = true // [3]
val words: String = “A value” // [4]
val character: Char = ‘z’ // [5]
val lines: String = “““Тройные кавычки позволяют
вам иметь много строк
в вашей строке””” // [6]
println(whole)
println(fractional)
println(trueOrFalse)
println(words)
println(character)
AtomicKotlin(www.AtomicKotlin.com)byBruceEckel&SvetlanaIsakova,©2021MindViewLLC
DataTypes 34
println(lines)
}
/
Вывод:
11
1.4
true
A value
z
Тройные кавычки позволяют
вам иметь много строк
в вашей строке
/
• [1] Тип данных Int — это целое число, что означает, что он хранит только целые числа.
• [2] Для хранения дробных чисел используйте Double.
• [3] Тип данных Boolean хранит только два специальных значения: true и false.
• [4] Строка хранит последовательность символов. Вы присваиваете значение, используя строку в двойных кавычках.
• [5] Char хранит один символ.
• [6] Если у вас много строк и/или специальных символов, окружите их тройными двойными кавычками (это строка с тройными кавычками).
Kotlin использует вывод типа, чтобы определить значение смешанных типов. Например, при сложении Int и Double Kotlin решает тип для результирующего значения:
// DataTypes/Inference.kt
fun main() {
val n = 1 + 1.2
println(n)
}
/
Вывод:
2.2
*/
Когда вы добавляете Int к Double, используя вывод типа, Kotlin определяет, что результат n — это Double, и гарантирует, что он соблюдает все правила для Double.
Вывод типа в Kotlin является частью его стратегии по выполнению работы для программиста. Если вы пропустите объявление типа, Kotlin обычно может его вывести.
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin(www.AtomicKotlin.com)byBruceEckel&SvetlanaIsakova,©2021MindViewLLC

Функции Link to heading

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

**fun** functionName(p1: Type1, p2: Type2, ...): ReturnType {
    lines of code
    **return** result
}

p1 и p2 — это параметры: информация, которую вы передаете в функцию. Каждый параметр имеет имя идентификатора (p1, p2), за которым следует двоеточие и тип этого параметра. Закрывающая скобка списка параметров сопровождается двоеточием и типом результата, который возвращает функция. Строки кода в теле функции заключены в фигурные скобки. Выражение, следующее за ключевым словом return, — это результат, который функция производит по завершении.

Параметр — это то, как вы определяете, что передается в функцию — это заполнитель. Аргумент — это фактическое значение, которое вы передаете в функцию.

Сочетание имени, параметров и типа возвращаемого значения называется сигнатурой функции. Вот простая функция под названием multiplyByTwo():

// Functions/MultiplyByTwo.kt
**fun** multiplyByTwo(x: **Int** ): **Int** { // [1]
    println("Inside multiplyByTwo") // [2]
    **return** x * 2
}
**fun** main() {
    **val** r = multiplyByTwo(5) // [3]
    println(r)
}
/* Вывод:
Inside multiplyByTwo
10
*/

• [1] Обратите внимание на ключевое слово fun, имя функции и список параметров, состоящий из одного параметра. Эта функция принимает параметр типа Int и возвращает Int.
• [2] Эти две строки — это тело функции. Последняя строка возвращает значение своего вычисления x * 2 как результат функции.
• [3] Эта строка вызывает функцию с соответствующим аргументом и сохраняет результат в val r. Вызов функции имитирует форму ее объявления: имя функции, за которым следуют аргументы в круглых скобках.

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

println() также является вызовом функции — она просто предоставляется Kotlin. Мы называем функции, определенные в Kotlin, библиотечными функциями. Если функция не возвращает значимого результата, ее тип возвращаемого значения — Unit. Вы можете явно указать Unit, если хотите, но Kotlin позволяет вам опустить его:

// Functions/SayHello.kt
**fun** sayHello() {
    println("Hallo!")
}
**fun** sayGoodbye(): **Unit** {
    println("Auf Wiedersehen!")
}
**fun** main() {
    sayHello()
    sayGoodbye()
}
/* Вывод:
Hallo!
Auf Wiedersehen!
*/

Обе функции sayHello() и sayGoodbye() возвращают Unit, но sayHello() опускает явное объявление. Функция main() также возвращает Unit.

Если функция состоит только из одного выражения, вы можете использовать сокращенный синтаксис с знаком равенства, за которым следует выражение:

**fun** functionName(arg1: Type1, arg2: Type2, ...): ReturnType = expression

Тело функции, окруженное фигурными скобками, называется блочным телом. Тело функции, использующее синтаксис с равенством, называется телом выражения. Здесь multiplyByThree() использует тело выражения:

// Functions/MultiplyByThree.kt
**fun** multiplyByThree(x: **Int** ): **Int** = x * 3
**fun** main() {
    println(multiplyByThree(5))
}
/* Вывод:
15
*/

Это короткая версия для возврата x * 3 внутри блочного тела. Kotlin выводит тип возвращаемого значения функции, которая имеет тело выражения:

// Functions/MultiplyByFour.kt
**fun** multiplyByFour(x: **Int** ) = x * 4
**fun** main() {
    **val** result: **Int** = multiplyByFour(5)
    println(result)
}
/* Вывод:
20
*/

Kotlin выводит, что multiplyByFour() возвращает Int. Kotlin может выводить типы возвращаемых значений только для тел выражений. Если функция имеет блочное тело и вы опускаете его тип, эта функция возвращает Unit.

• При написании функций выбирайте описательные имена. Это делает код более читаемым и часто может уменьшить необходимость в комментариях к коду. Мы не всегда можем быть такими описательными, как нам хотелось бы, с именами функций в этой книге, потому что мы ограничены длиной строк.

Упражнения и решения можно найти на www.AtomicKotlin.com.

if-выражения Link to heading

Выражение if делает выбор. Ключевое слово if проверяет выражение, чтобы узнать, истинно оно или ложно, и выполняет действие на основе результата. Выражение, которое может быть истинным или ложным, называется логическим (Boolean), в честь математика Джорджа Буля, который изобрел логику, лежащую в основе этих выражений. Вот пример с использованием символов > (больше) и < (меньше):

// IfExpressions/If1.kt
fun main() {
    if (1 > 0)
        println("Это правда!")
    if (10 < 11) {
        println("10 < 11")
        println("десять меньше одиннадцати")
    }
}
/* Вывод:
Это правда!
10 < 11
десять меньше одиннадцати
*/

Выражение внутри скобок после if должно оцениваться как истинное или ложное. Если истинно, выполняется следующее выражение. Чтобы выполнить несколько строк, поместите их в фигурные скобки.

Мы можем создать логическое выражение в одном месте и использовать его в другом:

// IfExpressions/If2.kt
fun main() {
    val x: Boolean = 1 >= 1
    if (x)
        println("Это правда!")
}
/* Вывод:
Это правда!
*/

Поскольку x является логическим, if может проверить его напрямую, сказав if(x). Логический оператор >= возвращает true, если выражение слева от оператора больше или равно тому, что справа. Аналогично, <= возвращает true, если выражение слева меньше или равно тому, что справа.

Ключевое слово else позволяет вам обрабатывать как истинные, так и ложные пути:

// IfExpressions/If3.kt
fun main() {
    val n: Int = -11
    if (n > 0)
        println("Это положительное число")
    else
        println("Это отрицательное число или ноль")
}
/* Вывод:
Это отрицательное число или ноль
*/

Ключевое слово else используется только в сочетании с if. Вы не ограничены одной проверкой — вы можете тестировать несколько комбинаций, комбинируя else и if:

// IfExpressions/If4.kt
fun main() {
    val n: Int = -11
    if (n > 0)
        println("Это положительное число")
    else if (n == 0)
        println("Это ноль")
    else
        println("Это отрицательное число")
}
/* Вывод:
Это отрицательное число
*/

Здесь мы используем == для проверки двух чисел на равенство. != проверяет на неравенство. Типичный шаблон — начать с if, за которым следуют столько else if, сколько вам нужно, заканчивая финальным else для всего, что не соответствует всем предыдущим тестам. Когда выражение if достигает определенного размера и сложности, вы, вероятно, будете использовать выражение when вместо этого. Выражение when описано позже в книге, в разделе “Выражения when”.

Оператор “не” ! проверяет противоположное логическому выражению:

// IfExpressions/If5.kt
fun main() {
    val y: Boolean = false
    if (!y)
        println("!y истинно")
}
/* Вывод:
!y истинно
*/

Чтобы выразить if(!y), скажите “если не y”. Все выражение if является выражением, поэтому оно может производить результат:

// IfExpressions/If6.kt
fun main() {
    val num = 10
    val result = if (num > 100) 4 else 42
    println(result)
}
/* Вывод:
42
*/

Здесь мы сохраняем значение, производимое всем выражением if, в промежуточном идентификаторе, называемом result. Если условие выполнено, первая ветвь производит result. Если нет, значение else становится result.

Давайте попрактикуемся в создании функций. Вот одна, которая принимает логический параметр:

// IfExpressions/TrueOrFalse.kt
fun trueOrFalse(exp: Boolean): String {
    if (exp)
        return "Это правда!" // [1]
    return "Это ложь" // [2]
}

fun main() {
    val b = 1
    println(trueOrFalse(b < 3))
    println(trueOrFalse(b >= 3))
}
/* Вывод:
Это правда!
Это ложь
*/

Логический параметр exp передается в функцию trueOrFalse(). Если аргумент передан как выражение, такое как b < 3, это выражение сначала оценивается, и результат передается в функцию. trueOrFalse() проверяет exp, и если результат истинный, выполняется строка [1], в противном случае выполняется строка [2].

• [1] return говорит: “Покиньте функцию и произведите это значение как результат функции.” Обратите внимание, что return может появляться в любом месте функции и не обязательно должен быть в конце.

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

// IfExpressions/OneOrTheOther.kt
fun oneOrTheOther(exp: Boolean): String =
    if (exp)
        "Правда!" // Нет необходимости в 'return'
    else
        "Ложь"

fun main() {
    val x = 1
    println(oneOrTheOther(x == 1))
    println(oneOrTheOther(x == 2))
}
/* Вывод:
Правда!
Ложь
*/

Вместо двух выражений в trueOrFalse(), oneOrTheOther() является единым выражением. Результат этого выражения является результатом функции, поэтому выражение if становится телом функции.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Шаблоны строк Link to heading

Шаблон строки — это программный способ генерации строки. Если вы поставите знак $ перед именем идентификатора, шаблон строки вставит содержимое этого идентификатора в строку: // StringTemplates/StringTemplates.kt fun main() { val answer = 42 println(“Найдено $ answer!”) // [1] println(“печатаем $ 1”) // [2] } /* Вывод: Найдено 42! печатаем $1 */ • [1] $answer заменяет значение переменной answer. • [2] Если то, что следует за $, не распознается как идентификатор программы, ничего особенного не происходит.

Вы также можете вставлять значения в строку, используя конкатенацию (+): // StringTemplates/StringConcatenation.kt fun main() { val s = “привет\n” // \n — это символ новой строки val n = 11 val d = 3.14 println(“первый: " + s + “второй: " + n + “, третий: " + d) } /* Вывод: первый: привет второй: 11, третий: 3.14 / Шаблоны строк 45 Помещение выражения внутрь ${} вычисляет его. Возвращаемое значение преобразуется в строку и вставляется в результирующую строку: // StringTemplates/ExpressionInTemplate.kt fun main() { val condition = true println( " ${if (condition) ‘a’ else ‘b’ } “) // [1] val x = 11 println(” $ x + 4 = ${ x + 4 } “) } / Вывод: a 11 + 4 = 15 */ • [1] if(condition) 'a' else 'b' вычисляется, и результат заменяется на всё выражение ${}.

Когда строка должна включать специальный символ, такой как кавычка, вы можете либо экранировать этот символ с помощью \ (обратный слэш), либо использовать строковый литерал в тройных кавычках: // StringTemplates/TripleQuotes.kt fun main() { val s = “значение” println(“s = " $ s".”) println(“““s = " $ s”.”””) } /* Вывод: s = “значение”. s = “значение”. */ С помощью тройных кавычек вы вставляете значение выражения так же, как и для строки в одинарных кавычках.

Упражнения и решения можно найти на www.AtomicKotlin.com. AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Типы чисел Link to heading

Разные типы чисел хранятся по-разному. Если вы создаете идентификатор и присваиваете ему целочисленное значение, Kotlin выводит тип Int: // NumberTypes/InferInt.kt fun main() { val million = 1_000_000 // Выводит Int println(million) } /* Вывод: 1000000 / Для удобочитаемости Kotlin позволяет использовать подчеркивания в числовых значениях. Основные математические операторы для чисел — это те, которые доступны в большинстве языков программирования: сложение ( + ), вычитание ( - ), деление ( / ), умножение ( * ) и остаток ( % ), который возвращает остаток от целочисленного деления: // NumberTypes/Modulus.kt fun main() { val numerator: Int = 19 val denominator: Int = 10 println(numerator % denominator) } / Вывод: 9 / Целочисленное деление отсекает свой результат: // NumberTypes/IntDivisionTruncates.kt fun main() { val numerator: Int = 19 val denominator: Int = 10 println(numerator / denominator) } / Вывод: 1 / Если бы операция округлила результат, вывод был бы 2. Порядок операций следует основным арифметическим правилам: // NumberTypes/OpOrder.kt fun main() { println(45 + 5 * 6) } / Вывод: 75 / Операция умножения 5 * 6 выполняется первой, за ней следует сложение 45 + 30. Если вы хотите, чтобы 45 + 5 выполнялось первым, используйте скобки: // NumberTypes/OpOrderParens.kt fun main() { println((45 + 5) * 6) } / Вывод: 300 / Теперь давайте рассчитаем индекс массы тела (ИМТ), который равен весу в килограммах, деленному на квадрат роста в метрах. Если ваш ИМТ меньше 18.5, вы имеете недостаточный вес. Между 18.5 и 24.9 — нормальный вес. ИМТ 25 и выше — избыточный вес. Этот пример также показывает предпочтительный стиль форматирования, когда вы не можете уместить параметры функции в одной строке: AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC NumberTypes 48 // NumberTypes/BMIMetric.kt fun bmiMetric( weight: Double , height: Double ): String { val bmi = weight / (height * height) // [1] return if (bmi < 18.5) “Недостаточный вес” else if (bmi < 25) “Нормальный вес” else “Избыточный вес” } fun main() { val weight = 72.57 // 160 фунтов val height = 1.727 // 68 дюймов val status = bmiMetric(weight, height) println(status) } / Вывод: Нормальный вес / • [1] Если вы уберете скобки, вы разделите вес на рост, а затем умножите этот результат на рост. Это будет гораздо большее число и неправильный ответ. bmiMetric() использует Double для веса и роста. Double может хранить очень большие и очень маленькие числа с плавающей запятой. Вот версия, использующая английские единицы, представленные параметрами Int: // NumberTypes/BMIEnglish.kt fun bmiEnglish( weight: Int , height: Int ): String { val bmi = weight / (height * height) * 703.07 // [1] return if (bmi < 18.5) “Недостаточный вес” else if (bmi < 25) “Нормальный вес” else “Избыточный вес” } AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC NumberTypes 49 fun main() { val weight = 160 val height = 68 val status = bmiEnglish(weight, height) println(status) } / Вывод: Недостаточный вес / Почему результат отличается от bmiMetric(), который использует Double? Когда вы делите целое число на другое целое число, Kotlin возвращает целочисленный результат. Стандартный способ обработки остатка при целочисленном делении — это отбрасывание, что означает «отсечь и выбросить» (округления нет). Поэтому, если вы делите 5 на 2, вы получаете 2, а 7/10 — это ноль. Когда Kotlin вычисляет bmi в выражении [1], он делит 160 на 68 * 68 и получает ноль. Затем он умножает ноль на 703.07 и получает ноль. Чтобы избежать этой проблемы, переместите 703.07 в начало вычисления. Вычисления тогда будут принудительно выполнены как Double: val bmi = 703.07 * weight / (height * height) Параметры Double в bmiMetric() предотвращают эту проблему. Преобразуйте вычисления в нужный тип как можно раньше, чтобы сохранить точность. Все языки программирования имеют ограничения на то, что они могут хранить в целочисленном формате. Тип Int в Kotlin может принимать значения от -2^31 до +2^31-1, что является ограничением 32-битного представления Int. Если вы складываете или умножаете два достаточно больших Int, вы получите переполнение результата: AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC NumberTypes 50 // NumberTypes/IntegerOverflow.kt fun main() { val i: Int = Int .MAX_VALUE println(i + i) } / Вывод: -2 / Int.MAX_VALUE — это предопределенное значение, которое является наибольшим числом, которое может хранить Int. Переполнение приводит к результату, который явно неверен, так как он и отрицательный, и гораздо меньше, чем мы ожидаем. Kotlin выдает предупреждение всякий раз, когда он обнаруживает потенциальное переполнение. Предотвращение переполнения — это ваша ответственность как разработчика. Kotlin не всегда может обнаружить переполнение во время компиляции, и он не предотвращает переполнение, так как это приведет к неприемлемому влиянию на производительность. Если ваша программа содержит большие числа, вы можете использовать Long, которые могут хранить значения от -2^63 до +2^63-1. Чтобы определить val типа Long, вы можете явно указать тип или добавить L в конец числового литерала, что говорит Kotlin рассматривать это значение как Long: // NumberTypes/LongConstants.kt fun main() { val i = 0 // Выводит Int val l1 = 0L // L создает Long val l2: Long = 0 // Явный тип println(” $ l1 $ l2”) } / Вывод: 0 0 / Изменив на Long, мы предотвращаем переполнение в IntegerOverflow.kt: AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC NumberTypes 51 // NumberTypes/UsingLongs.kt fun main() { val i = Int .MAX_VALUE println(0L + i + i) // [1] println(1_000_000 * 1_000_000L) // [2] } / Вывод: 4294967294 1000000000000 / Использование числового литерала в обоих [1] и [2] принуждает вычисления к типу Long и также дает результат типа Long. Место, где появляется L, не имеет значения. Если одно из значений — Long, результирующее выражение будет Long. Хотя они могут хранить гораздо большие значения, чем Int, Long все еще имеют ограничения по размеру: // NumberTypes/BiggestLong.kt fun main() { println( Long .MAX_VALUE) } / Вывод: 9223372036854775807 */ Long.MAX_VALUE — это наибольшее значение, которое может хранить Long. Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Булевы значения Link to heading

Выражения if продемонстрировали оператор “не” !, который отрицает булевое значение. Этот атом вводит больше булевой алгебры. Мы начинаем с операторов “и” и “или”:

  • && (и): Возвращает true только если булевое выражение слева от оператора и то, что справа, оба истинны.
  • || (или): Возвращает true, если хотя бы одно из выражений слева или справа от оператора истинно, или если оба истинны.

В этом примере мы определяем, открыто ли предприятие, основываясь на времени:

// Booleans/Open1.kt
fun isOpen1(hour: Int) {
    val open = 9
    val closed = 20
    println("Часы работы: $open - $closed")
    val status = if (hour >= open && hour < closed) // [1]
        true
    else
        false
    println("Открыто: $status")
}

fun main() = isOpen1(6)
/* Вывод:
Часы работы: 9 - 20
Открыто: false
*/

main() — это единичный вызов функции, поэтому мы можем использовать тело выражения, как описано в разделе о функциях. Выражение if в [1] проверяет, находится ли час между временем открытия и закрытия, поэтому мы комбинируем выражения с булевым && (и). Выражение if можно упростить. Результат выражения if(cond) true else false — это просто cond:

// Booleans/Open2.kt
fun isOpen2(hour: Int) {
    val open = 9
    val closed = 20
    println("Часы работы: $open - $closed")
    val status = hour >= open && hour < closed
    println("Открыто: $status")
}

fun main() = isOpen2(6)
/* Вывод:
Часы работы: 9 - 20
Открыто: false
*/

Давайте изменим логику и проверим, закрыто ли предприятие в данный момент. Оператор “или” || возвращает true, если хотя бы одно из условий выполнено:

// Booleans/Closed.kt
fun isClosed(hour: Int) {
    val open = 9
    val closed = 20
    println("Часы работы: $open - $closed")
    val status = hour < open || hour >= closed
    println("Закрыто: $status")
}

fun main() = isClosed(6)
/* Вывод:
Часы работы: 9 - 20
Закрыто: true
*/

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

// Booleans/EvaluationOrder.kt
fun main() {
    val sunny = true
    val hoursSleep = 6
    val exercise = false
    val temp = 55
    // [1]:
    val happy1 = sunny && temp > 50 || exercise && hoursSleep > 7
    println(happy1)
    // [2]:
    val sameHappy1 = (sunny && temp > 50) || (exercise && hoursSleep > 7)
    println(sameHappy1)
    // [3]:
    val notSame = (sunny && temp > 50 || exercise) && hoursSleep > 7
    println(notSame)
}
/* Вывод:
true
true
false
*/

Булевые выражения — это sunny, temp > 50, exercise и hoursSleep > 7. Мы читаем happy1 как “Солнечно и температура выше 50 или я занимался спортом и спал больше 7 часов”. Но имеет ли && приоритет над ||, или наоборот?

Выражение в [1] использует порядок вычисления по умолчанию в Kotlin. Это дает тот же результат, что и выражение в [2], потому что без скобок “и” вычисляются первыми, а затем “или”. Выражение в [3] использует скобки, чтобы получить другой результат. В [3] мы счастливы только если получили как минимум 7 часов сна.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Повторение с помощью while Link to heading

Компьютеры идеально подходят для повторяющихся задач.
Самая базовая форма повторения использует ключевое слово while. Это повторяет блок, пока управляющее логическое выражение истинно:
while ( логическое -выражение) {
// Код для повторения
}
Логическое выражение оценивается один раз в начале цикла и снова перед каждой дальнейшей итерацией через блок.
// RepetitionWithWhile/WhileLoop.kt
fun condition(i: Int) = i < 100 // [1]
fun main() {
var i = 0
while (condition(i)) { // [2]
print(”.")
i += 10 // [3]
}
}
/* Вывод:
……….
*/
• [1] Оператор сравнения < возвращает логический результат, поэтому Kotlin выводит логический тип как результат для condition().
• [2] Условное выражение для while говорит: “повторяйте операторы в теле, пока condition() возвращает true.”
• [3] Оператор += добавляет 10 к i и присваивает результат i в одной операции (i должен быть var, чтобы это работало). Это эквивалентно:
i = i + 10

Существует второй способ использования while, в сочетании с ключевым словом do:
do {
// Код для повторения
} while ( логическое -выражение)
Переписывание WhileLoop.kt с использованием do-while дает:
// RepetitionWithWhile/DoWhileLoop.kt
fun main() {
var i = 0
do {
print(".")
i += 10
} while (condition(i))
}
/* Вывод:
……….
*/
Единственное отличие между while и do-while заключается в том, что тело do-while всегда выполняется хотя бы один раз, даже если логическое выражение изначально возвращает false. В while, если условие ложно в первый раз, то тело никогда не выполняется. На практике do-while используется реже, чем while.

Короткие версии операторов присваивания доступны для всех арифметических операций: +=, -=, =, /= и %= . Это использует -= и %=:
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Повторение с помощью while 58
// RepetitionWithWhile/AssignmentOperators.kt
fun main() {
var n = 10
val d = 3
print(n)
while (n > d) {
n -= d
print(" - $ d")
}
println(" = $ n")
var m = 10
print(m)
m %= d
println(" % $ d = $ m")
}
/
Вывод:
10 - 3 - 3 - 3 = 1
10 % 3 = 1
/
Чтобы вычислить остаток от целочисленного деления двух натуральных чисел, мы начинаем с цикла while, а затем используем оператор остатка.
Прибавление 1 и вычитание 1 из числа так распространены, что у них есть свои собственные операторы инкремента и декремента: ++ и –. Вы можете заменить i += 1 на i++:
// RepetitionWithWhile/IncrementOperator.kt
fun main() {
var i = 0
while (i < 4) {
print(".")
i++
}
}
/
Вывод:
….
*/
На практике циклы while не используются для итерации по диапазону чисел. Вместо этого используется цикл for. Это будет рассмотрено в следующем атоме.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Повторение с помощью while 59
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Циклы и диапазоны Link to heading

Ключевое слово for выполняет блок кода для каждого значения в последовательности. Набор значений может быть диапазоном целых чисел, строкой или, как вы увидите позже в книге, коллекцией элементов. Ключевое слово in указывает, что вы проходите по значениям:

for (v in values) {
    // Выполнить что-то с v
}

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

// LoopingAndRanges/RepeatThreeTimes.kt
fun main() {
    for (i in 1..3) {
        println("Привет, $i!")
    }
}
/* Вывод:
Привет, 1!
Привет, 2!
Привет, 3!
*/

Вывод показывает, что индекс i получает каждое значение в диапазоне от 1 до 3. Диапазон — это интервал значений, определяемый парой конечных точек. Существует два основных способа определения диапазонов:

// LoopingAndRanges/DefiningRanges.kt
fun main() {
    val range1 = 1..10 // [1]
    val range2 = 0 until 10 // [2]
    println(range1)
    println(range2)
}
/* Вывод:
1..10
0..9
*/
  • [1] Использование синтаксиса .. включает обе границы в результирующий диапазон.
  • [2] until исключает конец. Вывод показывает, что 10 не является частью диапазона.

Отображение диапазона производит читаемый формат. Это сумма чисел от 10 до 100:

// LoopingAndRanges/SumUsingRange.kt
fun main() {
    var sum = 0
    for (n in 10..100) {
        sum += n
    }
    println("сумма = $sum")
}
/* Вывод:
сумма = 5005
*/

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

// LoopingAndRanges/ForWithRanges.kt
fun showRange(r: IntProgression) {
    for (i in r) {
        print(" $i ")
    }
    print(" // $r")
    println()
}

fun main() {
    showRange(1..5)
    showRange(0 until 5)
    showRange(5 downTo 1) // [1]
    showRange(0..9 step 2) // [2]
    showRange(0 until 10 step 3) // [3]
    showRange(9 downTo 2 step 3)
}
/* Вывод:
1 2 3 4 5 // 1..5
0 1 2 3 4 // 0..4
5 4 3 2 1 // 5 downTo 1 step 1
0 2 4 6 8 // 0..8 step 2
0 3 6 9 // 0..9 step 3
9 6 3 // 9 downTo 3 step 3
*/
  • [1] downTo создает убывающий диапазон.
  • [2] step изменяет интервал. Здесь диапазон увеличивается на два вместо одного.
  • [3] until также может использоваться с step. Обратите внимание, как это влияет на вывод.

В каждом случае последовательность чисел образует арифметическую прогрессию. showRange() принимает параметр IntProgression, который является встроенным типом, включающим диапазоны Int. Обратите внимание, что строковое представление каждого IntProgression, как оно появляется в комментарии к выводу для каждой строки, часто отличается от диапазона, переданного в showRange()IntProgression переводит входные данные в эквивалентную общую форму.

Вы также можете создать диапазон символов. Этот цикл проходит от a до z:

// LoopingAndRanges/ForWithCharRange.kt
fun main() {
    for (c in 'a'..'z') {
        print(c)
    }
}
/* Вывод:
abcdefghijklmnopqrstuvwxyz
*/

Вы можете итерировать по диапазону элементов, которые являются целыми количествами, такими как целые числа и символы, но не по значениям с плавающей запятой. Квадратные скобки позволяют получить доступ к символам по индексу. Поскольку мы начинаем считать символы в строке с нуля, s[0] выбирает первый символ строки s. Выбор s.lastIndex дает последний индекс:

// LoopingAndRanges/IndexIntoString.kt
fun main() {
    val s = "abc"
    for (i in 0..s.lastIndex) {
        print(s[i] + 1)
    }
}
/* Вывод:
bcd
*/

Иногда люди описывают s[0] как “нулевой символ”. Символы хранятся как числа, соответствующие их значениям Unicode, поэтому добавление целого числа к символу производит новый символ, соответствующий новому кодовому значению:

// LoopingAndRanges/AddingIntToChar.kt
fun main() {
    val ch: Char = 'a'
    println(ch + 25)
    println(ch < 'z')
}
/* Вывод:
z
true
*/

Второй println() показывает, что вы можете сравнивать коды символов. Цикл for может итерировать строку s напрямую:

// LoopingAndRanges/IterateOverString.kt
fun main() {
    for (ch in "Jnskhm ") {
        print(ch + 1)
    }
}
/* Вывод:
Kotlin!
*/

ch получает каждый символ по очереди. В следующем примере функция hasChar() итерирует по строке s и проверяет, содержит ли она данный символ ch. Возврат в середине функции останавливает выполнение функции, когда ответ найден:

// LoopingAndRanges/HasChar.kt
fun hasChar(s: String, ch: Char): Boolean {
    for (c in s) {
        if (c == ch) return true
    }
    return false
}

fun main() {
    println(hasChar("kotlin", 't'))
    println(hasChar("kotlin", 'a'))
}
/* Вывод:
true
false
*/

Следующий атом показывает, что hasChar() не нужен — вы можете использовать встроенный синтаксис вместо этого. Если вы просто хотите повторить действие фиксированное количество раз, вы можете использовать repeat() вместо цикла for:

// LoopingAndRanges/RepeatHi.kt
fun main() {
    repeat(2) {
        println("привет!")
    }
}
/* Вывод:
привет!
привет!
*/

repeat() — это функция стандартной библиотеки, а не ключевое слово. Вы увидите, как она была создана гораздо позже в книге. Упражнения и решения можно найти на www.AtomicKotlin.com.

Ключевое слово in Link to heading

Ключевое слово in проверяет, находится ли значение в пределах диапазона. // InKeyword/MembershipInRange.kt fun main() { val percent = 35 println(percent in 1..100) } /* Вывод: true / В логических выражениях вы научились явно проверять границы: // InKeyword/MembershipUsingBounds.kt fun main() { val percent = 35 println(0 <= percent && percent <= 100) } / Вывод: true / 0 <= x && x <= 100 логически эквивалентно x in 0..100. IntelliJ IDEA предлагает автоматически заменить первую форму на вторую, которая легче для чтения и понимания.
Ключевое слово in используется как для итерации, так и для проверки членства. Использование in в управляющем выражении цикла for означает итерацию, в противном случае in проверяет членство:
Ключевое слово in 67 // InKeyword/IterationVsMembership.kt fun main() { val values = 1..3 for (v in values) { println(“итерация $ v”) } val v = 2 if (v in values) println(" $ v является членом $ values") } /
Вывод: итерация 1 итерация 2 итерация 3 2 является членом 1..3 / Ключевое слово in не ограничивается диапазонами. Вы также можете проверить, является ли символ частью строки. В следующем примере используется in вместо hasChar() из предыдущего атома: // InKeyword/InString.kt fun main() { println(’t’ in “kotlin”) println(‘a’ in “kotlin”) } / Вывод: true false / Позже в книге вы увидите, что in работает и с другими типами.
Здесь in проверяет, принадлежит ли символ диапазону символов:
AtomicKotlin(www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Ключевое слово in 68 // InKeyword/CharRange.kt fun isDigit(ch: Char ) = ch in ‘0’..‘9’ fun notDigit(ch: Char ) = ch !in ‘0’..‘9’ // [1] fun main() { println(isDigit(‘a’)) println(isDigit(‘5’)) println(notDigit(‘z’)) } /
Вывод: false true true / • [1] !in проверяет, что значение не принадлежит диапазону.
Вы можете создать диапазон Double, но можете использовать его только для проверки членства: // InKeyword/FloatingPointRange.kt fun inFloatRange(n: Double ) { val r = 1.0..10.0 println(" $ n в $ r? ${ n in r } “) } fun main() { inFloatRange(0.999999) inFloatRange(5.0) inFloatRange(10.0) inFloatRange(10.0000001) } /
Вывод: 0.999999 в 1.0..10.0? false 5.0 в 1.0..10.0? true 10.0 в 1.0..10.0? true 10.0000001 в 1.0..10.0? false / Вы можете использовать только .. для определения диапазона с плавающей запятой в Kotlin.
Вы можете проверить, является ли строка членом диапазона строк:
AtomicKotlin(www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Ключевое слово in 69 // InKeyword/StringRange.kt fun main() { println(“ab” in “aa”..“az”) println(“ba” in “aa”..“az”) } /
Вывод: true false */ Здесь Kotlin использует алфавитное сравнение.
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin(www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC

Выражения и операторы Link to heading

Операторы и выражения — это наименьшие полезные фрагменты кода в большинстве языков программирования. Существует основное различие: оператор имеет эффект, но не производит результата. Выражение всегда производит результат. Поскольку оператор не производит результат, он должен изменить состояние окружающей среды, чтобы быть полезным. Другими словами, «оператор вызывается из-за его побочных эффектов» (то есть того, что он делает, кроме производства результата). В качестве напоминания:
Оператор изменяет состояние.

Одно из определений «выражать» — это «заставить или выжать», как в «выжать сок из апельсина». Таким образом,
Выражение выражает.
То есть, оно производит результат.

Цикл for является оператором в Kotlin. Вы не можете присвоить его, потому что у него нет результата:

// ExpressionsStatements/ForIsAStatement.kt
fun main() {
    // Нельзя сделать это:
    // val f = for(i in 1..10) {}
    // Сообщение об ошибке компилятора:
    // for не является выражением, и
    // здесь разрешены только выражения
}

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

// ExpressionsStatements/UnitReturnType.kt
fun unitFun() = Unit
fun main() {
    println(unitFun())
    val u1: Unit = println(42)
    println(u1)
    val u2 = println(0) // Вывод типа
    println(u2)
}
/* Вывод:
kotlin.Unit
42
kotlin.Unit
0
kotlin.Unit
*/

Тип Unit содержит единственное значение, называемое Unit, которое вы можете вернуть напрямую, как видно в unitFun(). Вызов println() также возвращает Unit. Переменная val u1 захватывает возвращаемое значение println() и явно объявляется как Unit, в то время как u2 использует вывод типа.
Оператор if создает выражение, поэтому вы можете присвоить его результат:

// ExpressionsStatements/AssigningAnIf.kt
fun main() {
    val result1 = if (11 > 42) 9 else 5
    val result2 = if (1 < 2) {
        val a = 11
        a + 42
    } else 42
    val result3 =
        if ('x' < 'y')
            println("x < y")
        else
            println("x > y")
    println(result1)
    println(result2)
    println(result3)
}
/* Вывод:
x < y
5
53
kotlin.Unit
*/

Первая строка вывода — x < y, даже несмотря на то, что result3 не отображается до конца main(). Это происходит потому, что при оценке result3 вызывается println(), и оценка происходит, когда result3 определяется.
Обратите внимание, что a определяется внутри блока кода для result2. Результат последнего выражения становится результатом выражения if; здесь это сумма 11 и 42.
Но что насчет a? Как только вы покидаете блок кода (выходите за фигурные скобки), вы не можете получить доступ к a. Он временный и удаляется, как только вы выходите из области видимости этого блока.
Оператор инкремента i++ также является выражением, даже если он выглядит как оператор. Kotlin следует подходу, используемому в языках, подобных C, и предоставляет две версии операторов инкремента и декремента с немного различной семантикой. Префиксный оператор появляется перед операндом, как в ++i, и возвращает значение после того, как инкремент произошел. Вы можете прочитать это как «сначала выполните инкремент, затем верните полученное значение». Постфиксный оператор ставится после операнда, как в i++, и возвращает значение i до того, как произойдет инкремент. Вы можете прочитать это как «сначала получите результат, затем выполните инкремент».

// ExpressionsStatements/PostfixVsPrefix.kt
fun main() {
    var i = 10
    println(i++)
    println(i)
    var j = 20
    println(++j)
    println(j)
}
/* Вывод:
10
11
21
21
*/

Оператор декремента также имеет две версии: –i и i–. Использование операторов инкремента и декремента внутри других выражений не рекомендуется, так как это может привести к запутанному коду:

// ExpressionsStatements/Confusing.kt
fun main() {
    var i = 1
    println(i++ + ++i)
}

Попробуйте угадать, каков будет вывод, а затем проверьте.
Упражнения и решения можно найти на www.AtomicKotlin.com.

Резюме 1 Link to heading

Этот атом подводит итоги и рассматривает атомы в Разделе I, начиная с “Привет, мир!” и заканчивая “Выражения и операторы”. Если вы опытный программист, это должен быть ваш первый атом. Новым программистам следует прочитать этот атом и выполнить упражнения в качестве повторения Раздела I. Если что-то вам неясно, изучите соответствующий атом по этой теме (подзаголовки соответствуют названиям атомов).

Привет, мир Link to heading

Kotlin поддерживает как // однострочные комментарии, так и /* -до- */ многострочные комментарии. Точка входа в программу — это функция main():

// Summary1/Hello.kt
**fun** main() {
    println("Hello, world!")
}
/* Вывод:
Hello, world!
*/

Первая строка каждого примера в этой книге — это комментарий, содержащий имя подкаталога атома, за которым следует / и имя файла. Вы можете найти все извлеченные примеры кода на сайте AtomicKotlin.com18.

Функция println() является стандартной библиотечной функцией, которая принимает один параметр типа String (или параметр, который может быть преобразован в String). Функция println() перемещает курсор на новую строку после отображения своего параметра, в то время как функция print() оставляет курсор на той же строке.

18 http://AtomicKotlin.com
Summary1 75
Kotlin не требует точки с запятой в конце выражения или оператора. Точки с запятой необходимы только для разделения нескольких выражений или операторов в одной строке.

var & val, Типы Данных Link to heading

Чтобы создать неизменяемый идентификатор, используйте ключевое слово val, за которым следует имя идентификатора, двоеточие и тип для этого значения. Затем добавьте знак равенства и значение, которое нужно присвоить этому val: val идентификатор: Тип = инициализация
Как только val присвоен, его нельзя переназначить.
Вывод типов в Kotlin обычно может автоматически определить тип на основе значения инициализации. Это приводит к более простой записи: val идентификатор = инициализация
Оба следующих варианта являются допустимыми: val daysInFebruary = 28
val daysInMarch: Int = 31

Определение var (переменной) выглядит так же, с использованием var вместо val: var идентификатор1 = инициализация
var идентификатор2: Тип = инициализация
В отличие от val, вы можете изменять var, поэтому следующее является законным: var hoursSpent = 20
hoursSpent = 25

Однако тип не может быть изменен, поэтому вы получите ошибку, если скажете: hoursSpent = 30.5
Kotlin выводит тип Int, когда hoursSpent определяется, поэтому он не примет изменение на значение с плавающей точкой.

Функции Link to heading

Функции — это именованные подпрограммы: AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Резюме 1 76
fun имяФункции(арг1: Тип1, арг2: Тип2, …): ТипВозврата {
// Строки кода …
return результат
}
Ключевое слово fun следует за именем функции и списком параметров в круглых скобках. Каждый параметр должен иметь явный тип, так как Kotlin не может вывести типы параметров. Сама функция также имеет тип, который определяется так же, как для переменной var или val (двоеточие, за которым следует тип). Тип функции — это тип возвращаемого результата.
Подпись функции сопровождается телом функции, заключенным в фигурные скобки. Оператор return предоставляет значение, возвращаемое функцией.
Вы можете использовать сокращенный синтаксис, когда функция состоит из одного выражения:
fun имяФункции(арг1: Тип1, арг2: Тип2, …): ТипВозврата = результат
Эта форма называется телом выражения. Вместо открывающей фигурной скобки используйте знак равенства, за которым следует выражение. Вы можете опустить тип возврата, так как Kotlin может его вывести.
Вот функция, которая вычисляет куб своего параметра, и другая, которая добавляет восклицательный знак к строке:

// Резюме 1/ОсновныеФункции.kt
**fun** куб(x: **Int** ): **Int** {  
**return** x * x * x  
}  
**fun** восклицание(s: **String** ) = s + "!"  
**fun** main() {  
println(куб(3))  
println(восклицание("pop"))  
}  
/* Вывод:  
27  
pop!  
*/  

AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Резюме 1 77
куб() имеет блочное тело с явным оператором return. восклицание() — это тело выражения, производящее значение, возвращаемое функцией. Kotlin выводит тип возврата восклицание() как String.

Булевы значения Link to heading

Для булевой алгебры Kotlin предоставляет такие операторы, как: • ! (не) логически отрицает значение (превращает true в false и наоборот). • && (и) возвращает true только если оба условия истинны. • || (или) возвращает true, если хотя бы одно из условий истинно. // Summary1/Booleans.kt fun main() { val opens = 9 val closes = 20 println(“Часы работы: $ opens - $ closes”) val hour = 6 println(“Текущее время: " + hour) val isOpen = hour >= opens && hour < closes println(“Открыто: " + isOpen) println(“Закрыто: " + !isOpen) val isClosed = hour < opens || hour >= closes println(“Закрыто: " + isClosed) } /* Вывод: Часы работы: 9 - 20 Текущее время: 6 Открыто: false Закрыто: true */ Инициализатор isOpen использует && для проверки, истинны ли оба условия. Первое условие hour >= opens ложно, поэтому результат всего выражения становится ложным. Инициализатор для isClosed использует ||, возвращая true, если хотя бы одно из условий истинно. Выражение hour < opens истинно, поэтому всё выражение истинно. AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC Summary1 78

if Выражения Link to heading

Поскольку if является выражением, оно производит результат. Этот результат может быть присвоен переменной var или val. Здесь вы также видите использование ключевого слова else: // Summary1/IfResult.kt fun main() { val result = if (99 < 100) 4 else 42 println(result) } /* Вывод: 4 / Любая ветка выражения if может быть многострочным блоком кода, окруженным фигурными скобками: // Summary1/IfExpression.kt fun main() { val activity = “плавание” val hour = 10 val isOpen = if ( activity == “плавание” || activity == “катание на коньках”) { val opens = 9 val closes = 20 println(“Часы работы: " + opens + " - " + closes) hour >= opens && hour < closes } else { false } println(isOpen) } / Вывод: Часы работы: 9 - 20 true */ AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккела и Светланы Исаковой, ©2021 MindView LLC Summary1 79 Значение, определенное внутри блока кода, такого как opens, недоступно за пределами области видимости этого блока. Поскольку они определены глобально для выражения if, activity и hour доступны внутри выражения if. Результат выражения if — это результат последнего выражения выбранной ветки. Здесь это hour >= opens && hour <= closes, что является истинным.

Шаблоны строк Link to heading

Вы можете вставить значение в строку, используя шаблоны строк. Используйте символ $ перед именем идентификатора: // Summary1/StrTemplates.kt fun main() { val answer = 42 println(“Найдено $ answer!”) // [1] val condition = true println( " ${if (condition) ‘a’ else ‘b’ } “) // [2] println(“печатаем a $ 1”) // [3] } /* Вывод: Найдено 42! a печатаем a $1 / • [1] $answer заменяет значение, содержащееся в answer. • [2] ${if(condition) ‘a’ else ‘b’} вычисляет и заменяет результат выражения внутри ${}. • [3] Если $ следует за чем-то, что не распознается как идентификатор программы, ничего особенного не происходит. Используйте тройные кавычки для хранения многострочного текста или текста со специальными символами: AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC Summary1 80 // Summary1/ThreeQuotes.kt fun json(q: String, a: Int) = “””{ “вопрос” : " $ q”, “ответ” : $ a }””” fun main() { println(json(“Ультимативный”, 42)) } / Вывод: { “вопрос” : “Ультимативный”, “ответ” : 42 } */ Вам не нужно экранировать специальные символы, такие как " внутри строки с тройными кавычками. (В обычной строке вы пишете " для вставки двойной кавычки). Как и в обычных строках, вы можете вставить идентификатор или выражение, используя $ внутри строки с тройными кавычками.

Типы чисел Link to heading

Kotlin предоставляет целочисленные типы (Int, Long) и типы с плавающей точкой (Double). Константа целого числа по умолчанию является Int, а Long, если вы добавите L. Константа является Double, если она содержит десятичную точку: // Summary1/NumberTypes.kt fun main() { val n = 1000 // Int val l = 1000L // Long val d = 1000.0 // Double println(” $ n $ l $ d”) } /* Вывод: 1000 1000 1000.0 / Int может хранить значения от -2^31 до +2^31-1. Целочисленные значения могут переполняться; например, добавление чего-либо к Int.MAX_VALUE приводит к переполнению: AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC Summary1 81 // Summary1/Overflow.kt fun main() { println( Int .MAX_VALUE + 1) println( Int .MAX_VALUE + 1L) } / Вывод: -2147483648 2147483648 */ Во втором операторе println() мы добавляем L к 1, заставляя всё выражение быть типа Long, что предотвращает переполнение. (Long может хранить значения от -2^63 до +2^63-1).

Когда вы делите Int на другой Int, Kotlin возвращает результат типа Int, и любое остаток отбрасывается. Таким образом, 1/2 дает 0. Если участвует Double, Int преобразуется в Double перед операцией, поэтому 1.0/2 дает 0.5.

Вы можете ожидать, что d1 в следующем примере даст 3.4: // Summary1/Truncation.kt fun main() { val d1: Double = 3.0 + 2 / 5 println(d1) val d2: Double = 3 + 2.0 / 5 println(d2) } /* Вывод: 3.0 3.4 */ Из-за порядка вычислений это не так. Kotlin сначала делит 2 на 5, и целочисленная арифметика дает 0, в результате чего получается 3.0. Тот же порядок вычислений действительно дает ожидаемый результат для d2. Деление 2.0 на 5 дает 0.4. Число 3 преобразуется в Double, потому что мы добавляем его к Double (0.4), что дает 3.4.

Понимание порядка вычислений помогает вам расшифровать, что делает программа, как с логическими операциями (булевыми выражениями), так и с математическими операциями. Если вы не уверены в порядке вычислений, используйте скобки, чтобы уточнить свои намерения. Это также делает ваши намерения ясными для тех, кто читает ваш код. AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC Summary1 82

Повторение с помощью while Link to heading

Цикл while продолжается до тех пор, пока управляющее логическое выражение возвращает true: while ( логическое -выражение) { // Код, который будет повторяться } Логическое выражение оценивается один раз в начале цикла и снова перед каждой последующей итерацией. // Summary1/While.kt fun testCondition(i: Int ) = i < 100 fun main() { var i = 0 while (testCondition(i)) { print(".") i += 10 } } /* Вывод: ………. */ Kotlin выводит логический тип как результат для testCondition(). Сокращенные версии операторов присваивания доступны для всех математических операций ( += , -= , = , /= , %= ). Kotlin также поддерживает операторы инкремента и декремента ++ и –, как в префиксной, так и в постфиксной форме. while может использоваться с ключевым словом do: do { // Код, который будет повторяться } while ( логическое -выражение) Переписываем While.kt: AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC Summary1 83 // Summary1/DoWhile.kt fun main() { var i = 0 do { print(".") i += 10 } while (testCondition(i)) } / Вывод: ………. */ Единственное отличие между while и do-while заключается в том, что тело do-while всегда выполняется хотя бы один раз, даже если логическое выражение возвращает false в первый раз.

Циклы и диапазоны Link to heading

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

// Summary1/StringIteration.kt
fun main() {
    for (c in "Kotlin") {
        print(" $c ")
        // c += 1 // ошибка: 
        // val не может быть переназначен
    }
}
/* Вывод:
K o t l i n
*/

Переменная c не может быть явно определена как var или val — Kotlin автоматически делает её val и выводит её тип как Char (вы можете явно указать тип, но на практике это редко делается).

Вы можете перебирать целочисленные значения, используя диапазоны:

// Summary1/RangeOfInt.kt
fun main() {
    for (i in 1..10) {
        print(" $i ")
    }
}
/* Вывод:
1 2 3 4 5 6 7 8 9 10
*/

Создание диапазона с помощью .. включает оба конца, но until исключает верхнюю границу: 1 until 10 эквивалентно 1..9. Вы можете указать значение шага, используя step: 1..21 step 3.

Ключевое слово in Link to heading

Тот же оператор in, который используется для итерации в цикле for, также позволяет проверять принадлежность к диапазону. Оператор !in возвращает true, если проверяемое значение не находится в диапазоне: // Summary1/Membership.kt fun inNumRange(n: Int) = n in 50..100
fun notLowerCase(ch: Char) = ch !in ‘a’..‘z’
fun main() {
val i1 = 11
val i2 = 100
val c1 = ‘K’
val c2 = ‘k’
println(" $ i1 ${ inNumRange(i1) } “)
println(” $ i2 ${ inNumRange(i2) } “)
println(” $ c1 ${ notLowerCase(c1) } “)
println(” $ c2 ${ notLowerCase(c2) } “)
}
/* Вывод: 11 false AtomicKotlin(www.AtomicKotlin.com) авторы Bruce Eckel и Svetlana Isakova, ©2021 MindView LLC Summary1 85 100 true K true k false */
in также может использоваться для проверки принадлежности к диапазонам с плавающей запятой, хотя такие диапазоны могут быть определены только с помощью .., а не до.

Выражения и операторы Link to heading

Наименьший полезный фрагмент кода в большинстве языков программирования — это либо оператор, либо выражение. У них есть одно основное различие:

  • Оператор изменяет состояние.
  • Выражение выражает.

То есть выражение производит результат, в то время как оператор — нет. Поскольку оператор не возвращает ничего, он должен изменить состояние своего окружения (то есть создать побочный эффект), чтобы сделать что-то полезное.

Практически все в Kotlin является выражением:

val hours = 10
val minutesPerHour = 60
val minutes = hours * minutesPerHour

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

Функции, такие как println(), не кажутся производящими результат, но поскольку они все еще являются выражениями, они должны что-то возвращать. В Kotlin для этого существует специальный тип Unit:

// Summary1/UnitReturn.kt
fun main() {
    val result = println("возвращает Unit")
    println(result)
}
/* Вывод:
возвращает Unit
kotlin.Unit
*/

Опытные программисты должны перейти к Резюме 2 после выполнения упражнений для этого атома. Упражнения и решения можно найти на www.AtomicKotlin.com.

Раздел II: Введение в Link to heading

Объекты Link to heading

Объекты являются основой для множества современных языков, включая Kotlin. В объектно-ориентированном (ОО) языке программирования вы обнаруживаете «существительные» в решаемой вами задаче и переводите эти существительные в объекты. Объекты хранят данные и выполняют действия. Объектно-ориентированный язык создает и использует объекты. Kotlin не только объектно-ориентированный; он также функциональный. Функциональные языки сосредотачиваются на действиях, которые вы выполняете («глаголы»). Kotlin является гибридным объектно-функциональным языком.

  • Этот раздел объясняет основы объектно-ориентированного программирования.
  • Раздел IV: Функциональное программирование вводит в функциональное программирование.
  • Раздел V: Объектно-ориентированное программирование подробно рассматривает объектно-ориентированное программирование.

Объекты повсюду Link to heading

Объекты хранят данные, используя свойства (val и var), и выполняют операции с этими данными, используя функции.
Некоторые определения:
• Класс: Определяет свойства и функции для того, что по сути является новым типом данных. Классы также называются пользовательскими типами.
• Член: Либо свойство, либо функция класса.
• Член-функция: Функция, которая работает только с конкретным классом объектов.
• Создание объекта: Создание val или var класса. Также называется созданием экземпляра этого класса.
Поскольку классы определяют состояние и поведение, мы можем даже ссылаться на экземпляры встроенных типов, таких как Double или Boolean, как на объекты.
Рассмотрим класс IntRange в Kotlin:

// ObjectsEverywhere/IntRanges.kt
fun main() {
    val r1 = IntRange(0, 10)
    val r2 = IntRange(5, 7)
    println(r1)
    println(r2)
}
/* Вывод:
0..10
5..7
*/

Мы создаем два объекта (экземпляра) класса IntRange. Каждый объект имеет свой собственный участок памяти. IntRange — это класс, но конкретный диапазон r1 от 0 до 10 является объектом, который отличается от диапазона r2.
Для объекта IntRange доступно множество операций. Некоторые из них просты, такие как sum(), а другие требуют большего понимания, прежде чем вы сможете их использовать. Если вы попытаетесь вызвать функцию, которая требует аргументов, IDE запросит эти аргументы.
Чтобы узнать о конкретной член-функции, посмотрите ее в документации Kotlin. Обратите внимание на значок лупы в верхнем правом углу страницы. Нажмите на него и введите IntRange в поле поиска. Нажмите на kotlin.ranges > IntRange из полученного поиска. Вы увидите документацию для класса IntRange. Вы можете изучить все член-функции — интерфейс программирования приложений (API) — этого класса. Хотя вы не поймете большую часть из этого на данный момент, полезно привыкнуть искать информацию в документации Kotlin.
IntRange — это своего рода объект, и определяющей характеристикой объекта является то, что вы выполняете операции с ним. Вместо “выполнения операции” мы говорим “вызов член-функции”. Чтобы вызвать член-функцию для объекта, начните с идентификатора объекта, затем поставьте точку, а затем укажите имя операции:

// ObjectsEverywhere/RangeSum.kt
fun main() {
    val r = IntRange(0, 10)
    println(r.sum())
}
/* Вывод:
55
*/

Поскольку sum() — это член-функция, определенная для IntRange, вы вызываете ее, написав r.sum(). Это суммирует все числа в этом IntRange.
Ранее объектно-ориентированные языки использовали фразу “отправка сообщения”, чтобы описать вызов член-функции для объекта. Иногда вы все еще увидите эту терминологию.
Классы могут иметь множество операций (член-функций). Легко исследовать классы, используя IDE (интегрированную среду разработки), которая включает функцию, называемую автозаполнением кода. Например, если вы наберете .s после идентификатора объекта в IntelliJ IDEA, она покажет все члены этого объекта, которые начинаются с s:
19 https://kotlinlang.org/api/latest/jvm/stdlib/index.html
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
Объекты повсюду 90
Автозаполнение кода
Попробуйте использовать автозаполнение кода на других объектах. Например, вы можете развернуть строку или преобразовать все символы в нижний регистр:

// ObjectsEverywhere/Strings.kt
fun main() {
    val s = "AbcD"
    println(s.reversed())
    println(s.lowercase())
}
/* Вывод:
DcbA
abcd
*/

Вы можете легко преобразовать строку в целое число и обратно:

// ObjectsEverywhere/Conversion.kt
fun main() {
    val s = "123"
    println(s.toInt())
    val i = 123
    println(i.toString())
}
/* Вывод:
123
123
*/

Позже в книге мы обсудим стратегии обработки ситуаций, когда строка, которую вы хотите преобразовать, не представляет собой корректное целое значение.
Вы также можете преобразовать один числовой тип в другой. Чтобы избежать путаницы, преобразования между числовыми типами являются явными. Например, вы можете преобразовать Int i в Long, вызвав i.toLong(), или в Double с помощью i.toDouble():

// ObjectsEverywhere/NumberConversions.kt
fun fraction(numerator: Long, denom: Long) =
    numerator.toDouble() / denom

fun main() {
    val num = 1
    val den = 2
    val f = fraction(num.toLong(), den.toLong())
    println(f)
}
/* Вывод:
0.5
*/

Хорошо определенные классы легко понять программисту и они производят код, который легко читать.
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC

Создание классов Link to heading

Вы можете не только использовать предопределенные типы, такие как IntRange и String, но и создавать свои собственные типы объектов. Действительно, создание новых типов составляет большую часть деятельности в объектно-ориентированном программировании. Вы создаете новые типы, определяя классы. Объект — это часть решения проблемы, которую вы пытаетесь решить. Начните с того, чтобы рассматривать объекты как выражение концепций. В качестве первого приближения, если вы обнаружите «вещь» в вашей проблеме, представьте эту вещь как объект в вашем решении.

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

// CreatingClasses/Animals.kt
// Создаем несколько классов:
class Giraffe
class Bear
class Hippo

fun main() {
    // Создаем несколько объектов:
    val g1 = Giraffe()
    val g2 = Giraffe()
    val b = Bear()
    val h = Hippo()
    // Каждый объект() уникален:
    println(g1)
    println(g2)
    println(h)
    println(b)
}
/* Пример вывода:
Giraffe@28d93b30
Giraffe@1b6d3586
Hippo@4554617c
Bear@74a14482
*/

Чтобы определить класс, начните с ключевого слова class, за которым следует идентификатор для вашего нового класса. Имя класса должно начинаться с буквы (A-Z, верхний или нижний регистр), но может включать такие вещи, как цифры и подчеркивания. Следуя общепринятой практике, мы пишем с заглавной буквы первую букву имени класса, а первую букву всех val и var — строчной. Animals.kt начинается с определения трех новых классов, а затем создает четыре объекта (также называемых экземплярами) этих классов. Giraffe — это класс, но конкретный пятилетний самец жирафа, который живет в Ботсване, — это объект. Каждый объект отличается от всех остальных, поэтому мы даем им имена, такие как g1 и g2.

Обратите внимание на довольно загадочный вывод последних четырех строк. Часть перед @ — это имя класса, а число после @ — это адрес, по которому объект находится в памяти вашего компьютера. Да, это число, даже если оно включает некоторые буквы — это называется «шестнадцатеричная нотация». Каждый объект в вашей программе имеет свой уникальный адрес.

Определенные здесь классы (Giraffe, Bear и Hippo) максимально просты: вся определение класса занимает одну строку. Более сложные классы используют фигурные скобки ({ и }) для создания тела класса, содержащего характеристики и поведение для этого класса. Функция, определенная внутри класса, принадлежит этому классу. В Kotlin мы называем эти функции членами класса. Некоторые объектно-ориентированные языки, такие как Java, предпочитают называть их методами, термин, который пришел из ранних объектно-ориентированных языков, таких как Smalltalk. Чтобы подчеркнуть функциональную природу Kotlin, разработчики решили отказаться от термина метод, так как некоторые начинающие программисты находили это различие запутанным. Вместо этого термин функция используется на протяжении всего языка.

Если это однозначно, мы просто скажем «функция». Если нам нужно сделать различие:

  • Члены функций принадлежат классу.
  • Функции верхнего уровня существуют сами по себе и не являются частью класса.

Вот пример, где bark() принадлежит классу Dog:

// CreatingClasses/Dog.kt
class Dog {
    fun bark() = "гав!"
}

fun main() {
    val dog = Dog()
}

В main() мы создаем объект Dog и присваиваем его переменной val dog. Kotlin выдает предупреждение, потому что мы никогда не используем dog. Члены функций вызываются (инвокируются) с именем объекта, за которым следует точка (.), затем имя функции и список параметров. Здесь мы вызываем функцию meow() и отображаем результат:

// CreatingClasses/Cat.kt
class Cat {
    fun meow() = "мяу!"
}

fun main() {
    val cat = Cat()
    // Вызываем 'meow()' для 'cat':
    val m1 = cat.meow()
    println(m1)
}
/* Вывод:
мяу!
*/

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

При вызове члена функции Kotlin отслеживает интересующий объект, тихо передавая ссылку на этот объект. Эта ссылка доступна внутри члена функции с помощью ключевого слова this. Члены функций имеют специальный доступ к другим элементам внутри класса, просто называя эти элементы. Вы также можете явно квалифицировать доступ к этим элементам, используя this. Здесь exercise() вызывает speak() с и без квалификации:

// CreatingClasses/Hamster.kt
class Hamster {
    fun speak() = "Пи-пи! "
    fun exercise() =
        this.speak() + // Квалифицировано с 'this'
        speak() + // Без 'this'
        "Бег на колесе"
}

fun main() {
    val hamster = Hamster()
    println(hamster.exercise())
}
/* Вывод:
Пи-пи! Пи-пи! Бег на колесе
*/

В exercise() мы сначала вызываем speak() с явным this, а затем опускаем квалификацию. Иногда вы увидите код, содержащий ненужный явный this. Такой код часто исходит от программистов, которые знают другой язык, где this либо требуется, либо является частью его стиля. Использование функции без необходимости сбивает с толку читателя, который тратит время на то, чтобы понять, почему вы это делаете. Мы рекомендуем избегать ненужного использования this.

Снаружи класса вы должны использовать hamster.exercise() и hamster.speak(). Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Свойства Link to heading

Свойство — это var или val, которые являются частью класса. Определение свойства поддерживает состояние внутри класса. Поддержание состояния является основной причиной создания класса, а не просто написания одной или нескольких независимых функций. Свойство var может быть переназначено, в то время как свойство val — нет. Каждый объект получает свое собственное хранилище для свойств:

// Properties/Cup.kt
class Cup {
    var percentFull = 0
}

fun main() {
    val c1 = Cup()
    c1.percentFull = 50
    val c2 = Cup()
    c2.percentFull = 100
    println(c1.percentFull)
    println(c2.percentFull)
}
/* Вывод:
50
100
*/

Определение var или val внутри класса выглядит так же, как и определение их внутри функции. Однако var или val становятся частью этого класса, и вы должны ссылаться на них, указывая его объект с помощью точечной нотации, ставя точку между объектом и именем свойства. Вы можете видеть, как используется точечная нотация для каждой ссылки на percentFull. Свойство percentFull представляет состояние соответствующего объекта Cup. c1.percentFull и c2.percentFull содержат разные значения, показывая, что каждый объект имеет свое собственное хранилище.

Член-функция может ссылаться на свойство внутри своего объекта без использования точечной нотации (то есть без квалификации):

// Properties/Cup2.kt
class Cup2 {
    var percentFull = 0
    val max = 100

    fun add(increase: Int): Int {
        percentFull += increase
        if (percentFull > max)
            percentFull = max
        return percentFull
    }
}

fun main() {
    val cup = Cup2()
    cup.add(50)
    println(cup.percentFull)
    cup.add(70)
    println(cup.percentFull)
}
/* Вывод:
50
100
*/

Функция add() пытается добавить increase к percentFull, но гарантирует, что оно не превысит 100%. Вы должны квалифицировать как свойства, так и член-функции снаружи класса.

Вы можете определить свойства верхнего уровня:

// Properties/TopLevelProperty.kt
val constant = 42
var counter = 0

fun inc() {
    counter++
}

Определение свойства верхнего уровня val безопасно, потому что его нельзя изменить. Однако определение изменяемого (var) свойства верхнего уровня считается антипаттерном. По мере усложнения вашей программы становится труднее правильно рассуждать о совместимом изменяемом состоянии. Если каждый в вашей кодовой базе может получить доступ к var counter, вы не можете гарантировать, что он будет изменяться правильно: пока inc() увеличивает counter на один, какая-то другая часть программы может уменьшить counter на десять, что приведет к неясным ошибкам. Лучше всего защищать изменяемое состояние внутри класса. В разделе “Ограничение видимости” вы увидите, как сделать его действительно скрытым.

Сказать, что var можно изменять, а val — нет, является упрощением. В качестве аналогии рассмотрим дом как val, а диван внутри дома как var. Вы можете изменить диван, потому что это var. Однако вы не можете переназначить дом, потому что это val:

// Properties/ChangingAVal.kt
class House {
    var sofa: String = ""
}

fun main() {
    val house = House()
    house.sofa = "Простой диван-кровать: $89.00"
    println(house.sofa)
    house.sofa = "Новый кожаный диван: $3,099.00"
    println(house.sofa)
    // Нельзя переназначить val на новый House:
    // house = House()
}
/* Вывод:
Простой диван-кровать: $89.00
Новый кожаный диван: $3,099.00
*/

Хотя house является val, его объект может быть изменен, потому что sofa в классе House является var. Определение house как val только предотвращает его переназначение на новый объект.

Если мы сделаем свойство val, его нельзя будет переназначить:

// Properties/AnUnchangingVar.kt
class Sofa {
    val cover: String = "Чехол для дивана"
}

fun main() {
    var sofa = Sofa()
    // Не разрешено:
    // sofa.cover = "Новый чехол"
    // Переназначение var:
    sofa = Sofa()
}

Хотя sofa является var, его объект не может быть изменен, потому что cover в классе Sofa является val. Однако sofa может быть переназначен на новый объект.

Мы говорили о идентификаторах, таких как house и sofa, как будто они были объектами. На самом деле это ссылки на объекты. Один из способов увидеть это — наблюдать, что два идентификатора могут ссылаться на один и тот же объект:

// Properties/References.kt
class Kitchen {
    var table: String = "Круглый стол"
}

fun main() {
    val kitchen1 = Kitchen()
    val kitchen2 = kitchen1
    println("kitchen1: ${kitchen1.table} ")
    println("kitchen2: ${kitchen2.table} ")
    kitchen1.table = "Квадратный стол"
    println("kitchen1: ${kitchen1.table} ")
    println("kitchen2: ${kitchen2.table} ")
}
/* Вывод:
kitchen1: Круглый стол
kitchen2: Круглый стол
kitchen1: Квадратный стол
kitchen2: Квадратный стол
*/

Когда kitchen1 изменяет table, kitchen2 видит это изменение. kitchen1.table и kitchen2.table отображают один и тот же вывод.

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

Изменяемость означает, что объект может изменять свое состояние. В приведенных выше примерах классы House и Kitchen определяют изменяемые объекты, в то время как класс Sofa определяет неизменяемые объекты.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Конструкторы Link to heading

Вы инициализируете новый объект, передавая информацию в конструктор. Каждый объект — это изолированный мир. Программа — это коллекция объектов, поэтому правильная инициализация каждого отдельного объекта решает большую часть проблемы инициализации. Kotlin включает механизмы, которые гарантируют правильную инициализацию объектов. Конструктор — это как специальная функция-член, которая инициализирует новый объект. Самая простая форма конструктора — это однострочное определение класса:

// Constructors/Wombat.kt
class Wombat
fun main() {
    val wombat = Wombat()
}

В main(), вызов Wombat() создает объект Wombat. Если вы приходите из другого объектно-ориентированного языка, вы можете ожидать увидеть здесь ключевое слово new, но new было бы избыточным в Kotlin, поэтому оно было опущено. Вы передаете информацию в конструктор, используя список параметров, так же как и в функции. Здесь конструктор Alien принимает единственный аргумент:

// Constructors/Arg.kt
class Alien(name: String) {
    val greeting = "Poor $name!"
}
fun main() {
    val alien = Alien("Mr. Meeseeks")
    println(alien.greeting)
    // alien.name // Ошибка // [1]
}
/* Вывод:
Poor Mr. Meeseeks!
*/

Создание объекта Alien требует аргумент (попробуйте сделать это без него). name инициализирует свойство greeting внутри конструктора, но оно недоступно вне конструктора — попробуйте раскомментировать строку [1]. Если вы хотите, чтобы параметр конструктора был доступен вне тела класса, определите его как var или val в списке параметров:

// Constructors/VisibleArgs.kt
class MutableNameAlien(var name: String)
class FixedNameAlien(val name: String)
fun main() {
    val alien1 = MutableNameAlien("Reverse Giraffe")
    val alien2 = FixedNameAlien("Krombopulos Michael")
    alien1.name = "Parasite"
    // Нельзя сделать это:
    // alien2.name = "Parasite"
}

Эти определения классов не имеют явных тел классов — тела подразумеваются. Когда name определяется как var или val, он становится свойством и, таким образом, доступен вне конструктора. Параметры конструктора val не могут быть изменены, в то время как параметры конструктора var изменяемы. Ваш класс может иметь множество параметров конструктора:

// Constructors/MultipleArgs.kt
class AlienSpecies(
    val name: String,
    val eyes: Int,
    val hands: Int,
    val legs: Int
) {
    fun describe() =
        "$name with $eyes eyes, " +
        "$hands hands and $legs legs"
}
fun main() {
    val kevin = AlienSpecies("Zigerion", 2, 2, 2)
    val mortyJr = AlienSpecies("Gazorpian", 2, 6, 2)
    println(kevin.describe())
    println(mortyJr.describe())
}
/* Вывод:
Zigerion with 2 eyes, 2 hands and 2 legs
Gazorpian with 2 eyes, 6 hands and 2 legs
*/

В разделе “Сложные конструкторы” вы увидите, что конструкторы также могут содержать сложную логику инициализации. Если объект используется, когда ожидается строка, Kotlin вызывает функцию-член toString() этого объекта. Если вы не напишете одну, вы все равно получите значение по умолчанию для toString():

// Constructors/DisplayAlienSpecies.kt
fun main() {
    val krombopulosMichael = AlienSpecies("Gromflomite", 2, 2, 2)
    println(krombopulosMichael)
}
/* Пример вывода:
AlienSpecies@4d7e1886
*/

Значение по умолчанию для toString() не очень полезно — оно выдает имя класса и физический адрес объекта (это варьируется от одного выполнения программы к другому). Вы можете определить свой собственный toString():

// Constructors/Scientist.kt
class Scientist(val name: String) {
    override fun toString() =
        "Scientist('$name')"
}
fun main() {
    val zeep = Scientist("Zeep Xanflorp")
    println(zeep)
}
/* Вывод:
Scientist('Zeep Xanflorp')
*/

override — это новое для нас ключевое слово. Оно требуется здесь, потому что toString() уже имеет определение, которое выдает примитивный результат. override говорит Kotlin, что да, мы действительно хотим заменить значение по умолчанию для toString() нашим собственным определением. Ясность override проясняет код и предотвращает ошибки. toString(), который отображает содержимое объекта в удобной форме, полезен для поиска и исправления ошибок программирования. Чтобы упростить процесс отладки, IDE предоставляют отладчики, которые позволяют вам наблюдать за каждым шагом выполнения программы и заглядывать внутрь ваших объектов.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Ограничение видимости Link to heading

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

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

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

Чтобы контролировать видимость, Kotlin и некоторые другие языки предоставляют модификаторы доступа. Создатели библиотек решают, что доступно, а что нет для клиентского программиста, используя модификаторы public, private, protected и internal. Этот раздел охватывает public и private, с кратким введением в internal. Мы объясним protected позже в книге.

Модификатор доступа, такой как private, появляется перед определением класса, функции или свойства. Модификатор доступа контролирует доступ только для этого конкретного определения. Определение public доступно клиентским программистам, поэтому изменения в этом определении напрямую влияют на клиентский код. Если вы не предоставите модификатор, ваше определение автоматически будет public, поэтому public технически избыточен. Тем не менее, вы иногда все равно будете указывать public ради ясности.

Определение private скрыто и доступно только из других членов того же класса. Изменение или даже удаление определения private не влияет напрямую на клиентских программистов.

private классы, функции верхнего уровня и свойства верхнего уровня доступны только внутри этого файла:

// Visibility/RecordAnimals.kt
private var index = 0 // [1]
private class Animal (val name: String) // [2]
private fun recordAnimal( // [3]
    animal: Animal
) {
    println("Animal #$index: ${animal.name}")
    index++
}
fun recordAnimals() {
    recordAnimal(Animal("Tiger"))
    recordAnimal(Animal("Antelope"))
}
fun recordAnimalsCount() {
    println("$index animals are here!")
}

Вы можете получить доступ к private свойствам верхнего уровня ( [1] ), классам ( [2] ) и функциям ( [3] ) из других функций и классов внутри RecordAnimals.kt. Kotlin предотвращает доступ к private элементам верхнего уровня из другого файла, сообщая вам, что это private в файле:

// Visibility/ObserveAnimals.kt
fun main() {
    // Нельзя получить доступ к private членам,
    // объявленным в другом файле.
    // Класс является private:
    // val rabbit = Animal("Rabbit")
    // Функция является private:
    // recordAnimal(rabbit)
    // Свойство является private:
    // index++
    recordAnimals()
    recordAnimalsCount()
}
/* Вывод:
Animal #0: Tiger
Animal #1: Antelope
2 animals are here!
*/

Часто private используется для членов класса:

// Visibility/Cookie.kt
class Cookie (
    private var isReady: Boolean // [1]
) {
    private fun crumble() = // [2]
        println("crumble")
    public fun bite() = // [3]
        println("bite")
    fun eat() { // [4]
        isReady = true // [5]
        crumble()
        bite()
    }
}
fun main() {
    val x = Cookie(false)
    x.bite()
    // Нельзя получить доступ к private членам:
    // x.isReady
    // x.crumble()
    x.eat()
}
/* Вывод:
bite
crumble
bite
*/
  • [1] private свойство, недоступное вне содержащего класса.
  • [2] private член-функция.
  • [3] public член-функция, доступная любому.
  • [4] Отсутствие модификатора доступа означает public.
  • [5] Только члены одного и того же класса могут получать доступ к private членам.

Ключевое слово private означает, что никто не может получить доступ к этому члену, кроме других членов этого класса. Другие классы не могут получить доступ к private членам, так что это как будто вы также изолируете класс от себя и ваших коллег. С private вы можете свободно изменять этот член, не беспокоясь о том, повлияет ли это на другой класс в том же пакете. Как дизайнер библиотеки, вы обычно будете держать вещи как можно более закрытыми и открывать только функции и классы для клиентских программистов.

Любая член-функция, которая является вспомогательной функцией для класса, может быть сделана private, чтобы гарантировать, что вы случайно не используете ее в другом месте в пакете и тем самым не запрещаете себе изменять или удалять эту функцию. То же самое верно и для private свойства внутри класса. Если вам не нужно раскрывать основную реализацию (что менее вероятно, чем вы могли бы подумать), сделайте свойства private. Однако просто потому, что ссылка на объект является private внутри класса, это не означает, что какой-то другой объект не может иметь публичную ссылку на тот же объект:

// Visibility/MultipleRef.kt
class Counter (var start: Int) {
    fun increment() {
        start += 1
    }
    override fun toString() = start.toString()
}
class CounterHolder (counter: Counter) {
    private val ctr = counter
    override fun toString() =
        "CounterHolder: " + ctr
}
fun main() {
    val c = Counter(11) // [1]
    val ch = CounterHolder(c) // [2]
    println(ch)
    c.increment() // [3]
    println(ch)
    val ch2 = CounterHolder(Counter(9)) // [4]
    println(ch2)
}
/* Вывод:
CounterHolder: 11
CounterHolder: 12
CounterHolder: 9
*/
  • [1] c теперь определен в области, окружающей создание объекта CounterHolder на следующей строке.
  • [2] Передача c в качестве аргумента конструктору CounterHolder означает, что новый CounterHolder теперь ссылается на тот же объект Counter, на который ссылается c.
  • [3] Counter, который предположительно является private внутри ch, все еще может быть изменен через c.
  • [4] Counter(9) не имеет других ссылок, кроме как внутри CounterHolder, поэтому его нельзя получить или изменить ничем, кроме ch2.

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

Модули Link to heading

В отличие от небольших примеров в этой книге, реальные программы часто бывают большими. Полезно делить такие программы на один или несколько модулей. Модуль — это логически независимая часть кодовой базы. То, как вы делите проект на модули, зависит от системы сборки (например, Gradle22 или Maven23) и выходит за рамки этой книги.

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

Модули — это концепция более высокого уровня. Следующий атом вводит пакеты, которые позволяют более тонкую структуру. Библиотека часто является одним модулем, состоящим из нескольких пакетов, поэтому внутренние элементы доступны внутри библиотеки, но недоступны для потребителей этой библиотеки.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.
22 https://gradle.org/
23 https://maven.apache.org/
AtomicKotlin (www.AtomicKotlin.com) Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Пакеты Link to heading

Фундаментальным принципом программирования является акроним DRY: Не Повторяй Себя. Несколько идентичных фрагментов кода требуют обслуживания каждый раз, когда вы вносите исправления или улучшения. Поэтому дублирование кода — это не просто лишняя работа: каждое дублирование создает возможности для ошибок. Ключевое слово import повторно использует код из других файлов. Один из способов использования import — указать имя класса, функции или свойства: import packagename.ClassName import packagename.functionName import packagename.propertyName Пакет — это связанная коллекция кода. Каждый пакет обычно предназначен для решения конкретной задачи и часто содержит несколько функций и классов. Например, мы можем импортировать математические константы и функции из библиотеки kotlin.math: // Packages/ImportClass.kt import kotlin.math.PI import kotlin.math.cos // Косинус fun main() { println(PI) println(cos(PI)) println(cos(2 * PI)) } /* Вывод: 3.141592653589793 -1.0 1.0 / Иногда вам нужно использовать несколько сторонних библиотек, содержащих классы или функции с одинаковыми именами. Ключевое слово as позволяет вам изменить имена при импорте: // Packages/ImportNameChange.kt import kotlin.math.PI as circleRatio import kotlin.math.cos as cosine fun main() { println(circleRatio) println(cosine(circleRatio)) println(cosine(2 * circleRatio)) } / Вывод: 3.141592653589793 -1.0 1.0 / as полезно, если имя библиотеки выбрано неудачно или слишком длинное. Вы можете полностью квалифицировать импорт в теле вашего кода. В следующем примере код может быть менее читаемым из-за явных имен пакетов, но происхождение каждого элемента абсолютно ясно: // Packages/FullyQualify.kt fun main() { println(kotlin.math.PI) println(kotlin.math.cos(kotlin.math.PI)) println(kotlin.math.cos(2 * kotlin.math.PI)) } / Вывод: 3.141592653589793 -1.0 1.0 / Чтобы импортировать все из пакета, используйте звездочку: AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC // Packages/ImportEverything.kt import kotlin.math. fun main() { println(E) println(E.roundToInt()) println(E.toInt()) } /* Вывод: 2.718281828459045 3 2 / Пакет kotlin.math содержит удобный метод roundToInt(), который округляет значение типа Double до ближайшего целого числа, в отличие от toInt(), который просто отсекает все после десятичной точки. Чтобы повторно использовать ваш код, создайте пакет с помощью ключевого слова package. Оператор package должен быть первым некомментарий оператором в файле. После package следует имя вашего пакета, которое по соглашению пишется строчными буквами: // Packages/PythagoreanTheorem.kt package pythagorean import kotlin.math.sqrt class RightTriangle ( val a: Double , val b: Double ) { fun hypotenuse() = sqrt(a * a + b * b) fun area() = a * b / 2 } Вы можете назвать файл исходного кода как угодно, в отличие от Java, которая требует, чтобы имя файла совпадало с именем класса. Kotlin позволяет вам выбирать любое имя для вашего пакета, но считается хорошим стилем, если имя пакета совпадает с именем директории, в которой находятся файлы пакета (это не всегда будет так для примеров в этой книге). Элементы в пакете pythagorean теперь доступны с помощью import: // Packages/ImportPythagorean.kt import pythagorean.RightTriangle fun main() { val rt = RightTriangle(3.0, 4.0) println(rt.hypotenuse()) println(rt.area()) } / Вывод: 5.0 6.0 */ В оставшейся части этой книги мы используем операторы package для любого файла, который определяет функции, классы и т. д., вне main(), чтобы предотвратить конфликты имен с другими файлами в книге, но обычно мы не будем помещать оператор package в файл, который содержит только main(). Упражнения и решения можно найти на www.AtomicKotlin.com. AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Тестирование Link to heading

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

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

Использование println() для проверки корректности кода — это слабый подход: вам нужно каждый раз внимательно проверять вывод и сознательно удостоверяться, что он правильный.

Чтобы упростить ваш опыт работы с этой книгой, мы создали нашу собственную небольшую систему тестирования. Цель — минимальный подход, который:

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

Хотя наша система полезна для этой книги, она не является тестовой системой для рабочего места. Другие трудились долго и упорно, чтобы создать такие тестовые системы. Например:

  • JUnit — один из самых популярных тестовых фреймворков для Java и легко используется из Kotlin.
  • Kotest разработан специально для Kotlin и использует возможности языка Kotlin.
  • Spek Framework производит другую форму тестирования, называемую Спецификационным тестированием.

Чтобы использовать наш тестовый фреймворк, мы сначала должны его импортировать. Основные элементы фреймворка — это eq (равно) и neq (не равно):

// Testing/TestingExample.kt
import atomictest.*

fun main() {
    val v1 = 11
    val v2 = "Ontology"
    // 'eq' означает "равно":
    v1 eq 11
    v2 eq "Ontology"
    // 'neq' означает "не равно"
    v2 neq "Epistemology"
    // [Ошибка] Epistemology != Ontology
    // v2 eq "Epistemology"
}
/* Вывод:
11
Ontology
Ontology
*/

Код для пакета atomictest находится в Приложении A: AtomicTest. Мы не предполагаем, что вы поймете все в AtomicTest.kt прямо сейчас, потому что он использует некоторые функции, которые появятся позже в книге.

Чтобы создать чистый и удобный вид, AtomicTest использует функцию Kotlin, которую вы еще не видели: возможность записывать вызов функции в текстовом виде a.function(b) в форме a function b. Это называется инфиксной нотацией. Только функции, определенные с использованием ключевого слова infix, могут быть вызваны таким образом. AtomicTest.kt определяет инфиксные функции eq и neq, используемые в TestingExample.kt:

expression eq expected
expression neq expected

eq и neq являются гибкими — почти все работает как тестовое выражение. Если expected является строкой, то expression преобразуется в строку, и две строки сравниваются. В противном случае expression и expected сравниваются напрямую (без предварительного преобразования). В любом случае результат expression отображается на консоли, так что вы видите что-то, когда программа выполняется. Даже когда тесты проходят успешно, вы все равно видите результат слева от eq или neq. Если expression и expected не эквивалентны, AtomicTest показывает ошибку, когда программа выполняется.

Последний тест в TestingExample.kt намеренно не проходит, чтобы вы увидели пример вывода ошибки. Если два значения не равны, Kotlin отображает соответствующее сообщение, начинающееся с [Ошибка]. Если вы раскомментируете последнюю строку и запустите пример выше, вы увидите, после всех успешных тестов:

[Ошибка] Epistemology != Ontology

Фактическое значение, хранящееся в v2, не соответствует тому, что утверждается в выражении “ожидаемое”. AtomicTest отображает строковые представления как ожидаемых, так и фактических значений.

eq и neq — это основные (инфиксные) функции, определенные для AtomicTest — это действительно минимальная тестовая система. Когда вы вставляете выражения eq и neq в свои примеры, вы создаете как тест, так и некоторый вывод в консоль. Вы проверяете корректность программы, запуская ее.

В AtomicTest есть второй инструмент. Объект trace захватывает вывод для последующего сравнения:

// Testing/Trace1.kt
import atomictest.*

fun main() {
    trace("line 1")
    trace(47)
    trace("line 2")
    trace eq """
    line 1
    47
    line 2
    """
}

Добавление результатов в trace выглядит как вызов функции, так что вы можете эффективно заменить println() на trace().

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

С этого момента мы редко используем закомментированные блоки вывода, потому что AtomicTest сделает все за нас. Однако иногда мы все же включаем закомментированные блоки вывода, когда это дает более полезный эффект.

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

Тестирование как часть программирования Link to heading

Тестирование наиболее эффективно, когда оно встроено в процесс разработки программного обеспечения. Написание тестов гарантирует, что вы получите ожидаемые результаты. Многие специалисты рекомендуют писать тесты до написания кода реализации — вы сначала заставляете тест не проходить, прежде чем напишете код, который его пройдет. Эта техника, называемая Разработкой через Тестирование (TDD), является способом убедиться, что вы действительно тестируете то, что думаете. Более полное описание TDD можно найти на Wikipedia (поиск по запросу “TestDrivenDevelopment”).

Существует еще одно преимущество написания тестируемого кода — это меняет способ, которым вы создаете свой код. Вы могли бы просто выводить результаты на консоль. Но в тестовом мышлении вы задаетесь вопросом: “Как я это протестирую?” Когда вы создаете функцию, вы решаете, что должны вернуть что-то из функции, если не по другой причине, то чтобы протестировать этот результат. Функции, которые делают только то, что принимают входные данные и производят выходные данные, как правило, приводят к лучшим проектам.

Вот упрощенный пример использования TDD для реализации расчета ИМТ (индекса массы тела) из раздела “Типы чисел”. Сначала мы пишем тесты вместе с начальной реализацией, которая не проходит (поскольку мы еще не реализовали функциональность):

// Testing/TDDFail.kt
package testing1
import atomictest.eq

fun main() {
    calculateBMI(160, 68) eq "Normal weight"
    // calculateBMI(100, 68) eq "Underweight"
    // calculateBMI(200, 68) eq "Overweight"
}

fun calculateBMI(lbs: Int, height: Int) = "Normal weight"

Только первый тест проходит. Остальные тесты не проходят и закомментированы. Далее мы добавляем код для определения, какие веса относятся к каким категориям. Теперь все тесты не проходят:

// Testing/TDDStillFails.kt
package testing2
import atomictest.eq

fun main() {
    // Все тесты не проходят:
    // calculateBMI(160, 68) eq "Normal weight"
    // calculateBMI(100, 68) eq "Underweight"
    // calculateBMI(200, 68) eq "Overweight"
}

fun calculateBMI(
    lbs: Int,
    height: Int
): String {
    val bmi = lbs / (height * height) * 703.07
    return if (bmi < 18.5) "Underweight"
    else if (bmi < 25) "Normal weight"
    else "Overweight"
}

Мы используем Int вместо Double, что приводит к нулевому результату. Тесты направляют нас к исправлению:

// Testing/TDDWorks.kt
package testing3
import atomictest.eq

fun main() {
    calculateBMI(160.0, 68.0) eq "Normal weight"
    calculateBMI(100.0, 68.0) eq "Underweight"
    calculateBMI(200.0, 68.0) eq "Overweight"
}

fun calculateBMI(
    lbs: Double,
    height: Double
): String {
    val bmi = lbs / (height * height) * 703.07
    return if (bmi < 18.5) "Underweight"
    else if (bmi < 25) "Normal weight"
    else "Overweight"
}

Вы можете выбрать добавление дополнительных тестов для граничных условий. В упражнениях для этой книги мы включаем тесты, которые ваш код должен пройти. Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Исключения Link to heading

Слово “исключение” используется в том же смысле, что и фраза “Я возражаю против этого”. Исключительное состояние предотвращает продолжение текущей функции или области. В момент, когда возникает проблема, вы можете не знать, что с ней делать, но не можете продолжать в текущем контексте. У вас недостаточно информации, чтобы исправить проблему. Поэтому вы должны остановиться и передать проблему в другой контекст, который сможет предпринять соответствующие действия. Этот атом охватывает основы исключений как механизма отчетности об ошибках. В разделе VI: Предотвращение сбоев мы рассмотрим другие способы решения проблем.

Важно различать исключительное состояние и обычную проблему. Обычная проблема имеет достаточно информации в текущем контексте, чтобы справиться с ней. В случае исключительного состояния вы не можете продолжать обработку. Все, что вы можете сделать, это покинуть, передав проблему внешнему контексту. Это то, что происходит, когда вы выбрасываете исключение. Исключение — это объект, который “выбрасывается” из места возникновения ошибки.

Рассмотрим функцию toInt(), которая преобразует строку в целое число. Что произойдет, если вы вызовете эту функцию для строки, которая не содержит целочисленного значения?

// Exceptions/ToIntException.kt
package exceptions

fun erroneousCode() {
    // Раскомментируйте эту строку, чтобы получить исключение:
    // val i = "1$".toInt() // [1]
}

fun main() {
    erroneousCode()
}

Раскомментирование строки [1] вызывает исключение. Здесь неудачная строка закомментирована, чтобы не остановить сборку книги, которая проверяет, компилируется ли каждый пример и работает ли он как ожидалось.

Когда исключение выбрасывается, путь выполнения — тот, который нельзя продолжить — останавливается, и объект исключения выталкивается из текущего контекста. Здесь он выходит из контекста erroneousCode() и переходит в контекст main(). В этом случае Kotlin просто сообщает об ошибке; программист, предположительно, допустил ошибку и должен исправить код.

Когда исключение не перехватывается, программа прерывается и отображает трассировку стека, содержащую подробную информацию. Раскомментирование строки [1] в ToIntException.kt приводит к следующему выводу:

Exception in thread "main" java.lang.NumberFormatException: For input string: "1$"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at ToIntExceptionKt.erroneousCode(at ToIntException.kt:6)
at ToIntExceptionKt.main(at ToIntException.kt:10)

Трассировка стека дает детали, такие как файл и строка, где произошло исключение, так что вы можете быстро обнаружить проблему. Последние две строки показывают проблему: в строке 10 функции main() мы вызываем erroneousCode(). Затем, более точно, в строке 6 функции erroneousCode() мы вызываем toInt().

Чтобы избежать комментирования и раскомментирования кода для отображения исключений, мы используем функцию capture() из пакета AtomicTest:

// Exceptions/IntroducingCapture.kt
import atomictest.*

fun main() {
    capture {
        "1$".toInt()
    } eq "NumberFormatException: " +
    """For input string: "1$""""
}

Используя capture(), мы сравниваем сгенерированное исключение с ожидаемым сообщением об ошибке. capture() не очень полезен для обычного программирования — он разработан специально для этой книги, чтобы вы могли видеть исключение и знать, что вывод был проверен системой сборки книги.

Еще одна стратегия, когда вы не можете успешно получить ожидаемый результат, — вернуть null, что является специальной константой, обозначающей “нет значения”. Вы можете вернуть null вместо значения любого типа. Позже в разделе “Nullable Types” мы обсудим, как null влияет на тип результирующего выражения.

Стандартная библиотека Kotlin содержит String.toIntOrNull(), которая выполняет преобразование, если строка содержит целое число, или возвращает null, если преобразование невозможно — null является простым способом указать на сбой:

// Exceptions/IntroducingNull.kt
import atomictest.eq

fun main() {
    "1$".toIntOrNull() eq null
}

Предположим, мы рассчитываем средний доход за период месяцев:

// Exceptions/AverageIncome.kt
package firstversion

import atomictest.*

fun averageIncome(income: Int, months: Int) =
    income / months

fun main() {
    averageIncome(3300, 3) eq 1100
    capture {
        averageIncome(5000, 0)
    } eq "ArithmeticException: / by zero"
}

Если months равен нулю, деление в averageIncome() вызывает ArithmeticException. К сожалению, это не говорит нам ничего о том, почему произошла ошибка, что означает делитель и может ли он законно быть равен нулю в первую очередь. Это явно ошибка в коде — averageIncome() должен справляться с нулевыми месяцами таким образом, чтобы предотвратить ошибку деления на ноль.

Давайте изменим averageIncome(), чтобы предоставить больше информации о источнике проблемы. Если months равен нулю, мы не можем вернуть обычное целое значение в качестве результата. Одна из стратегий — вернуть null:

// Exceptions/AverageIncomeWithNull.kt
package withnull

import atomictest.eq

fun averageIncome(income: Int, months: Int) =
    if (months == 0)
        null
    else
        income / months

fun main() {
    averageIncome(3300, 3) eq 1100
    averageIncome(5000, 0) eq null
}

Если функция может вернуть null, Kotlin требует, чтобы вы проверили результат перед его использованием (это рассматривается в “Nullable Types”). Даже если вы хотите просто отобразить вывод пользователю, лучше сказать: “Не прошло полных месяцев”, чем “Ваш средний доход за период: null.”

Вместо того чтобы выполнять averageIncome() с неправильными аргументами, вы можете выбросить исключение — выйти и заставить другую часть программы управлять этой проблемой. Вы могли бы просто позволить по умолчанию ArithmeticException, но часто полезнее выбросить конкретное исключение с подробным сообщением об ошибке. Когда, спустя пару лет в производстве, ваше приложение внезапно выбрасывает исключение, потому что новая функция вызывает averageIncome() без должной проверки аргументов, вы будете благодарны за это сообщение:

// Exceptions/AverageIncomeWithException.kt
package properexception

import atomictest.*

fun averageIncome(income: Int, months: Int) =
    if (months == 0)
        throw IllegalArgumentException( // [1]
            "Months can't be zero")
    else
        income / months

fun main() {
    averageIncome(3300, 3) eq 1100
    capture {
        averageIncome(5000, 0)
    } eq "IllegalArgumentException: " +
    "Months can't be zero"
}

• [1] При выбрасывании исключения ключевое слово throw следует за исключением, которое должно быть выброшено, вместе с любыми аргументами, которые могут понадобиться. Здесь мы используем стандартный класс исключения IllegalArgumentException.

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

Упражнения и решения можно найти на www.AtomicKotlin.com.

Списки Link to heading

Список — это контейнер, который является объектом, содержащим другие объекты. Контейнеры также называются коллекциями. Когда нам нужен базовый контейнер для примеров в этой книге, мы обычно используем список. Списки являются частью стандартных пакетов Kotlin, поэтому они не требуют импорта. Следующий пример создает список, заполненный целыми числами (Int), вызывая стандартную библиотечную функцию listOf() с начальными значениями:

// Lists/Lists.kt
import atomictest.eq

fun main() {
    val ints = listOf(99, 3, 5, 7, 11, 13)
    ints eq "[99, 3, 5, 7, 11, 13]" // [1]
    // Выбор каждого элемента в списке:
    var result = ""
    for (i in ints) { // [2]
        result += " $i "
    }
    result eq "99 3 5 7 11 13"
    // "Индексация" в списке:
    ints[4] eq 11 // [3]
}
  • [1] Список использует квадратные скобки при отображении.
  • [2] Циклы for хорошо работают со списками: for(i in ints) означает, что i получает каждое значение из ints. Вам не нужно объявлять val i или задавать ему тип; Kotlin знает из контекста, что i является идентификатором цикла for.
  • [3] Квадратные скобки индексируют список. Список сохраняет свои элементы в порядке инициализации, и вы выбираете их по номеру. Как и в большинстве языков программирования, Kotlin начинает индексацию с нуля, что в этом случае дает значение 99. Таким образом, индекс 4 дает значение 11.

Забывание о том, что индексация начинается с нуля, приводит к так называемой ошибке “off-by-one”. В языке, таком как Kotlin, мы часто не выбираем элементы по одному, а вместо этого проходим через весь контейнер, используя in. Это устраняет ошибки “off-by-one”. Если вы используете индекс, выходящий за пределы последнего элемента в списке, Kotlin выбрасывает исключение ArrayIndexOutOfBoundsException:

// Lists/OutOfBounds.kt
import atomictest.*

fun main() {
    val ints = listOf(1, 2, 3)
    capture {
        ints[3]
    } contains listOf("ArrayIndexOutOfBoundsException")
}

Список может содержать все разные типы. Вот список из чисел с плавающей запятой (Double) и список строк (String):

// Lists/ListUsefulFunction.kt
import atomictest.eq

fun main() {
    val doubles = listOf(1.1, 2.2, 3.3, 4.4)
    doubles.sum() eq 11.0
    val strings = listOf("Twas", "Brillig", "And", "Slithy", "Toves")
    strings eq listOf("Twas", "Brillig", "And", "Slithy", "Toves")
    strings.sorted() eq listOf("And", "Brillig", "Slithy", "Toves", "Twas")
    strings.reversed() eq listOf("Toves", "Slithy", "And", "Brillig", "Twas")
    strings.first() eq "Twas"
    strings.takeLast(2) eq listOf("Slithy", "Toves")
}

Это демонстрирует некоторые операции со списками. Обратите внимание на название “sorted” вместо “sort”. Когда вы вызываете sorted(), он создает новый список, содержащий те же элементы, что и старый, в отсортированном порядке — но оставляет оригинальный список без изменений. Называя его “sort”, подразумевается, что оригинальный список изменяется напрямую (т.е. сортируется на месте). На протяжении всего Kotlin вы видите эту тенденцию “оставлять оригинальный объект без изменений и создавать новый объект”. reversed() также создает новый список.

Параметризованные типы Link to heading

Мы считаем хорошей практикой использовать вывод типов — это делает код более чистым и легким для чтения. Однако иногда Kotlin жалуется, что не может определить, какой тип использовать, а в других случаях явное указание типа делает код более понятным. Вот как мы сообщаем Kotlin о типе, содержащемся в List:

// Lists/ParameterizedTypes.kt
import atomictest.eq

fun main() {
    // Тип выводится автоматически:
    val numbers = listOf(1, 2, 3)
    val strings = listOf("one", "two", "three")
    
    // Точно так же, но с явным указанием типа:
    val numbers2: List<Int> = listOf(1, 2, 3)
    val strings2: List<String> = listOf("one", "two", "three")
    
    numbers eq numbers2
    strings eq strings2
}

Kotlin использует значения инициализации, чтобы вывести, что numbers содержит List целых чисел (Int), в то время как strings содержит List строк (String). numbers2 и strings2 — это версии с явным указанием типа для numbers и strings, созданные путем добавления объявлений типов List<Int> и List<String>. Вы, возможно, видели угловые скобки раньше — они обозначают параметр типа, позволяя вам сказать: «этот контейнер содержит объекты ‘параметра’». Мы произносим List<Int> как «список целых чисел».

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

// Lists/ParameterizedReturn.kt
package lists

import atomictest.eq

// Возвращаемый тип выводится автоматически:
fun inferred(p: Char, q: Char) = listOf(p, q)

// Явный возвращаемый тип:
fun explicit(p: Char, q: Char): List<Char> = listOf(p, q)

fun main() {
    inferred('a', 'b') eq "[a, b]"
    explicit('y', 'z') eq "[y, z]"
}

Kotlin выводит возвращаемый тип для inferred(), в то время как explicit() указывает тип возвращаемого значения функции. Вы не можете просто сказать, что она возвращает List; Kotlin будет жаловаться, поэтому вы должны также указать параметр типа. Когда вы указываете возвращаемый тип функции, Kotlin обеспечивает соблюдение вашего намерения.

Списки только для чтения и изменяемые списки Link to heading

Если вы явно не укажете, что хотите изменяемый список, вы его не получите. listOf() создает список только для чтения, который не имеет функций изменения. Если вы создаете список постепенно (то есть у вас нет всех элементов на момент создания), используйте mutableListOf(). Это создаст изменяемый список, который можно модифицировать:

// Lists/MutableList.kt
import atomictest.eq

fun main() {
    val list = mutableListOf<Int>()
    list.add(1)
    list.addAll(listOf(2, 3))
    list += 4
    list += listOf(5, 6)
    list eq listOf(1, 2, 3, 4, 5, 6)
}

Поскольку у списка нет начальных элементов, мы должны указать Kotlin, какого типа он, предоставив спецификацию <Int> в вызове mutableListOf(). Вы можете добавлять элементы в изменяемый список, используя add() и addAll(), или оператор +=, который добавляет либо один элемент, либо другую коллекцию.

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

// Lists/MutListIsList.kt
package lists

import atomictest.eq

fun makeList(): List<Int> = mutableListOf(1, 2, 3)

fun main() {
    // makeList() создает список только для чтения:
    val list = makeList()
    // list.add(3) // Неразрешенная ссылка: add
    list eq listOf(1, 2, 3)
}

Список не имеет функций изменения, несмотря на то, что он изначально был создан с использованием mutableListOf() внутри makeList(). Обратите внимание, что тип результата makeList()List<Int>. Исходный объект все еще является изменяемым списком, но он рассматривается через призму списка.

Список является только для чтения — вы можете читать его содержимое, но не записывать в него. Если базовая реализация является изменяемым списком и вы сохраняете изменяемую ссылку на эту реализацию, вы все равно можете модифицировать его через эту изменяемую ссылку, и любые ссылки только для чтения увидят эти изменения. Это еще один пример алиасинга, введенного в разделе “Ограничение видимости”:

// Lists/MultipleListRefs.kt
import atomictest.eq

fun main() {
    val first = mutableListOf(1)
    val second: List<Int> = first
    second eq listOf(1)
    first.add(2)
    // second видит изменение:
    second eq listOf(1, 2)
}

first — это неизменяемая ссылка (val) на изменяемый объект, созданный с помощью mutableListOf(1). Когда second ссылается на first, он становится представлением того же объекта. second является только для чтения, потому что List<Int> не включает функции изменения. Без явного объявления типа List<Int> Kotlin бы вывел, что second также является ссылкой на изменяемый объект.

Мы можем добавить элемент (2) в объект, потому что first является ссылкой на изменяемый список. Обратите внимание, что second наблюдает за этими изменениями — он не может изменить список, хотя список изменяется через first.

Загадка с += Link to heading

Оператор += может создать иллюзию того, что неизменяемый список на самом деле изменяемый:
AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Списки 132

// Lists/ApparentlyMutableList.kt
import atomictest.eq

fun main() {
    var list = listOf('X') // Неизменяемый
    list += 'Y' // Кажется, что он изменяемый
    list eq "[X, Y]"
}

Функция listOf() создает неизменяемый список, но list += ‘Y’ кажется, что модифицирует этот список. Нарушает ли += как-то неизменяемость?
Это происходит только потому, что list является var. Вот более детальный пример, который показывает различные комбинации изменяемых/неизменяемых списков с val/var:

// Lists/PlusAssignPuzzle.kt
import atomictest.eq

fun main() {
    // Изменяемый список, присвоенный 'val'/'var':
    val list1 = mutableListOf('A') // или 'var'
    list1 += 'A' // То же самое, что и:
    list1.plusAssign('A') // [1]
    
    // Неизменяемый список, присвоенный 'val':
    val list2 = listOf('B')
    // list2 += 'B' // То же самое, что и:
    // list2 = list2 + 'B' // [2]
    
    // Неизменяемый список, присвоенный 'var':
    var list3 = listOf('C')
    list3 += 'C' // То же самое, что и:
    val newList = list3 + 'C' // [3]
    list3 = newList // [4]
    
    list1 eq "[A, A, A]"
    list2 eq "[B]"
    list3 eq "[C, C, C]"
}

• [1] list1 ссылается на изменяемый объект, который, следовательно, может быть изменен на месте. Компилятор переводит += в вызов plusAssign(). Не имеет значения, является ли list1 val или var, потому что после создания ничего не переназначается в list1 — он всегда ссылается на один и тот же изменяемый список. Сделайте его var, и IntelliJ укажет, что он никогда не меняется, и предложит сделать его val.
• [2] Это пытается создать новый список, комбинируя list2 и ‘B’, но не может переназначить этот новый список в list2, потому что list2 является val. Без возможности выполнить это переназначение += не может быть скомпилирован.
• [3] Создает newList, не модифицируя существующий неизменяемый список, на который ссылается list3.
• [4] Поскольку list3 является var, компилятор присваивает newList обратно в list3. Предыдущие содержимое list3 затем забывается, и кажется, что list3 был изменен. На самом деле старый list3 был утилизирован и заменен вновь созданным newList, создавая иллюзию, что list3 изменяемый.
Это поведение += также происходит с другими коллекциями. Результирующая путаница — еще одна причина выбрать val вместо var для ваших идентификаторов.
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC

Списки переменных аргументов Link to heading

Ключевое слово vararg создает список аргументов переменной длины. В разделе “Списки” мы представили функцию listOf(), которая принимает любое количество параметров и возвращает List: // Varargs/ListOf.kt import atomictest.eq
fun main() {
listOf(1) eq “[1]”
listOf(“a”, “b”) eq “[a, b]”
}
Используя ключевое слово vararg, вы можете определить функцию, которая принимает любое количество аргументов, так же как это делает listOf(). vararg является сокращением для “список переменных аргументов”: // Varargs/VariableArgList.kt package varargs
fun v(s: String, vararg d: Double) {}
fun main() {
v(“abc”, 1.0, 2.0)
v(“def”, 1.0, 2.0, 3.0, 4.0)
v(“ghi”, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
}
В определении функции может быть указан только один параметр как vararg. Хотя возможно указать любой элемент в списке параметров как vararg, обычно проще сделать это для последнего.
vararg позволяет передавать любое количество (включая ноль) аргументов. Все аргументы должны быть указанного типа. Аргументы vararg доступны с использованием имени параметра, которое становится массивом: // Varargs/VarargSum.kt package varargs
import atomictest.eq
fun sum(vararg numbers: Int): Int {
var total = 0
for (n in numbers) {
total += n
}
return total
}
fun main() {
sum(13, 27, 44) eq 84
sum(1, 3, 5, 7, 9, 11) eq 36
sum() eq 0
}
Хотя массивы (Array) и списки (List) выглядят похоже, они реализованы по-разному — List является обычным классом библиотеки, в то время как Array имеет специальную поддержку на низком уровне. Массивы необходимы для совместимости с другими языками, особенно с Java.
В повседневном программировании используйте List, когда вам нужна простая последовательность. Используйте массивы только тогда, когда сторонний API требует массив, или когда вы работаете с vararg.
В большинстве случаев вы можете просто игнорировать тот факт, что vararg создает массив, и обращаться с ним так, как если бы это был List: // Varargs/VarargLikeList.kt package varargs
import atomictest.eq
fun evaluate(vararg ints: Int) =
“Size: ${ ints.size } \n” +
“Sum: ${ ints.sum() } \n” +
“Average: ${ ints.average() } "
fun main() {
evaluate(10, -3, 8, 1, 9) eq "””
Size: 5
Sum: 25
Average: 5.0
"""
}
Вы можете передавать массив элементов туда, где принимается vararg. Чтобы создать массив, используйте arrayOf() так же, как вы используете listOf(). Массив всегда изменяемый. Чтобы преобразовать массив в последовательность аргументов (а не только в один элемент типа Array), используйте оператор распаковки *: // Varargs/SpreadOperator.kt import varargs.sum
import atomictest.eq
fun main() {
val array = intArrayOf(4, 5)
sum(1, 2, 3, *array, 6) eq 21 // [1]
// Не компилируется:
// sum(1, 2, 3, array, 6)
val list = listOf(9, 10, 11)
sum(*list.toIntArray()) eq 30 // [2]
}
Если вы передаете массив примитивных типов (таких как Int, Double или Boolean), как в приведенном выше примере, функция создания массива должна быть конкретно типизирована. Если вы используете arrayOf(4, 5) вместо intArrayOf(4, 5), строка [1] вызовет ошибку, сообщающую, что ожидается IntArray, но был получен Array<Int>.
Оператор распаковки работает только с массивами. Если у вас есть List, который вы хотите передать в качестве последовательности аргументов, сначала преобразуйте его в массив, а затем примените оператор распаковки, как в [2]. Поскольку результатом является массив примитивного типа, мы снова должны использовать конкретную функцию преобразования toIntArray().
Оператор распаковки особенно полезен, когда вам нужно передать аргументы vararg в другую функцию, которая также ожидает vararg: // Varargs/TwoFunctionsWithVarargs.kt package varargs
import atomictest.eq
fun first(vararg numbers: Int): String {
var result = ""
for (i in numbers) {
result += “[ $ i]”
}
return result
}
fun second(vararg numbers: Int) =
first(*numbers)
fun main() {
second(7, 9, 32) eq “[7][9][32]”
}

Аргументы командной строки Link to heading

При вызове программы из командной строки вы можете передать ей переменное количество аргументов. Чтобы захватить аргументы командной строки, вы должны предоставить определённый параметр для main():

// Varargs/MainArgs.kt
fun main(args: Array< String >) {
    for (a in args) {
        println(a)
    }
}

Параметр традиционно называется args (хотя вы можете назвать его как угодно), а тип для args может быть только Array (массив строк). Если вы используете IntelliJ IDEA, вы можете передать аргументы программы, отредактировав соответствующую “Конфигурацию запуска”, как показано в последнем упражнении для этого атома.

Вы также можете использовать компилятор kotlinc для создания программы командной строки. Если kotlinc не установлен на вашем компьютере, следуйте инструкциям на главном сайте Kotlin. После того как вы ввели и сохранили код для MainArgs.kt, введите следующее в командной строке:

kotlinc MainArgs.kt

Вы передаете аргументы командной строки после вызова программы, вот так:

kotlin MainArgsKt hamster 42 3.14159

Вы увидите следующий вывод:

hamster
42
3.14159

Если вы хотите преобразовать строковый параметр в конкретный тип, Kotlin предоставляет функции преобразования, такие как toInt() для преобразования в Int и toFloat() для преобразования в Float. Использование этих функций предполагает, что аргументы командной строки появляются в определённом порядке. Здесь программа ожидает строку, за которой следует что-то, что можно преобразовать в Int, а затем что-то, что можно преобразовать в Float:

// Varargs/MainArgConversion.kt
fun main(args: Array< String >) {
    if (args.size < 3) return
    val first = args[0]
    val second = args[1].toInt()
    val third = args[2].toFloat()
    println(" $first $second $third")
}

Первая строка в main() завершает программу, если недостаточно аргументов. Если вы не предоставите что-то, что можно преобразовать в Int и Float в качестве второго и третьего аргументов командной строки, вы увидите ошибки во время выполнения (попробуйте, чтобы увидеть ошибки). Скомпилируйте и запустите MainArgConversion.kt с теми же аргументами командной строки, которые мы использовали ранее, и вы увидите:

hamster 42 3.14159

Упражнения и решения можно найти на www.AtomicKotlin.com.

Множества Link to heading

Множество — это коллекция, которая позволяет иметь только один элемент каждого значения. Наиболее распространённая операция с множествами — это проверка на принадлежность с помощью in или contains():

// Sets/Sets.kt
import atomictest.eq

fun main() {
    val intSet = setOf(1, 1, 2, 3, 9, 9, 4)
    // Дубликаты отсутствуют:
    intSet eq setOf(1, 2, 3, 4, 9)
    // Порядок элементов не важен:
    setOf(1, 2) eq setOf(2, 1)
    // Принадлежность множеству:
    (9 in intSet) eq true
    (99 in intSet) eq false
    intSet.contains(9) eq true
    intSet.contains(99) eq false
    // Содержит ли это множество другое множество?
    intSet.containsAll(setOf(1, 9, 2)) eq true
    // Объединение множеств:
    intSet.union(setOf(3, 4, 5, 6)) eq setOf(1, 2, 3, 4, 5, 6, 9)
    // Пересечение множеств:
    intSet intersect setOf(0, 1, 2, 7, 8) eq setOf(1, 2)
    // Разность множеств:
    intSet subtract setOf(0, 1, 9, 10) eq setOf(2, 3, 4)
    intSet - setOf(0, 1, 9, 10) eq setOf(2, 3, 4)
}

Этот пример показывает:

  1. Помещение дублирующихся элементов в множество автоматически удаляет эти дубликаты.
  2. Порядок элементов не важен для множеств. Два множества равны, если они содержат одни и те же элементы.
  3. Оба оператора in и contains() проверяют на принадлежность.
  4. Вы можете выполнять обычные операции Венна, такие как проверка на подмножество, объединение, пересечение и разность, используя либо точечную нотацию (set.union(other)), либо инфиксную нотацию (set intersect other). Функции union, intersect и subtract могут использоваться в инфиксной нотации.
  5. Разность множеств может быть выражена с помощью subtract() или оператора минус.

Чтобы удалить дубликаты из списка, преобразуйте его в множество:

// Sets/RemoveDuplicates.kt
import atomictest.eq

fun main() {
    val list = listOf(3, 3, 2, 1, 2)
    list.toSet() eq setOf(1, 2, 3)
    list.distinct() eq listOf(3, 2, 1)
    "abbcc".toSet() eq setOf('a', 'b', 'c')
}

Вы также можете использовать distinct(), который возвращает список. Вы можете вызвать toSet() на строке, чтобы преобразовать её в множество уникальных символов.

Как и в случае со списками, Kotlin предоставляет две функции для создания множеств. Результат setOf() является только для чтения. Чтобы создать изменяемое множество, используйте mutableSetOf():

// Sets/MutableSet.kt
import atomictest.eq

fun main() {
    val mutableSet = mutableSetOf<Int>()
    mutableSet += 42
    mutableSet += 42
    mutableSet eq setOf(42)
    mutableSet -= 42
    mutableSet eq setOf<Int>()
}

Операторы += и -= добавляют и удаляют элементы из множеств, так же как и в списках.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Карты Link to heading

Карта связывает ключи со значениями и ищет значение, когда дан ключ. Вы создаете карту, предоставляя пары ключ-значение в mapOf(). Используя to, мы разделяем каждый ключ от его связанного значения: // Maps/Maps.kt import atomictest.eq
fun main() {
val constants = mapOf(
“Pi” to 3.141,
“e” to 2.718,
“phi” to 1.618
)
constants eq
“{Pi=3.141, e=2.718, phi=1.618}”
// Получить значение по ключу:
constants[“e”] eq 2.718 // [1]
constants.keys eq setOf(“Pi”, “e”, “phi”)
constants.values eq “[3.141, 2.718, 1.618]”
var s = ""
// Перебор пар ключ-значение:
for (entry in constants) { // [2]
s += " ${ entry.key } = ${ entry.value } , "
}
s eq “Pi=3.141, e=2.718, phi=1.618,”
s = ""
// Распаковка во время итерации:
for ((key, value) in constants) // [3]
s += " $ key= $ value, "
s eq “Pi=3.141, e=2.718, phi=1.618,”
}
Карты 143
• [1] Оператор [] ищет значение, используя ключ. Вы можете получить все ключи, используя keys, и все значения, используя values. Вызов keys возвращает Set, потому что все ключи в карте должны быть уникальными, иначе у вас возникнет неоднозначность при поиске.
• [2] Перебор карты возвращает пары ключ-значение в виде записей карты.
• [3] Вы можете распаковывать ключи и значения во время итерации.
Обычная карта является только для чтения. Вот MutableMap:
// Maps/MutableMaps.kt
import atomictest.eq
fun main() {
val m =
mutableMapOf(5 to “five”, 6 to “six”)
m[5] eq “five”
m[5] = “5ive”
m[5] eq “5ive”
m += 4 to “four”
m eq mapOf(5 to “5ive”,
4 to “four”, 6 to “six”)
}
map[key] = value добавляет или изменяет значение, связанное с ключом. Вы также можете явно добавить пару, сказав map += key to value.
mapOf() и mutableMapOf() сохраняют порядок, в котором элементы добавляются в карту. Это не гарантируется для других типов карт.
Карта только для чтения не позволяет вносить изменения:
AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Карты 144
// Maps/ReadOnlyMaps.kt
import atomictest.eq
fun main() {
val m = mapOf(5 to “five”, 6 to “six”)
m[5] eq “five”
// m[5] = “5ive” // Ошибка
// m += (4 to “four”) // Ошибка
m + (4 to “four”) // Не изменяет m
m eq mapOf(5 to “five”, 6 to “six”)
val m2 = m + (4 to “four”)
m2 eq mapOf(
5 to “five”, 6 to “six”, 4 to “four”)
}
Определение m создает карту, связывающую Int с String. Если мы попытаемся заменить строку, Kotlin выдаст ошибку.
Выражение с + создает новую карту, которая включает как старые элементы, так и новый, но не влияет на оригинальную карту. Единственный способ “добавить” элемент в карту только для чтения — это создать новую карту.
Карта возвращает null, если она не содержит записи для данного ключа. Если вам нужен результат, который не может быть null, используйте getValue() и ловите NoSuchElementException, если ключ отсутствует:
// Maps/GetValue.kt
import atomictest.*
fun main() {
val map = mapOf(‘a’ to “attempt”)
map[‘b’] eq null
capture {
map.getValue(‘b’)
} eq “NoSuchElementException: " +
“Ключ b отсутствует в карте.”
map.getOrDefault(‘a’, “??”) eq “attempt”
map.getOrDefault(‘b’, “??”) eq “??”
}
getOrDefault() обычно является безопасной альтернативой null или исключению.
AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Карты 145
Вы можете хранить экземпляры классов в качестве значений в карте. Вот карта, которая извлекает Contact, используя номер телефона в виде строки:
// Maps/ContactMap.kt
package maps
import atomictest.eq
class Contact (
val name: String,
val phone: String
) {
override fun toString() =
“Contact(’ $ name’, ’ $ phone’)”
}
fun main() {
val miffy = Contact(“Miffy”, “1-234-567890”)
val cleo = Contact(“Cleo”, “098-765-4321”)
val contacts = mapOf(
miffy.phone to miffy,
cleo.phone to cleo)
contacts[“1-234-567890”] eq miffy
contacts[“1-111-111111”] eq null
}
Возможно использовать экземпляры классов в качестве ключей в карте, но это сложнее, поэтому мы обсудим это позже в книге.
• -
Карты выглядят как простые небольшие базы данных. Их иногда называют ассоциативными массивами, потому что они связывают ключи со значениями. Хотя они довольно ограничены по сравнению с полнофункциональной базой данных, они тем не менее удивительно полезны (и гораздо более эффективны, чем база данных).
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin(www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Доступ к свойствам Link to heading

Чтобы прочитать свойство, используйте его имя. Чтобы присвоить значение изменяемому свойству, используйте оператор присваивания =. Это читает и записывает свойство i:

// PropertyAccessors/Data.kt
package propertyaccessors
import atomictest.eq

class Data(var i: Int)

fun main() {
    val data = Data(10)
    data.i eq 10 // Чтение свойства 'i'
    data.i = 20 // Запись в свойство 'i'
}

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

Аксессор, используемый для получения значения свойства, называется геттером. Вы создаете геттер, определяя get() сразу после определения свойства. Аксессор, используемый для изменения изменяемого свойства, называется сеттером. Вы создаете сеттер, определяя set() сразу после определения свойства.

Аксессоры свойств, определенные в следующем примере, имитируют реализации по умолчанию, генерируемые Kotlin. Мы отображаем дополнительную информацию, чтобы вы могли увидеть, что аксессоры свойств действительно вызываются во время чтения и записи. Мы отступаем get() и set(), чтобы визуально ассоциировать их со свойством, но фактическая ассоциация происходит потому, что get() и set() определены сразу после этого свойства (Kotlin не заботится о выравнивании):

// PropertyAccessors/Default.kt
package propertyaccessors
import atomictest.*

class Default {
    var i: Int = 0
        get() {
            trace("get()")
            return field // [1]
        }
        set(value) {
            trace("set($value)")
            field = value // [2]
        }
}

fun main() {
    val d = Default()
    d.i = 2
    trace(d.i)
    trace eq """
        set(2)
        get()
        2
    """
}

Порядок определения get() и set() не важен. Вы можете определить get() без определения set(), и наоборот.

Поведение по умолчанию для свойства возвращает его сохраненное значение из геттера и изменяет его с помощью сеттера — действия [1] и [2]. Внутри геттера и сеттера сохраненное значение манипулируется косвенно с использованием ключевого слова field, которое доступно только внутри этих двух функций.

В следующем примере используется реализация по умолчанию геттера и добавляется сеттер для отслеживания изменений свойства n:

// PropertyAccessors/LogChanges.kt
package propertyaccessors
import atomictest.*

class LogChanges {
    var n: Int = 0
        set(value) {
            trace("$field становится $value")
            field = value
        }
}

fun main() {
    val lc = LogChanges()
    lc.n eq 0
    lc.n = 2
    lc.n eq 2
    trace eq "0 становится 2"
}

Если вы определяете свойство как private, оба аксессора становятся приватными. Вы также можете сделать сеттер приватным, а геттер публичным. Тогда вы сможете читать свойство вне класса, но изменять его значение только внутри класса:

// PropertyAccessors/Counter.kt
package propertyaccessors
import atomictest.eq

class Counter {
    var value: Int = 0
        private set

    fun inc() = value++
}

fun main() {
    val counter = Counter()
    repeat(10) {
        counter.inc()
    }
    counter.value eq 10
}

Используя private set, мы контролируем свойство value, чтобы его можно было увеличивать только на единицу.

Обычные свойства хранят свои данные в поле. Вы также можете создать свойство, которое не имеет поля:

// PropertyAccessors/Hamsters.kt
package propertyaccessors
import atomictest.eq

class Hamster(val name: String)

class Cage(private val maxCapacity: Int) {
    private val hamsters = mutableListOf<Hamster>()

    val capacity: Int
        get() = maxCapacity - hamsters.size

    val full: Boolean
        get() = hamsters.size == maxCapacity

    fun put(hamster: Hamster): Boolean =
        if (full) false
        else {
            hamsters += hamster
            true
        }

    fun take(): Hamster =
        hamsters.removeAt(0)
}

fun main() {
    val cage = Cage(2)
    cage.full eq false
    cage.capacity eq 2
    cage.put(Hamster("Alice")) eq true
    cage.put(Hamster("Bob")) eq true
    cage.full eq true
    cage.capacity eq 0
    cage.put(Hamster("Charlie")) eq false
    cage.take()
    cage.capacity eq 1
}

Свойства capacity и full не содержат подлежащего состояния — они вычисляются в момент каждого доступа. Оба свойства capacity и full похожи на функции, и вы можете определить их как таковые:

// PropertyAccessors/Hamsters2.kt
package propertyaccessors

class Cage2(private val maxCapacity: Int) {
    private val hamsters = mutableListOf<Hamster>()

    fun capacity(): Int =
        maxCapacity - hamsters.size

    fun isFull(): Boolean =
        hamsters.size == maxCapacity
}

В этом случае использование свойств улучшает читаемость, потому что capacity и full являются свойствами клетки. Однако не стоит просто преобразовывать все ваши функции в свойства — сначала посмотрите, как они читаются.

  • Руководство по стилю Kotlin предпочитает свойства функциям, когда значение легко вычислить и свойство возвращает один и тот же результат для каждого вызова, пока состояние объекта не изменилось.

Аксессоры свойств предоставляют своего рода защиту для свойств. Многие объектно-ориентированные языки полагаются на то, чтобы сделать физическое поле приватным, чтобы контролировать доступ к этому свойству. С помощью аксессоров свойств вы можете добавить код для контроля или изменения этого доступа, позволяя при этом любому использовать свойство.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Резюме 2 Link to heading

Этот атом подводит итоги и рассматривает атомы в Разделе II, от Объектов до Доступа к Свойствам.
Если вы опытный программист, это ваш следующий атом после Резюме 1, и вы будете проходить атомы последовательно после этого.
Новым программистам следует прочитать этот атом и выполнить упражнения в качестве повторения. Если какая-либо информация здесь вам не ясна, вернитесь и изучите атом по этой теме.
Темы представлены в соответствующем порядке для опытных программистов, что не совпадает с порядком атомов в книге. Например, мы начинаем с введения в пакеты и импорты, чтобы мы могли использовать нашу минимальную тестовую среду для остальной части атома.

Пакеты и тестирование Link to heading

Любое количество переиспользуемых компонентов библиотеки может быть объединено под одним именем библиотеки с помощью ключевого слова package: // Summary2/ALibrary.kt package com.yoururl.libraryname // Компоненты для переиспользования … fun f() = “result” Вы можете разместить несколько компонентов в одном файле или распределить компоненты по нескольким файлам под одним и тем же именем пакета. В данном примере мы определили f() как единственный компонент. Чтобы сделать его уникальным, имя пакета обычно начинается с вашего доменного имени в обратном порядке. В этом примере доменное имя — yoururl.com.

В Kotlin имя пакета может быть независимым от директории, в которой находятся его содержимое. Java требует, чтобы структура директорий соответствовала полному имени пакета, поэтому пакет com.yoururl.libraryname должен находиться в директории com/yoururl/libraryname. Для смешанных проектов на Kotlin и Java стиль руководства Kotlin рекомендует ту же практику. Для чисто Kotlin проектов поместите директорию libraryname на верхний уровень структуры директорий вашего проекта.

Оператор импорта приносит одно или несколько имен в текущее пространство имен: // Summary2/UseALibrary.kt import com.yoururl.libraryname.* fun main() { val x = f() } Звезда после libraryname говорит Kotlin импортировать все компоненты библиотеки. Вы также можете выбирать компоненты по отдельности; подробности приведены в разделе “Пакеты”.

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

Важной библиотекой для этой книги является atomictest, наша простая тестовая рамка. atomictest определен в Приложении A: AtomicTest, хотя он использует языковые функции, которые вы не поймете на данном этапе книги.

После импорта atomictest вы используете eq (равно) и neq (не равно) почти так, как если бы это были ключевые слова языка: AtomicKotlin (www.AtomicKotlin.com) Брус Эккель и Светлана Исакова, ©2021 MindView LLC // Summary2/UsingAtomicTest.kt import atomictest.* fun main() { val pi = 3.14 val pie = “Круглый десерт” pi eq 3.14 pie eq “Круглый десерт” pi neq pie } /* Вывод: 3.14 Круглый десерт 3.14 */ Возможность использовать eq / neq без каких-либо точек или скобок называется инфиксной нотацией. Вы можете вызывать инфиксные функции либо обычным способом: pi.eq(3.14), либо используя инфиксную нотацию: pi eq 3.14. Оба eq и neq являются утверждениями истинности, которые также отображают результат с левой стороны выражения eq / neq и сообщение об ошибке, если выражение справа от eq не эквивалентно левому (или эквивалентно, в случае neq). Таким образом, вы видите проверенные результаты в исходном коде.

atomictest.trace использует синтаксис вызова функции для добавления результатов, которые затем могут быть проверены с помощью eq: // Testing/UsingTrace.kt import atomictest.* fun main() { trace(“Привет,”) trace(47) trace(“Мир!”) trace eq "”" Привет, 47 Мир! """ } Вы можете эффективно заменить println() на trace(). AtomicKotlin (www.AtomicKotlin.com) Брус Эккель и Светлана Исакова, ©2021 MindView LLC

Объекты повсюду Link to heading

Kotlin — это гибридный объектно-функциональный язык: он поддерживает как объектно-ориентированные, так и функциональные парадигмы программирования. Объекты содержат val и var, чтобы хранить данные (они называются свойствами) и выполнять операции с помощью функций, определенных внутри класса, которые называются членами класса (когда это однозначно, мы просто говорим «функции»). Класс определяет свойства и функции-члены для того, что по сути является новым, пользовательским типом данных. Когда вы создаете val или var класса, это называется созданием объекта или созданием экземпляра.

Особенно полезным типом объекта является контейнер, также называемый коллекцией. Контейнер — это объект, который содержит другие объекты. В этой книге мы часто используем List, потому что это наиболее универсальная последовательность. Здесь мы выполняем несколько операций над List, который содержит Double. listOf() создает новый List из своих аргументов:

// Summary2/ListCollection.kt
import atomictest.eq

fun main() {
    val lst = listOf(19.2, 88.3, 22.1)
    lst[1] eq 88.3 // Индексация
    lst.reversed() eq listOf(22.1, 88.3, 19.2)
    lst.sorted() eq listOf(19.2, 22.1, 88.3)
    lst.sum() eq 129.6
}

Не требуется оператор импорта для использования List. Kotlin использует квадратные скобки для индексации в последовательностях. Индексация начинается с нуля. Этот пример также показывает несколько из множества функций стандартной библиотеки, доступных для List: sorted(), reversed() и sum(). Чтобы понять эти функции, обратитесь к онлайн-документации по Kotlin.

Когда вы вызываете sorted() или reversed(), lst не изменяется. Вместо этого создается и возвращается новый List, содержащий желаемый результат. Этот подход, при котором оригинальный объект никогда не модифицируется, последователен во всех библиотеках Kotlin, и вы должны стремиться следовать этой модели при написании собственного кода.

Создание классов Link to heading

Определение класса состоит из ключевого слова class, имени класса и необязательного тела. Тело содержит определения свойств (val и var) и определения функций. Этот пример определяет класс NoBody без тела и классы с свойствами val:

// Summary2/ClassBodies.kt
package summary2
class NoBody
class SomeBody {
    val name = "Janet Doe"
}
class EveryBody {
    val all = listOf(SomeBody(), SomeBody(), SomeBody())
}
fun main() {
    val nb = NoBody()
    val sb = SomeBody()
    val eb = EveryBody()
}

Чтобы создать экземпляр класса, поставьте скобки после его имени, вместе с аргументами, если они необходимы. Свойства внутри тел классов могут быть любого типа. SomeBody содержит свойство типа String, а свойство EveryBody — это список, содержащий объекты SomeBody.

Вот класс с членами-функциями:

// Summary2/Temperature.kt
package summary2
import atomictest.eq

class Temperature {
    var current = 0.0
    var scale = "f"
    
    fun setFahrenheit(now: Double) {
        current = now
        scale = "f"
    }
    
    fun setCelsius(now: Double) {
        current = now
        scale = "c"
    }
    
    fun getFahrenheit(): Double =
        if (scale == "f") current
        else current * 9.0 / 5.0 + 32.0
    
    fun getCelsius(): Double =
        if (scale == "c") current
        else (current - 32.0) * 5.0 / 9.0
}

fun main() {
    val temp = Temperature() // [1]
    temp.setFahrenheit(98.6)
    temp.getFahrenheit() eq 98.6
    temp.getCelsius() eq 37.0
    temp.setCelsius(100.0)
    temp.getFahrenheit() eq 212.0
}

Эти функции-члены аналогичны функциям верхнего уровня, которые мы определили вне классов, за исключением того, что они принадлежат классу и имеют неквалифицированный доступ к другим членам класса, таким как current и scale. Функции-члены также могут вызывать другие функции-члены в том же классе без квалификации.

• [1] Хотя temp является val, мы позже модифицируем объект Temperature. Определение val предотвращает переназначение ссылки temp на новый объект, но не ограничивает поведение самого объекта.

Следующие два класса являются основой для игры в крестики-нолики:

// Summary2/TicTacToe.kt
package summary2
import atomictest.eq

class Cell {
    var entry = ' ' // [1]
    
    fun setValue(e: Char): String = // [2]
        if (entry == ' ' && (e == 'X' || e == 'O')) {
            entry = e
            "Успешный ход"
        } else {
            "Недопустимый ход"
        }
}

class Grid {
    val cells = listOf(
        listOf(Cell(), Cell(), Cell()),
        listOf(Cell(), Cell(), Cell()),
        listOf(Cell(), Cell(), Cell())
    )
    
    fun play(e: Char, x: Int, y: Int): String =
        if (x !in 0..2 || y !in 0..2) {
            "Недопустимый ход"
        } else {
            cells[x][y].setValue(e) // [3]
        }
}

fun main() {
    val grid = Grid()
    grid.play('X', 1, 1) eq "Успешный ход"
    grid.play('X', 1, 1) eq "Недопустимый ход"
    grid.play('O', 1, 3) eq "Недопустимый ход"
}

Класс Grid содержит список, состоящий из трех списков, каждый из которых содержит три ячейки — матрицу.

• [1] Свойство entry в Cell является var, поэтому его можно модифицировать. Одинарные кавычки в инициализации создают тип Char, поэтому все присваивания entry также должны быть Char.

• [2] setValue() проверяет, доступна ли ячейка и передан ли правильный символ. Он возвращает строковый результат, чтобы указать на успех или неудачу.

• [3] play() проверяет, находятся ли аргументы x и y в пределах диапазона, затем индексирует их в матрице, полагаясь на проверки, выполненные setValue().

Конструкторы Link to heading

Конструкторы создают новые объекты. Вы передаете информацию конструктору, используя его список параметров, который размещается в скобках сразу после имени класса. Вызов конструктора выглядит как вызов функции, за исключением того, что первая буква имени написана с заглавной буквы (в соответствии с руководством по стилю Kotlin). Конструктор возвращает объект класса:

// Summary2/WildAnimals.kt
package summary2
import atomictest.eq

class Badger(id: String, years: Int) {
    val name = id
    val age = years
    override fun toString() = "Badger: $name, age: $age"
}

class Snake(
    var type: String,
    var length: Double
) {
    override fun toString() = "Snake: $type, length: $length"
}

class Moose(
    val age: Int,
    val height: Double
) {
    override fun toString() = "Moose, age: $age, height: $height"
}

fun main() {
    Badger("Bob", 11) eq "Badger: Bob, age: 11"
    Snake("Garden", 2.4) eq "Snake: Garden, length: 2.4"
    Moose(16, 7.2) eq "Moose, age: 16, height: 7.2"
}

Параметры id и years в классе Badger доступны только в теле конструктора. Тело конструктора состоит из строк кода, отличных от определений функций; в данном случае это определения для name и age.

Часто вы хотите, чтобы параметры конструктора были доступны в других частях класса, а не только в теле конструктора, но без необходимости явно определять новые идентификаторы, как мы сделали с name и age. Если вы определяете свои параметры как var или val, они становятся свойствами и доступны повсюду в классе. Оба класса Snake и Moose используют этот подход, и вы можете видеть, что параметры конструктора теперь доступны внутри их соответствующих функций toString().

Параметры конструктора, объявленные с помощью val, не могут быть изменены, но те, которые объявлены с помощью var, могут.

Когда вы используете объект в ситуации, где ожидается String, Kotlin создает строковое представление этого объекта, вызывая его функцию-член toString(). Чтобы определить toString(), вы должны понять новое ключевое слово: override. Это необходимо (Kotlin настаивает на этом), потому что toString() уже определен. override говорит Kotlin, что мы действительно хотим заменить стандартный toString() нашим собственным определением. Ясность override делает это понятным для читателя и помогает предотвратить ошибки.

Обратите внимание на форматирование многострочного списка параметров для Snake и Moose — это рекомендуемый стандарт, когда у вас слишком много параметров, чтобы уместить их в одной строке, как для конструкторов, так и для функций.

Ограничение видимости Link to heading

Kotlin предоставляет модификаторы доступа, аналогичные тем, что доступны в других языках, таких как C++ или Java. Эти модификаторы позволяют создателям компонентов решать, что доступно клиентскому программисту. Модификаторы доступа в Kotlin включают ключевые слова public, private, protected и internal. protected будет объяснен позже.

Модификатор доступа, такой как public или private, появляется перед определением класса, функции или свойства. Каждый модификатор доступа контролирует доступ только к этому конкретному определению.

Определение public доступно всем, в частности, клиентскому программисту, который использует этот компонент. Таким образом, любые изменения в определении public повлияют на клиентский код.

Если вы не укажете модификатор, ваше определение автоматически будет public. Для ясности в некоторых случаях программисты все же иногда избыточно указывают public.

Если вы определяете класс, функцию верхнего уровня или свойство как private, оно доступно только в этом файле: // Summary2/Boxes.kt package summary2
import atomictest.*
private var count = 0 // [1]
private class Box (val dimension: Int) { // [2]
fun volume() =
dimension * dimension * dimension
override fun toString() =
“Box volume: ${ volume() } "
}
private fun countBox(box: Box) { // [3]
trace(” $ box")
count++
}
fun countBoxes() {
countBox(Box(4))
countBox(Box(5))
}
fun main() {
countBoxes()
trace(" $ count boxes")
trace eq """
Box volume: 64
Box volume: 125
2 boxes
"""
}

Вы можете получить доступ к private свойствам ([1]), классам ([2]) и функциям ([3]) только из других функций и классов в файле Boxes.kt. Kotlin предотвращает доступ к private элементам верхнего уровня из другого файла.

Члены класса могут быть private:
// Summary2/JetPack.kt
package summary2
import atomictest.eq
class JetPack (
private var fuel: Double // [1]
) {
private var warning = false
private fun burn() = // [2]
if (fuel - 1 <= 0) {
fuel = 0.0
warning = true
} else
fuel -= 1
public fun fly() = burn() // [3]
fun check() = // [4]
if (warning) // [5]
“Warning”
else
“OK”
}
fun main() {
val jetPack = JetPack(3.0)
while (jetPack.check() != “Warning”) {
jetPack.check() eq “OK”
jetPack.fly()
}
jetPack.check() eq “Warning”
}

  • [1] fuel и warning — это оба private свойства и не могут использоваться не членами класса JetPack.
  • [2] burn() является private, и, следовательно, доступна только внутри JetPack.
  • [3] fly() и check() являются public и могут использоваться везде.
  • [4] Отсутствие модификатора доступа означает публичную видимость.
  • [5] Только члены одного и того же класса могут получать доступ к private членам.

Поскольку private определение недоступно для всех, вы можете в общем случае изменять его без опасений для клиентского программиста. Как дизайнер библиотеки, вы обычно будете держать все как можно более закрытым и открывать только функции и классы, которые хотите, чтобы клиентские программисты использовали. Чтобы ограничить размер и сложность примеров в этой книге, мы используем private только в особых случаях.

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

Полезно делить большие программы на модули. Модуль — это логически независимая часть кодовой базы. Определение internal доступно только внутри модуля, в котором оно определено. Способ, которым вы делите проект на модули, зависит от системы сборки (например, Gradle или Maven) и выходит за рамки этой книги.

Модули — это концепция более высокого уровня, в то время как пакеты позволяют более тонкую структуру.

Исключения Link to heading

Рассмотрим функцию toDouble(), которая преобразует строку в Double. Что произойдет, если вы вызовете ее для строки, которая не может быть преобразована в Double?

// Summary2/ToDoubleException.kt
fun main() {
    // val i = "$1.9".toDouble()
}

Если раскомментировать строку в main(), произойдет исключение. Здесь неудачная строка закомментирована, чтобы не остановить сборку книги (которая проверяет, компилируется ли каждый пример и работает ли он как ожидалось).

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

Чтобы избежать отображения исключений путем комментирования и раскомментирования кода, atomictest.capture() сохраняет исключение и сравнивает его с тем, что мы ожидаем:

// Summary2/AtomicTestCapture.kt
import atomictest.*

fun main() {
    capture {
        "$1.9".toDouble()
    } eq "NumberFormatException: " +
        """For input string: "$1.9""""
}

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

Еще одна стратегия, когда ваша функция не может успешно вернуть ожидаемый результат, — это вернуть null. Позже в разделе о Nullable Types мы обсудим, как null влияет на тип результирующего выражения.

Чтобы выбросить исключение, используйте ключевое слово throw, за которым следует исключение, которое вы хотите выбросить, вместе с любыми аргументами, которые могут понадобиться. Функция quadraticZeroes() в следующем примере решает квадратное уравнение, которое определяет параболу:

ax² + bx + c = 0

Решение — это квадратная формула:

Квадратная формула

Пример находит нули параболы, где линии пересекают ось x. Мы выбрасываем исключения для двух ограничений:

  1. a не может быть нулем.
  2. Чтобы нули существовали, b² - 4ac не может быть отрицательным.

Если нули существуют, их два, поэтому мы создаем класс Roots, чтобы хранить возвращаемые значения:

// Summary2/Quadratic.kt
package summary2

import kotlin.math.sqrt
import atomictest.*

class Roots(
    val root1: Double,
    val root2: Double
)

fun quadraticZeroes(
    a: Double,
    b: Double,
    c: Double
): Roots {
    if (a == 0.0)
        throw IllegalArgumentException("a is zero")
    val underRadical = b * b - 4 * a * c
    if (underRadical < 0)
        throw IllegalArgumentException("Negative underRadical: $underRadical")
    val squareRoot = sqrt(underRadical)
    val root1 = (-b - squareRoot) / (2 * a)
    val root2 = (-b + squareRoot) / (2 * a)
    return Roots(root1, root2)
}
fun main() {
    capture {
        quadraticZeroes(0.0, 4.0, 5.0)
    } eq "IllegalArgumentException: " +
        "a is zero"
    capture {
        quadraticZeroes(3.0, 4.0, 5.0)
    } eq "IllegalArgumentException: " +
        "Negative underRadical: -44.0"
    val roots = quadraticZeroes(1.0, 2.0, -8.0)
    roots.root1 eq -4.0
    roots.root2 eq 2.0
}

Здесь мы используем стандартный класс исключений IllegalArgumentException. Позже вы научитесь определять свои собственные типы исключений и делать их специфичными для ваших обстоятельств. Ваша цель — генерировать наиболее полезные сообщения, чтобы упростить поддержку вашего приложения в будущем.

Списки Link to heading

Списки — это базовый последовательный контейнер в Kotlin. Вы создаете список только для чтения с помощью listOf() и изменяемый список с помощью mutableListOf() : // Summary2/ReadonlyVsMutableList.kt import atomictest.* fun main() { val ints = listOf(5, 13, 9) // ints.add(11) // ‘add()’ недоступен for (i in ints) { if (i > 10) { trace(i) } } val chars = mutableListOf(‘a’, ‘b’, ‘c’) chars.add(’d’) // ‘add()’ доступен chars += ’e’ AtomicKotlin(www.AtomicKotlin.com)byBruceEckel&SvetlanaIsakova,©2021MindViewLLC Summary2 166 trace(chars) trace eq """ 13 [a, b, c, d, e] """ } Базовый список является только для чтения и не включает функции модификации. Таким образом, функция модификации add() не работает с ints. Циклы for хорошо работают со списками: for(i in ints) означает, что я получаю каждое значение в ints. chars создается как MutableList; его можно модифицировать с помощью таких функций, как add() или remove(). Вы также можете использовать += и -= для добавления или удаления элементов. Список только для чтения не является тем же самым, что и неизменяемый список, который нельзя модифицировать вообще. Здесь мы присваиваем first, изменяемый список, ссылке second, которая является списком только для чтения. Свойство только для чтения second не мешает изменению списка через first : // Summary2/MultipleListReferences.kt import atomictest.eq fun main() { val first = mutableListOf(1) val second: List< Int > = first second eq listOf(1) first += 2 // second видит изменение: second eq listOf(1, 2) } first и second ссылаются на один и тот же объект в памяти. Мы модифицируем список через ссылку first, а затем наблюдаем это изменение через ссылку second. Вот список строк, созданный путем разбивки параграфа с тройными кавычками. Это демонстрирует мощь некоторых функций стандартной библиотеки. Обратите внимание, как эти функции могут быть связаны: AtomicKotlin(www.AtomicKotlin.com)byBruceEckel&SvetlanaIsakova,©2021MindViewLLC Summary2 167 // Summary2/ListOfStrings.kt import atomictest.* fun main() { val wocky = """ Twas brillig, and the slithy toves Did gyre and gimble in the wabe: All mimsy were the borogoves, And the mome raths outgrabe. “”".trim().split(Regex("\W+")) trace(wocky.take(5)) trace(wocky.slice(6..12)) trace(wocky.slice(6..18 step 2)) trace(wocky.sorted().takeLast(5)) trace(wocky.sorted().distinct().takeLast(5)) trace eq """ [Twas, brillig, and, the, slithy] [Did, gyre, and, gimble, in, the, wabe] [Did, and, in, wabe, mimsy, the, And] [the, the, toves, wabe, were] [slithy, the, toves, wabe, were] """ } trim() создает новую строку с удаленными начальными и конечными пробелами (включая переносы строк). split() делит строку в соответствии с ее аргументом. В этом случае мы используем объект Regex, который создает регулярное выражение — шаблон, который соответствует частям для разделения. \W — это специальный шаблон, который означает «не символ слова», а + означает «один или более из предыдущего». Таким образом, split() будет разбивать строку на один или более несловесных символов, и, следовательно, делит блок текста на его составные слова. В строковом литерале \ предшествует специальному символу и создает, например, символ новой строки (\n) или табуляцию (\t). Чтобы получить фактический \ в результирующей строке, вам нужно два обратных слэша: "\\". Таким образом, все регулярные выражения требуют дополнительного \ для вставки обратного слэша, если вы не используете строку с тройными кавычками: """\\W+""". take(n) создает новый список, содержащий первые n элементов. slice() создает новый список, содержащий элементы, выбранные по его аргументу Range, и этот диапазон может включать шаг. AtomicKotlin(www.AtomicKotlin.com)byBruceEckel&SvetlanaIsakova,©2021MindViewLLC Summary2 168 Обратите внимание на название sorted() вместо sort(). Когда вы вызываете sorted(), он создает отсортированный список, оставляя оригинальный список без изменений. sort() работает только с MutableList, и этот список сортируется на месте — оригинальный список модифицируется. Как следует из названия, takeLast(n) создает новый список из последних n элементов. Вы можете видеть из вывода, что «the» дублируется. Это устраняется добавлением функции distinct() в цепочку вызовов.

Параметризованные типы Link to heading

Параметры типов позволяют нам описывать составные типы, чаще всего контейнеры. В частности, параметры типов указывают, что содержит контейнер. Здесь мы говорим Kotlin, что числа содержат List<Int>, а строки содержат List<String>: // Summary2/ExplicitTyping.kt package summary2
import atomictest.eq
fun main() {
val numbers: List< Int > = listOf(1, 2, 3)
val strings: List< String > =
listOf(“one”, “two”, “three”)
numbers eq “[1, 2, 3]”
strings eq “[one, two, three]”
toCharList(“seven”) eq “[s, e, v, e, n]”
}
fun toCharList(s: String ): List< Char > =
s.toList()

Для обоих определений numbers и strings мы добавляем двоеточия и объявления типов List<Int> и List<String>. Угловые скобки обозначают параметр типа, позволяя нам сказать: “контейнер содержит объекты ‘параметра’.” Обычно вы произносите List<Int> как “список целых чисел”.

Возвращаемое значение также может иметь параметр типа, как видно в toCharList(). Вы не можете просто сказать, что оно возвращает List — Kotlin будет жаловаться, поэтому вы должны указать параметр типа тоже.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Summary2 169

Списки переменных аргументов Link to heading

Ключевое слово vararg является сокращением от списка переменных аргументов и позволяет функции принимать любое количество аргументов (включая ноль) указанного типа. Vararg становится массивом, который похож на список:

// Summary2/VarArgs.kt
package summary2
import atomictest.*

fun varargs(s: String, vararg ints: Int) {
    for (i in ints) {
        trace("$i")
    }
    trace(s)
}

fun main() {
    varargs("primes", 5, 7, 11, 13, 17, 19, 23)
    trace eq "5 7 11 13 17 19 23 primes"
}

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

Вы можете передать массив элементов везде, где принимается vararg. Чтобы создать массив, используйте arrayOf() так же, как вы используете listOf(). Массив всегда изменяемый. Чтобы преобразовать массив в последовательность аргументов (а не просто в один элемент типа массив), используйте оператор распространения *:

// Summary2/ArraySpread.kt
import summary2.varargs
import atomictest.trace

fun main() {
    val array = intArrayOf(4, 5) // [1]
    varargs("x", 1, 2, 3, *array, 6) // [2]
    val list = listOf(9, 10, 11)
    varargs("y", 7, 8, *list.toIntArray()) // [3]
    trace eq "1 2 3 4 5 6 x 7 8 9 10 11 y"
}

Если вы передаете массив примитивных типов, как в приведенном выше примере, функция создания массива должна быть явно типизирована. Если [1] использует arrayOf(4, 5) вместо intArrayOf(4, 5), то [2] вызовет ошибку: inferred type is Array but IntArray was expected.

Оператор распространения работает только с массивами. Если у вас есть список, который нужно передать в качестве последовательности аргументов, сначала преобразуйте его в массив, а затем примените оператор распространения, как в [3]. Поскольку результат является массивом примитивного типа, мы должны использовать специальную функцию преобразования toIntArray().

Множества Link to heading

Множества — это коллекции, которые допускают только один элемент каждого значения. Множество автоматически предотвращает дубликаты. // Summary2/ColorSet.kt package summary2
import atomictest.eq
val colors =
“Желтый Зеленый Зеленый Синий”
.split(Regex("""\W+""")).sorted() // [1]
fun main() {
colors eq
listOf(“Синий”, “Зеленый”, “Зеленый”, “Желтый”)
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
Summary2 171
val colorSet = colors.toSet() // [2]
colorSet eq
setOf(“Желтый”, “Зеленый”, “Синий”)
(colorSet + colorSet) eq colorSet // [3]
val mSet = colorSet.toMutableSet() // [4]
mSet -= “Синий”
mSet += “Красный” // [5]
mSet eq
setOf(“Желтый”, “Зеленый”, “Красный”)
// Членство в множестве:
(“Зеленый” in colorSet) eq true // [6]
colorSet.contains(“Красный”) eq false
}
• [1] Строка разбивается с помощью метода split() с использованием регулярного выражения, как описано ранее в ListOfStrings.kt.
• [2] Когда цвета копируются в только для чтения множество colorSet, одна из двух строк “Зеленый” удаляется, так как она является дубликатом.
• [3] Здесь мы создаем и отображаем новое множество, используя оператор +. Помещение дублирующихся элементов в множество автоматически удаляет эти дубликаты.
• [4] toMutableSet() создает новое изменяемое множество из только для чтения множества.
• [5] Для изменяемого множества операторы += и -= добавляют и удаляют элементы, как это делается с изменяемыми списками.
• [6] Проверка на членство в множестве с использованием in или contains()
Обычные математические операции над множествами, такие как объединение, пересечение, разность и т. д., также доступны.

Карты Link to heading

Карта (Map) связывает ключи со значениями и ищет значение, когда дан ключ. Вы создаете карту, предоставляя пары ключ-значение функции mapOf(). Используя to, мы разделяем каждый ключ от его связанного значения:

import atomictest.eq

fun main() {
    val ascii = mapOf(
        "A" to 65,
        "B" to 66,
        "C" to 67,
        "I" to 73,
        "J" to 74,
        "K" to 75
    )
    ascii eq "{A=65, B=66, C=67, I=73, J=74, K=75}"
    ascii["B"] eq 66 // [1]
    ascii.keys eq "[A, B, C, I, J, K]"
    ascii.values eq "[65, 66, 67, 73, 74, 75]"
    
    var kv = ""
    for (entry in ascii) { // [2]
        kv += " ${entry.key} : ${entry.value},"
    }
    kv eq "A:65,B:66,C:67,I:73,J:74,K:75,"
    kv = ""
    
    for ((key, value) in ascii) // [3]
        kv += " $key: $value,"
    kv eq "A:65,B:66,C:67,I:73,J:74,K:75,"
    
    val mutable = ascii.toMutableMap() // [4]
    mutable.remove("I")
    mutable eq "{A=65, B=66, C=67, J=74, K=75}"
    mutable.put("Z", 90)
    mutable eq "{A=65, B=66, C=67, J=74, K=75, Z=90}"
    mutable.clear()
    mutable["A"] = 100
    mutable eq "{A=100}"
}
  • [1] Ключ (“B”) используется для поиска значения с помощью оператора []. Вы можете получить все ключи, используя keys, и все значения, используя values. Доступ к ключам возвращает Set, потому что все ключи в карте должны быть уникальными (в противном случае возникнет неоднозначность при поиске).
  • [2] Итерация по карте возвращает пары ключ-значение в виде записей карты.
  • [3] Вы можете распаковывать пары ключ-значение во время итерации.
  • [4] Вы можете создать MutableMap из неизменяемой карты, используя toMutableMap(). Теперь мы можем выполнять операции, которые изменяют mutable, такие как remove(), put() и clear(). Квадратные скобки могут присваивать новую пару ключ-значение в mutable. Вы также можете добавить пару, сказав map += key to value.

Accessorы свойств Link to heading

Доступ к свойству i кажется простым:

// Summary2/PropertyReadWrite.kt
package summary2
import atomictest.eq

class Holder(var i: Int)

fun main() {
    val holder = Holder(10)
    holder.i eq 10 // Чтение свойства 'i'
    holder.i = 20 // Запись в свойство 'i'
}

Однако Kotlin вызывает функции для выполнения операций чтения и записи. Поведение этих функций по умолчанию заключается в том, чтобы читать и записывать данные, хранящиеся в i. Создавая accessorы свойств, вы изменяете действия, которые происходят во время чтения и записи.

Accessor, используемый для получения значения свойства, называется getter. Чтобы создать свой собственный getter, определите get() сразу после объявления свойства. Accessor, используемый для изменения изменяемого свойства, называется setter. Чтобы создать свой собственный setter, определите set() сразу после объявления свойства. Порядок определения getter’ов и setter’ов не важен, и вы можете определить один без другого.

Accessor’ы свойств в следующем примере имитируют реализации по умолчанию, одновременно отображая дополнительную информацию, чтобы вы могли увидеть, что accessor’ы действительно вызываются во время чтения и записи. Мы визуально связываем функции get() и set() с свойством, но фактическая ассоциация происходит потому, что они определены сразу после этого свойства:

// Summary2/GetterAndSetter.kt
package summary2
import atomictest.*

class GetterAndSetter {
    var i: Int = 0
        get() {
            trace("get()")
            return field
        }
        set(value) {
            trace("set($value)")
            field = value
        }
}

fun main() {
    val gs = GetterAndSetter()
    gs.i = 2
    trace(gs.i)
    trace eq """
    set(2)
    get()
    2
    """
}

Внутри getter’а и setter’а хранимое значение манипулируется косвенно с использованием ключевого слова field, которое доступно только внутри этих двух функций. Вы также можете создать свойство, которое не имеет поля, а просто вызывает getter для получения результата.

Если вы объявите приватное свойство, оба accessor’а станут приватными. Вы можете сделать setter приватным, а getter публичным. Это означает, что вы можете читать свойство вне класса, но изменять его значение только внутри класса.

Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC.

Раздел III: Удобство использования Link to heading

Компьютерные языки различаются не столько тем, что они делают возможным, сколько тем, что они делают простым. — Ларри Уолл, изобретатель языка Perl

Функции расширения Link to heading

Предположим, вы обнаружили библиотеку, которая делает всё, что вам нужно… почти. Если бы в ней было всего одна или две дополнительные функции-члены, она бы идеально решила вашу проблему. Но это не ваш код — либо у вас нет доступа к исходному коду, либо вы не контролируете его. Вам пришлось бы повторять ваши модификации каждый раз, когда выходила новая версия.

Функции расширения Kotlin эффективно добавляют функции-члены к существующим классам. Тип, который вы расширяете, называется получателем (receiver). Чтобы определить функцию расширения, вы предшествуете имени функции типом получателя:

fun ReceiverType.extensionFunction() { … }

Это добавляет две функции расширения к классу String:

// ExtensionFunctions/Quoting.kt
**package extensionfunctions**
**import atomictest.eq**
**fun** String.singleQuote() = "' **$** this'"
**fun** String.doubleQuote() = "\" **$** this\""
**fun** main() {
    "Hi".singleQuote() eq "'Hi'"
    "Hi".doubleQuote() eq "\"Hi\""
}

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

// ExtensionFunctions/Quote.kt
**package other**
**import atomictest.eq**
**import extensionfunctions.doubleQuote**
**import extensionfunctions.singleQuote**
**fun** main() {
    "Single".singleQuote() eq "'Single'"
    "Double".doubleQuote() eq "\"Double\""
}

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

// ExtensionFunctions/StrangeQuote.kt
**package extensionfunctions**
**import atomictest.eq**
// Применяем два набора одинарных кавычек:
**fun** String.strangeQuote() =
    **this**.singleQuote().singleQuote() // [1]
**fun** String.tooManyQuotes() =
    doubleQuote().doubleQuote() // [2]
**fun** main() {
    "Hi".strangeQuote() eq "''Hi''"
    "Hi".tooManyQuotes() eq "\"\"Hi\"\""
}

• [1] this ссылается на получателя String.
• [2] Мы опускаем объект получателя (this) в первом вызове функции doubleQuote().

Создание расширений для ваших собственных классов иногда может привести к более простому коду:

// ExtensionFunctions/BookExtensions.kt
**package extensionfunctions**
**import atomictest.eq**
**class Book** ( **val** title: **String** )
**fun** Book.categorize(category: **String**) =
    """title: " **$** title", category: **$** category"""
**fun** main() {
    Book("Dracula").categorize("Vampire") eq
    """title: "Dracula", category: Vampire"""
}

Внутри функции categorize() мы получаем доступ к свойству title без явного указания.
Функции расширения могут получать доступ только к публичным элементам типа, который расширяется. Таким образом, расширения могут выполнять только те же действия, что и обычные функции. Вы можете переписать Book.categorize(String) как categorize(Book, String). Единственная причина для использования функции расширения — это синтаксис, но этот синтаксический сахар мощен. Для вызывающего кода расширения выглядят так же, как функции-члены, и IDE показывает расширения при перечислении функций, которые вы можете вызвать для объекта.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Именованные и стандартные аргументы Link to heading

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

// NamedAndDefaultArgs/NamedArguments.kt
package color1
import atomictest.eq

fun color(red: Int, green: Int, blue: Int) =
    "( $red, $green, $blue)"

fun main() {
    color(1, 2, 3) eq "(1, 2, 3)" // [1]
    color(
        red = 76, // [2]
        green = 89,
        blue = 0
    ) eq "(76, 89, 0)"
    color(52, 34, blue = 0) eq // [3]
        "(52, 34, 0)"
}
  • [1] Это не дает вам много информации. Вам придется обратиться к документации, чтобы узнать, что означают аргументы.
  • [2] Значение каждого аргумента ясно.
  • [3] Вам не обязательно указывать имена для всех аргументов.

Именованные аргументы позволяют вам изменить порядок цветов. Здесь мы указываем синий цвет первым:

// NamedAndDefaultArgs/ArgumentOrder.kt
import color1.color
import atomictest.eq

fun main() {
    color(blue = 0, red = 99, green = 52) eq
        "(99, 52, 0)"
    color(red = 255, 255, 0) eq
        "(255, 255, 0)"
}

Вы можете смешивать именованные и обычные (позиционные) аргументы. Если вы изменяете порядок аргументов, вам следует использовать именованные аргументы на протяжении всего вызова — не только для читаемости, но и компилятору часто нужно указывать, где находятся аргументы.

Именованные аргументы становятся еще более полезными в сочетании со стандартными аргументами, которые представляют собой значения по умолчанию для аргументов, указанные в определении функции:

// NamedAndDefaultArgs/Color2.kt
package color2
import atomictest.eq

fun color(
    red: Int = 0,
    green: Int = 0,
    blue: Int = 0,
) = "( $red, $green, $blue)"

fun main() {
    color(139) eq "(139, 0, 0)"
    color(blue = 139) eq "(0, 0, 139)"
    color(255, 165) eq "(255, 165, 0)"
    color(red = 128, blue = 128) eq
        "(128, 0, 128)"
}

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

Этот пример также использует завершающую запятую в определении функции color(). Завершающая запятая — это дополнительная запятая после последнего параметра (blue). Это полезно, когда ваши параметры или значения записаны на нескольких строках. С завершающей запятой вы можете добавлять новые элементы и изменять их порядок, не добавляя и не удаляя запятые.

Именованные и стандартные аргументы (а также завершающие запятые) также работают для конструкторов:

// NamedAndDefaultArgs/Color3.kt
package color3
import atomictest.eq

class Color(
    val red: Int = 0,
    val green: Int = 0,
    val blue: Int = 0,
) {
    override fun toString() =
        "( $red, $green, $blue)"
}

fun main() {
    Color(red = 77).toString() eq "(77, 0, 0)"
}

Функция joinToString() является стандартной библиотечной функцией, которая использует стандартные аргументы. Она объединяет содержимое итерируемого объекта (списка, множества или диапазона) в строку. Вы можете указать разделитель, префикс и постфикс:

// NamedAndDefaultArgs/CreateString.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, 3,)
    list.toString() eq "[1, 2, 3]"
    list.joinToString() eq "1, 2, 3"
    list.joinToString(prefix = "(",
                      postfix = ")") eq "(1, 2, 3)"
    list.joinToString(separator = ":") eq
        "1:2:3"
}

Стандартный toString() для списка возвращает содержимое в квадратных скобках, что может быть не тем, что вам нужно. Значения по умолчанию для параметров joinToString() — это запятая для разделителя и пустые строки для префикса и постфикса. В приведенном выше примере мы используем именованные и стандартные аргументы, чтобы указать только те аргументы, которые мы хотим изменить.

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

Если вы используете объект в качестве стандартного аргумента, новый экземпляр этого объекта создается для каждого вызова:

Если вы передаете экземпляр объекта в качестве стандартного аргумента (da в функции g() в следующем примере), тот же экземпляр используется для каждого вызова g(). Если вы передаете синтаксис вызова конструктора (DefaultArg() в функции h()), этот конструктор вызывается каждый раз, когда вы вызываете h():

// NamedAndDefaultArgs/Evaluation.kt
package namedanddefault

class DefaultArg
val da = DefaultArg()

fun g(d: DefaultArg = da) = println(d)
fun h(d: DefaultArg = DefaultArg()) = println(d)

fun main() {
    g()
    g()
    h()
    h()
}
/* Пример вывода:
namedanddefault.DefaultArg@7440e464
namedanddefault.DefaultArg@7440e464
namedanddefault.DefaultArg@49476842
namedanddefault.DefaultArg@78308db1
*/

Вывод двух вызовов g() показывает одинаковые адреса объектов. Для двух вызовов h() адреса объектов DefaultArg различны, что показывает, что это два разных объекта.

Указывайте имена аргументов, когда это улучшает читаемость. Сравните следующие два вызова joinToString():

// NamedAndDefaultArgs/CreateString2.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, 3)
    list.joinToString(". ", "", "!") eq
        "1. 2. 3!"
    list.joinToString(separator = ". ",
                      postfix = "!") eq "1. 2. 3!"
}

Трудно угадать, является ли “. " или "” разделителем, если вы не запомнили порядок параметров, что непрактично.

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

// NamedAndDefaultArgs/TrimMargin.kt
import atomictest.eq

fun main() {
    val poem = """
    |->Last night I saw upon the stair
    |->A little man who wasn't there
    |->He wasn't there again today
    |->Oh, how I wish he'd go away."""
    
    poem.trimMargin() eq
        """->Last night I saw upon the stair
        ->A little man who wasn't there
        ->He wasn't there again today
        ->Oh, how I wish he'd go away."""
    
    poem.trimMargin(marginPrefix = "|->") eq
        """Last night I saw upon the stair
        A little man who wasn't there
        He wasn't there again today
        Oh, how I wish he'd go away."""
}

Символ | (“пайп”) является стандартным аргументом для префикса, и вы можете заменить его на строку по вашему выбору.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Перегрузка Link to heading

Языки, не поддерживающие аргументы по умолчанию, часто используют перегрузку, чтобы имитировать эту функцию. Термин “перегрузка” относится к имени функции: вы используете одно и то же имя (перегружаете это имя) для различных функций, если списки параметров различаются. Вот пример перегрузки член-функции f():

// Overloading/Overloading.kt
package overloading
import atomictest.eq

class Overloading {
    fun f() = 0
    fun f(n: Int) = n + 2
}

fun main() {
    val o = Overloading()
    o.f() eq 0
    o.f(11) eq 13
}

В Overloading вы видите две функции с одинаковым именем, f(). Подпись функции состоит из имени, списка параметров и типа возвращаемого значения. Kotlin различает одну функцию от другой, сравнивая подписи. При перегрузке функций списки параметров должны быть уникальными — вы не можете перегружать по типам возвращаемых значений. Вызовы показывают, что это действительно разные функции. Подпись функции также включает информацию о содержащем классе (или типе получателя, если это функция-расширение).

Если класс уже имеет член-функцию с такой же подписью, как функция-расширение, Kotlin предпочитает член-функцию. Тем не менее, вы можете перегрузить член-функцию с помощью функции-расширения:

// Overloading/MemberVsExtension.kt
package overloading
import atomictest.eq

class My {
    fun foo() = 0
}

fun My.foo() = 1 // [1]
fun My.foo(i: Int) = i + 2 // [2]

fun main() {
    My().foo() eq 0
    My().foo(1) eq 3
}

• [1] Бессмысленно объявлять расширение, которое дублирует член, потому что его никогда не смогут вызвать.
• [2] Вы можете перегрузить член-функцию, используя функцию-расширение, предоставив другой список параметров.

Не используйте перегрузку для имитации аргументов по умолчанию. То есть, не делайте так:

// Overloading/WithoutDefaultArguments.kt
package withoutdefaultarguments
import atomictest.eq

fun f(n: Int) = n + 373
fun f() = f(0)

fun main() {
    f() eq 373
}

Функция без параметров просто вызывает первую функцию. Эти две функции могут быть заменены одной функцией с использованием аргумента по умолчанию:

// Overloading/WithDefaultArguments.kt
package withdefaultarguments
import atomictest.eq

fun f(n: Int = 0) = n + 373

fun main() {
    f() eq 373
}

В обоих примерах вы можете вызвать функцию либо без аргумента, либо передав целочисленное значение. Предпочтительнее использовать форму из WithDefaultArguments.kt.

При использовании перегруженных функций вместе с аргументами по умолчанию вызов перегруженной функции ищет “ближайшее” соответствие. В следующем примере вызов foo() в main() не вызывает первую версию функции с ее аргументом по умолчанию 99, а вместо этого вызывает вторую версию, ту, что без параметров:

// Overloading/OverloadedVsDefaultArg.kt
package overloadingvsdefaultargs
import atomictest.*

fun foo(n: Int = 99) = trace("foo-1- $n")
fun foo() {
    trace("foo-2")
    foo(14)
}

fun main() {
    foo()
    trace eq """
    foo-2
    foo-1-14
    """
}

Вы никогда не сможете использовать аргумент по умолчанию 99, потому что foo() всегда вызывает вторую версию f().

Почему перегрузка полезна? Она позволяет более четко выразить “вариации на тему”, чем если бы вам пришлось использовать разные имена функций. Предположим, вы хотите функции сложения:

// Overloading/OverloadingAdd.kt
package overloading
import atomictest.eq

fun addInt(i: Int, j: Int) = i + j
fun addDouble(i: Double, j: Double) = i + j
fun add(i: Int, j: Int) = i + j
fun add(i: Double, j: Double) = i + j

fun main() {
    addInt(5, 6) eq add(5, 6)
    addDouble(56.23, 44.77) eq add(56.23, 44.77)
}

addInt() принимает два Int и возвращает Int, в то время как addDouble() принимает два Double и возвращает Double. Без перегрузки вы не можете просто назвать операцию add(), поэтому программисты обычно смешивают “что” с “как”, чтобы создать уникальные имена (вы также можете создать уникальные имена, используя случайные символы, но типичный шаблон — использовать значимую информацию, такую как типы параметров). В отличие от этого, перегруженная add() гораздо яснее.

Отсутствие перегрузки в языке не является ужасным недостатком, но эта функция предоставляет ценное упрощение, создавая более читаемый код. С перегрузкой вы просто говорите “что”, что повышает уровень абстракции и снижает умственную нагрузку на читателя. Если вы хотите знать “как”, посмотрите на параметры. Также обратите внимание, что перегрузка уменьшает избыточность: если мы должны говорить addInt() и addDouble(), то мы по сути повторяем информацию о параметрах в имени функции.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Выражения when Link to heading

Большая часть программирования заключается в выполнении действия, когда совпадает шаблон. Всё, что упрощает эту задачу, является благом для программистов. Если у вас есть более двух или трёх вариантов выбора, выражения when гораздо удобнее, чем выражения if. Выражение when сравнивает значение с набором возможных вариантов. Оно начинается с ключевого слова when и скобочного значения для сравнения. За ним следует тело, содержащее набор возможных совпадений и их связанные действия. Каждое совпадение — это выражение, за которым следует стрелка ->. Стрелка состоит из двух отдельных символов - и > без пробелов между ними. Выражение вычисляется и сравнивается с целевым значением. Если оно совпадает, выражение справа от -> производит результат выражения when.

Функция ordinal() в следующем примере строит немецкое слово для порядкового числа на основе слова для.cardinal числа. Она сопоставляет целое число с фиксированным набором чисел, чтобы проверить, применимо ли это к общему правилу или является исключением (что происходит болезненно часто в немецком):

// WhenExpressions/GermanOrdinals.kt
package whenexpressions
import atomictest.eq

val numbers = mapOf(
    1 to "eins", 2 to "zwei", 3 to "drei",
    4 to "vier", 5 to "fuenf", 6 to "sechs",
    7 to "sieben", 8 to "acht", 9 to "neun",
    10 to "zehn", 11 to "elf", 12 to "zwoelf",
    13 to "dreizehn", 14 to "vierzehn",
    15 to "fuenfzehn", 16 to "sechzehn",
    17 to "siebzehn", 18 to "achtzehn",
    19 to "neunzehn", 20 to "zwanzig"
)

fun ordinal(i: Int): String =
    when (i) { // [1]
        1 -> "erste" // [2]
        3 -> "dritte"
        7 -> "siebte"
        8 -> "achte"
        20 -> "zwanzigste"
        else -> numbers.getValue(i) + "te" // [3]
    }

fun main() {
    ordinal(2) eq "zweite"
    ordinal(3) eq "dritte"
    ordinal(11) eq "elfte"
}
  • [1] Выражение when сравнивает i с выражениями совпадений в теле.
  • [2] Первое успешное совпадение завершает выполнение выражения when — здесь создаётся строка, которая становится возвращаемым значением функции ordinal().
  • [3] Ключевое слово else предоставляет “падение” в случае, если совпадений нет. Случай else всегда появляется последним в списке совпадений. Когда мы проверяем на 2, оно не совпадает с 1, 3, 7, 8 или 20, и поэтому переходит к случаю else.

Если вы забудете ветвь else в приведённом выше примере, ошибка компиляции будет: «выражение ‘when’ должно быть исчерпывающим, добавьте необходимую ветвь ’else’». Если вы рассматриваете выражение when как оператор — то есть не используете результат when — вы можете опустить ветвь else. Несовпадающие значения просто игнорируются.

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

// WhenExpressions/AnalyzeInput.kt
package whenexpressions
import atomictest.*

class Coordinates {
    var x: Int = 0
        set(value) {
            trace("x gets $value")
            field = value
        }
    var y: Int = 0
        set(value) {
            trace("y gets $value")
            field = value
        }
    override fun toString() = "( $x, $y)"
}

fun processInputs(inputs: List<String>) {
    val coordinates = Coordinates()
    for (input in inputs) {
        when (input) { // [1]
            "up", "u" -> coordinates.y-- // [2]
            "down", "d" -> coordinates.y++
            "left", "l" -> coordinates.x--
            "right", "r" -> { // [3]
                trace("Moving right")
                coordinates.x++
            }
            "nowhere" -> {} // [4]
            "exit" -> return // [5]
            else -> trace("bad input: $input")
        }
    }
}

fun main() {
    processInputs(listOf("up", "d", "nowhere", "left", "right", "exit", "r"))
    trace eq """
    y gets -1
    y gets 0
    x gets -1
    Moving right
    x gets 0
    """
}
  • [1] input сопоставляется с различными вариантами.
  • [2] Вы можете перечислить несколько значений в одной ветви, используя запятые. Здесь, если пользователь вводит либо “up”, либо “u”, мы интерпретируем это как движение вверх.
  • [3] Несколько действий в ветви должны быть в теле блока.
  • [4] “Ничего не делать” выражается пустым блоком.
  • [5] Возврат из внешней функции является допустимым действием в ветви. В этом случае return завершает вызов processInputs().

Любое выражение может быть аргументом для when, и совпадения могут быть любыми значениями (не только константами):

// WhenExpressions/MatchingAgainstVals.kt
import atomictest.*

fun main() {
    val yes = "A"
    val no = "B"
    for (choice in listOf(yes, no, yes)) {
        when (choice) {
            yes -> trace("Hooray!")
            no -> trace("Too bad!")
        }
        // То же самое с использованием 'if':
        if (choice == yes) trace("Hooray!")
        else if (choice == no) trace("Too bad!")
    }
    trace eq """
    Hooray!
    Hooray!
    Too bad!
    Too bad!
    Hooray!
    Hooray!
    """
}

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

Мы можем сопоставить набор значений с другим набором значений:

// WhenExpressions/MixColors.kt
package whenexpressions
import atomictest.eq

fun mixColors(first: String, second: String) =
    when (setOf(first, second)) {
        setOf("red", "blue") -> "purple"
        setOf("red", "yellow") -> "orange"
        setOf("blue", "yellow") -> "green"
        else -> "unknown"
    }

fun main() {
    mixColors("red", "blue") eq "purple"
    mixColors("blue", "red") eq "purple"
    mixColors("blue", "purple") eq "unknown"
}

Внутри mixColors() мы используем набор в качестве аргумента when и сравниваем его с различными наборами. Мы используем набор, потому что порядок элементов не важен — нам нужен один и тот же результат, когда мы смешиваем “red” и “blue”, как и когда мы смешиваем “blue” и “red”.

Выражение when имеет специальную форму, которая не принимает аргументов. Пропуск аргумента означает, что ветви могут проверять различные логические условия. Вы можете использовать любое логическое выражение в качестве условия ветви. В качестве примера мы переписываем bmiMetric(), представленную в NumberTypes, сначала показывая оригинальное решение, а затем используя when вместо if:

// WhenExpressions/BmiWhen.kt
package whenexpressions
import atomictest.eq

fun bmiMetricOld(
    kg: Double,
    heightM: Double
): String {
    val bmi = kg / (heightM * heightM)
    return if (bmi < 18.5) "Underweight"
    else if (bmi < 25) "Normal weight"
    else "Overweight"
}

fun bmiMetricWithWhen(
    kg: Double,
    heightM: Double
): String {
    val bmi = kg / (heightM * heightM)
    return when {
        bmi < 18.5 -> "Underweight"
        bmi < 25 -> "Normal weight"
        else -> "Overweight"
    }
}

fun main() {
    bmiMetricOld(72.57, 1.727) eq
    bmiMetricWithWhen(72.57, 1.727)
}

Решение с использованием when является более элегантным способом выбора между несколькими вариантами. Более простая синтаксическая структура облегчает различение этих вариантов.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Перечисления Link to heading

Перечисление — это коллекция имен.
Класс enum в Kotlin — это удобный способ управления этими именами:

// Enumerations/Level.kt
package enumerations
import atomictest.eq

enum class Level {
    Overflow, High, Medium, Low, Empty
}

fun main() {
    Level.Medium eq "Medium"
}

Создание перечисления генерирует метод toString() для имен перечисления.
Вы должны квалифицировать каждую ссылку на имя перечисления, как в случае с Level.Medium в main(). Вы можете устранить эту квалификацию, используя импорт, чтобы перенести все имена из перечисления в текущее пространство имен (пространства имен предотвращают конфликты имен):

// Enumerations/EnumImport.kt
import atomictest.eq
import enumerations.Level.* // [1]

fun main() {
    Overflow eq "Overflow"
    High eq "High"
}

• [1] Звездочка (*) импортирует все имена внутри перечисления Level, но не импортирует само имя Level.

Вы можете импортировать значения перечисления в тот же файл, где определен класс перечисления:

// Enumerations/RecursiveEnumImport.kt
package enumerations
import atomictest.eq
import enumerations.Size.* // [1]

enum class Size {
    Tiny, Small, Medium, Large, Huge, Gigantic
}

fun main() {
    Gigantic eq "Gigantic" // [2]
    Size.values().toList() eq // [3]
        listOf(Tiny, Small, Medium, Large, Huge, Gigantic)
    Tiny.ordinal eq 0 // [4]
    Huge.ordinal eq 4
}

• [1] Мы импортируем значения Size до того, как определение Size появится в файле.
• [2] После импорта нам больше не нужно квалифицировать доступ к именам перечисления.
• [3] Вы можете итерировать по именам перечисления, используя values(). values() возвращает массив, поэтому мы вызываем toList(), чтобы преобразовать его в список.
• [4] Первая объявленная константа перечисления имеет порядковое значение ноль. Каждая последующая константа получает следующее целое значение.

Вы можете выполнять разные действия для разных записей перечисления, используя выражение when. Здесь мы импортируем имя Level, а также все его записи:

// Enumerations/CheckingOptions.kt
package checkingoptions
import atomictest.*
import enumerations.Level
import enumerations.Level.*

fun checkLevel(level: Level) {
    when (level) {
        Overflow -> trace(">>> Overflow!")
        Empty -> trace("Alert: Empty")
        else -> trace("Level $level OK")
    }
}

fun main() {
    checkLevel(Empty)
    checkLevel(Low)
    checkLevel(Overflow)
    trace eq """
        Alert: Empty
        Level Low OK
        >>> Overflow!
    """
}

Функция checkLevel() выполняет специфические действия только для двух констант, в то время как для всех остальных вариантов ведет себя обычно (в случае else).

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

// Enumerations/Direction.kt
package enumerations
import atomictest.eq
import enumerations.Direction.*

enum class Direction(val notation: String) {
    North("N"), South("S"),
    East("E"), West("W"); // Точка с запятой обязательна

    val opposite: Direction
        get() = when (this) {
            North -> South
            South -> North
            West -> East
            East -> West
        }
}

fun main() {
    North.notation eq "N"
    North.opposite eq South
    West.opposite.opposite eq West
    North.opposite.notation eq "S"
}

Класс Direction содержит свойство notation, которое хранит различное значение для каждого экземпляра. Вы передаете значения для параметра конструктора notation в скобках (например, North("N")), так же как вы создаете экземпляр обычного класса.
Геттер для свойства opposite динамически вычисляет результат при его доступе.
Обратите внимание, что в этом примере when не требует ветки else, потому что все возможные записи перечисления охвачены.

Перечисления могут сделать ваш код более читаемым, что всегда желательно.
Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Классы данных Link to heading

Kotlin уменьшает количество повторяющегося кода. Механизм классов выполняет значительную часть работы за вас. Однако создание классов, которые в основном хранят данные, все еще требует значительного количества повторяющегося кода. Когда вам нужен класс, который по сути является хранилищем данных, классы данных упрощают ваш код и выполняют общие задачи. Вы определяете класс данных, используя ключевое слово data, которое сообщает Kotlin сгенерировать дополнительный функционал. Каждый параметр конструктора должен быть предшествован var или val:

// DataClasses/Simple.kt
package dataclasses
import atomictest.eq

data class Simple (
    val arg1: String,
    var arg2: Int
)

fun main() {
    val s1 = Simple("Hi", 29)
    val s2 = Simple("Hi", 29)
    s1 eq "Simple(arg1=Hi, arg2=29)"
    s1 eq s2
}

Этот пример демонстрирует две особенности классов данных:

  1. Строка, производимая s1, отличается от того, что мы обычно видим; она включает имена параметров и значения данных, хранящихся в объекте. Классы данных отображают объекты в приятном, читаемом формате без необходимости в дополнительном коде.
  2. Если вы создаете два экземпляра одного и того же класса данных, содержащих идентичные данные (равные значения для свойств), вы, вероятно, также хотите, чтобы эти два экземпляра были равны. Чтобы достичь такого поведения для обычного класса, вы должны определить специальную функцию equals(), чтобы сравнивать экземпляры. В классах данных эта функция автоматически генерируется; она сравнивает значения всех свойств, указанных как параметры конструктора.

Вот обычный класс Person и класс данных Contact:

// DataClasses/DataClasses.kt
package dataclasses
import atomictest.*

class Person(val name: String)

data class Contact(
    val name: String,
    val number: String
)

fun main() {
    // Эти экземпляры кажутся одинаковыми, но это не так:
    Person("Cleo") neq Person("Cleo")
    // Класс данных определяет равенство разумно:
    Contact("Miffy", "1-234-567890") eq
    Contact("Miffy", "1-234-567890")
}
/* Пример вывода:
dataclasses.Person@54bedef2
Contact(name=Miffy, number=1-234-567890)
*/

Поскольку класс Person определен без ключевого слова data, два экземпляра с одинаковым именем не равны. К счастью, создание Contact как класса данных дает разумный результат. Обратите внимание на разницу между форматом отображения класса данных и Person, который просто показывает информацию по умолчанию об объекте.

Еще одной полезной функцией, генерируемой для каждого класса данных, является copy(), которая создает новый объект, содержащий данные из текущего объекта. Однако она также позволяет вам изменить выбранные значения в процессе:

// DataClasses/CopyDataClass.kt
package dataclasses
import atomictest.eq

data class DetailedContact(
    val name: String,
    val surname: String,
    val number: String,
    val address: String
)

fun main() {
    val contact = DetailedContact(
        "Miffy",
        "Miller",
        "1-234-567890",
        "1600 Amphitheater Parkway"
    )
    val newContact = contact.copy(
        number = "098-765-4321",
        address = "Brandschenkestrasse 110"
    )
    newContact eq DetailedContact(
        "Miffy",
        "Miller",
        "098-765-4321",
        "Brandschenkestrasse 110"
    )
}

Имена параметров для copy() идентичны параметрам конструктора. Все аргументы имеют значения по умолчанию, равные текущим значениям, поэтому вы указываете только те, которые хотите заменить.

HashMap и HashSet Link to heading

Создание класса данных также генерирует соответствующую хеш-функцию, чтобы объекты могли использоваться в качестве ключей в HashMap и HashSet: AtomicKotlin (www.AtomicKotlin.com) Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
КлассыДанных 202

// DataClasses/HashCode.kt
package dataclasses
import atomictest.eq

data class Key(val name: String, val id: Int)

fun main() {
    val korvo: Key = Key("Korvo", 19)
    korvo.hashCode() eq -2041757108
    val map = HashMap<Key, String>()
    map[korvo] = "Alien"
    map[korvo] eq "Alien"
    val set = HashSet<Key>()
    set.add(korvo)
    set.contains(korvo) eq true
}

hashCode() используется вместе с equals() для быстрого поиска ключа в HashMap или HashSet. Создание правильного hashCode() вручную — это сложная и подверженная ошибкам задача, поэтому очень полезно, что класс данных делает это за вас. Перегрузка операторов охватывает equals() и hashCode() более подробно.
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) Брюс Эккель и Светлана Исакова, ©2021 MindView LLC

Декларации деструктурирования Link to heading

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

// Destructuring/Pairs.kt
package destructuring
import atomictest.eq

fun compute(input: Int): Pair<Int, String> =
    if (input > 5)
        Pair(input * 2, "High")
    else
        Pair(input * 2, "Low")

fun main() {
    compute(7) eq Pair(14, "High")
    compute(4) eq Pair(8, "Low")
    val result = compute(5)
    result.first eq 10
    result.second eq "Low"
}

Мы указываем тип возвращаемого значения функции compute() как Pair<Int, String>. Pair — это параметризованный тип, как List или Set. Возвращение нескольких значений полезно, но нам также хотелось бы удобный способ распаковать результаты. Как показано выше, вы можете получить доступ к компонентам Pair, используя его свойства first и second, но вы также можете объявить и инициализировать несколько идентификаторов одновременно, используя декларацию деструктурирования:

val (a, b, c) = composedValue

Эта конструкция деструктурирует составное значение и позиционно присваивает его компоненты. Синтаксис отличается от определения одного идентификатора — для деструктурирования вы помещаете имена идентификаторов в круглые скобки.

Вот декларация деструктурирования для Pair, возвращаемого из compute():

// Destructuring/PairDestructuring.kt
import destructuring.compute
import atomictest.eq

fun main() {
    val (value, description) = compute(7)
    value eq 14
    description eq "High"
}

Класс Triple объединяет три значения, но на этом все. Это сделано намеренно: если вам нужно хранить больше значений, или если вы часто используете множество Pair или Triple, рассмотрите возможность создания специальных классов вместо этого.

Классы данных автоматически позволяют использовать декларации деструктурирования:

// Destructuring/Computation.kt
package destructuring
import atomictest.eq

data class Computation(
    val data: Int,
    val info: String
)

fun evaluate(input: Int) =
    if (input > 5)
        Computation(input * 2, "High")
    else
        Computation(input * 2, "Low")

fun main() {
    val (value, description) = evaluate(7)
    value eq 14
    description eq "High"
}

Яснее возвращать Computation вместо Pair<Int, String>. Выбор хорошего имени для результата почти так же важен, как и выбор хорошего самодокументируемого имени для самой функции. Добавление или удаление информации о Computation проще, если это отдельный класс, а не Pair.

Когда вы распаковываете экземпляр класса данных, вы должны присвоить значения новым идентификаторам в том же порядке, в котором вы определяете свойства в классе:

// Destructuring/Tuple.kt
package destructuring
import atomictest.eq

data class Tuple(
    val i: Int,
    val d: Double,
    val s: String,
    val b: Boolean,
    val l: List<Int>
)

fun main() {
    val tuple = Tuple(1, 3.14, "Mouse", false, listOf())
    val (i, d, s, b, l) = tuple
    i eq 1
    d eq 3.14
    s eq "Mouse"
    b eq false
    l eq listOf()
    val (_, _, animal) = tuple // [1]
    animal eq "Mouse"
}

• [1] Если вам не нужны некоторые идентификаторы, вы можете использовать подчеркивания вместо их имен или полностью опустить их, если они находятся в конце. Здесь распакованные значения 1 и 3.14 отбрасываются с помощью подчеркиваний, “Mouse” сохраняется в animal, а false и пустой список отбрасываются, потому что они находятся в конце списка.

Свойства класса данных присваиваются по порядку, а не по имени. Если вы деструктурируете объект и позже добавите свойство в любое место, кроме конца его класса данных, это новое свойство будет деструктурировано поверх вашего предыдущего идентификатора, что приведет к неожиданным результатам (см. Упражнение 3). Если ваш пользовательский класс данных имеет свойства с одинаковыми типами, компилятор не сможет обнаружить неправильное использование, поэтому вам может понадобиться избегать его деструктурирования. Деструктурирование библиотечных классов данных, таких как Pair или Triple, безопасно, потому что они не изменяются.

Используя цикл for, вы можете итерироваться по Map или List пар (или другим классам данных) и деструктурировать каждый элемент:

// Destructuring/ForLoop.kt
import atomictest.eq

fun main() {
    var result = ""
    val map = mapOf(1 to "one", 2 to "two")
    for ((key, value) in map) {
        result += " $key = $value, "
    }
    result eq "1 = one, 2 = two,"
    result = ""
    val listOfPairs = listOf(Pair(1, "one"), Pair(2, "two"))
    for ((i, s) in listOfPairs) {
        result += "( $i, $s), "
    }
    result eq "(1, one), (2, two),"
}

Функция withIndex() является стандартной библиотечной функцией расширения для List. Она возвращает коллекцию IndexedValue, которую можно деструктурировать:

// Destructuring/LoopWithIndex.kt
import atomictest.trace

fun main() {
    val list = listOf('a', 'b', 'c')
    for ((index, value) in list.withIndex()) {
        trace(" $index: $value")
    }
    trace eq "0:a 1:b 2:c"
}

Декларации деструктурирования разрешены только для локальных var и val и не могут использоваться для создания свойств класса. Упражнения и решения можно найти на www.AtomicKotlin.com.

Nullable Типы Link to heading

Рассмотрим функцию, которая иногда возвращает «нет результата». Когда это происходит, функция не выдает ошибку как таковую. Ничего не пошло не так, просто «нет ответа». Хорошим примером является получение значения из Map. Если Map не содержит значения для данного ключа, он не может дать вам ответ и возвращает ссылку null, чтобы указать на «нет значения»: // NullableTypes/NullInMaps.kt import atomictest.eq fun main() { val map = mapOf(0 to “yes”, 1 to “no”) map[2] eq null } Языки, такие как Java, позволяют результату быть либо null, либо значимым значением. К сожалению, если вы обращаетесь с null так же, как с значимым значением, вы получаете драматический сбой (в Java это приводит к NullPointerException; в более примитивном языке, таком как C, нулевая ссылка может привести к сбою процессора, даже операционной системы или машины). Создатель нулевой ссылки, Тони Хоар, называет это «моей миллиардной ошибкой» (хотя, возможно, это стоило гораздо больше). Одно из возможных решений этой проблемы заключается в том, чтобы язык никогда не позволял null изначально и вместо этого вводил специальный индикатор «нет значения». Kotlin мог бы сделать это, если бы не необходимость взаимодействовать с Java, а Java использует null. Решение Kotlin, возможно, является лучшим компромиссом: типы по умолчанию являются ненулевыми. Если что-то может вернуть нулевой результат, вы должны добавить вопросительный знак к имени типа, чтобы явно пометить этот результат как нулевой: // NullableTypes/NullableTypes.kt import atomictest.eq fun main() { val s1 = “abc” // [1] // Ошибка компиляции: // val s2: String = null // [2] // Нулевые определения: val s3: String? = null // [3] val s4: String? = s1 // [4] // Ошибка компиляции: // val s5: String = s4 // [5] val s6 = s4 // [6] s1 eq “abc” s3 eq null s4 eq “abc” s6 eq “abc” } • [1] s1 не может содержать нулевую ссылку. Все var и val, которые мы создали в книге до сих пор, автоматически ненулевые. • [2] Сообщение об ошибке: null не может быть значением ненулевого типа String. • [3] Чтобы определить идентификатор, который может содержать нулевую ссылку, вы ставите ? в конце имени типа. Такой идентификатор может содержать либо null, либо обычное значение. • [4] Как null, так и обычные ненулевые значения могут храниться в нулевом типе. • [5] Вы не можете присвоить идентификатор нулевого типа идентификатору ненулевого типа. Kotlin выдает: Несоответствие типов: выведенный тип - String?, но ожидался String. Даже если фактическое значение ненулевое, как в этом случае (мы знаем, что это “abc”), Kotlin не позволит это, потому что это два разных типа. • [6] Если вы используете вывод типов, Kotlin создает соответствующий тип. Здесь s6 является нулевым, потому что s4 является нулевым. Хотя это выглядит так, будто мы просто модифицируем существующий тип, добавляя ? в конце, на самом деле мы указываем другой тип. Например, String и String? — это два разных типа. Тип String запрещает операции в строках [2] и [5], тем самым гарантируя, что значение ненулевого типа никогда не будет null. Получение значения из Map с использованием квадратных скобок дает нулевой результат, потому что основная реализация Map происходит из Java: // NullableTypes/NullableInMap.kt import atomictest.eq fun main() { val map = mapOf(0 to “yes”, 1 to “no”) val first: String? = map[0] val second: String? = map[2] first eq “yes” second eq null } Почему важно знать, что значение не может быть null? Многие операции подразумевают ненулевой результат. Например, вызов функции-члена завершится с исключением, если значение получателя равно null. В Java такой вызов завершится с NullPointerException (часто сокращается до NPE). Поскольку почти любое значение может быть null в Java, любой вызов функции может завершиться таким образом. В этих случаях вам нужно писать код для проверки на нулевые результаты или полагаться на другие части кода, чтобы защититься от null. В Kotlin вы не можете просто разыменовать (вызвать функцию-члена или получить доступ к свойству-члену) нулевой идентификатор: // NullableTypes/Dereference.kt import atomictest.eq fun main() { val s1: String = “abc” val s2: String? = s1 s1.length eq 3 // [1] // Не компилируется: // s2.length // [2] } Вы можете получить доступ к членам ненулевого типа, как в [1]. Если вы ссылаетесь на члены нулевого типа, как в [2], Kotlin выдает ошибку. Значения большинства типов хранятся как ссылки на объекты в памяти. Это и есть значение термина разыменование — чтобы получить доступ к объекту, вы извлекаете его значение из памяти. Самый простой способ гарантировать, что разыменование нулевого типа не вызовет NullPointerException, — это явно проверить, что ссылка не равна null: // NullableTypes/ExplicitCheck.kt import atomictest.eq fun main() { val s: String? = “abc” if (s != null ) s.length eq 3 } После явной проверки if Kotlin позволяет вам разыменовать нулевое значение. Но писать этот if каждый раз, когда вы работаете с нулевыми типами, слишком громоздко для такой распространенной операции. У Kotlin есть лаконичный синтаксис, чтобы облегчить эту проблему, о котором вы узнаете в последующих атомах. Каждый раз, когда вы создаете новый класс, Kotlin автоматически включает нулевые и ненулевые типы: // NullableTypes/Amphibian.kt package nullabletypes class Amphibian enum class Species { Frog, Toad, Salamander, Caecilian } fun main() { val a1: Amphibian = Amphibian() val a2: Amphibian? = null val at1: Species = Species.Toad val at2: Species? = null } Как вы можете видеть, мы не делали ничего особенного, чтобы получить дополнительные нулевые типы — они доступны по умолчанию. Упражнения и решения можно найти на www.AtomicKotlin.com.

Безопасные вызовы и оператор Эльвиса Link to heading

Kotlin предоставляет удобные операции для работы с нулевыми значениями. Nullable типы имеют множество ограничений. Вы не можете просто разыменовать nullable идентификатор: // SafeCallsAndElvis/DereferenceNull.kt fun main() { val s: String? = null // Не компилируется: // s.length // [1] } Раскомментирование [1] приводит к ошибке компиляции: Разрешены только безопасные ( ?. ) или утвержденные как ненулевые ( !!. ) вызовы для nullable-получателя типа String? . Безопасный вызов заменяет точку ( . ) в обычном вызове на вопросительный знак и точку ( ?. ), без промежуточного пробела. Безопасные вызовы обращаются к членам nullable таким образом, чтобы исключения не возникали. Они выполняют операцию только тогда, когда получатель не равен null : // SafeCallsAndElvis/SafeOperation.kt package safecalls import atomictest.* fun String.echo() { trace(uppercase()) trace( this ) trace(lowercase()) } fun main() { val s1: String? = “Howdy!” s1?.echo() // [1] val s2: String? = null s2?.echo() // [2] trace eq """ HOWDY! Howdy! howdy! """ } Строка [1] вызывает echo() и производит результаты в trace, в то время как строка [2] ничего не делает, потому что получатель s2 равен null. Безопасные вызовы — это чистый способ захвата результатов: // SafeCallsAndElvis/SafeCall.kt package safecalls import atomictest.eq fun checkLength(s: String? , expected: Int? ) { val length1 = if (s != null ) s.length else null // [1] val length2 = s?.length // [2] length1 eq expected length2 eq expected } fun main() { checkLength(“abc”, 3) checkLength( null , null ) } Строка [2] достигает того же эффекта, что и строка [1]. Если получатель не равен null, выполняется обычный доступ ( s.length ). Если получатель равен null, вызов s.length не выполняется (что вызвало бы исключение), но для выражения возвращается null. Что если вам нужно что-то большее, чем null, производимый ?. ? Оператор Эльвиса предоставляет альтернативу. Этот оператор представляет собой вопросительный знак, за которым следует двоеточие ( ?: ), без промежуточного пробела. Он назван в честь эмотикона музыканта Элвиса Пресли и также является игрой слов “иначе-если” (что звучит отдаленно как “Элвис”). Некоторые языки программирования предоставляют оператор объединения с null, который выполняет то же действие, что и оператор Эльвиса в Kotlin. AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC Безопасные вызовы и оператор Эльвиса 214 Если выражение слева от ?: не равно null, то это выражение становится результатом. Если левое выражение равно null, то выражение справа от ?: становится результатом: // SafeCallsAndElvis/ElvisOperator.kt import atomictest.eq fun main() { val s1: String? = “abc” (s1 ?: “—”) eq “abc” val s2: String? = null (s2 ?: “—”) eq “—” } s1 не равен null, поэтому оператор Эльвиса производит “abc” в качестве результата. Поскольку s2 равен null, оператор Эльвиса производит альтернативный результат “—”. Оператор Эльвиса обычно используется после безопасного вызова, чтобы получить значимое значение вместо стандартного null, как вы видите в [2] : // SafeCallsAndElvis/ElvisCall.kt package safecalls import atomictest.eq fun checkLength(s: String? , expected: Int ) { val length1 = if (s != null ) s.length else 0 // [1] val length2 = s?.length ?: 0 // [2] length1 eq expected length2 eq expected } fun main() { checkLength(“abc”, 3) checkLength( null , 0) } Эта функция checkLength() довольно похожа на ту, что в SafeCall.kt выше. Тип параметра expected теперь ненулевой. [1] и [2] производят ноль вместо null. AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC Безопасные вызовы и оператор Эльвиса 215 Безопасные вызовы позволяют вам записывать цепочки вызовов лаконично, когда некоторые элементы в цепочке могут быть null, и вас интересует только конечный результат: // SafeCallsAndElvis/ChainedCalls.kt package safecalls import atomictest.eq class Person ( val name: String , var friend: Person? = null ) fun main() { val alice = Person(“Alice”) alice.friend?.friend?.name eq null // [1] val bob = Person(“Bob”) val charlie = Person(“Charlie”, bob) bob.friend = charlie bob.friend?.friend?.name eq “Bob” // [2] (alice.friend?.friend?.name ?: “Unknown”) eq “Unknown” // [3] } Когда вы связываете доступ к нескольким членам, используя безопасные вызовы, результат равен null, если любое из промежуточных выражений равно null. • [1] Свойство alice.friend равно null, поэтому остальные вызовы возвращают null. • [2] Все промежуточные вызовы производят значимые значения. • [3] Оператор Эльвиса после цепочки безопасных вызовов предоставляет альтернативное значение, если любой промежуточный элемент равен null. Упражнения и решения можно найти на www.AtomicKotlin.com. AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Утверждения о ненулевых значениях Link to heading

Второй подход к проблеме типов, допускающих значение null, заключается в наличии специального знания о том, что рассматриваемая ссылка не равна null. Чтобы сделать это утверждение, используйте двойной восклицательный знак, !!, который называется утверждением о ненулевом значении. Если это выглядит тревожно, так и должно быть: вера в то, что что-то не может быть null, является источником большинства сбоев программ, связанных с null (остальные происходят из-за непонимания того, что null может возникнуть).

x!! означает «забудьте о том, что x может быть null — я гарантирую, что он не null». x!! возвращает x, если x не null, в противном случае выбрасывает исключение:

// NonNullAssertions/NonNullAssert.kt
import atomictest.*

fun main() {
    var x: String? = "abc"
    x!! eq "abc"
    x = null
    capture {
        val s: String = x!!
    } eq "NullPointerException"
}

Определение val s: String = x!! говорит Kotlin игнорировать то, что он думает, что знает о x, и просто присвоить его s, который является ссылкой на ненулевое значение. К счастью, существует поддержка во время выполнения, которая выбрасывает NullPointerException, когда x равен null.

Обычно вы не будете использовать !! самостоятельно, а вместо этого в сочетании с оператором разыменования.

// NonNullAssertions/NonNullAssertCall.kt
import atomictest.eq

fun main() {
    val s: String? = "abc"
    s!!.length eq 3
}

Если вы ограничите себя одним вызовом с утверждением о ненулевом значении на строку, вам будет легче найти сбой, когда исключение укажет вам номер строки.

Безопасный вызов ?. является единым оператором, но вызов с утверждением о ненулевом значении состоит из двух операторов: утверждения о ненулевом значении (!!) и разыменования (.). Как вы видели в NonNullAssert.kt, вы можете использовать утверждение о ненулевом значении самостоятельно.

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

Если вы часто используете утверждения о ненулевых значениях в своем коде для одной и той же операции, лучше использовать отдельную функцию с конкретным утверждением, описывающим проблему. Например, предположим, что логика вашей программы требует, чтобы определенный ключ присутствовал в Map, и вы предпочитаете получать исключение вместо того, чтобы молча ничего не делать, если ключ отсутствует. Вместо того чтобы извлекать значение обычным способом (квадратные скобки), getValue() выбрасывает NoSuchElementException, если ключ отсутствует:

// NonNullAssertions/ValueFromMap.kt
import atomictest.*

fun main() {
    val map = mapOf(1 to "one")
    map[1]!!.uppercase() eq "ONE"
    map.getValue(1).uppercase() eq "ONE"
    capture {
        map[2]!!.uppercase()
    } eq "NullPointerException"
    capture {
        map.getValue(2).uppercase()
    } eq "NoSuchElementException: " +
        "Ключ 2 отсутствует в карте."
}

Выбрасывание конкретного NoSuchElementException дает вам более полезные детали, когда что-то идет не так.

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

Упражнения и решения можно найти на www.AtomicKotlin.com.

Расширения для Nullable типов Link to heading

Иногда это не то, чем кажется.
s?.f() подразумевает, что s является nullable — в противном случае вы могли бы просто вызвать s.f(). Аналогично, t.f() кажется подразумевающим, что t не является nullable, потому что Kotlin не требует безопасного вызова или программной проверки. Однако t не обязательно является ненулевым.
Стандартная библиотека Kotlin предоставляет функции расширения для String, включая:
isNullOrEmpty(): Проверяет, является ли строка-получатель null или пустой.
isNullOrBlank(): Выполняет ту же проверку, что и isNullOrEmpty(), и позволяет строке-получателю состоять исключительно из пробельных символов, включая табуляции (\t) и переводы строк (\n).

Вот базовая проверка этих функций:

// NullableExtensions/StringIsNullOr.kt
import atomictest.eq

fun main() {
    val s1: String? = null
    s1.isNullOrEmpty() eq true
    s1.isNullOrBlank() eq true

    val s2 = ""
    s2.isNullOrEmpty() eq true
    s2.isNullOrBlank() eq true

    val s3: String = " \t\n"
    s3.isNullOrEmpty() eq false
    s3.isNullOrBlank() eq true
}

Названия функций предполагают, что они предназначены для nullable типов. Однако, даже если s1 является nullable, вы можете вызвать isNullOrEmpty() или isNullOrBlank() без безопасного вызова или явной проверки. Это связано с тем, что это функции расширения для nullable типа String?.
Мы можем переписать isNullOrEmpty() как не функцию расширения, которая принимает nullable String s в качестве параметра:

// NullableExtensions/NullableParameter.kt
package nullableextensions
import atomictest.eq

fun isNullOrEmpty(s: String?): Boolean =
    s == null || s.isEmpty()

fun main() {
    isNullOrEmpty(null) eq true
    isNullOrEmpty("") eq true
}

Поскольку s является nullable, мы явно проверяем на null или пустоту. Выражение s == null || s.isEmpty() использует короткое замыкание: если первая часть выражения истинна, остальная часть выражения не вычисляется, тем самым предотвращая исключение нулевого указателя.
Функции расширения используют это для представления получателя (объекта типа, который расширяется). Чтобы сделать получателя nullable, добавьте ? к типу, который расширяется:

// NullableExtensions/NullableExtension.kt
package nullableextensions
import atomictest.eq

fun String?.isNullOrEmpty(): Boolean =
    this == null || isEmpty()

fun main() {
    "".isNullOrEmpty() eq true
}

isNullOrEmpty() более читаема как функция расширения.
• -
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Будьте осторожны при использовании расширений для nullable типов. Они отлично подходят для простых случаев, таких как isNullOrEmpty() и isNullOrBlank(), особенно с самодостаточными названиями, которые подразумевают, что получатель может быть null. В общем, лучше объявлять обычные (ненулевые) расширения. Безопасные вызовы и явные проверки проясняют нулевость получателя, в то время как расширения для nullable типов могут скрывать нулевость и путать читателя вашего кода (вероятно, “будущего вас”).
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Введение в обобщения Link to heading

Обобщения создают параметризованные типы: компоненты, которые работают с несколькими типами. Термин “обобщенный” означает “относящийся или подходящий для больших групп классов”. Первоначальная цель обобщений в языках программирования заключалась в том, чтобы предоставить программисту максимальную выразительность при написании классов или функций, ослабляя ограничения типов для этих классов или функций.

Одной из самых убедительных первоначальных мотиваций для обобщений является создание классов коллекций, которые вы видели в примерах с List, Set и Map, использованных в этой книге. Коллекция — это объект, который содержит другие объекты. Многие программы требуют, чтобы вы хранили группу объектов, пока используете их, поэтому коллекции являются одной из самых многоразовых библиотек классов.

Давайте рассмотрим класс, который хранит один объект. Этот класс указывает точный тип этого объекта:

// IntroGenerics/RigidHolder.kt
package introgenerics
import atomictest.eq

data class Automobile(val brand: String)

class RigidHolder(private val a: Automobile) {
    fun getValue() = a
}

fun main() {
    val holder = RigidHolder(Automobile("BMW"))
    holder.getValue() eq "Automobile(brand=BMW)"
}

RigidHolder не является особенно многоразовым инструментом; он не может хранить ничего, кроме Automobile. Мы предпочли бы не писать новый тип контейнера для каждого различного типа. Чтобы достичь этого, мы используем параметр типа вместо Automobile.

Чтобы определить обобщенный тип, добавьте угловые скобки (<>), содержащие один или несколько обобщенных заполнителей, и поместите это определение обобщения после имени класса. Здесь обобщенный заполнитель T представляет неизвестный тип и используется внутри класса так, как если бы это был обычный тип:

// IntroGenerics/GenericHolder.kt
package introgenerics
import atomictest.eq

class GenericHolder<T>( // [1]
    private val value: T
) {
    fun getValue(): T = value
}

fun main() {
    val h1 = GenericHolder(Automobile("Ford"))
    val a: Automobile = h1.getValue() // [2]
    a eq "Automobile(brand=Ford)"
    
    val h2 = GenericHolder(1)
    val i: Int = h2.getValue() // [3]
    i eq 1
    
    val h3 = GenericHolder("Chartreuse")
    val s: String = h3.getValue() // [4]
    s eq "Chartreuse"
}

• [1] GenericHolder хранит T. Его член-функция getValue() возвращает T. Когда вы вызываете getValue(), как в [2], [3] или [4], результат автоматически имеет правильный тип.

Кажется, что мы могли бы решить эту проблему с помощью “универсального типа” — типа, который является родителем всех других типов. В Kotlin этот универсальный тип называется Any. Как следует из названия, Any позволяет использовать любой тип аргумента. Если вы хотите передать различные типы в функцию, и они не имеют ничего общего, Any решает эту проблему.

На первый взгляд, кажется, что мы могли бы использовать Any вместо T в GenericHolder.kt:

// IntroGenerics/AnyInstead.kt
package introgenerics
import atomictest.eq

class AnyHolder(private val value: Any) {
    fun getValue(): Any = value
}

class Dog {
    fun bark() = "Ruff!"
}

fun main() {
    val holder = AnyHolder(Dog())
    val any = holder.getValue()
    // Не компилируется:
    // any.bark()
    
    val genericHolder = GenericHolder(Dog())
    val dog = genericHolder.getValue()
    dog.bark() eq "Ruff!"
}

Any действительно работает для простых случаев, но как только нам нужен конкретный тип — чтобы вызвать bark() для Dog — это не сработает, потому что мы теряем информацию о том, что это Dog, когда он присваивается Any. Когда мы передаем Dog как Any, результат просто Any, который не имеет bark().

GenericHolder сохраняет информацию о том, что в данном случае у нас действительно есть Dog, что означает, что мы можем выполнять операции Dog на объекте, возвращаемом getValue().

Общие функции Link to heading

Чтобы определить общую функцию, укажите параметр обобщенного типа в угловых скобках перед именем функции: AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Введение в обобщения 225

// IntroGenerics/GenericFunction.kt
**package introgenerics**
**import atomictest.eq**
**fun** <T> identity(arg: T): T = arg
**fun** main() {
    identity("Yellow") eq "Yellow"
    identity(1) eq 1
    **val** d: Dog = identity(Dog())
    d.bark() eq "Ruff!"
}

d имеет тип Dog, потому что identity() является обобщенной функцией и возвращает T.
Стандартная библиотека Kotlin содержит множество обобщенных функций-расширений для коллекций. Чтобы написать обобщенную функцию-расширение, поместите обобщенную спецификацию перед приемником. Например, обратите внимание, как определены first() и firstOrNull():

// IntroGenerics/GenericListExtensions.kt
**package introgenerics**
**import atomictest.eq**
**fun** <T> List<T>.first(): T {
    **if** (isEmpty())
        **throw** NoSuchElementException("Пустой список")
    **return this** [0]
}
**fun** <T> List<T>.firstOrNull(): T? =
    **if** (isEmpty()) **null else this** [0]
**fun** main() {
    listOf(1, 2, 3).first() eq 1
    **val** i: **Int?** = // [1]
    listOf(1, 2, 3).firstOrNull()
    i eq 1
    **val** s: **String?** = // [2]
    listOf< **String** >().firstOrNull()
    s eq **null**
}

AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Введение в обобщения 226
first() и firstOrNull() работают с любым типом List. Чтобы вернуть T, они должны быть обобщенными функциями.
Обратите внимание, как firstOrNull() указывает на возвращаемый тип, который может быть null. Строка [1] показывает, что вызов функции на List возвращает тип Int?. Строка [2] показывает, что вызов firstOrNull() на List возвращает String?. Kotlin требует наличие ? в строках [1] и [2] — уберите их и посмотрите сообщения об ошибках.
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC

Свойства расширения Link to heading

Так же, как функции могут быть функциями расширения, свойства могут быть свойствами расширения. Спецификация типа получателя для свойств расширения аналогична синтаксису для функций расширения — расширяемый тип идет сразу перед именем функции или свойства: fun ReceiverType.extensionFunction() { … }
val ReceiverType.extensionProperty: PropType
get () { … }

Свойство расширения требует пользовательского геттера. Значение свойства вычисляется при каждом доступе: // ExtensionProperties/StringIndices.kt
package extensionproperties
import atomictest.eq
val String.indices: IntRange
get () = 0 until length
fun main() {
“abc”.indices eq 0..2
}

Хотя вы можете преобразовать любую функцию расширения без параметров в свойство, мы рекомендуем сначала обдумать это. Причины, описанные в разделе “Accessor Properties” для выбора между свойствами и функциями, также применимы к свойствам расширения. Предпочтение свойства функции имеет смысл только в том случае, если оно достаточно простое и улучшает читаемость.

Вы можете определить обобщенное свойство расширения. Здесь мы преобразуем firstOrNull() из “Введения в обобщения” в свойство расширения: // ExtensionProperties/GenericListExt.kt
package extensionproperties
import atomictest.eq
val List.firstOrNull: T?
get () = if (isEmpty()) null else this [0]
fun main() {
listOf(1, 2, 3).firstOrNull eq 1
listOf< String >().firstOrNull eq null
}

Руководство по стилю Kotlin рекомендует использовать функцию вместо свойства, если функция выбрасывает исключение. Когда тип обобщенного аргумента не используется, вы можете заменить его на * . Это называется звездной проекцией: // ExtensionProperties/ListOfStar.kt
package extensionproperties
import atomictest.eq
val List<*>.indices: IntRange
get () = 0 until size
fun main() {
listOf(1).indices eq 0..0
listOf(‘a’, ‘b’, ‘c’, ’d’).indices eq 0..3
emptyList< Int >().indices eq IntRange.EMPTY
}

Когда вы используете List<> , вы теряете всю конкретную информацию о типе, содержащемся в List . Например, элемент List<> может быть присвоен только типу Any? :
// ExtensionProperties/AnyFromListOfStar.kt
import atomictest.eq
fun main() {
val list: List<*> = listOf(1, 2)
val any: Any? = list[0]
any eq 1
}

У нас нет информации о том, является ли значение, хранящееся в List<*> , нулевым или нет, поэтому его можно присвоить только типу Any? .
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin(www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC

break и continue Link to heading

break и continue прекращают выполнение оставшегося кода в цикле и либо выходят из него, либо возвращаются к началу этого цикла. Ранние программисты писали напрямую для процессора, используя либо числовые коды операций в качестве инструкций, либо ассемблерный язык, который переводится в коды операций. Этот вид программирования является максимально низкоуровневым. Например, многие решения по коду упрощались за счет “прыжков” непосредственно в другие места кода. Ранние языки высокого уровня (включая FORTRAN, ALGOL, Pascal, C и C++) дублировали эту практику, внедрив ключевое слово goto.

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

Тем не менее, нам все еще нужно иметь возможность решать, что мы не хотим выполнять оставшуюся часть кода в блоке — то есть отказаться от остальной части текущего действия. Это можно было бы решить с помощью goto, но открытая природа неконтролируемого перехода приносит с собой слишком много негативных последствий.

Чтобы отказаться от остальной части текущего действия, используйте break и continue. Эти конструкции доступны только в циклах for, while и do-while. Вызов continue возвращает к началу цикла, в то время как break завершает цикл.

На практике вы редко используете break и continue, когда пишете код на Kotlin. Эти функции являются артефактами более ранних языков. Хотя они иногда полезны, вы узнаете в этой книге, что Kotlin предоставляет более совершенные альтернативы.

Вот пример цикла for, который содержит как continue, так и break:

// BreakAndContinue/ForControl.kt
import atomictest.eq

fun main() {
    val nums = mutableListOf(0)
    for (i in 4 until 100 step 4) { // [1]
        if (i == 8) continue // [2]
        if (i == 40) break // [3]
        nums.add(i)
    } // [4]
    nums eq "[0, 4, 12, 16, 20, 24, 28, 32, 36]"
}

Этот код агрегирует Int в изменяемый List. continue на [2] отказывается от остальной части блока и возвращается к началу цикла на [1]. Он “продолжает” выполнение, начиная с следующей итерации цикла. Код, следующий за continue, игнорируется: nums.add(i) не вызывается, когда i == 8, поэтому вы не видите его в результирующем nums.

Когда i == 40, break выполняется на [3], что отказывается от остальной части блока и переходит к концу области видимости на [4]. Числа, начиная с 40, не добавляются в результирующий List, потому что остальная часть цикла for не выполняется.

Строки [2] и [3] взаимозаменяемы, потому что их логика не пересекается. Попробуйте поменять местами строки и убедитесь, что вывод не изменится.

Мы можем переписать ForControl.kt, используя цикл while:

// BreakAndContinue/WhileControl.kt
import atomictest.eq

fun main() {
    val nums = mutableListOf(0)
    var i = 0
    while (i < 100) {
        i += 4
        if (i == 8) continue
        if (i == 40) break
        nums.add(i)
    }
    nums eq "[0, 4, 12, 16, 20, 24, 28, 32, 36]"
}

Поведение break и continue остается тем же, как и в цикле do-while:

// BreakAndContinue/DoWhileControl.kt
import atomictest.eq

fun main() {
    val nums = mutableListOf(0)
    var i = 0
    do {
        i += 4
        if (i == 8) continue
        if (i == 40) break
        nums.add(i)
    } while (i < 100)
    nums eq "[0, 4, 12, 16, 20, 24, 28, 32, 36]"
}

Цикл do-while всегда выполняется хотя бы один раз, потому что проверка while находится в конце цикла.

Метки Link to heading

Обычные операторы break и continue не могут выходить за пределы своего локального цикла. Метки позволяют операторам break и continue прыгать к границам внешних циклов, так что вы не ограничены областью видимости текущего цикла. Вы создаете метку, используя label@, где label может быть любым именем. Здесь метка — outer:

// BreakAndContinue/ForLabeled.kt
import atomictest.eq

fun main() {
    val strings = mutableListOf<String>()
    outer@ for (c in 'a'..'e') {
        for (i in 1..9) {
            if (i == 5) continue@outer
            if ("$c$i" == "c3") break@outer
            strings.add("$c$i")
        }
    }
    strings eq listOf("a1", "a2", "a3", "a4",
                      "b1", "b2", "b3", "b4", "c1", "c2")
}

Выражение с меткой continue@outer возвращает к метке outer@. Выражение с меткой break@outer находит конец блока с именем outer@ и продолжает выполнение оттуда. Метки также работают с while и do-while:

// BreakAndContinue/WhileLabeled.kt
import atomictest.eq

fun main() {
    val strings = mutableListOf<String>()
    var c = 'a' - 1
    outer@ while (c < 'f') {
        c += 1
        var i = 0
        do {
            i++
            if (i == 5) continue@outer
            if ("$c$i" == "c3") break@outer
            strings.add("$c$i")
        } while (i < 10)
    }
    strings eq listOf("a1", "a2", "a3", "a4",
                      "b1", "b2", "b3", "b4", "c1", "c2")
}

WhileLabeled.kt можно переписать как:

// BreakAndContinue/Improved.kt
import atomictest.eq

fun main() {
    val strings = mutableListOf<String>()
    for (c in 'a'..'c') {
        for (i in 1..4) {
            val value = "$c$i"
            if (value < "c3") { // [1]
                strings.add(value)
            }
        }
    }
    strings eq listOf("a1", "a2", "a3", "a4",
                      "b1", "b2", "b3", "b4", "c1", "c2")
}

Это гораздо более понятно. В строке [1] мы добавляем только строки, которые находятся (в алфавитном порядке) перед “c3”. Это приводит к такому же поведению, как использование break при достижении “c3” в предыдущих версиях примера.

Операторы break и continue, как правило, создают сложный и трудный для сопровождения код. Хотя эти конструкции более цивилизованные, чем “goto”, они все равно прерывают поток выполнения программы. Код без переходов почти всегда легче понять.

В некоторых случаях вы можете явно записать условия для итерации вместо использования break и continue, как мы сделали в Improved.kt. В других случаях вы можете изменить структуру вашего кода и ввести новые функции. Оба оператора break и continue могут быть заменены на return, если вы извлечете весь цикл или тело цикла в новые функции. В следующем разделе, “Функциональное программирование”, вы научитесь писать понятный код без использования break и continue.

Рассмотрите альтернативные подходы и выберите более простое и читаемое решение. Обычно это не будет включать break и continue.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Раздел IV: Функциональный Link to heading

Программирование Link to heading

Неизбежная цена надежности — простота. — C.A.R. Хоар

Лямбды Link to heading

Лямбды создают компактный код, который легче понять. Лямбда (также называемая литералом функции) — это функция с низким уровнем формальностей: у нее нет имени, требуется минимальное количество кода для создания, и вы можете вставить ее непосредственно в другой код. В качестве отправной точки рассмотрим функцию map(), которая работает с коллекциями, такими как List. Параметр для map() — это функция преобразования, которая применяется к каждому элементу в коллекции. map() возвращает новый List, содержащий все преобразованные элементы. Здесь мы преобразуем каждый элемент списка в строку, окруженную квадратными скобками:

// Lambdas/BasicLambda.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, 3, 4)
    val result = list.map({ n: Int -> "[ $n]" })
    result eq listOf("[1]", "[2]", "[3]", "[4]")
}

Лямбда — это код внутри фигурных скобок, используемый при инициализации result. Параметр n отделен от тела функции стрелкой -> (та же стрелка используется в выражениях when). Тело функции может состоять из одного или нескольких выражений. Последнее выражение становится значением возврата лямбды.

BasicLambda.kt показывает полный синтаксис лямбды, но его часто можно упростить. Обычно мы создаем и используем лямбду на месте, что означает, что Kotlin может обычно вывести информацию о типе. Здесь тип n выводится:

// Lambdas/LambdaTypeInference.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, 3, 4)
    val result = list.map({ n -> "[ $n]" })
    result eq listOf("[1]", "[2]", "[3]", "[4]")
}

Kotlin может определить, что n — это Int, потому что лямбда используется с List<Int>. Если есть только один параметр, Kotlin генерирует имя it для этого параметра, что означает, что нам больше не нужно n ->:

// Lambdas/LambdaIt.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, 3, 4)
    val result = list.map({ "[ $it]" })
    result eq listOf("[1]", "[2]", "[3]", "[4]")
}

map() работает с List любого типа. Здесь Kotlin выводит тип аргумента лямбды it как Char:

// Lambdas/Mapping.kt
import atomictest.eq

fun main() {
    val list = listOf('a', 'b', 'c', 'd')
    val result = list.map({ "[ ${it.uppercaseChar()} ]" })
    result eq listOf("[A]", "[B]", "[C]", "[D]")
}

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

// Lambdas/OmittingParentheses.kt
import atomictest.eq

fun main() {
    val list = listOf('a', 'b', 'c', 'd')
    val result = list.map { "[ ${it.uppercaseChar()} ]" }
    result eq listOf("[A]", "[B]", "[C]", "[D]")
}

Если функция принимает более одного аргумента, все, кроме последнего аргумента лямбды, должны быть в скобках. Например, вы можете указать последний аргумент для joinToString() как лямбду. Лямбда используется для преобразования каждого элемента в строку, а затем все элементы объединяются:

// Lambdas/JoinToString.kt
import atomictest.eq

fun main() {
    val list = listOf(9, 11, 23, 32)
    list.joinToString(" ") { "[ $it]" } eq "[9] [11] [23] [32]"
}

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

// Lambdas/LambdaAndNamedArgs.kt
import atomictest.eq

fun main() {
    val list = listOf(9, 11, 23, 32)
    list.joinToString(
        separator = " ",
        transform = { "[ $it]" }
    ) eq "[9] [11] [23] [32]"
}

Вот синтаксис для лямбды с более чем одним параметром:

// Lambdas/TwoArgLambda.kt
import atomictest.eq

fun main() {
    val list = listOf('a', 'b', 'c')
    list.mapIndexed { index, element ->
        "[ $index: $element]"
    } eq listOf("[0: a]", "[1: b]", "[2: c]")
}

Это использует библиотечную функцию mapIndexed(), которая берет каждый элемент в списке и производит индекс этого элемента вместе с элементом. Лямбда, которую мы применяем после mapIndexed(), требует два аргумента, чтобы соответствовать индексу и элементу (который является символом в случае List<Char>).

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

// Lambdas/Underscore.kt
import atomictest.eq

fun main() {
    val list = listOf('a', 'b', 'c')
    list.mapIndexed { index, _ ->
        "[ $index]"
    } eq listOf("[0]", "[1]", "[2]")
}

Underscore.kt можно переписать, используя list.indices:

// Lambdas/ListIndicesMap.kt
import atomictest.eq

fun main() {
    val list = listOf('a', 'b', 'c')
    list.indices.map {
        "[ $it]"
    } eq listOf("[0]", "[1]", "[2]")
}

Лямбды могут иметь ноль параметров, в этом случае вы можете оставить стрелку для акцента, но руководство по стилю Kotlin рекомендует опустить стрелку:

// Lambdas/ZeroArguments.kt
import atomictest.*

fun main() {
    run { -> trace("A Lambda") }
    run { trace("Without args") }
    trace eq """
    A Lambda
    Without args
    """
}

Стандартная библиотека run() просто вызывает свой аргумент-лямбду.

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

Упражнения и решения можно найти на www.AtomicKotlin.com.

Важность Лямбд Link to heading

Лямбды могут показаться синтаксическим сахаром, но они предоставляют важные возможности для вашего программирования. Код часто манипулирует содержимым коллекции и, как правило, повторяет эти манипуляции с незначительными изменениями. Рассмотрим выбор элементов из коллекции, таких как люди младше определенного возраста, сотрудники с конкретной ролью, граждане определенного города или незавершенные заказы. Вот пример, который выбирает четные числа из списка. Предположим, у нас нет богатой библиотеки функций для работы с коллекциями — нам придется реализовать собственную операцию filterEven():

// ImportanceOfLambdas/FilterEven.kt
package importanceoflambdas
import atomictest.eq

fun filterEven(nums: List<Int>): List<Int> {
    val result = mutableListOf<Int>()
    for (i in nums) {
        if (i % 2 == 0) { // [1]
            result += i
        }
    }
    return result
}

fun main() {
    filterEven(listOf(1, 2, 3, 4)) eq listOf(2, 4)
}

Если элемент имеет остаток 0 при делении на 2, он добавляется к результату. Представьте, что вам нужно что-то подобное, но для чисел, которые больше 2. Вы можете скопировать filterEven() и изменить небольшую часть, которая выбирает элементы, включенные в результат:

// ImportanceOfLambdas/GreaterThan2.kt
package importanceoflambdas
import atomictest.eq

fun greaterThan2(nums: List<Int>): List<Int> {
    val result = mutableListOf<Int>()
    for (i in nums) {
        if (i > 2) { // [1]
            result += i
        }
    }
    return result
}

fun main() {
    greaterThan2(listOf(1, 2, 3, 4)) eq listOf(3, 4)
}

Единственное заметное различие между предыдущими двумя примерами — это строка кода ([1] в обоих случаях), указывающая на желаемые элементы. С помощью лямбд мы можем использовать одну и ту же функцию для обоих случаев. Функция стандартной библиотеки filter() принимает предикат, указывающий элементы, которые вы хотите сохранить, и этот предикат может быть лямбдой:

// ImportanceOfLambdas/Filter.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, 3, 4)
    val even = list.filter { it % 2 == 0 }
    val greaterThan2 = list.filter { it > 2 }
    even eq listOf(2, 4)
    greaterThan2 eq listOf(3, 4)
}

Теперь у нас есть ясный, лаконичный код, который избегает повторений. И even, и greaterThan2 используют filter() и различаются только предикатом. filter() был тщательно протестирован, поэтому вы менее склонны ввести ошибку. Обратите внимание, что filter() обрабатывает итерацию, которая в противном случае потребовала бы написания кода вручную. Хотя управление итерацией самостоятельно может не казаться большой работой, это еще одна деталь, подверженная ошибкам, и еще одно место, где можно ошибиться. Поскольку они так “очевидны”, такие ошибки особенно трудно найти.

Это одна из отличительных черт функционального программирования, примерами которого являются map() и filter(). Функциональное программирование решает задачи небольшими шагами. Функции часто делают то, что кажется тривиальным — не так уж сложно написать свой собственный код, вместо того чтобы использовать map() и filter(). Однако, как только у вас есть коллекция этих небольших, отлаженных решений, вы можете легко комбинировать их без отладки на каждом уровне. Это позволяет вам создавать более надежный код быстрее.

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

// ImportanceOfLambdas/StoringLambda.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, 3, 4)
    val isEven = { e: Int -> e % 2 == 0 }
    list.filter(isEven) eq listOf(2, 4)
    list.any(isEven) eq true
}

isEven проверяет, является ли число четным, и эта ссылка передается в качестве аргумента как filter(), так и any(). Функция библиотеки any() проверяет, есть ли хотя бы один элемент в списке, удовлетворяющий данному предикату. Когда мы определяем isEven, мы должны указать тип параметра, потому что нет контекста для вывода типа.

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

Когда язык поддерживает замыкания, это “просто работает” так, как вы ожидаете:

// ImportanceOfLambdas/Closures.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 5, 7, 10)
    val divider = 5
    list.filter { it % divider == 0 } eq listOf(5, 10)
}

Здесь лямбда “захватывает” val divider, который определен вне лямбды. Лямбда не только читает захваченные элементы, но также может их изменять:

// ImportanceOfLambdas/Closures2.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 5, 7, 10)
    var sum = 0
    val divider = 5
    list.filter { it % divider == 0 }
        .forEach { sum += it }
    sum eq 15
}

Функция библиотеки forEach() применяет указанное действие к каждому элементу коллекции. Хотя вы можете захватить изменяемую переменную sum, как в Closures2.kt, вы обычно можете изменить свой код и избежать изменения состояния вашего окружения:

// ImportanceOfLambdas/Sum.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 5, 7, 10)
    val divider = 5
    list.filter { it % divider == 0 }
        .sum() eq 15
}

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

// ImportanceOfLambdas/FunctionClosure.kt
package importanceoflambdas
import atomictest.eq

var x = 100
fun useX() {
    x++
}

fun main() {
    useX()
    x eq 101
}

useX() захватывает и изменяет x из своего окружения. Упражнения и решения можно найти на www.AtomicKotlin.com.

Операции с коллекциями Link to heading

Неотъемлемым аспектом функциональных языков является возможность легко выполнять пакетные операции над коллекциями объектов. Большинство функциональных языков предоставляют мощную поддержку для работы с коллекциями, и Kotlin не является исключением. Вы уже видели функции map(), filter(), any() и forEach(). Этот раздел показывает дополнительные операции, доступные для List и других коллекций.

Мы начнем с различных способов создания List. Здесь мы инициализируем List, используя лямбды:

// OperationsOnCollections/CreatingLists.kt
import atomictest.eq

fun main() {
    // Аргумент лямбды — это индекс элемента:
    val list1 = List(10) { it }
    list1 eq "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
    
    // Список из одного значения:
    val list2 = List(10) { 0 }
    list2 eq "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]"
    
    // Список букв:
    val list3 = List(10) { 'a' + it }
    list3 eq "[a, b, c, d, e, f, g, h, i, j]"
    
    // Цикл по последовательности:
    val list4 = List(10) { list3[it % 3] }
    list4 eq "[a, b, c, a, b, c, a, b, c, a]"
}

Эта версия конструктора List имеет два параметра: размер списка и лямбду, которая инициализирует каждый элемент списка (индекс элемента передается как аргумент it). Помните, что если лямбда является последним аргументом, ее можно отделить от списка аргументов.

MutableList можно инициализировать тем же способом. Здесь мы видим инициализацию лямбды как внутри списка аргументов (mutableList1), так и отделенной от списка аргументов (mutableList2):

// OperationsOnCollections/ListInit.kt
import atomictest.eq

fun main() {
    val mutableList1 = MutableList(5, { 10 * (it + 1) })
    mutableList1 eq "[10, 20, 30, 40, 50]"
    
    val mutableList2 = MutableList(5) { 10 * (it + 1) }
    mutableList2 eq "[10, 20, 30, 40, 50]"
}

List() и MutableList() не являются конструкторами, а функциями. Их имена намеренно начинаются с заглавной буквы, чтобы они выглядели как конструкторы. Многие функции коллекций принимают предикат и проверяют его на элементах коллекции, некоторые из которых мы уже видели:

  • filter() создает список, содержащий все элементы, соответствующие заданному предикату.
  • any() возвращает true, если хотя бы один элемент соответствует предикату.
  • all() проверяет, соответствуют ли все элементы предикату.
  • none() проверяет, что ни один элемент не соответствует предикату.
  • find() и firstOrNull() возвращают первый элемент, соответствующий предикату, или null, если такой элемент не найден.
  • lastOrNull() возвращает последний элемент, соответствующий предикату, или null.
  • count() возвращает количество элементов, соответствующих предикату.

Вот простые примеры для каждой функции:

// OperationsOnCollections/Predicates.kt
import atomictest.eq

fun main() {
    val list = listOf(-3, -1, 5, 7, 10)
    list.filter { it > 0 } eq listOf(5, 7, 10)
    list.count { it > 0 } eq 3
    list.find { it > 0 } eq 5
    list.firstOrNull { it > 0 } eq 5
    list.lastOrNull { it < 0 } eq -1
    list.any { it > 0 } eq true
    list.any { it != 0 } eq true
    list.all { it > 0 } eq false
    list.all { it != 0 } eq true
    list.none { it > 0 } eq false
    list.none { it == 0 } eq true
}

filter() и count() применяют предикат к каждому элементу, в то время как any() или find() останавливаются, когда найден первый подходящий результат. Например, если первый элемент удовлетворяет предикату, any() сразу возвращает true, в то время как find() возвращает первый подходящий элемент. Единственный случай, когда обрабатываются все элементы, — это если список не содержит элементов, соответствующих заданному предикату.

filter() возвращает группу элементов, удовлетворяющих заданному предикату. Иногда вас может интересовать оставшаяся группа — элементы, которые не удовлетворяют предикату. filterNot() создает эту оставшуюся группу, но partition() может быть более полезным, так как одновременно создает оба списка:

// OperationsOnCollections/Partition.kt
import atomictest.eq

fun main() {
    val list = listOf(-3, -1, 5, 7, 10)
    val isPositive = { i: Int -> i > 0 }
    list.filter(isPositive) eq "[5, 7, 10]"
    list.filterNot(isPositive) eq "[-3, -1]"
    val (pos, neg) = list.partition { it > 0 }
    pos eq "[5, 7, 10]"
    neg eq "[-3, -1]"
}

partition() создает объект Pair, содержащий List. Используя деструктурирующее присваивание, вы можете присвоить элементы Pair родительской группе переменных var или val. Деструктурирование означает определение нескольких переменных var или val и их одновременную инициализацию из выражения справа от присваивания. Здесь деструктурирование используется с пользовательской функцией:

// OperationsOnCollections/PairOfLists.kt
package operationsoncollections

import atomictest.eq

fun createPair() = Pair(1, "one")

fun main() {
    val (i, s) = createPair()
    i eq 1
    s eq "one"
}

filterNotNull() создает новый список с удаленными null:

// OperationsOnCollections/FilterNotNull.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, null)
    list.filterNotNull() eq "[1, 2]"
}

В List мы видели функции, такие как sum() или sorted(), применяемые к списку сопоставимых элементов. Эти функции не могут быть вызваны для списков несуммируемых или несопоставимых элементов, но у них есть аналоги с именами sumOf() и sortedBy(). Вы передаете функцию (часто лямбду) в качестве аргумента, которая указывает атрибут, который следует использовать для операции:

// OperationsOnCollections/ByOperations.kt
package operationsoncollections

import atomictest.eq

data class Product(
    val description: String,
    val price: Double
)

fun main() {
    val products = listOf(
        Product("bread", 2.0),
        Product("wine", 5.0)
    )
    products.sumOf { it.price } eq 7.0
    products.sortedByDescending { it.price } eq
        "[Product(description=wine, price=5.0), Product(description=bread, price=2.0)]"
    products.minByOrNull { it.price } eq
        Product("bread", 2.0)
}

sumOf() суммирует значения, полученные путем вызова аргумента лямбды для каждого элемента. sorted() и sortedBy() сортируют коллекцию в порядке возрастания, в то время как sortedDescending() и sortedByDescending() сортируют коллекцию в порядке убывания.

minByOrNull возвращает минимальное значение на основе заданного критерия или null, если список пуст.

take() и drop() создают или удаляют (соответственно) первый элемент, в то время как takeLast() и dropLast() создают или удаляют последний элемент. У этих функций есть аналоги, которые принимают предикат, указывающий, какие элементы взять или удалить:

// OperationsOnCollections/TakeOrDrop.kt
import atomictest.eq

fun main() {
    val list = listOf('a', 'b', 'c', 'X', 'Z')
    list.takeLast(3) eq "[c, X, Z]"
    list.takeLastWhile { it.isUpperCase() } eq "[X, Z]"
    list.drop(1) eq "[b, c, X, Z]"
    list.dropWhile { it.isLowerCase() } eq "[X, Z]"
}

Операции, которые вы видели для List, также доступны для Set:

// OperationsOnCollections/SetOperations.kt
import atomictest.eq

fun main() {
    val set = setOf("a", "ab", "ac")
    set.maxByOrNull { it.length }?.length eq 2
    set.filter { it.contains('b') } eq listOf("ab")
    set.map { it.length } eq listOf(1, 2, 2)
}

maxByOrNull() возвращает null, если коллекция пуста, поэтому его результат может быть null. При применении к Set функции filter() и map() возвращают свои результаты в виде List.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Ссылки на члены Link to heading

Вы можете передать ссылку на член в качестве аргумента функции. Ссылки на члены — для функций, свойств и конструкторов — могут заменить тривиальные лямбда-выражения, которые просто вызывают соответствующую функцию, свойство или конструктор. Ссылка на член использует двойное двоеточие для разделения имени класса и функции или свойства. Здесь Message::isRead является ссылкой на член:

// MemberReferences/PropertyReference.kt
package memberreferences1
import atomictest.eq

data class Message (
    val sender: String,
    val text: String,
    val isRead: Boolean
)

fun main() {
    val messages = listOf(
        Message("Kitty", "Hey!", true),
        Message("Kitty", "Where are you?", false)
    )
    val unread = messages.filterNot(Message::isRead)
    unread.size eq 1
    unread.single().text eq "Where are you?"
}

Чтобы отфильтровать непрочитанные сообщения, мы используем библиотечную функцию filterNot(), которая принимает предикат. В нашем случае предикат указывает, было ли сообщение уже прочитано. Мы могли бы передать лямбда-выражение, но вместо этого мы передаем ссылку на свойство Message::isRead.

Ссылки на свойства полезны при указании нетривиального порядка сортировки:

// MemberReferences/SortWith.kt
import memberreferences1.Message
import atomictest.eq

fun main() {
    val messages = listOf(
        Message("Kitty", "Hey!", true),
        Message("Kitty", "Where are you?", false),
        Message("Boss", "Meeting today", false)
    )
    messages.sortedWith(compareBy(
        Message::isRead, Message::sender)) eq
    listOf(
        // Сначала непрочитанные, отсортированные по отправителю:
        Message("Boss", "Meeting today", false),
        Message("Kitty", "Where are you?", false),
        // Затем прочитанные, также отсортированные по отправителю:
        Message("Kitty", "Hey!", true)
    )
}

Библиотечная функция sortedWith() сортирует список, используя компаратор, который является объектом, используемым для сравнения двух элементов. Библиотечная функция compareBy() строит компаратор на основе своих параметров, которые представляют собой список предикатов. Использование compareBy() с одним аргументом эквивалентно вызову sortedBy().

Ссылки на функции Link to heading

Предположим, вы хотите проверить, содержит ли список какие-либо важные сообщения, а не просто непрочитанные сообщения. У вас могут быть несколько сложных критериев, чтобы определить, что означает “важное”. Вы можете поместить эту логику в лямбда-выражение, но это лямбда-выражение может легко стать большим и сложным. Код становится более понятным, если вы извлечете его в отдельную функцию. В Kotlin вы не можете передать функцию, где ожидается тип функции, но вы можете передать ссылку на эту функцию: AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

// MemberReferences/FunctionReference.kt package memberreferences2
import atomictest.eq
data class Message (
val sender: String,
val text: String,
val isRead: Boolean,
val attachments: List
)
data class Attachment (
val type: String,
val name: String
)
fun Message.isImportant(): Boolean =
text.contains(“Повышение зарплаты”) ||
attachments.any {
it.type == “image” &&
it.name.contains(“кот”)
}
fun main() {
val messages = listOf(Message(
“Босс”, “Давайте обсудим цели " +
“на следующий год”, false,
listOf(Attachment(“image”, “милые коты”))))
messages.any(Message::isImportant) eq true
}

Этот новый класс Message добавляет свойство attachments, и функция-расширение Message.isImportant() использует эту информацию. В вызове messages.any() мы создаем ссылку на функцию-расширение — ссылки не ограничиваются только членами функций.
Если у вас есть функция верхнего уровня, принимающая Message в качестве единственного параметра, вы можете передать ее как ссылку. Когда вы создаете ссылку на функцию верхнего уровня, имени класса нет, поэтому она записывается как ::function:

AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

// MemberReferences/TopLevelFunctionRef.kt package memberreferences2
import atomictest.eq
fun ignore(message: Message) =
!message.isImportant() &&
message.sender in setOf(“Босс”, “Мама”)
fun main() {
val text = “Давайте обсудим цели " +
“на следующий год”
val msgs = listOf(
Message(“Босс”, text, false, listOf()),
Message(“Босс”, text, false, listOf(
Attachment(“image”, “милые коты”))))
msgs.filter(::ignore).size eq 1
msgs.filterNot(::ignore).size eq 1
}

Ссылки на конструкторы Link to heading

Вы можете создать ссылку на конструктор, используя имя класса. Здесь names.mapIndexed() принимает ссылку на конструктор ::Student:

// MemberReferences/ConstructorReference.kt
package memberreferences3
import atomictest.eq

data class Student (
    val id: Int,
    val name: String
)

fun main() {
    val names = listOf("Alice", "Bob")
    val students = names.mapIndexed { index, name ->
        Student(index, name)
    }
    AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
    MemberReferences 256
    students eq listOf(Student(0, "Alice"), Student(1, "Bob"))
    names.mapIndexed(::Student) eq students
}

mapIndexed() был введен в лямбдах. Он преобразует каждый элемент в names в индекс этого элемента вместе с самим элементом. В определении students эти значения явно сопоставляются с конструктором, но идентичный эффект достигается с помощью names.mapIndexed(::Student). Таким образом, ссылки на функции и конструкторы могут устранить необходимость в указании длинного списка параметров, которые просто передаются в лямбду. Ссылки на функции и конструкторы часто более читаемы, чем лямбды.

Ссылки на функции-расширения Link to heading

Чтобы создать ссылку на функцию-расширение, префиксируйте ссылку именем расширяемого типа: // MemberReferences/ExtensionReference.kt package memberreferences
import atomictest.eq
fun Int.times47() = times(47)
class Frog
fun Frog.speak() = “Ribbit!”
fun goInt(n: Int, g: (Int) -> Int) = g(n)
fun goFrog(frog: Frog, g: (Frog) -> String) = g(frog)
fun main() {
goInt(12, Int::times47) eq 564
goFrog(Frog(), Frog::speak) eq “Ribbit!”
}

В функции goInt() g — это функция, которая ожидает аргумент типа Int и возвращает Int. В функции goFrog() g ожидает объект типа Frog и возвращает String.
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Функции высшего порядка Link to heading

Язык поддерживает функции высшего порядка, если его функции могут принимать другие функции в качестве аргументов и возвращать функции в качестве значений. Функции высшего порядка являются неотъемлемой частью языков функционального программирования. В предыдущих разделах мы видели функции высшего порядка, такие как filter(), map() и any().

Вы можете сохранить лямбда-выражение в ссылке. Давайте посмотрим на тип этого хранения:

// HigherOrderFunctions/IsPlus.kt
package higherorderfunctions
import atomictest.eq

val isPlus: (Int) -> Boolean = { it > 0 }

fun main() {
    listOf(1, 2, -3).any(isPlus) eq true
}

(Int) -> Boolean — это тип функции: он начинается с круглых скобок, окружающих ноль или более типов параметров, затем следует стрелка (->), за которой следует тип возвращаемого значения: `(Arg1Type, Arg2Type… ArgNType) -> ReturnType.

Синтаксис для вызова функции через ссылку идентичен обычному вызову функции:

// HigherOrderFunctions/CallingReference.kt
package higherorderfunctions
import atomictest.eq

val helloWorld: () -> String = { "Hello, world!" }
val sum: (Int, Int) -> Int = { x, y -> x + y }

fun main() {
    helloWorld() eq "Hello, world!"
    sum(1, 2) eq 3
}

Когда функция принимает параметр функции, вы можете передать ей либо ссылку на функцию, либо лямбда-выражение. Рассмотрим, как вы могли бы определить any() из стандартной библиотеки:

// HigherOrderFunctions/Any.kt
package higherorderfunctions
import atomictest.eq

fun <T> List<T>.any( // [1]
    predicate: (T) -> Boolean // [2]
): Boolean {
    for (element in this) {
        if (predicate(element)) // [3]
            return true
    }
    return false
}

fun main() {
    val ints = listOf(1, 2, -3)
    ints.any { it > 0 } eq true // [4]
    val strings = listOf("abc", " ")
    strings.any { it.isBlank() } eq true // [5]
    strings.any(String::isNotBlank) eq true // [6]
}
  • [1] any() должен быть использован с List различных типов, поэтому мы определяем его как расширение для обобщенного List<T>.
  • [2] Функция предиката вызывается с параметром типа T, поэтому мы можем применять ее к элементам List.
  • [3] Применение predicate() определяет, соответствует ли этот элемент нашим критериям.
  • Тип лямбда-выражения различен: это Int в [4] и String в [5].
  • [6] Ссылка на член — это еще один способ передать ссылку на функцию.

repeat() из стандартной библиотеки принимает функцию в качестве второго параметра. Она повторяет действие заданное количество раз:

// HigherOrderFunctions/RepeatByInt.kt
import atomictest.*

fun main() {
    repeat(4) { trace("hi!") }
    trace eq "hi! hi! hi! hi!"
}

Рассмотрим, как может быть определен repeat():

// HigherOrderFunctions/Repeat.kt
package higherorderfunctions
import atomictest.*

fun repeat(
    times: Int,
    action: (Int) -> Unit // [1]
) {
    for (index in 0 until times) {
        action(index) // [2]
    }
}

fun main() {
    repeat(3) { trace("#$it") } // [3]
    trace eq "#0 #1 #2"
}
  • [1] repeat() принимает параметр action типа функции (Int) -> Unit.
  • [2] Когда action() вызывается, ей передается текущий индекс повторения.
  • [3] При вызове repeat() вы получаете доступ к индексу повторения, используя it внутри лямбда-выражения.

Тип возвращаемого значения функции может быть нулевым:

// HigherOrderFunctions/NullableReturn.kt
import atomictest.eq

fun main() {
    val transform: (String) -> Int? = { s: String -> s.toIntOrNull() }
    transform("112") eq 112
    transform("abc") eq null
    val x = listOf("112", "abc")
    x.mapNotNull(transform) eq "[112]"
    x.mapNotNull { it.toIntOrNull() } eq "[112]"
}

toIntOrNull() может вернуть null, поэтому transform() принимает String и возвращает нулевой Int?. mapNotNull() преобразует каждый элемент в List в нулевое значение и удаляет все null из результата. Это имеет тот же эффект, что и сначала вызвать map(), а затем применить filterNotNull() к результирующему списку.

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

// HigherOrderFunctions/NullableFunction.kt
import atomictest.eq

fun main() {
    val returnTypeNullable: (String) -> Int? = { null }
    val mightBeNull: ((String) -> Int)? = null
    returnTypeNullable("abc") eq null
    // Не компилируется без проверки на null:
    // mightBeNull("abc")
    if (mightBeNull != null) {
        mightBeNull("abc")
    }
}

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

Упражнения и решения можно найти на www.AtomicKotlin.com.

Манипуляция списками Link to heading

Сжатие и выравнивание — это две распространенные операции, которые манипулируют списками.

Сжатие Link to heading

zip() объединяет два списка, имитируя поведение молнии на вашей куртке, сочетая соседние элементы списков: // ManipulatingLists/Zipper.kt import atomictest.eq fun main() { val left = listOf(“a”, “b”, “c”, “d”) val right = listOf(“q”, “r”, “s”, “t”) left.zip(right) eq // [1] “[(a, q), (b, r), (c, s), (d, t)]” left.zip(0..4) eq // [2] “[(a, 0), (b, 1), (c, 2), (d, 3)]” (10..100).zip(right) eq // [3] “[(10, q), (11, r), (12, s), (13, t)]” } • [1] Сжатие left с right приводит к списку пар, объединяя каждый элемент из left с соответствующим элементом из right. • [2] Вы также можете использовать zip() для объединения списка с диапазоном. • [3] Диапазон 10..100 значительно больше, чем right, но процесс сжатия останавливается, когда один из последовательностей заканчивается. zip() также может выполнять операцию над каждой парой, которую он создает: ManipulatingLists 263 // ManipulatingLists/ZipAndTransform.kt package manipulatinglists import atomictest.eq data class Person ( val name: String , val id: Int ) fun main() { val names = listOf(“Bob”, “Jill”, “Jim”) val ids = listOf(1731, 9274, 8378) names.zip(ids) { name, id -> Person(name, id) } eq “[Person(name=Bob, id=1731), " + “Person(name=Jill, id=9274), " + “Person(name=Jim, id=8378)]” } names.zip(ids) { … } создает последовательность пар имя-идентификатор и применяет лямбда-функцию к каждой паре. Результатом является список инициализированных объектов Person. Чтобы объединить два соседних элемента из одного списка, используйте zipWithNext(): // ManipulatingLists/ZippingWithNext.kt import atomictest.eq fun main() { val list = listOf(‘a’, ‘b’, ‘c’, ’d’) list.zipWithNext() eq listOf( Pair(‘a’, ‘b’), Pair(‘b’, ‘c’), Pair(‘c’, ’d’)) list.zipWithNext { a, b -> " $ a $ b” } eq “[ab, bc, cd]” } Второй вызов zipWithNext() выполняет дополнительную операцию после сжатия. AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC ManipulatingLists 264

Сглаживание Link to heading

flatten() принимает список, содержащий элементы, которые сами являются списками — список списков — и сглаживает его в список отдельных элементов: // ManipulatingLists/Flatten.kt import atomictest.eq fun main() { val list = listOf( listOf(1, 2), listOf(4, 5), listOf(7, 8), ) list.flatten() eq “[1, 2, 4, 5, 7, 8]” } flatten() помогает нам понять еще одну важную операцию над коллекциями: flatMap(). Давайте создадим все возможные пары для диапазона целых чисел: // ManipulatingLists/FlattenAndFlatMap.kt import atomictest.eq fun main() { val intRange = 1..3 intRange.map { a -> // [1] intRange.map { b -> a to b } } eq “[” + “[(1, 1), (1, 2), (1, 3)], " + “[(2, 1), (2, 2), (2, 3)], " + “[(3, 1), (3, 2), (3, 3)]” + “]” intRange.map { a -> // [2] intRange.map { b -> a to b } }.flatten() eq “[” + “(1, 1), (1, 2), (1, 3), " + “(2, 1), (2, 2), (2, 3), " + “(3, 1), (3, 2), (3, 3)” + “]” AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC ManipulatingLists 265 intRange.flatMap { a -> // [3] intRange.map { b -> a to b } } eq “[” + “(1, 1), (1, 2), (1, 3), " + “(2, 1), (2, 2), (2, 3), " + “(3, 1), (3, 2), (3, 3)” + “]” } Лямбда в каждом случае идентична: каждый элемент intRange комбинируется с каждым элементом intRange, чтобы создать все возможные пары a to b. Но в [1] map() полезно сохраняет дополнительную информацию о том, что мы создали три списка, по одному для каждого элемента в intRange. Есть ситуации, когда эта дополнительная информация необходима, но здесь она нам не нужна — нам просто нужен один плоский список всех комбинаций без дополнительной структуры. Есть два варианта. [2] показывает применение функции flatten(), чтобы удалить эту дополнительную структуру и сгладить результат в один список, что является приемлемым подходом. Однако это такая распространенная задача, что Kotlin предоставляет комбинированную операцию, называемую flatMap(), которая выполняет как map(), так и flatten() одним вызовом. [3] показывает flatMap() в действии. Вы найдете flatMap() в большинстве языков, поддерживающих функциональное программирование. Вот второй пример flatMap(): // ManipulatingLists/WhyFlatMap.kt package manipulatinglists import atomictest.eq class Book ( val title: String , val authors: List< String > ) fun main() { val books = listOf( Book(“1984”, listOf(“George Orwell”)), Book(“Ulysses”, listOf(“James Joyce”)) ) books.map { it.authors }.flatten() eq AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC ManipulatingLists 266 listOf(“George Orwell”, “James Joyce”) books.flatMap { it.authors } eq listOf(“George Orwell”, “James Joyce”) } Мы хотим получить список авторов. map() создает список списков авторов, что не очень удобно. flatten() берет это и создает простой список. flatMap() производит те же результаты за один шаг. Здесь мы используем map() и flatMap(), чтобы объединить перечисления Suit и Rank, создавая колоду карт: // ManipulatingLists/PlayingCards.kt package manipulatinglists import kotlin.random.Random import atomictest.* enum class Suit { Spade, Club, Heart, Diamond } enum class Rank ( val faceValue: Int ) { Ace(1), Two(2), Three(3), Four(4), Five(5), Six(6), Seven(7), Eight(8), Nine(9), Ten(10), Jack(10), Queen(10), King(10) } class Card ( val rank: Rank, val suit: Suit) { override fun toString() = " $ rank of ${ suit } s” } val deck: List = Suit.values().flatMap { suit -> Rank.values().map { rank -> Card(rank, suit) } } fun main() { val rand = Random(26) repeat(7) { AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC ManipulatingLists 267 trace(”’ ${ deck.random(rand) } ‘”) } trace eq "”” ‘Jack of Hearts’ ‘Four of Hearts’ ‘Five of Clubs’ ‘Seven of Clubs’ ‘Jack of Diamonds’ ‘Ten of Spades’ ‘Seven of Spades’ "”” } В инициализации deck внутренний Rank.values().map создает четыре списка, по одному для каждого Suit, поэтому мы используем flatMap() во внешнем цикле, чтобы создать список Card для колоды. Упражнения и решения можно найти на www.AtomicKotlin.com. AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC

Создание карт Link to heading

Карты являются чрезвычайно полезными инструментами программирования, и существует множество способов их создания. Чтобы создать повторяемый набор данных, мы используем техники, показанные в разделе “Манипулирование списками”, где два списка объединяются, и результат используется в лямбда-выражении для вызова конструктора, создавая List<Person>:

// BuildingMaps/People.kt
package buildingmaps

data class Person(
    val name: String,
    val age: Int
)

val names = listOf("Alice", "Arthricia", "Bob", "Bill", "Birdperson", "Charlie", "Crocubot", "Franz", "Revolio")
val ages = listOf(21, 15, 25, 25, 42, 21, 42, 21, 33)

fun people(): List<Person> =
    names.zip(ages) { name, age ->
        Person(name, age)
    }

Карта использует ключи для быстрого доступа к своим значениям. Создавая карту с возрастом в качестве ключа, мы можем быстро находить группы людей по возрасту. Функция библиотеки groupBy() — это один из способов создания такой карты:

// BuildingMaps/GroupBy.kt
import buildingmaps.*
import atomictest.eq

fun main() {
    val map: Map<Int, List<Person>> =
        people().groupBy(Person::age)

    map[15] eq listOf(Person("Arthricia", 15))
    map[21] eq listOf(
        Person("Alice", 21),
        Person("Charlie", 21),
        Person("Franz", 21)
    )
    map[22] eq null
    map[25] eq listOf(
        Person("Bob", 25),
        Person("Bill", 25)
    )
    map[33] eq listOf(Person("Revolio", 33))
    map[42] eq listOf(
        Person("Birdperson", 42),
        Person("Crocubot", 42)
    )
}

Параметр groupBy() создает карту, где каждый ключ связан со списком элементов. Здесь все люди одного возраста выбираются по ключу возраста. Вы можете получить те же группы, используя функцию filter(), но groupBy() предпочтительнее, потому что она выполняет группировку только один раз. С filter() вам нужно повторять группировку для каждого нового ключа:

// BuildingMaps/GroupByVsFilter.kt
import buildingmaps.*
import atomictest.eq

fun main() {
    val groups =
        people().groupBy { it.name.first() }

    // groupBy() обеспечивает быстрый доступ к карте:
    groups['A'] eq listOf(Person("Alice", 21), Person("Arthricia", 15))
    groups['Z'] eq null

    // Необходимо повторять filter() для каждого символа:
    people().filter {
        it.name.first() == 'A'
    } eq listOf(Person("Alice", 21), Person("Arthricia", 15))

    people().filter {
        it.name.first() == 'F'
    } eq listOf(Person("Franz", 21))

    people().partition {
        it.name.first() == 'A'
    } eq Pair(
        listOf(Person("Alice", 21), Person("Arthricia", 15)),
        listOf(Person("Bob", 25), Person("Bill", 25), Person("Birdperson", 42), Person("Charlie", 21), Person("Crocubot", 42), Person("Franz", 21), Person("Revolio", 33))
    )
}

Здесь groupBy() группирует people() по их первому символу, выбранному с помощью first(). Мы также можем использовать filter(), чтобы получить тот же результат, повторяя код лямбды для каждого символа. Если вам нужно только две группы, функция partition() более прямая, потому что она делит содержимое на два списка на основе предиката. groupBy() подходит, когда вам нужно больше двух результирующих групп.

Функция associateWith() позволяет вам взять список ключей и создать карту, ассоциируя каждый из этих ключей со значением, созданным по его параметру (в данном случае, лямбда):

// BuildingMaps/AssociateWith.kt
import buildingmaps.*
import atomictest.eq

fun main() {
    val map: Map<Person, String> =
        people().associateWith { it.name }

    map eq mapOf(
        Person("Alice", 21) to "Alice",
        Person("Arthricia", 15) to "Arthricia",
        Person("Bob", 25) to "Bob",
        Person("Bill", 25) to "Bill",
        Person("Birdperson", 42) to "Birdperson",
        Person("Charlie", 21) to "Charlie",
        Person("Crocubot", 42) to "Crocubot",
        Person("Franz", 21) to "Franz",
        Person("Revolio", 33) to "Revolio"
    )
}

Функция associateBy() меняет порядок ассоциации, созданной associateWith() — селектор (лямбда в следующем примере) становится ключом:

// BuildingMaps/AssociateBy.kt
import buildingmaps.*
import atomictest.eq

fun main() {
    val map: Map<String, Person> =
        people().associateBy { it.name }

    map eq mapOf(
        "Alice" to Person("Alice", 21),
        "Arthricia" to Person("Arthricia", 15),
        "Bob" to Person("Bob", 25),
        "Bill" to Person("Bill", 25),
        "Birdperson" to Person("Birdperson", 42),
        "Charlie" to Person("Charlie", 21),
        "Crocubot" to Person("Crocubot", 42),
        "Franz" to Person("Franz", 21),
        "Revolio" to Person("Revolio", 33)
    )
}

associateBy() должен использоваться с уникальным ключом выбора и возвращает карту, которая связывает каждый уникальный ключ с единственным элементом, выбранным по этому ключу.

// BuildingMaps/AssociateByUnique.kt
import buildingmaps.*
import atomictest.eq

fun main() {
    // associateBy() не работает, когда ключ не уникален — значения исчезают:
    val ages = people().associateBy { it.age }

    ages eq mapOf(
        21 to Person("Franz", 21),
        15 to Person("Arthricia", 15),
        25 to Person("Bill", 25),
        42 to Person("Crocubot", 42),
        33 to Person("Revolio", 33)
    )
}

Если несколько значений выбираются по предикату, как в случае с ages, только последнее значение появляется в сгенерированной карте.

Функция getOrElse() пытается найти значение в карте. Связанная с ней лямбда вычисляет значение по умолчанию, когда ключ отсутствует. Поскольку это лямбда, мы вычисляем значение по умолчанию только при необходимости:

// BuildingMaps/GetOrPut.kt
import atomictest.eq

fun main() {
    val map = mapOf(1 to "one", 2 to "two")
    map.getOrElse(0) { "zero" } eq "zero"

    val mutableMap = map.toMutableMap()
    mutableMap.getOrPut(0) { "zero" } eq "zero"
    mutableMap eq "{1=one, 2=two, 0=zero}"
}

getOrPut() работает с MutableMap. Если ключ присутствует, он просто возвращает связанное значение. Если ключ не найден, он вычисляет значение, добавляет его в карту и возвращает это значение.

Многие операции с картами дублируют операции со списками. Например, вы можете использовать filter() или map() для содержимого карты. Вы можете фильтровать ключи и значения отдельно:

// BuildingMaps/FilterMap.kt
import atomictest.eq

fun main() {
    val map = mapOf(1 to "one", 2 to "two", 3 to "three", 4 to "four")

    map.filterKeys { it % 2 == 1 } eq "{1=one, 3=three}"
    map.filterValues { it.contains('o') } eq "{1=one, 2=two, 4=four}"
    map.filter { entry ->
        entry.key % 2 == 1 && entry.value.contains('o')
    } eq "{1=one}"
}

Все три функции filter(), filterKeys() и filterValues() создают новую карту, содержащую только элементы, которые удовлетворяют предикату. filterKeys() применяет свой предикат к ключам, а filterValues() применяет свой предикат к значениям.

Применение операций к картам Link to heading

Применение map() к Map звучит как тавтология, как если бы сказать «соль соленая». Слово map представляет собой две различные идеи:

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

Во многих языках программирования слово map используется для обоих понятий. Для ясности мы говорим «преобразовать карту», когда применяем map() к Map. Здесь мы демонстрируем map(), mapKeys() и mapValues():

AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
BuildingMaps 274

// BuildingMaps/TransformingMap.kt
**import atomictest.eq**
**fun** main() {
    **val** even = mapOf(2 to "two", 4 to "four")
    even.map { // [1]
        " **${** it.key **}** = **${** it.value **}** "
    } eq listOf("2=two", "4=four")
    even.map { (key, value) -> // [2]
        " **$** key= **$** value"
    } eq listOf("2=two", "4=four")
    even.mapKeys { (num, _) -> -num } // [3]
        .mapValues { (_, str) -> "minus **$** str" } eq
        mapOf(-2 to "minus two",
               -4 to "minus four")
    even.map { (key, value) ->
        -key to "minus **$** value"
    }.toMap() eq mapOf(-2 to "minus two", // [4]
                        -4 to "minus four")
}
  • [1] Здесь map() принимает предикат с аргументом Map.Entry. Мы получаем доступ к его содержимому как it.key и it.value.
  • [2] Вы также можете использовать деструктурирующее объявление, чтобы поместить содержимое записи в key и value.
  • [3] Если параметр не используется, подчеркивание (_) предотвращает жалобы компилятора. mapKeys() и mapValues() возвращают новую карту, в которой все ключи или значения преобразованы соответственно.
  • [4] map() возвращает список пар, поэтому для получения Map мы используем явное преобразование toMap().

Функции, такие как any() и all(), также могут быть применены к Map:

AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
BuildingMaps 275

// BuildingMaps/SimilarOperation.kt
**import atomictest.eq**
**fun** main() {
    **val** map = mapOf(1 to "one",
                       -2 to "minus two")
    map.any { (key, _) -> key < 0 } eq **true**
    map.all { (key, _) -> key < 0 } eq **false**
    map.maxByOrNull { it.key }?.value eq "one"
}

Функция any() проверяет, удовлетворяет ли хотя бы одна из записей в Map заданному предикату, в то время как all() возвращает true только в том случае, если все записи в Map удовлетворяют предикату. maxByOrNull() находит максимальную запись на основе заданных критериев. Максимальной записи может не быть, поэтому результат может быть нулевым.

Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Последовательности Link to heading

Последовательность в Kotlin похожа на список, но вы можете только итерироваться по последовательности — вы не можете индексировать последовательность. Это ограничение приводит к очень эффективным цепочкам операций. Последовательности в Kotlin называются потоками в других функциональных языках. Kotlin пришлось выбрать другое название, чтобы сохранить совместимость с библиотекой Stream Java 8. Операции над списками выполняются жадно — они всегда происходят немедленно. При цепочке операций со списками первый результат должен быть получен перед началом следующей операции. Здесь каждая операция filter(), map() и any() применяется ко всем элементам списка:

// Sequences/EagerEvaluation.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, 3, 4)
    list.filter { it % 2 == 0 }
        .map { it * it }
        .any { it < 10 } eq true
    // Эквивалентно:
    val mid1 = list.filter { it % 2 == 0 }
    mid1 eq listOf(2, 4)
    val mid2 = mid1.map { it * it }
    mid2 eq listOf(4, 16)
    mid2.any { it < 10 } eq true
}

Жадная оценка интуитивно понятна и проста, но может быть не оптимальной. В EagerEvaluation.kt имело бы больше смысла остановиться после нахождения первого элемента, который удовлетворяет условию any(). Для длинной последовательности эта оптимизация может быть значительно быстрее, чем оценка каждого элемента и последующий поиск единственного совпадения.

Жадная оценка иногда называется горизонтальной оценкой: Link to heading

Горизонтальная оценка Link to heading

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

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

Вертикальная оценка Link to heading

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

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

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

// Sequences/EagerVsLazyEvaluation.kt
package sequences
import atomictest.*

fun Int.isEven(): Boolean {
    trace("$this.isEven()")
    return this % 2 == 0
}

fun Int.square(): Int {
    trace("$this.square()")
    return this * this
}

fun Int.lessThanTen(): Boolean {
    trace("$this.lessThanTen()")
    return this < 10
}

fun main() {
    val list = listOf(1, 2, 3, 4)
    trace(">>> List:")
    trace(
        list
            .filter(Int::isEven)
            .map(Int::square)
            .any(Int::lessThanTen)
    )
    trace(">>> Sequence:")
    trace(
        list.asSequence()
            .filter(Int::isEven)
            .map(Int::square)
            .any(Int::lessThanTen)
    )
}

Вывод:

>>> List:
1.isEven()
2.isEven()
3.isEven()
4.isEven()
2.square()
4.square()
4.lessThanTen()
true
>>> Sequence:
1.isEven()
2.isEven()
2.square()
4.lessThanTen()
true

Единственное различие между двумя подходами — это добавление вызова asSequence(), но для кода со списком обрабатывается больше элементов, чем для кода с последовательностью. Вызов любой из функций filter() или map() на последовательности производит другую последовательность. Ничего не происходит, пока вы не запросите результат вычисления. Вместо этого новая последовательность хранит всю информацию о отложенных операциях и выполнит эти операции только по мере необходимости:

// Sequences/NoComputationYet.kt
import atomictest.eq
import sequences.*

fun main() {
    val r = listOf(1, 2, 3, 4)
        .asSequence()
        .filter(Int::isEven)
        .map(Int::square)
    r.toString().substringBefore("@") eq "kotlin.sequences.TransformingSequence"
}

Преобразование r в строку не производит результаты, которые мы хотим, а только идентификатор объекта (включая адрес объекта в памяти, который мы удаляем с помощью стандартной библиотеки substringBefore()). TransformingSequence просто хранит операции, но не выполняет их.

Существуют две категории операций последовательности: промежуточные и терминальные. Промежуточные операции возвращают другую последовательность в качестве результата. filter() и map() являются промежуточными операциями. Терминальные операции возвращают не последовательность. Для этого терминальная операция выполняет все сохраненные вычисления. В предыдущих примерах any() является терминальной операцией, потому что она принимает последовательность и возвращает логическое значение. В следующем примере toList() является терминальной, потому что она преобразует последовательность в список, выполняя все сохраненные операции в процессе:

// Sequences/TerminalOperations.kt
import sequences.*
import atomictest.*

fun main() {
    val list = listOf(1, 2, 3, 4)
    trace(list.asSequence()
        .filter(Int::isEven)
        .map(Int::square)
        .toList())
    trace eq """
    1.isEven()
    2.isEven()
    2.square()
    3.isEven()
    4.isEven()
    4.square()
    [4, 16]
    """
}

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

Следующий пример использует функцию стандартной библиотеки generateSequence() для создания бесконечной последовательности натуральных чисел. Первый аргумент — это начальный элемент в последовательности, за которым следует лямбда, определяющая, как вычисляется следующий элемент из предыдущего элемента:

// Sequences/GenerateSequence1.kt
import atomictest.eq

fun main() {
    val naturalNumbers = generateSequence(1) { it + 1 }
    naturalNumbers.take(3).toList() eq listOf(1, 2, 3)
    naturalNumbers.take(10).sum() eq 55
}

Коллекции имеют известный размер, который можно узнать через их свойство size. Последовательности рассматриваются так, как будто они бесконечны. Здесь мы решаем, сколько элементов нам нужно, используя take(), за которым следует терминальная операция (toList() или sum()).

Существует перегруженная версия generateSequence(), которая не требует первого параметра, только лямбду, которая возвращает следующий элемент в последовательности. Когда больше нет элементов, она возвращает null. Следующий пример генерирует последовательность до тех пор, пока не появится “флаг завершения” XXX в ее входных данных:

// Sequences/GenerateSequence2.kt
import atomictest.*

fun main() {
    val items = mutableListOf("first", "second", "third", "XXX", "4th")
    val seq = generateSequence {
        items.removeAt(0).takeIf { it != "XXX" }
    }
    seq.toList() eq "[first, second, third]"
    capture {
        seq.toList()
    } eq "IllegalStateException: This sequence can be consumed only once."
}

removeAt(0) удаляет и возвращает нулевой элемент из списка. takeIf() возвращает полученное значение (строку, полученную с помощью removeAt(0)), если оно удовлетворяет заданному предикату, и null, если предикат не выполняется (когда строка равна “XXX”).

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

Вот реализация для takeIf(), определенная с использованием обобщенного типа T, чтобы она могла работать с любым типом аргумента:

// Sequences/DefineTakeIf.kt
package sequences
import atomictest.eq

fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    return if (predicate(this)) this else null
}

fun main() {
    "abc".takeIf { it != "XXX" } eq "abc"
    "XXX".takeIf { it != "XXX" } eq null
}

Здесь generateSequence() и takeIf() создают убывающую последовательность чисел:

// Sequences/NumberSequence2.kt
import atomictest.eq

fun main() {
    generateSequence(6) {
        (it - 1).takeIf { it > 0 }
    }.toList() eq listOf(6, 5, 4, 3, 2, 1)
}

Обычное выражение if всегда может быть использовано вместо takeIf(), но введение дополнительного идентификатора может сделать выражение if громоздким. Версия с takeIf() более функциональна, особенно если она используется как часть цепочки вызовов.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Локальные функции Link to heading

Вы можете определять функции где угодно — даже внутри других функций. Функции с именем, определенные внутри других функций, называются локальными функциями. Локальные функции уменьшают дублирование, извлекая повторяющийся код. В то же время они видимы только в окружающей функции, поэтому они не «загрязняют ваше пространство имен». Здесь, хотя log() определена так же, как и любая другая функция, она вложена внутри main():

// LocalFunctions/LocalFunctions.kt
import atomictest.eq

fun main() {
    val logMsg = StringBuilder()
    fun log(message: String) =
        logMsg.appendLine(message)
    
    log("Starting computation")
    val x = 42 // Имитируем вычисление
    log("Computation result: $x")
    
    logMsg.toString() eq """
        Starting computation
        Computation result: 42
    """
}

Локальные функции являются замыканиями: они захватывают переменные var или val из окружающей среды, которые в противном случае пришлось бы передавать в качестве дополнительных параметров. log() использует logMsg, который определен в ее внешней области видимости. Таким образом, вам не нужно постоянно передавать logMsg в log().

Вы также можете создавать локальные функции-расширения:

// LocalFunctions/LocalExtensions.kt
import atomictest.eq

fun main() {
    fun String.exclaim() = " $this!"
    
    "Hello".exclaim() eq "Hello!"
    "Hallo".exclaim() eq "Hallo!"
    "Bonjour".exclaim() eq "Bonjour!"
    "Ciao".exclaim() eq "Ciao!"
}

exclaim() доступна только внутри main().

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

// LocalFunctions/Session.kt
package localfunctions

class Session(
    val title: String,
    val speaker: String
)

val sessions = listOf(Session("Kotlin Coroutines", "Roman Elizarov"))
val favoriteSpeakers = setOf("Roman Elizarov")

Вы можете ссылаться на локальную функцию, используя ссылку на функцию:

// LocalFunctions/LocalFunctionReference.kt
import localfunctions.*
import atomictest.eq

fun main() {
    fun interesting(session: Session): Boolean {
        if (session.title.contains("Kotlin") && session.speaker in favoriteSpeakers) {
            return true
        }
        // ... дополнительные проверки
        return false
    }
    
    sessions.any(::interesting) eq true
}

interesting() используется только один раз, поэтому мы можем быть склонны определить ее как лямбду. Как вы увидите позже в этом атоме, выражения return внутри interesting() усложняют задачу превращения ее в лямбду. Мы можем избежать этого усложнения с помощью анонимной функции. Как и локальные функции, анонимные функции определяются внутри других функций — однако анонимная функция не имеет имени. Анонимные функции концептуально похожи на лямбды, но используют ключевое слово fun. Вот LocalFunctionReference.kt, переписанный с использованием анонимной функции:

// LocalFunctions/InterestingSessions.kt
import localfunctions.*
import atomictest.eq

fun main() {
    sessions.any(
        fun(session: Session): Boolean { // [1]
            if (session.title.contains("Kotlin") && session.speaker in favoriteSpeakers) {
                return true
            }
            // ... дополнительные проверки
            return false
        }
    ) eq true
}

• [1] Анонимная функция выглядит как обычная функция без имени. Здесь анонимная функция передается в качестве аргумента в sessions.any().

Если лямбда становится слишком сложной и трудной для чтения, замените ее на локальную функцию или анонимную функцию.

Метки Link to heading

Здесь forEach() действует на лямбду, содержащую оператор return: AtomicKotlin (www.AtomicKotlin.com) Бруса Эккеля и Светланы Исаковой, ©2021 MindView LLC
LocalFunctions 286

// LocalFunctions/ReturnFromFun.kt
**import atomictest.eq**
**fun** main() {
    **val** list = listOf(1, 2, 3, 4, 5)
    **val** value = 3
    **var** result = ""
    list.forEach {
        result += " **$** it"
        **if** (it == value) {
            result eq "123"
            **return** // [1]
        }
    }
    result eq "Never gets here" // [2]
}

Выражение return завершает функцию, определенную с помощью fun (то есть, не лямбду). В строке [1] это означает возврат из main(). Строка [2] никогда не вызывается, и вы не видите никакого вывода.
Чтобы вернуть только из лямбды, а не из окружающей функции, используйте помеченный return:

// LocalFunctions/LabeledReturn.kt
**import atomictest.eq**
**fun** main() {
    **val** list = listOf(1, 2, 3, 4, 5)
    **val** value = 3
    **var** result = ""
    list.forEach {
        result += " **$** it"
        **if** (it == value) **return** @forEach
    }
    result eq "12345"
}

Здесь метка — это имя функции, которая вызвала лямбду. Выражение помеченного возврата return@forEach указывает на возврат только к имени forEach.
Вы можете создать метку, предшествуя лямбде label@, где label может быть любым именем: AtomicKotlin (www.AtomicKotlin.com) Бруса Эккеля и Светланы Исаковой, ©2021 MindView LLC
LocalFunctions 287

// LocalFunctions/CustomLabel.kt
**import atomictest.eq**
**fun** main() {
    **val** list = listOf(1, 2, 3, 4, 5)
    **val** value = 3
    **var** result = ""
    list.forEach tag@{ // [1]
        result += " **$** it"
        **if** (it == value) **return** @tag // [2]
    }
    result eq "12345"
}

• [1] Эта лямбда помечена как tag.
• [2] return@tag возвращает из лямбды, а не из main().
Давайте заменим анонимную функцию в InterestingSessions.kt на лямбду:

// LocalFunctions/ReturnInsideLambda.kt
**import localfunctions.***
**import atomictest.eq**
**fun** main() {
    sessions.any { session ->
        **if** (session.title.contains("Kotlin") &&
            session.speaker **in** favoriteSpeakers) {
            **return** @any **true**
        }
        // ... дополнительные проверки
        **false**
    } eq **true**
}

Мы должны вернуть к метке, чтобы выйти только из лямбды, а не из main().

Манипулирование локальными функциями Link to heading

Вы можете сохранить лямбда-выражение или анонимную функцию в переменной var или val, а затем использовать этот идентификатор для вызова функции. Чтобы сохранить локальную функцию, используйте ссылку на функцию (см. раздел “Ссылки на члены”). AtomicKotlin (www.AtomicKotlin.com) Бруса Эккела и Светланы Исаковой, ©2021 MindView LLC

Локальные функции 288

В следующем примере first() создает анонимную функцию, second() использует лямбда-выражение, а third() возвращает ссылку на локальную функцию. fourth() достигает того же эффекта, что и third(), но использует более компактное тело выражения. fifth() производит тот же эффект, используя лямбда-выражение:

// LocalFunctions/ReturningFunc.kt
package localfunctions
import atomictest.eq

fun first(): (Int) -> Int {
    val func = fun(i: Int) = i + 1
    func(1) eq 2
    return func
}

fun second(): (String) -> String {
    val func2 = { s: String -> "$s!" }
    func2("abc") eq "abc!"
    return func2
}

fun third(): () -> String {
    fun greet() = "Hi!"
    return ::greet
}

fun fourth() = fun() = "Hi!"
fun fifth() = { "Hi!" }

fun main() {
    val funRef1: (Int) -> Int = first()
    val funRef2: (String) -> String = second()
    val funRef3: () -> String = third()
    val funRef4: () -> String = fourth()
    val funRef5: () -> String = fifth()

    funRef1(42) eq 43
    funRef2("xyz") eq "xyz!"
    funRef3() eq "Hi!"
    funRef4() eq "Hi!"
    funRef5() eq "Hi!"

    first()(42) eq 43
    second()("xyz") eq "xyz!"
    third()() eq "Hi!"
    fourth()() eq "Hi!"
    fifth()() eq "Hi!"
}

В функции main() сначала проверяется, что вызов каждой функции действительно возвращает ссылку на функцию ожидаемого типа. Затем каждая funRef вызывается с соответствующим аргументом. Наконец, каждая функция вызывается, и возвращенная ссылка на функцию немедленно вызывается с добавлением соответствующего списка аргументов. Например, вызов first() возвращает функцию, поэтому мы вызываем эту функцию, добавив список аргументов (42).

Упражнения и решения можно найти на www.AtomicKotlin.com. AtomicKotlin (www.AtomicKotlin.com) Бруса Эккела и Светланы Исаковой, ©2021 MindView LLC

Складывающие списки Link to heading

fold() объединяет все элементы списка, чтобы сгенерировать единый результат. Обычным упражнением является реализация операций, таких как sum() или reverse(), с использованием fold(). Здесь fold() суммирует последовательность:

// FoldingLists/SumViaFold.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 10, 100, 1000)
    list.fold(0) { sum, n ->
        sum + n
    } eq 1111
}

fold() принимает начальное значение (в данном случае 0) и последовательно применяет операцию (выраженную здесь как лямбда), чтобы объединить текущее накопленное значение с каждым элементом. fold() сначала добавляет 0 (начальное значение) и 1, чтобы получить 1. Это становится суммой, которая затем добавляется к 10, чтобы получить 11, что становится новой суммой. Операция повторяется для двух других элементов: 100 и 1000. Это дает 111 и 1111. fold() остановится, когда в списке не останется элементов, возвращая окончательную сумму 1111. Конечно, fold() на самом деле не знает, что он делает “сумму” — выбор имени идентификатора был нашим, чтобы сделать его легче для понимания.

Чтобы проиллюстрировать шаги в fold(), вот SumViaFold.kt, использующий обычный цикл for:

// FoldingLists/FoldVsForLoop.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 10, 100, 1000)
    var accumulator = 0
    val operation = { sum: Int, i: Int -> sum + i }
    for (i in list) {
        accumulator = operation(accumulator, i)
    }
    accumulator eq 1111
}

fold() накапливает значения, последовательно применяя операцию для объединения текущего элемента с значением аккумулятора. Хотя fold() является важной концепцией и единственным способом накапливать значения в чисто функциональных языках, вы иногда все же можете использовать обычный цикл for в Kotlin.

foldRight() обрабатывает элементы, начиная справа налево, в отличие от fold(), который обрабатывает элементы слева направо. Этот пример демонстрирует разницу:

// FoldingLists/FoldRight.kt
import atomictest.eq

fun main() {
    val list = listOf('a', 'b', 'c', 'd')
    list.fold("*") { acc, elem ->
        "($acc) + $elem"
    } eq "((((*) + a) + b) + c) + d"
    list.foldRight("*") { elem, acc ->
        "$elem + ($acc)"
    } eq "a + (b + (c + (d + (*))))"
}

fold() сначала применяет операцию к a, как мы можем видеть в (*) + a, в то время как foldRight() сначала обрабатывает правый элемент d и обрабатывает a последним. fold() и foldRight() принимают явное значение аккумулятора в качестве первого аргумента, за которым следует лямбда.

Иногда первый элемент списка может выступать в качестве начального значения. reduce() и reduceRight() ведут себя как fold() и foldRight(), но используют первый и последний элемент соответственно в качестве начального значения:

// FoldingLists/ReduceAndReduceRight.kt
import atomictest.eq

fun main() {
    val chars = "A B C D E".split(" ")
    chars.fold("*") { acc, e -> "$acc $e" } eq "* A B C D E"
    chars.foldRight("*") { e, acc -> "$acc $e" } eq "* E D C B A"
    chars.reduce { acc, e -> "$acc $e" } eq "A B C D E"
    chars.reduceRight { e, acc -> "$acc $e" } eq "E D C B A"
}

runningFold() и runningReduce() создают список, содержащий все промежуточные шаги процесса. Окончательное значение в списке — это результат fold() или reduce():

// FoldingLists/RunningFold.kt
import atomictest.eq

fun main() {
    val list = listOf(11, 13, 17, 19)
    list.fold(7) { sum, n ->
        sum + n
    } eq 67
    list.runningFold(7) { sum, n ->
        sum + n
    } eq "[7, 18, 31, 48, 67]"
    list.reduce { sum, n ->
        sum + n
    } eq 60
    list.runningReduce { sum, n ->
        sum + n
    } eq "[11, 24, 41, 60]"
}

runningFold() сначала сохраняет начальное значение (7), затем сохраняет каждое промежуточное значение. runningReduce() отслеживает каждое значение суммы.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Рекурсия Link to heading

Рекурсия — это техника программирования, заключающаяся в вызове функции внутри самой себя. Хвостовая рекурсия — это оптимизация, которая может быть явно применена к некоторым рекурсивным функциям. Рекурсивная функция использует результат предыдущего рекурсивного вызова. Факториалы — это распространенный пример: factorial(n) умножает все числа от 1 до n и может быть определен следующим образом:

  • factorial(1) равно 1
  • factorial(n) равно n * factorial(n - 1)

factorial() является рекурсивной, потому что использует результат той же функции, примененной к измененному аргументу. Вот рекурсивная реализация factorial():

// Recursion/Factorial.kt
package recursion
import atomictest.eq

fun factorial(n: Long): Long {
    if (n <= 1) return 1
    return n * factorial(n - 1)
}

fun main() {
    factorial(5) eq 120
    factorial(17) eq 355687428096000
}

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

// Recursion/CallStack.kt
package recursion

fun illegalState() {
// throw IllegalStateException()
}

fun fail() = illegalState()

fun main() {
    fail()
}

Если вы раскомментируете строку, содержащую исключение, вы увидите следующее:

Исключение в потоке "main" java.lang.IllegalStateException
    в recursion.CallStackKt.illegalState(CallStack.kt:5)
    в recursion.CallStackKt.fail(CallStack.kt:8)
    в recursion.CallStackKt.main(CallStack.kt:11)

Трассировка стека отображает состояние стека вызовов в момент, когда было выброшено исключение. Для CallStack.kt стек вызовов состоит всего из трех функций:

Стек вызовов Мы начинаем в main(), который вызывает fail(). Вызов fail() добавляется в стек вызовов вместе с его аргументами. Затем fail() вызывает illegalState(), который также добавляется в стек вызовов. Когда вы вызываете рекурсивную функцию, каждый рекурсивный вызов добавляет фрейм в стек вызовов. Это может легко привести к StackOverflowError, что означает, что ваш стек вызовов стал слишком большим и исчерпал доступную память.

Программисты часто вызывают StackOverflowError, забывая завершить цепочку рекурсивных вызовов — это бесконечная рекурсия:

// Recursion/InfiniteRecursion.kt
package recursion

fun recurse(i: Int): Int = recurse(i + 1)

fun main() {
// println(recurse(1))
}

Если вы раскомментируете строку в main(), вы увидите трассировку стека с множеством дублирующих вызовов:

Исключение в потоке "main" java.lang.StackOverflowError
    в recursion.InfiniteRecursionKt.recurse(InfiniteRecursion.kt:4)
    в recursion.InfiniteRecursionKt.recurse(InfiniteRecursion.kt:4)
    ...
    в recursion.InfiniteRecursionKt.recurse(InfiniteRecursion.kt:4)

Рекурсивная функция продолжает вызывать саму себя (с каждым разом с другим аргументом) и заполняет стек вызовов. Бесконечная рекурсия всегда заканчивается StackOverflowError.

Давайте суммируем целые числа до заданного числа, рекурсивно определяя sum(n) как n + sum(n - 1):

// Recursion/RecursionLimits.kt
package recursion
import atomictest.eq

fun sum(n: Long): Long {
    if (n == 0L) return 0
    return n + sum(n - 1)
}

fun main() {
    sum(2) eq 3
    sum(1000) eq 500500
    // sum(100_000) eq 500050000 // [1]
    (1..100_000L).sum() eq 5000050000 // [2]
}

Эта рекурсия быстро становится дорогой. Если вы раскомментируете строку [1], вы обнаружите, что она занимает слишком много времени для завершения, и все эти рекурсивные вызовы переполняют стек. Если sum(100_000) все еще работает на вашем компьютере, попробуйте большее число. Вызов sum(100_000) вызывает StackOverflowError, добавляя 100_000 вызовов функции sum() в стек вызовов. Для сравнения, строка [2] использует библиотечную функцию sum() для сложения чисел в диапазоне, и это не вызывает ошибок.

Чтобы избежать StackOverflowError, вы можете использовать итеративное решение вместо рекурсии:

// Recursion/Iteration.kt
package iteration
import atomictest.eq

fun sum(n: Long): Long {
    var accumulator = 0L
    for (i in 1..n) {
        accumulator += i
    }
    return accumulator
}

fun main() {
    sum(10000) eq 50005000
    sum(100000) eq 5000050000
}

Нет риска возникновения StackOverflowError, потому что мы делаем только один вызов sum() и результат вычисляется в цикле for. Хотя итеративное решение является простым, оно должно использовать изменяемую переменную состояния accumulator для хранения изменяющегося значения, а функциональное программирование пытается избежать мутации.

Чтобы предотвратить переполнение стека вызовов, функциональные языки (включая Kotlin) используют технику, называемую хвостовой рекурсией, также известную как оптимизация хвостового вызова. Цель хвостовой рекурсии — уменьшить размер стека вызовов. В примере sum() стек вызовов становится единственным вызовом функции, как это было в Iteration.kt:

Обычная рекурсия против хвостовой рекурсии Чтобы создать хвостовую рекурсию, используйте ключевое слово tailrec. При правильных условиях это преобразует рекурсивные вызовы в итерацию, устраняя накладные расходы на стек вызовов. Это оптимизация компилятора, но она не сработает для любого рекурсивного вызова.

Чтобы успешно использовать tailrec, рекурсия должна быть последней операцией, что означает, что не должно быть дополнительных вычислений над результатом рекурсивного вызова перед его возвратом. Например, если мы просто поставим tailrec перед fun для sum() в RecursionLimits.kt, Kotlin выдаст следующие предупреждающие сообщения:

  • Функция помечена как хвостовая рекурсивная, но хвостовые вызовы не найдены
  • Рекурсивный вызов не является хвостовым вызовом

Проблема в том, что n комбинируется с результатом рекурсивного вызова sum() перед возвратом этого результата. Чтобы tailrec сработал, результат рекурсивного вызова должен быть возвращен без каких-либо действий с ним во время возврата. Это часто требует некоторой работы по перестановке функции. Для sum() успешный tailrec выглядит так:

// Recursion/TailRecursiveSum.kt
package tailrecursion
import atomictest.eq

private tailrec fun sum(
    n: Long,
    accumulator: Long
): Long =
    if (n == 0L) accumulator
    else sum(n - 1, accumulator + n)

fun sum(n: Long) = sum(n, 0)

fun main() {
    sum(2) eq 3
    sum(10000) eq 50005000
    sum(100000) eq 5000050000
}

Включив параметр accumulator, сложение происходит во время рекурсивного вызова, и вы не делаете ничего с результатом, кроме как возвращаете его. Ключевое слово tailrec теперь успешно, потому что код был переписан, чтобы делегировать все действия рекурсивному вызову. Кроме того, accumulator становится неизменяемым значением, устраняя жалобу, которую мы имели для Iteration.kt.

factorial() является распространенным примером для демонстрации хвостовой рекурсии и является одним из упражнений для этого атома. Другим примером является последовательность Фибоначчи, где каждое новое число Фибоначчи является суммой двух предыдущих. Первые два числа — 0 и 1, что дает следующую последовательность: 0, 1, 1, 2, 3, 5, 8, 13, 21… Это можно выразить рекурсивно:

// Recursion/VerySlowFibonacci.kt
package slowfibonacci
import atomictest.eq

fun fibonacci(n: Long): Long {
    return when (n) {
        0L -> 0
        1L -> 1
        else -> fibonacci(n - 1) + fibonacci(n - 2)
    }
}

fun main() {
    fibonacci(0) eq 0
    fibonacci(22) eq 17711
    // Очень времязатратно:
    // fibonacci(50) eq 12586269025
}

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

Неэффективное вычисление чисел Фибоначчи При вычислении 50-го числа Фибоначчи мы сначала вычисляем 49-е и 48-е числа независимо, что означает, что мы вычисляем 48-е число дважды. 46-е число вычисляется столько же, сколько 4 раза, и так далее.

Используя хвостовую рекурсию, вычисления становятся значительно более эффективными:

// Recursion/Fibonacci.kt
package recursion
import atomictest.eq

fun fibonacci(n: Int): Long {
    tailrec fun fibonacci(
        n: Int,
        current: Long,
        next: Long
    ): Long {
        if (n == 0) return current
        return fibonacci(n - 1, next, current + next)
    }
    return fibonacci(n, 0L, 1L)
}

fun main() {
    (0..8).map { fibonacci(it) } eq
    "[0, 1, 1, 2, 3, 5, 8, 13, 21]"
    fibonacci(22) eq 17711
    fibonacci(50) eq 12586269025
}

Мы могли бы избежать локальной функции fibonacci(), используя значения по умолчанию. Однако значения по умолчанию подразумевают, что пользователь может подставить другие значения в эти значения по умолчанию, что приведет к неправильным результатам. Поскольку вспомогательная функция fibonacci() является локальной, мы не раскрываем дополнительные параметры, и вы можете вызывать только fibonacci(n).

main() показывает первые восемь элементов последовательности Фибоначчи, результат для 22 и, наконец, 50-е число Фибоначчи, которое теперь вычисляется очень быстро.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Раздел V: Объектно-ориентированный Link to heading

Программирование Link to heading

…наследование — это очень гибкий механизм. Его можно и, на самом деле, довольно часто неправильно использовать, но это не причина систематически недоверять ему, как, похоже, стало модным. — Бернард Мейер

Интерфейсы Link to heading

Интерфейс описывает концепцию типа. Это прототип для всех классов, которые реализуют интерфейс. Он описывает, что класс должен делать, но не как он должен это делать. Интерфейс предоставляет форму, но, как правило, не содержит реализации. Он специфицирует действия объекта, не уточняя, как эти действия выполняются. Интерфейс описывает миссию или цель сущности, в отличие от класса, который содержит детали реализации. Одно из определений в словаре гласит, что интерфейс — это «Место, где независимые и часто не связанные системы встречаются и действуют или общаются друг с другом». Таким образом, интерфейс является средством коммуникации между различными частями системы.

Интерфейс прикладного программирования (API) — это набор четко определенных путей коммуникации между различными компонентами программного обеспечения. В объектно-ориентированном программировании API объекта — это набор публичных членов, которые он использует для взаимодействия с другими объектами. Код, использующий определенный интерфейс, знает только о том, какие функции могут быть вызваны для этого интерфейса. Интерфейс устанавливает «протокол» между классами. (Некоторые объектно-ориентированные языки имеют ключевое слово, называемое протоколом, для выполнения той же функции.)

Чтобы создать интерфейс, используйте ключевое слово interface вместо ключевого слова class. При определении класса, который реализует интерфейс, следуйте за именем класса двоеточием (:) и именем интерфейса:

// Interfaces/Computer.kt
package interfaces
import atomictest.*

interface Computer {
    fun prompt(): String
    fun calculateAnswer(): Int
}

class Desktop : Computer {
    override fun prompt() = "Hello!"
    override fun calculateAnswer() = 11
}

class DeepThought : Computer {
    override fun prompt() = "Thinking..."
    override fun calculateAnswer() = 42
}

class Quantum : Computer {
    override fun prompt() = "Probably..."
    override fun calculateAnswer() = -1
}

fun main() {
    val computers = listOf(
        Desktop(), DeepThought(), Quantum()
    )
    computers.map { it.calculateAnswer() } eq
        "[11, 42, -1]"
    computers.map { it.prompt() } eq
        "[Hello!, Thinking..., Probably...]"
}

Класс Computer объявляет функции prompt() и calculateAnswer(), но не предоставляет реализаций. Класс, который реализует интерфейс, должен предоставить тела для всех объявленных функций, делая эти функции конкретными. В main() вы видите, что разные реализации интерфейса выражают разные поведения через свои определения функций. При реализации члена интерфейса вы должны использовать модификатор override. override говорит Kotlin, что вы намеренно используете то же имя, которое появляется в интерфейсе (или базовом классе) — то есть вы не случайно переопределяете.

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

// Interfaces/PlayerInterface.kt
package interfaces
import atomictest.eq

interface Player {
    val symbol: Char
}

class Food : Player {
    override val symbol = '.'
}

class Robot : Player {
    override val symbol get() = 'R'
}

class Wall(override val symbol: Char) : Player

fun main() {
    listOf(Food(), Robot(), Wall('|')).map {
        it.symbol
    } eq "[., R, |]"
}

Каждый подкласс переопределяет свойство symbol по-разному:

  • Food напрямую заменяет значение symbol.
  • Robot имеет пользовательский геттер, который возвращает значение (см. Accessors свойств).
  • Wall переопределяет symbol внутри списка аргументов конструктора (см. Конструкторы).

Перечисление может реализовать интерфейс:

// Interfaces/Hotness.kt
package interfaces
import atomictest.*

interface Hotness {
    fun feedback(): String
}

enum class SpiceLevel : Hotness {
    Mild {
        override fun feedback() = "It adds flavor!"
    },
    Medium {
        override fun feedback() = "Is it warm in here?"
    },
    Hot {
        override fun feedback() = "I'm suddenly sweating a lot."
    },
    Flaming {
        override fun feedback() = "I'm in pain. I am suffering."
    }
}

fun main() {
    SpiceLevel.values().map { it.feedback() } eq
        "[It adds flavor!, " +
        "Is it warm in here?, " +
        "I'm suddenly sweating a lot., " +
        "I'm in pain. I am suffering.]"
}

Компилятор гарантирует, что каждый элемент перечисления предоставляет определение для feedback().

Преобразования SAM Link to heading

Интерфейс с единственным абстрактным методом (SAM) пришел из Java, где функции-члены называют “методами”. В Kotlin есть специальный синтаксис для определения интерфейсов SAM: AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Интерфейсы 307

fun interface ZeroArg {
    fun f(): Int
}

fun interface OneArg {
    fun g(n: Int): Int
}

fun interface TwoArg {
    fun h(i: Int, j: Int): Int
}

Когда вы говорите fun interface, компилятор гарантирует, что существует только одна функция-член. Вы можете реализовать интерфейс SAM обычным многословным способом или передав ему лямбду; последнее называется преобразованием SAM. В преобразовании SAM лямбда становится реализацией для единственного метода в интерфейсе. Здесь мы показываем оба способа реализации трех интерфейсов:

package interfaces

import atomictest.eq

class VerboseZero : ZeroArg {
    override fun f() = 11
}

val verboseZero = VerboseZero()
val samZero = ZeroArg { 11 }

class VerboseOne : OneArg {
    override fun g(n: Int) = n + 47
}

val verboseOne = VerboseOne()
val samOne = OneArg { it + 47 }

class VerboseTwo : TwoArg {
    override fun h(i: Int, j: Int) = i + j
}

val verboseTwo = VerboseTwo()
val samTwo = TwoArg { i, j -> i + j }

fun main() {
    verboseZero.f() eq 11
    samZero.f() eq 11
    verboseOne.g(92) eq 139
    samOne.g(92) eq 139
    verboseTwo.h(11, 47) eq 58
    samTwo.h(11, 47) eq 58
}

Сравнивая “многословные” реализации с “sam” реализациями, вы можете увидеть, что преобразования SAM создают гораздо более лаконичный синтаксис для часто используемого идиома, и вам не нужно определять класс для создания единственного объекта. Вы можете передать лямбду, где ожидается интерфейс SAM, не оборачивая ее сначала в объект:

package interfaces

import atomictest.trace

fun interface Action {
    fun act()
}

fun delayAction(action: Action) {
    trace("Delaying...")
    action.act()
}

fun main() {
    delayAction { trace("Hey!") }
}

В main() мы передаем лямбду вместо объекта, который реализует интерфейс Action. Kotlin автоматически создает объект Action из этой лямбды. Упражнения и решения можно найти на www.AtomicKotlin.com. AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Сложные Конструкторы Link to heading

Чтобы код работал корректно, объекты должны быть правильно инициализированы. Конструктор — это специальная функция, которая создает новый объект. В конструкторах мы видели простые конструкторы, которые только инициализируют свои аргументы. Использование var или val в списке параметров делает эти параметры свойствами, доступными извне объекта:

// ComplexConstructors/SimpleConstructor.kt
package complexconstructors
import atomictest.eq

class Alien(val name: String)

fun main() {
    val alien = Alien("Pencilvester")
    alien.name eq "Pencilvester"
}

В этих случаях мы не пишем код конструктора — Kotlin делает это за нас. Для большей настройки добавьте код конструктора в тело класса. Код внутри секции init выполняется во время создания объекта:

// ComplexConstructors/InitSection.kt
package complexconstructors
import atomictest.eq

private var counter = 0

class Message(text: String) {
    private val content: String

    init {
        counter += 10
        content = "[${counter}] $text"
    }

    override fun toString() = content
}

fun main() {
    val m1 = Message("Big ba-da boom!")
    m1 eq "[10] Big ba-da boom!"
    val m2 = Message("Bzzzzt!")
    m2 eq "[20] Bzzzzt!"
}

Параметры конструктора доступны внутри секции init, даже если они не помечены как свойства с использованием var или val. Хотя content определен как val, он не инициализируется в момент определения. В этом случае Kotlin гарантирует, что инициализация происходит в одной (и только в одной) точке во время создания. Переопределение content или забывание инициализировать его приводит к сообщению об ошибке.

  • Конструктор — это комбинация его списка параметров конструктора, инициализируемого перед входом в тело класса, и секций init, выполняемых во время создания объекта. Kotlin позволяет иметь несколько секций init, которые выполняются в порядке определения. Однако в большом и сложном классе распределение секций init может вызвать проблемы с обслуживанием для программистов, привыкших к одной секции init.

Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Вторичные Конструкторы Link to heading

Когда вам требуется несколько способов создать объект, именованные и стандартные аргументы обычно являются самым простым подходом. Однако иногда вам необходимо создать несколько перегруженных конструкторов. Конструктор называется «перегруженным», потому что вы создаете разные способы создания объектов одного и того же класса. В Kotlin перегруженные конструкторы называются вторичными конструкторами. Список параметров конструктора (непосредственно после имени класса), объединенный с инициализациями свойств и блоком init, называется первичным конструктором.

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

// SecondaryConstructors/WithSecondary.kt
package secondaryconstructors
import atomictest.*

class WithSecondary(i: Int) {
    init {
        trace("Primary: $i")
    }

    constructor(c: Char) : this(c - 'A') {
        trace("Secondary: '$c'")
    }

    constructor(s: String) : this(s.first()) { // [1]
        trace("Secondary: \"$s\"")
    }
    /* Не компилируется без вызова
    первичного конструктора:
    constructor(f: Float) { // [2]
        trace("Secondary: $f")
    }
    */
}

fun main() {
    fun sep() = trace("-".repeat(10))
    WithSecondary(1)
    sep()
    WithSecondary('D')
    sep()
    WithSecondary("Last Constructor")
    trace eq """
    Primary: 1
    ----------
    Primary: 3
    Secondary: 'D'
    ----------
    Primary: 11
    Secondary: 'L'
    Secondary: "Last Constructor"
    """
}

Вызов другого конструктора из вторичного конструктора (с использованием this) должен происходить до дополнительной логики конструктора, потому что тело конструктора может зависеть от этих других инициализаций. Таким образом, он предшествует телу конструктора. Список аргументов определяет, какой конструктор вызывать. WithSecondary(1) соответствует первичному конструктору, WithSecondary('D') соответствует первому вторичному конструктору, а WithSecondary("Last Constructor") соответствует второму вторичному конструктору. Вызов this() в [1] соответствует первому вторичному конструктору, и вы можете увидеть цепочку вызовов в выводе.

Первичный конструктор всегда должен быть вызван, либо напрямую, либо через вызов вторичного конструктора. В противном случае Kotlin генерирует ошибку времени компиляции, как в [2]. Таким образом, вся общая логика инициализации, которая может быть разделена между конструкторами, должна быть помещена в первичный конструктор.

Секция init не требуется при использовании вторичных конструкторов:

// SecondaryConstructors/GardenItem.kt
package secondaryconstructors
import atomictest.eq
import secondaryconstructors.Material.*

enum class Material {
    Ceramic, Metal, Plastic
}

class GardenItem(val name: String) {
    var material: Material = Plastic

    constructor(
        name: String, material: Material // [1]
    ) : this(name) { // [2]
        this.material = material // [3]
    }

    constructor(
        material: Material
    ) : this("Strange Thing", material) // [4]

    override fun toString() = "$material $name"
}

fun main() {
    GardenItem("Elf").material eq Plastic
    GardenItem("Snowman").name eq "Snowman"
    GardenItem("Gazing Ball", Metal) eq // [5]
        "Metal Gazing Ball"
    GardenItem(material = Ceramic) eq
        "Ceramic Strange Thing"
}
  • [1] Только параметры первичного конструктора могут быть объявлены как свойства через val или var.
  • [2] Вы не можете объявить тип возвращаемого значения для вторичного конструктора.
  • [3] Параметр material имеет то же имя, что и свойство, поэтому мы уточняем его с помощью this.
  • [4] Тело вторичного конструктора является необязательным (хотя вы все равно должны включить явный вызов this()).

При вызове первого вторичного конструктора в строке [5] свойство material присваивается дважды. Сначала значение Plastic присваивается во время вызова первичного конструктора (в [2]) и инициализации всех свойств класса, затем оно изменяется на параметр material в [3].

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

Упражнения и решения можно найти на www.AtomicKotlin.com.

Наследование Link to heading

Наследование — это механизм создания нового класса путем повторного использования и модификации существующего класса. Объекты хранят данные в свойствах и выполняют действия через функции-члены. Каждый объект занимает уникальное место в памяти, поэтому свойства одного объекта могут иметь разные значения по сравнению с другими объектами. Объект также принадлежит категории, называемой классом, который определяет форму (свойства и функции) для своих объектов. Таким образом, объект выглядит как класс, который его создал.

Создание и отладка класса может потребовать значительных усилий. Что, если вы хотите создать класс, который похож на существующий класс, но с некоторыми изменениями? Кажется, что было бы расточительно создавать новый класс с нуля. Объектно-ориентированные языки предоставляют механизм повторного использования, называемый наследованием.

Наследование следует концепции биологического наследования. Вы говорите: «Я хочу создать новый класс на основе существующего класса, но с некоторыми дополнениями и модификациями». Синтаксис для наследования похож на реализацию интерфейса. Чтобы унаследовать новый класс Derived от существующего класса Base, используйте двоеточие ::

// Inheritance/BasicInheritance.kt
package inheritance
open class Base
class Derived : Base()

Следующий атом объясняет причину наличия скобок после Base при наследовании. Термины “базовый класс” и “производный класс” (или “родительский класс” и “дочерний класс”, или “суперкласс” и “подкласс”) часто используются для описания отношений наследования. Базовый класс должен быть открытым. Закрытый класс не позволяет наследование — он закрыт по умолчанию. Это отличается от большинства других объектно-ориентированных языков. В Java, например, класс автоматически может быть унаследован, если вы явно не запрещаете наследование, объявив этот класс как final. Хотя Kotlin это позволяет, модификатор final избыточен, потому что каждый класс по умолчанию фактически является final:

// Inheritance/OpenAndFinalClasses.kt
package inheritance
// Этот класс может быть унаследован:
open class Parent
class Child : Parent()
// Child не открыт, поэтому это не сработает:
// class GrandChild : Child()
// Этот класс не может быть унаследован:
final class Single
// То же самое, что и использование 'final':
class AnotherSingle

Kotlin заставляет вас уточнять ваше намерение, используя ключевое слово open, чтобы указать, что класс предназначен для наследования. В следующем примере GreatApe является базовым классом и имеет два свойства с фиксированными значениями. Производные классы Bonobo, Chimpanzee и BonoboB — это новые типы, которые идентичны своему родительскому классу:

// Inheritance/GreatApe.kt
package inheritance.ape1
import atomictest.eq
open class GreatApe {
    val weight = 100.0
    val age = 12
}
open class Bonobo : GreatApe()
class Chimpanzee : GreatApe()
class BonoboB : Bonobo()
fun GreatApe.info() = "wt: $weight age: $age"

AtomicKotlin (www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC

fun main() {
    GreatApe().info() eq "wt: 100.0 age: 12"
    Bonobo().info() eq "wt: 100.0 age: 12"
    Chimpanzee().info() eq "wt: 100.0 age: 12"
    BonoboB().info() eq "wt: 100.0 age: 12"
}

info() — это расширение для GreatApe, так что, естественно, вы можете вызвать его на GreatApe. Но вы также можете вызвать info() на Bonobo, Chimpanzee или BonoboB! Хотя последние три являются различными типами, Kotlin с радостью принимает их так, как если бы они были тем же типом, что и GreatApe. Это работает на любом уровне наследования — BonoboB находится на два уровня наследования от GreatApe.

Наследование гарантирует, что все, что наследует от GreatApe, является GreatApe. Весь код, который работает с объектами производных классов, знает, что GreatApe находится в их основе, поэтому любые функции и свойства в GreatApe также будут доступны в его дочерних классах. Наследование позволяет вам написать один фрагмент кода (функцию info()), который работает не только с одним классом, но и с каждым классом, который наследует этот класс. Таким образом, наследование создает возможности для упрощения и повторного использования кода.

GreatApe.kt немного слишком прост, потому что все классы идентичны. Наследование становится интересным, когда вы начинаете переопределять функции, что означает переопределение функции из базового класса, чтобы сделать что-то другое в производном классе. Давайте посмотрим на другую версию GreatApe.kt. На этот раз мы включим функции-члены, которые модифицированы в подклассах:

// Inheritance/GreatApe2.kt
package inheritance.ape2
import atomictest.eq
open class GreatApe {
    protected var energy = 0
    open fun call() = "Hoo!"
    open fun eat() {
        energy += 10
    }
    fun climb(x: Int) {
        energy -= x
    }
    fun energyLevel() = "Energy: $energy"
}
class Bonobo : GreatApe() {
    override fun call() = "Eep!"
    override fun eat() {
        // Модифицируем переменную базового класса:
        energy += 10
        // Вызываем версию базового класса:
        super.eat()
    }
    // Добавляем функцию:
    fun run() = "Bonobo run"
}
class Chimpanzee : GreatApe() {
    // Новое свойство:
    val additionalEnergy = 20
    override fun call() = "Yawp!"
    override fun eat() {
        energy += additionalEnergy
        super.eat()
    }
    // Добавляем функцию:
    fun jump() = "Chimp jump"
}
fun talk(ape: GreatApe): String {
    // ape.run() // Не функция для GreatApe
    // ape.jump() // И это тоже
    ape.eat()
    ape.climb(10)
    return " ${ape.call()} ${ape.energyLevel()}"
}
fun main() {
    // Нельзя получить доступ к 'energy':
    // GreatApe().energy
    talk(GreatApe()) eq "Hoo! Energy: 0"
    talk(Bonobo()) eq "Eep! Energy: 10"
    talk(Chimpanzee()) eq "Yawp! Energy: 20"
}

Каждый GreatApe имеет call(). Они хранят энергию, когда едят (eat()), и расходуют энергию, когда лазят (climb()). Как описано в разделе “Ограничение видимости”, производный класс не может получить доступ к приватным членам базового класса. Иногда создатель базового класса хочет взять конкретный член и предоставить доступ производным классам, но не всему миру. Вот что делает protected: защищенные члены закрыты для внешнего мира, но могут быть доступны или переопределены в подклассах.

Если мы объявим energy как private, то не будет возможности изменять его, когда используется GreatApe, что хорошо, но мы также не сможем получить к нему доступ в подклассах. Сделав его protected, мы можем сохранить его доступным для подклассов, но невидимым для внешнего мира.

call() определяется так же в Bonobo и Chimpanzee, как и в GreatApe. У него нет параметров, и выводимый тип определяется как String. Оба Bonobo и Chimpanzee должны иметь разные поведения для call(), чем GreatApe, поэтому мы хотим изменить их определения call(). Если вы создаете идентичную сигнатуру функции в производном классе, как в базовом классе, вы заменяете поведение, определенное в базовом классе, своим новым поведением. Это называется переопределением.

Когда Kotlin видит идентичную сигнатуру функции в производном классе, как в базовом классе, он решает, что вы сделали ошибку, называемую случайным переопределением. Если вы пишете функцию, которая имеет то же имя, что и функция в базовом классе, вы получаете сообщение об ошибке, в котором говорится, что вы забыли ключевое слово override. Kotlin предполагает, что вы непреднамеренно выбрали то же имя, параметры и возвращаемый тип, если вы не используете ключевое слово override (которое вы впервые увидели в разделе “Конструкторы”), чтобы сказать: «да, я намерен это сделать». Ключевое слово override также помогает при чтении кода, так что вам не нужно сравнивать сигнатуры, чтобы заметить переопределения.

Kotlin накладывает дополнительное ограничение при переопределении функций. Так же, как вы не можете наследовать от базового класса, если этот базовый класс не открыт, вы не можете переопределить функцию из базового класса, если эта функция не определена как open в базовом классе. climb() и energyLevel() не являются open, поэтому их нельзя переопределить. Наследование и переопределение не могут быть выполнены в Kotlin без четких намерений.

Особенно интересно взять Bonobo или Chimpanzee и рассматривать их как обычный GreatApe. Внутри talk(), call() производит правильное поведение в каждом случае. talk() каким-то образом знает точный тип объекта и производит соответствующую вариацию call(). Это и есть полиморфизм.

Внутри talk() вы можете вызывать только функции-члены GreatApe, потому что параметр talk() — это GreatApe. Хотя Bonobo определяет run(), а Chimpanzee определяет jump(), ни одна из этих функций не является частью GreatApe.

Часто, когда вы переопределяете функцию, вы хотите вызвать версию этой функции из базового класса (в частности, чтобы повторно использовать код), как видно в переопределениях для eat(). Это создает загадку: если вы просто вызываете eat(), вы вызываете ту же функцию, в которой находитесь (как мы видели в разделе “Рекурсия”). Чтобы вызвать версию функции базового класса eat(), используйте ключевое слово super, сокращение для “суперкласс”.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Инициализация базового класса Link to heading

Когда класс наследует другой класс, Kotlin гарантирует, что оба класса правильно инициализированы. Kotlin создает корректные объекты, обеспечивая вызов конструкторов:

  • Конструкторы для член-объектов.
  • Конструкторы для новых объектов, добавленных в производном классе.
  • Конструктор для базового класса.

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

// BaseClassInit/GreatApe3.kt
package baseclassinit
import atomictest.eq

open class GreatApe(
    val weight: Double,
    val age: Int
)

open class Bonobo(weight: Double, age: Int) : GreatApe(weight, age)

class Chimpanzee(weight: Double, age: Int) : GreatApe(weight, age)

class BonoboB(weight: Double, age: Int) : Bonobo(weight, age)

fun GreatApe.info() = "wt: $weight age: $age"

fun main() {
    GreatApe(100.0, 12).info() eq "wt: 100.0 age: 12"
    Bonobo(110.0, 13).info() eq "wt: 110.0 age: 13"
    Chimpanzee(120.0, 14).info() eq "wt: 120.0 age: 14"
    BonoboB(130.0, 15).info() eq "wt: 130.0 age: 15"
}

При наследовании от GreatApe вы должны передать необходимые аргументы конструктора в базовый класс GreatApe, иначе вы получите сообщение об ошибке компиляции. После того как Kotlin выделяет память для вашего объекта, он сначала вызывает конструктор базового класса, затем конструктор следующего производного класса и так далее, пока не достигнет самого производного конструктора. Таким образом, все вызовы конструкторов могут полагаться на корректность всех под-объектов, созданных до них. Действительно, это единственные вещи, о которых он знает; Bonobo знает, что он наследует от GreatApe, и конструктор Bonobo может вызывать функции в классе GreatApe, но GreatApe не может знать, является ли он Bonobo или Chimpanzee, или вызывать функции, специфичные для этих подклассов.

При наследовании от класса вы должны предоставить аргументы для конструктора базового класса после имени базового класса. Это вызывает конструктор базового класса во время создания объекта:

// BaseClassInit/NoArgConstructor.kt
package baseclassinit

open class SuperClass1(val i: Int)

class SubClass1(i: Int) : SuperClass1(i)

open class SuperClass2

class SubClass2 : SuperClass2()

Когда в базовом классе нет параметров конструктора, Kotlin все равно требует пустые скобки после имени базового класса, чтобы вызвать этот конструктор без аргументов. Если в базовом классе есть вторичные конструкторы, вы можете вызвать один из них вместо этого:

// BaseClassInit/House.kt
package baseclassinit
import atomictest.eq

open class House(
    val address: String,
    val state: String,
    val zip: String
) {
    constructor(fullAddress: String) : this(
        fullAddress.substringBefore(", "),
        fullAddress.substringAfter(", ").substringBefore(" "),
        fullAddress.substringAfterLast(" ")
    )

    val fullAddress: String
        get() = "$address, $state $zip"
}

class VacationHouse(
    address: String,
    state: String,
    zip: String,
    val startMonth: String,
    val endMonth: String
) : House(address, state, zip) {
    override fun toString() =
        "Vacation house at $fullAddress from $startMonth to $endMonth"
}

class TreeHouse(
    val name: String
) : House("Tree Street, TR 00000") {
    override fun toString() =
        "$name tree house at $fullAddress"
}

fun main() {
    val vacationHouse = VacationHouse(
        address = "8 Target St.",
        state = "KS",
        zip = "66632",
        startMonth = "May",
        endMonth = "September"
    )
    vacationHouse eq "Vacation house at 8 Target St., KS 66632 from May to September"
    TreeHouse("Oak") eq "Oak tree house at Tree Street, TR 00000"
}

Когда VacationHouse наследует от House, он передает соответствующие аргументы в основной конструктор House. Он также добавляет свои собственные параметры startMonth и endMonth — вы не ограничены количеством, типом или порядком параметров в базовом классе. Ваша единственная ответственность — предоставить правильные аргументы в вызове конструктора базового класса.

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

// BaseClassInit/OtherConstructors.kt
package baseclassinit
import atomictest.eq

open class Base(val i: Int)

class Derived : Base {
    constructor(i: Int) : super(i)
    constructor() : this(9)
}

fun main() {
    val d1 = Derived(11)
    d1.i eq 11
    val d2 = Derived()
    d2.i eq 9
}

Чтобы вызвать конструктор базового класса, используйте ключевое слово super, передавая аргументы конструктора так, как если бы это был вызов функции. Используйте это, чтобы вызвать другой конструктор того же класса.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Абстрактные классы Link to heading

Абстрактный класс похож на обычный класс, за исключением того, что одна или несколько функций или свойств являются неполными: функция не имеет определения, или свойство объявлено без инициализации. Интерфейс похож на абстрактный класс, но не имеет состояния. Вы должны использовать модификатор abstract, чтобы пометить члены класса, у которых отсутствуют определения. Класс, содержащий абстрактные функции или свойства, также должен быть помечен как абстрактный. Попробуйте удалить любой из модификаторов abstract ниже и посмотрите, какое сообщение вы получите: // Abstract/AbstractKeyword.kt package abstractclasses abstract class WithProperty { abstract val x: Int } abstract class WithFunctions { abstract fun f(): Int abstract fun g(n: Double) } WithProperty объявляет x без значения инициализации (объявление описывает что-то, не предоставляя определения для создания хранилища для значения или кода для функции). Если нет инициализатора, Kotlin требует, чтобы ссылки были абстрактными, и ожидает модификатор abstract на классе. Без инициализатора Kotlin не может вывести тип, поэтому он также требует информацию о типе для абстрактной ссылки. WithFunctions объявляет f() и g(), но не предоставляет определения функций, снова заставляя вас добавить модификатор abstract к функциям и содержащему классу. Если вы не укажете возвращаемый тип для функции, как в случае с g(), Kotlin предполагает, что она возвращает Unit.

Абстрактные функции и свойства должны каким-то образом существовать (быть реализованными) в классе, который вы в конечном итоге создаете из абстрактного класса. Все функции и свойства, объявленные в интерфейсе, по умолчанию являются абстрактными, что делает интерфейс похожим на абстрактный класс. Когда интерфейс содержит объявление функции или свойства, модификатор abstract является избыточным и может быть удален. Эти два интерфейса эквивалентны: // Abstract/Redundant.kt package abstractclasses interface Redundant { abstract val x: Int abstract fun f(): Int abstract fun g(n: Double) } interface Removed { val x: Int fun f(): Int fun g(n: Double) } Разница между интерфейсами и абстрактными классами заключается в том, что абстрактный класс может содержать состояние, в то время как интерфейс не может. Состояние — это данные, хранящиеся внутри свойств. В следующем примере состояние IntList состоит из значений, хранящихся в свойствах name и list. // Abstract/StateOfAClass.kt package abstractstate import atomictest.eq class IntList (val name: String) { val list = mutableListOf<Int>() } fun main() { val ints = IntList(“numbers”) ints.name eq “numbers” ints.list += 7 ints.list eq listOf(7) } AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC Абстрактные классы 329 Интерфейс может объявлять свойства, но фактические данные хранятся только в классах, которые реализуют интерфейс. Интерфейсу не разрешается хранить значения в своих свойствах: // Abstract/NoStateInInterfaces.kt package abstractclasses interface IntList { val name: String // Не компилируется: // val list = listOf(0) } Как интерфейсы, так и абстрактные классы могут содержать функции с реализациями. Вы можете вызывать другие абстрактные члены из таких функций: // Abstract/Implementations.kt package abstractclasses import atomictest.eq interface Parent { val ch: Char fun f(): Int fun g() = “ch = $ ch; f() = ${ f() } " } class Actual ( override val ch: Char // [1] ): Parent { override fun f() = 17 // [2] } class Other : Parent { override val ch: Char // [3] get () = ‘B’ override fun f() = 34 // [4] } fun main() { Actual(‘A’).g() eq “ch = A; f() = 17” // [5] Other().g() eq “ch = B; f() = 34” // [6] } AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC Абстрактные классы 330 Parent объявляет абстрактное свойство ch и абстрактную функцию f(), которые должны быть переопределены в любых реализующих классах. Строки [1] - [4] показывают разные реализации этих членов в подклассах. Parent.g() использует абстрактные члены, которые не имеют определений в точке, где g() определен. Интерфейсы и абстрактные классы гарантируют, что все абстрактные свойства и функции реализованы до того, как могут быть созданы какие-либо объекты — и вы не можете вызывать функцию-член, если у вас нет объекта. Строки [5] и [6] вызывают разные реализации ch и f(). Поскольку интерфейс может содержать реализации функций, он также может содержать пользовательские аксессоры свойств, если соответствующее свойство не изменяет состояние: // Abstract/PropertyAccessor.kt package abstractclasses import atomictest.eq interface PropertyAccessor { val a: Int get () = 11 } class Impl : PropertyAccessor fun main() { Impl().a eq 11 } Вы можете задаться вопросом, зачем нам нужны интерфейсы, когда абстрактные классы более мощные. Чтобы понять важность “класса без состояния”, давайте рассмотрим концепцию множественного наследования, которую Kotlin не поддерживает. В Kotlin класс может наследоваться только от одного базового класса: AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC Абстрактные классы 331 // Abstract/NoMultipleInheritance.kt package multipleinheritance1 open class Animal open class Mammal : Animal() open class AquaticAnimal : Animal() // Более одного базового класса не компилируется: // class Dolphin : Mammal(), AquaticAnimal() Попытка скомпилировать закомментированный код вызывает ошибку: “Только один класс может появляться в списке суперклассов”. Java работает так же. Первоначальные разработчики Java решили, что множественное наследование C++ — это плохая идея. Основная сложность и недовольство в то время возникали из-за множественного наследования состояния. Правила, управляющие наследованием нескольких состояний, сложны и могут легко вызвать путаницу и неожиданное поведение. Java добавила элегантное решение этой проблемы, введя интерфейсы, которые не могут содержать состояние. Java запрещает множественное наследование состояния, но разрешает множественное наследование интерфейсов, и Kotlin следует этому дизайну: // Abstract/MultipleInterfaceInheritance.kt package multipleinheritance2 interface Animal interface Mammal : Animal interface AquaticAnimal : Animal class Dolphin : Mammal, AquaticAnimal Точно так же, как классы, интерфейсы могут наследоваться друг от друга. При наследовании от нескольких интерфейсов возможно одновременно переопределить две или более функции с одинаковой сигнатурой (имя в сочетании с параметрами и возвращаемым типом). Если сигнатуры функций или свойств совпадают, вы должны разрешить конфликты вручную, как видно в классе C: // Abstract/InterfaceCollision.kt package collision import atomictest.eq interface A { fun f() = 1 fun g() = “A.g” val n: Double get () = 1.1 } interface B { fun f() = 2 fun g() = “B.g” val n: Double get () = 2.2 } class C : A, B { override fun f() = 0 override fun g() = super .g() override val n: Double get () = super .n + super .n } fun main() { val c = C() c.f() eq 0 c.g() eq “A.g” c.n eq 3.3 } Функции f() и g() и свойство n имеют идентичные сигнатуры в интерфейсах A и B, поэтому Kotlin не знает, что делать, и выдает сообщение об ошибке, если вы не разрешите проблему (попробуйте поочередно закомментировать определения в C). Функции-члены и свойства могут быть переопределены с новыми определениями, как в f(), но функции также могут обращаться к базовым версиям самих себя, используя ключевое слово super, указывая базовый класс в угловых скобках, как в определениях C.g() и C.n. Конфликты, где идентификатор одинаковый, но тип различен, не допускаются в Kotlin и не могут быть разрешены. AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC Абстрактные классы 333 Упражнения и решения можно найти на www.AtomicKotlin.com. AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC

Апкастинг Link to heading

Принятие ссылки на объект и обращение с ней как с ссылкой на его базовый тип называется апкастингом. Термин “апкаст” относится к тому, как традиционно представляются иерархии наследования, где базовый класс находится на вершине, а производные классы расходятся ниже.

Наследование и добавление новых функций-членов является практикой в Smalltalk, одном из первых успешных объектно-ориентированных языков. В Smalltalk всё является объектом, и единственный способ создать класс — это наследовать от существующего класса, часто добавляя новые функции-члены. Smalltalk оказал значительное влияние на Java, которая также требует, чтобы всё было объектом.

Kotlin освобождает нас от этих ограничений. У нас есть независимые функции, так что всё не обязательно должно содержаться в классах. Функции-расширения позволяют нам добавлять функциональность без наследования. Действительно, требование ключевого слова open для наследования делает это очень осознанным и целенаправленным выбором, а не чем-то, что следует использовать постоянно.

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

Рассмотрим некоторые фигуры, которые могут быть нарисованы и стерты:

// Upcasting/Shapes.kt
package upcasting

interface Shape {
    fun draw(): String
    fun erase(): String
}

class Circle : Shape {
    override fun draw() = "Circle.draw"
    override fun erase() = "Circle.erase"
}

class Square : Shape {
    override fun draw() = "Square.draw"
    override fun erase() = "Square.erase"
    fun color() = "Square.color"
}

class Triangle : Shape {
    override fun draw() = "Triangle.draw"
    override fun erase() = "Triangle.erase"
    fun rotate() = "Triangle.rotate"
}

Функция show() принимает любой Shape:

// Upcasting/Drawing.kt
package upcasting

import atomictest.*

fun show(shape: Shape) {
    trace("Show: ${shape.draw()} ")
}

fun main() {
    listOf(Circle(), Square(), Triangle())
        .forEach(::show)
    trace eq """
    Show: Circle.draw
    Show: Square.draw
    Show: Triangle.draw
    AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
    """
}

В main() функция show() вызывается с тремя разными типами: Circle, Square и Triangle. Параметр show() является базовым классом Shape, поэтому show() принимает все три типа. Каждый из этих типов рассматривается как базовый Shape — мы говорим, что конкретные типы апкастятся к базовому типу.

Обычно мы рисуем диаграмму для этой иерархии с базовым классом на вершине:

ShapeHierarchy

Когда мы передаем Circle, Square или Triangle в качестве аргумента типа Shape в show(), мы поднимаем по этой иерархии наследования. В процессе апкастинга мы теряем конкретную информацию о том, является ли объект типом Circle, Square или Triangle. В каждом случае он становится ничем иным, как объектом Shape.

Обращение с конкретным типом как с более общим типом — это вся суть наследования. Механика наследования существует исключительно для того, чтобы выполнить цель апкастинга к базовому типу. Благодаря этой абстракции (“всё — это Shape”), мы можем написать одну функцию show(), вместо того чтобы писать одну для каждого типа элемента. Апкастинг — это способ повторного использования кода для объектов.

Действительно, практически в каждом случае, когда есть наследование без апкастинга, наследование используется неправильно — оно ненужно и делает код излишне сложным. Это неправильное использование является причиной максимы:

Предпочитайте композицию наследованию.

Если цель наследования — это возможность подмены производного типа базовым типом, что происходит с дополнительными функциями-членами: color() в Square и rotate() в Triangle?

Субституируемость, также называемая принципом подстановки Лисков, говорит о том, что после апкастинга производный тип может рассматриваться точно так же, как базовый тип — не больше и не меньше. Это означает, что любые функции-члены, добавленные в производной классе, фактически “обрезаются”. Они всё ещё существуют, но поскольку они не являются частью интерфейса базового класса, они недоступны в show():

// Upcasting/TrimmedMembers.kt
package upcasting

import atomictest.*

fun trim(shape: Shape) {
    trace(shape.draw())
    trace(shape.erase())
    // Не компилируется:
    // shape.color() // [1]
    // shape.rotate() // [2]
}

fun main() {
    trim(Square())
    trim(Triangle())
    trace eq """
    Square.draw
    Square.erase
    Triangle.draw
    Triangle.erase
    """
}

Вы не можете вызвать color() в строке [1], потому что экземпляр Square был апкастирован к Shape, и вы не можете вызвать rotate() в строке [2], потому что экземпляр Triangle также был апкастирован к Shape. Единственные доступные функции-члены — это те, которые общие для всех Shape — те, что определены в базовом типе Shape.

Та же логика применяется, когда вы напрямую присваиваете подтип Shape общему Shape. Указанный тип определяет доступные члены:

// Upcasting/Assignment.kt
import upcasting.*

fun main() {
    val shape1: Shape = Square()
    val shape2: Shape = Triangle()
    // Не компилируется:
    // shape1.color()
    // shape2.rotate()
}

После апкастинга вы можете вызывать только члены базового типа.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Полиморфизм Link to heading

Полиморфизм — это древнегреческий термин, означающий «много форм». В программировании полиморфизм означает, что объект или его члены имеют несколько реализаций. Рассмотрим простую иерархию типов домашних животных. Класс Pet говорит, что все Pet могут говорить (speak()). Классы Dog и Cat переопределяют функцию-член speak():

// Polymorphism/Pet.kt
package polymorphism
import atomictest.eq

open class Pet {
    open fun speak() = "Pet"
}

class Dog : Pet() {
    override fun speak() = "Bark!"
}

class Cat : Pet() {
    override fun speak() = "Meow"
}

fun talk(pet: Pet) = pet.speak()

fun main() {
    talk(Dog()) eq "Bark!" // [1]
    talk(Cat()) eq "Meow" // [2]
}

Обратите внимание на параметр функции talk(). При передаче Dog или Cat в talk(), конкретный тип забывается и становится обычным Pet — как Dog, так и Cat поднимаются к Pet. Объекты теперь обрабатываются как обычные Pet, так что не должны ли оба вывода для строк [1] и [2] быть “Pet”?

Функция talk() не знает точный тип Pet, который она получает. Тем не менее, когда вы вызываете speak() через ссылку на базовый класс Pet, вызывается правильная реализация подкласса, и вы получаете желаемое поведение. Полиморфизм происходит, когда ссылка на родительский класс содержит экземпляр дочернего класса. Когда вы вызываете член на этой ссылке родительского класса, полиморфизм производит правильный переопределенный член из дочернего класса.

Связывание вызова функции с телом функции называется связыванием. Обычно вы не задумываетесь о связывании, потому что оно происходит статически, на этапе компиляции. С полиморфизмом одна и та же операция должна вести себя по-разному для разных типов — но компилятор не может заранее знать, какое тело функции использовать. Тело функции должно определяться динамически, во время выполнения, с использованием динамического связывания. Динамическое связывание также называется поздним связыванием или динамической диспетчеризацией. Только во время выполнения Kotlin может определить, какую именно функцию speak() вызвать. Таким образом, мы говорим, что связывание для полиморфного вызова pet.speak() происходит динамически.

Рассмотрим фэнтезийную игру. Каждый Персонаж в игре имеет имя и может играть (play()). Мы комбинируем Воителя и Магистра, чтобы создать конкретных персонажей:

// Polymorphism/FantasyGame.kt
package polymorphism
import atomictest.*

abstract class Character(val name: String) {
    abstract fun play(): String
}

interface Fighter {
    fun fight() = "Fight!"
}

interface Magician {
    fun doMagic() = "Magic!"
}

class Warrior : Character("Warrior"), Fighter {
    override fun play() = fight()
}

open class Elf(name: String = "Elf") : Character(name), Magician {
    override fun play() = doMagic()
}

class FightingElf : Elf("FightingElf"), Fighter {
    override fun play() = super.play() + fight()
}

fun Character.playTurn() = // [1]
    trace(name + ": " + play()) // [2]

fun main() {
    val characters: List<Character> = listOf(
        Warrior(), Elf(), FightingElf()
    )
    characters.forEach { it.playTurn() } // [3]
    trace eq """
    Warrior: Fight!
    Elf: Magic!
    FightingElf: Magic!Fight!
    """
}

В main() каждый объект поднимается к Character, когда он помещается в List. Трассировка показывает, что вызов playTurn() для каждого Character в List дает разные результаты. playTurn() — это функция расширения для базового типа Character. Когда она вызывается в строке [3], она статически связана, что означает, что точная функция, которую нужно вызвать, определяется на этапе компиляции. В строке [3] компилятор определяет, что существует только одна реализация функции playTurn() — та, что определена в строке [1].

Когда компилятор анализирует вызов функции play() в строке [2], он не знает, какую реализацию функции использовать. Если Character — это Elf, он должен вызвать play() у Elf. Если Character — это FightingElf, он должен вызвать play() у FightingElf. Он также может потребовать вызвать функцию из еще не определенного подкласса. Связывание функции отличается от вызова к вызову. На этапе компиляции единственной определенностью является то, что play() в строке [2] — это член-функция одного из подклассов Character.

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

Динамическое связывание не бесплатно. Дополнительная логика, которая определяет тип во время выполнения, немного влияет на производительность по сравнению со статическим связыванием. Чтобы обеспечить ясность, Kotlin по умолчанию использует закрытые классы и член-функции. Чтобы наследовать и переопределять, вы должны быть явными.

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

Упражнения и решения можно найти на www.AtomicKotlin.com.

Состав Link to heading

Одним из самых убедительных аргументов в пользу объектно-ориентированного программирования является повторное использование кода. Вы можете сначала подумать о “повторном использовании” как о “копировании кода”. Копирование кажется простым решением, но оно не работает очень хорошо. С течением времени ваши потребности эволюционируют. Внесение изменений в код, который был скопирован, становится кошмаром для сопровождения. Вы нашли все копии? Вы внесли изменения одинаковым образом для каждой копии? Повторно используемый код можно изменить всего в одном месте.

В объектно-ориентированном программировании вы повторно используете код, создавая новые классы, но вместо того, чтобы создавать их с нуля, вы используете существующие классы, которые кто-то уже создал и отладил. Умение использовать классы, не загрязняя существующий код, является ключевым моментом. Наследование — это один из способов достичь этого. Наследование создает новый класс как тип существующего класса. Вы добавляете код к форме существующего класса, не изменяя оригинал. Наследование является краеугольным камнем объектно-ориентированного программирования.

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

Композиция часто используется в этой книге. Композиция часто игнорируется, потому что кажется такой простой — вы просто помещаете объект внутрь класса. Композиция — это отношение “имеет”. “Дом — это здание и имеет кухню” можно выразить так:

// Composition/House1.kt
package composition1
interface Building
interface Kitchen
interface House : Building {
    val kitchen: Kitchen
}

Наследование описывает отношение “является”, и часто полезно читать описание вслух: “Дом — это здание”. Это звучит правильно, не так ли? Когда отношение “является” имеет смысл, наследование обычно тоже имеет смысл.

Если у вашего дома есть две кухни, композиция дает простое решение:

// Composition/House2.kt
package composition2
interface Building
interface Kitchen
interface House : Building {
    val kitchen1: Kitchen
    val kitchen2: Kitchen
}

Чтобы позволить иметь любое количество кухонь, используйте композицию с коллекцией:

// Composition/House3.kt
package composition3
interface Building
interface Kitchen
interface House : Building {
    val kitchens: List<Kitchen>
}

Мы тратим время и усилия на понимание наследования, потому что оно более сложное, и эта сложность может создать впечатление, что оно как-то более важно. Напротив:

Atomic Kotlin (www.AtomicKotlin.com) Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Предпочитайте композицию наследованию. Композиция создает более простые дизайны и реализации. Это не значит, что вы должны избегать наследования. Просто мы склонны запутываться в более сложных отношениях. Максимум “предпочитайте композицию наследованию” — это напоминание отступить, взглянуть на ваш дизайн и задуматься, можете ли вы упростить его с помощью композиции. Конечная цель — правильно применять ваши инструменты и создавать хороший дизайн.

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

Выбор между композицией и Link to heading

Наследование Link to heading

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

// Composition/Embedding.kt
package composition

class Features {
    fun f1() = "feature1"
    fun f2() = "feature2"
}

class Form {
    private val features = Features()
    
    fun operation1() = features.f2() + features.f1()
    fun operation2() = features.f1() + features.f2()
}

Класс Features предоставляет реализации для операций класса Form, но клиент-программист, который использует Form, не имеет доступа к features — на самом деле, пользователь фактически не осознает, как реализован Form. Это означает, что если вы найдете лучший способ реализовать Form, вы можете удалить features и перейти к новому подходу без какого-либо влияния на код, который вызывает Form.
Если бы Form наследовал Features, клиент-программист мог бы ожидать, что сможет выполнить восходящее преобразование Form в Features. Отношение наследования тогда стало бы частью Form — связь была бы явной. Если вы измените это, вы сломаете код, который зависит от этой связи.
Иногда имеет смысл позволить пользователю класса напрямую получать доступ к композиции вашего нового класса; то есть сделать объекты-члены публичными. Это относительно безопасно, при условии, что объекты-члены используют соответствующее сокрытие реализации. Для некоторых систем этот подход может сделать интерфейс более понятным. Рассмотрим класс Car:

// Composition/Car.kt
package composition

import atomictest.*

class Engine {
    fun start() = trace("Engine start")
    fun stop() = trace("Engine stop")
}

class Wheel {
    fun inflate(psi: Int) = trace("Wheel inflate($psi)")
}

class Window(val side: String) {
    fun rollUp() = trace("$side Window roll up")
    fun rollDown() = trace("$side Window roll down")
}

class Door(val side: String) {
    val window = Window(side)
    
    fun open() = trace("$side Door open")
    fun close() = trace("$side Door close")
}

class Car {
    val engine = Engine()
    val wheel = List(4) { Wheel() }
    // Две двери:
    val leftDoor = Door("left")
    val rightDoor = Door("right")
}

fun main() {
    val car = Car()
    car.leftDoor.open()
    car.rightDoor.window.rollUp()
    car.wheel[0].inflate(72)
    car.engine.start()
    trace eq """
    left Door open
    right Window roll up
    Wheel inflate(72)
    Engine start
    """
}

Композиция класса Car является частью анализа проблемы, а не просто частью базовой реализации. Это помогает клиент-программисту понять, как использовать класс, и требует меньшей сложности кода для создателя класса.
Когда вы наследуете, вы создаете пользовательскую версию существующего класса. Это берет класс общего назначения и специализирует его для конкретной нужды. В этом примере не имеет смысла составлять Car, используя объект класса Vehicle — Car не содержит Vehicle, он является Vehicle. Отношение “является” выражается через наследование, а отношение “имеет” выражается через композицию.
Умение полиморфизма может заставить вас думать, что все должно быть унаследовано. Это обременит ваши проекты. На самом деле, если вы сначала выберете наследование, когда используете существующий класс для создания нового класса, все может стать ненужным образом сложным. Лучший подход — сначала попробовать композицию, особенно когда не очевидно, какой подход работает лучше.
Упражнения и решения можно найти на www.AtomicKotlin.com.

Наследование и расширения Link to heading

Наследование иногда используется для добавления функций в класс как способ повторного использования его для новой цели. Это может привести к коду, который трудно понять и поддерживать.

Предположим, кто-то создал класс Heater вместе с функциями, которые действуют на Heater: // InheritanceExtensions/Heater.kt

package inheritanceextensions
import atomictest.eq

open class Heater {
    fun heat(temperature: Int) = "heating to $temperature"
}

fun warm(heater: Heater) {
    heater.heat(70) eq "heating to 70"
}

Для аргументации представьте, что Heater гораздо сложнее, чем это, и что есть много дополнительных функций, таких как warm(). Мы не хотим модифицировать эту библиотеку — мы хотим использовать её как есть.

Если то, что мы на самом деле хотим, — это система HVAC (отопление, вентиляция и кондиционирование воздуха), мы можем унаследовать Heater и добавить функцию cool(). Существующая функция warm() и все другие функции, которые действуют на Heater, по-прежнему работают с нашим новым типом HVAC — что не было бы верно, если бы мы использовали композицию: // InheritanceExtensions/InheritAdd.kt

package inheritanceextensions
import atomictest.eq

class HVAC : Heater() {
    fun cool(temperature: Int) = "cooling to $temperature"
}

fun warmAndCool(hvac: HVAC) {
    hvac.heat(70) eq "heating to 70"
    hvac.cool(60) eq "cooling to 60"
}

fun main() {
    val heater = Heater()
    val hvac = HVAC()
    warm(heater)
    warm(hvac)
    warmAndCool(hvac)
}

Это кажется практичным: Heater не делал всего, что мы хотели, поэтому мы унаследовали HVAC от Heater и добавили еще одну функцию.

Как вы видели в Upcasting, объектно-ориентированные языки имеют механизм для работы с членами функций, добавленными во время наследования: добавленные функции отсекаются во время upcasting и недоступны для базового класса. Это принцип подстановки Лисков, также известный как “заменяемость”, который говорит, что функции, принимающие базовый класс, должны уметь использовать объекты производных классов, не зная об этом. Замена — это причина, по которой warm() все еще работает на HVAC.

Хотя современное объектно-ориентированное программирование позволяет добавлять функции во время наследования, это может быть “запахом кода” — это кажется разумным и целесообразным, но может привести к проблемам. То, что это кажется работающим, не означает, что это хорошая идея. В частности, это может негативно повлиять на будущего поддерживающего код (что можете быть вы). Этот вид проблемы называется техническим долгом.

Добавление функций во время наследования может быть полезным, когда новый класс строго рассматривается как базовый класс на протяжении всей вашей системы, игнорируя тот факт, что у него есть свои собственные базы. В разделе “Проверка типов” вы увидите больше примеров, где добавление функций во время наследования может быть жизнеспособной техникой.

Что мы на самом деле хотели при создании класса HVAC, так это класс Heater с добавленной функцией cool(), чтобы он работал с warmAndCool(). Это именно то, что делает функция расширения, без наследования: // InheritanceExtensions/ExtensionFuncs.kt

package inheritanceextensions2
import inheritanceextensions.Heater
import atomictest.eq

fun Heater.cool(temperature: Int) = "cooling to $temperature"

fun warmAndCool(heater: Heater) {
    heater.heat(70) eq "heating to 70"
    heater.cool(60) eq "cooling to 60"
}

fun main() {
    val heater = Heater()
    warmAndCool(heater)
}

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

Если бы у нас был контроль над библиотекой Heater, мы могли бы спроектировать её иначе, чтобы она была более гибкой: // InheritanceExtensions/TemperatureDelta.kt

package inheritanceextensions
import atomictest.*

class TemperatureDelta(
    val current: Double,
    val target: Double
)

fun TemperatureDelta.heat() {
    if (current < target)
        trace("heating to $target")
}

fun TemperatureDelta.cool() {
    if (current > target)
        trace("cooling to $target")
}

fun adjust(deltaT: TemperatureDelta) {
    deltaT.heat()
    deltaT.cool()
}

fun main() {
    adjust(TemperatureDelta(60.0, 70.0))
    adjust(TemperatureDelta(80.0, 60.0))
    trace eq """
    heating to 70.0
    cooling to 60.0
    """
}

В этом подходе мы контролируем температуру, выбирая среди множества стратегий. Мы также могли бы сделать heat() и cool() членами функций вместо функций расширения.

Интерфейс по соглашению Link to heading

Функцию расширения можно рассматривать как создание интерфейса, содержащего единственную функцию:

// InheritanceExtensions/Convention.kt
package inheritanceextensions

class X
fun X.f() {}

class Y
fun Y.f() {}

fun callF(x: X) = x.f()
fun callF(y: Y) = y.f()

fun main() {
    val x = X()
    val y = Y()
    x.f()
    y.f()
    callF(x)
    callF(y)
}

Теперь и X, и Y, похоже, имеют член-функцию под названием f(), но мы не получаем полиморфного поведения, поэтому мы должны перегрузить callF(), чтобы это работало для обоих типов.

Это «интерфейс по соглашению» широко используется в библиотеках Kotlin, особенно при работе с коллекциями. Хотя это в основном коллекции Java, библиотека Kotlin превращает их в коллекции функционального стиля, добавляя большое количество функций расширения. Например, на практически любом объекте, похожем на коллекцию, вы можете ожидать найти функции map() и reduce(), среди многих других. Поскольку программист начинает ожидать это соглашение, программирование становится проще.

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

Документация по Sequence

Шаблон Адаптера Link to heading

Библиотека часто определяет тип и предоставляет функции, которые принимают параметры этого типа и/или возвращают этот тип: // InheritanceExtensions/UsefulLibrary.kt package usefullibrary
interface LibType {
fun f1()
fun f2()
}
fun utility1(lt: LibType) {
lt.f1()
lt.f2()
}
fun utility2(lt: LibType) {
lt.f2()
lt.f1()
}
Чтобы использовать эту библиотеку, вам необходимо каким-то образом преобразовать ваши существующие классы в LibType. Здесь мы наследуемся от существующего MyClass, чтобы создать MyClassAdaptedForLib, который реализует LibType и таким образом может быть передан в функции из UsefulLibrary.kt : // InheritanceExtensions/Adapter.kt package inheritanceextensions
import usefullibrary.*
import atomictest.*
open class MyClass {
fun g() = trace(“g()”)
fun h() = trace(“h()”)
}
fun useMyClass(mc: MyClass) {
mc.g()
mc.h()
}
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
Inheritance & Extensions 354
class MyClassAdaptedForLib :
MyClass(), LibType {
override fun f1() = h()
override fun f2() = g()
}
fun main() {
val mc = MyClassAdaptedForLib()
utility1(mc)
utility2(mc)
useMyClass(mc)
trace eq “h() g() g() h() g() h()”
}
Хотя это действительно расширяет класс во время наследования, новые функции-члены используются только с целью адаптации к UsefulLibrary. Везде, где это возможно, объекты MyClassAdaptedForLib могут рассматриваться как объекты MyClass, как в вызове useMyClass(). Нет кода, который использует расширенный MyClassAdaptedForLib, где пользователи базового класса должны знать о производном классе.
Adapter.kt полагается на то, что MyClass открыт для наследования. Что если вы не контролируете MyClass и он не открыт? К счастью, адаптеры также могут быть построены с использованием композиции. Здесь мы добавляем поле MyClass внутри MyClassAdaptedForLib : // InheritanceExtensions/ComposeAdapter.kt package inheritanceextensions2
import usefullibrary.*
import atomictest.*
class MyClass { // Не открыт
fun g() = trace(“g()”)
fun h() = trace(“h()”)
}
fun useMyClass(mc: MyClass) {
mc.g()
mc.h()
}
class MyClassAdaptedForLib : LibType {
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
Inheritance & Extensions 355
val field = MyClass()
override fun f1() = field.h()
override fun f2() = field.g()
}
fun main() {
val mc = MyClassAdaptedForLib()
utility1(mc)
utility2(mc)
useMyClass(mc.field)
trace eq “h() g() g() h() g() h()”
}
Это не так чисто, как в Adapter.kt — вам нужно явно обращаться к объекту MyClass, как видно в вызове useMyClass(mc.field). Но это все же удобно решает проблему адаптации к библиотеке.
Функции расширения кажутся очень полезными для создания адаптеров. К сожалению, вы не можете реализовать интерфейс, собирая функции расширения.

Члены против расширений Link to heading

Существуют случаи, когда вы вынуждены использовать функции-члены, а не расширения. Если функция должна получить доступ к приватному члену, у вас нет другого выбора, кроме как сделать её функцией-членом:

// InheritanceExtensions/PrivateAccess.kt
package inheritanceextensions
import atomictest.eq

class Z(var i: Int = 0) {
    private var j = 0

    fun increment() {
        i++
        j++
    }
}

fun Z.decrement() {
    i--
    // j-- // Невозможно получить доступ
}

Функция-член increment() может манипулировать j, но функция-расширение decrement() не имеет доступа к j, потому что j является приватным.

Наиболее значительное ограничение функций-расширений заключается в том, что их нельзя переопределять:

// InheritanceExtensions/NoExtOverride.kt
package inheritanceextensions
import atomictest.*

open class Base {
    open fun f() = "Base.f()"
}

class Derived : Base() {
    override fun f() = "Derived.f()"
}

fun Base.g() = "Base.g()"
fun Derived.g() = "Derived.g()"

fun useBase(b: Base) {
    trace("Received ${b::class.simpleName} ")
    trace(b.f())
    trace(b.g())
}

fun main() {
    useBase(Base())
    useBase(Derived())
    trace eq """
        Received Base
        Base.f()
        Base.g()
        Received Derived
        Derived.f()
        Base.g()
    """
}

Вывод трассировки показывает, что полиморфизм работает с функцией-членом f(), но не с функцией-расширением g(). Когда функция не требует переопределения и у вас есть достаточный доступ к членам класса, вы можете определить её как функцию-член или функцию-расширение — это стилистический выбор, который должен максимизировать ясность кода.

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

Рассмотрим интерфейс Device. Свойства model и productionYear являются неотъемлемыми для Device, поскольку они описывают ключевые характеристики. Функции, такие как overpriced() и outdated(), могут быть определены как члены интерфейса или как функции-расширения. Вот они как функции-члены:

// InheritanceExtensions/DeviceMembers.kt
package inheritanceextensions1
import atomictest.eq

interface Device {
    val model: String
    val productionYear: Int
    fun overpriced() = model.startsWith("i")
    fun outdated() = productionYear < 2050
}

class MyDevice(
    override val model: String,
    override val productionYear: Int
) : Device

fun main() {
    val gadget: Device = MyDevice("my first phone", 2000)
    gadget.outdated() eq true
    gadget.overpriced() eq false
}

Если мы предполагаем, что overpriced() и outdated() не будут переопределены в подклассах, их можно определить как расширения:

// InheritanceExtensions/DeviceExtensions.kt
package inheritanceextensions2
import atomictest.eq

interface Device {
    val model: String
    val productionYear: Int
}

fun Device.overpriced() = model.startsWith("i")
fun Device.outdated() = productionYear < 2050

class MyDevice(
    override val model: String,
    override val productionYear: Int
) : Device

fun main() {
    val gadget: Device = MyDevice("my first phone", 2000)
    gadget.outdated() eq true
    gadget.overpriced() eq false
}

Интерфейсы, которые содержат только описательные члены, легче воспринимаются и анализируются, поэтому интерфейс Device во втором примере, вероятно, является лучшим выбором. В конечном итоге это решение по дизайну.

Языки, такие как C++ и Java, позволяют наследование, если вы специально не запрещаете его. Kotlin предполагает, что вы не будете использовать наследование — он активно предотвращает наследование и полиморфизм, если они не разрешены с помощью ключевого слова open. Это дает представление о направлении Kotlin: часто функции — это всё, что вам нужно. Иногда объекты очень полезны. Объекты — это один из инструментов среди многих, но они не для всего.

Если вы размышляете о том, как использовать наследование в конкретной ситуации, подумайте, нужно ли вам наследование вообще, и примените максиму «Предпочитайте функции-расширения и композицию наследованию» (модифицированную из книги «Шаблоны проектирования»).

Упражнения и решения можно найти на www.AtomicKotlin.com.

Делегирование классов Link to heading

Как композиция, так и наследование помещают подобъекты внутри вашего нового класса. В случае композиции подобъект является явным, а в случае наследования — неявным. Композиция использует функциональность встроенного объекта, но не раскрывает его интерфейс. Чтобы класс мог использовать существующую реализацию и реализовать его интерфейс, у вас есть два варианта: наследование и делегирование классов. Делегирование классов находится на полпути между наследованием и композицией. Как и в случае композиции, вы помещаете объект-член в класс, который вы создаете. Как и в случае наследования, делегирование классов раскрывает интерфейс подобъекта. Кроме того, вы можете выполнить восходящее преобразование к типу члена. Для повторного использования кода делегирование классов делает композицию столь же мощной, как и наследование.

Как бы вы достигли этого без поддержки языка? Здесь космическому кораблю нужен модуль управления:

// ClassDelegation/SpaceShipControls.kt
package classdelegation

interface Controls {
    fun up(velocity: Int): String
    fun down(velocity: Int): String
    fun left(velocity: Int): String
    fun right(velocity: Int): String
    fun forward(velocity: Int): String
    fun back(velocity: Int): String
    fun turboBoost(): String
}

class SpaceShipControls : Controls {
    override fun up(velocity: Int) = "up $velocity"
    override fun down(velocity: Int) = "down $velocity"
    override fun left(velocity: Int) = "left $velocity"
    override fun right(velocity: Int) = "right $velocity"
    override fun forward(velocity: Int) = "forward $velocity"
    override fun back(velocity: Int) = "back $velocity"
    override fun turboBoost() = "turbo boost"
}

Если мы хотим расширить функциональность управления или изменить некоторые команды, мы можем попробовать наследоваться от SpaceShipControls. Это не сработает, потому что SpaceShipControls не открыт.

Чтобы раскрыть функции-члены в Controls, вы можете создать экземпляр SpaceShipControls как свойство и явно делегировать все раскрытые функции-члены этому экземпляру:

// ClassDelegation/ExplicitDelegation.kt
package classdelegation

import atomictest.eq

class ExplicitControls : Controls {
    private val controls = SpaceShipControls()
    
    // Делегирование вручную:
    override fun up(velocity: Int) = controls.up(velocity)
    override fun back(velocity: Int) = controls.back(velocity)
    override fun down(velocity: Int) = controls.down(velocity)
    override fun forward(velocity: Int) = controls.forward(velocity)
    override fun left(velocity: Int) = controls.left(velocity)
    override fun right(velocity: Int) = controls.right(velocity)
    
    // Измененная реализация:
    override fun turboBoost(): String = controls.turboBoost() + "... boooooost!"
}

fun main() {
    val controls = ExplicitControls()
    controls.forward(100) eq "forward 100"
    controls.turboBoost() eq "turbo boost... boooooost!"
}

Функции перенаправляются на основной объект управления, и полученный интерфейс такой же, как если бы вы использовали обычное наследование. Вы также можете предоставить изменения реализации, как с turboBoost().

Kotlin автоматизирует процесс делегирования классов, поэтому вместо того, чтобы писать явные реализации функций, как в ExplicitDelegation.kt, вы указываете объект для использования в качестве делегата.

Чтобы делегировать классу, поместите ключевое слово by после имени интерфейса, за которым следует свойство-член, которое будет использоваться в качестве делегата:

// ClassDelegation/BasicDelegation.kt
package classdelegation

interface AI

class A : AI

class B(val a: A) : AI by a

Читать это можно как “класс B реализует интерфейс AI, используя объект-член a”. Вы можете делегировать только интерфейсам, поэтому нельзя сказать A by a. Делегируемый объект (a) должен быть аргументом конструктора.

ExplicitDelegation.kt теперь можно переписать с использованием by:

// ClassDelegation/DelegatedControls.kt
package classdelegation

import atomictest.eq

class DelegatedControls(
    private val controls: SpaceShipControls = SpaceShipControls()
) : Controls by controls {
    override fun turboBoost(): String = "${controls.turboBoost()} ... boooooost!"
}

fun main() {
    val controls = DelegatedControls()
    controls.forward(100) eq "forward 100"
    controls.turboBoost() eq "turbo boost... boooooost!"
}

Когда Kotlin видит ключевое слово by, он генерирует код, аналогичный тому, что мы написали для ExplicitDelegation.kt. После делегирования функции объекта-члена доступны через внешний объект, но без написания всего этого дополнительного кода.

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

// ClassDelegation/ModelingMI.kt
package classdelegation

import atomictest.eq

interface Rectangle {
    fun paint(): String
}

class ButtonImage(
    val width: Int,
    val height: Int
) : Rectangle {
    override fun paint() = "painting ButtonImage($width, $height)"
}

interface MouseManager {
    fun clicked(): Boolean
    fun hovering(): Boolean
}

class UserInput : MouseManager {
    override fun clicked() = true
    override fun hovering() = true
}

// Даже если мы сделаем классы открытыми, мы
// получим ошибку, потому что только один класс может
// появляться в списке суперклассов:
// class Button : ButtonImage(), UserInput()

class Button(
    val width: Int,
    val height: Int,
    var image: Rectangle = ButtonImage(width, height),
    private var input: MouseManager = UserInput()
) : Rectangle by image, MouseManager by input

fun main() {
    val button = Button(10, 5)
    button.paint() eq "painting ButtonImage(10, 5)"
    button.clicked() eq true
    button.hovering() eq true
    // Можно выполнить восходящее преобразование к обоим делегированным типам:
    val rectangle: Rectangle = button
    val mouseManager: MouseManager = button
}

Класс Button реализует два интерфейса: Rectangle и MouseManager. Он не может наследоваться от реализаций как ButtonImage, так и UserInput, но он может делегировать обоим.

Обратите внимание, что определение для image в списке аргументов конструктора является как публичным, так и var. Это позволяет программисту-клиенту динамически заменять ButtonImage.

Последние две строки в main() показывают, что Button может быть преобразован к обоим своим делегированным типам. Это была цель множественного наследования, поэтому делегирование эффективно решает необходимость в множественном наследовании.

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

Используйте делегирование классов с осторожностью. Среди трех вариантов — наследование, композиция и делегирование классов — сначала попробуйте композицию. Это самый простой подход и решает большинство случаев использования. Наследование необходимо, когда вам нужна иерархия типов, чтобы создать отношения между этими типами. Делегирование классов может работать, когда эти варианты не подходят.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Даунакастинг Link to heading

Даунакастинг обнаруживает конкретный тип ранее выполненного апкастинга объекта. Апкастинг всегда безопасен, потому что базовый класс не может иметь интерфейс, больший, чем у производного класса. Каждый член базового класса гарантированно существует и, следовательно, безопасен для вызова. Хотя объектно-ориентированное программирование в основном сосредоточено на апкастинге, существуют ситуации, когда даунакастинг может быть полезным и целесообразным подходом. Даунакастинг происходит во время выполнения и также называется определением типа во время выполнения (RTTI).

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

// DownCasting/NarrowingUpcast.kt
package downcasting

interface Base {
    fun f()
}

class Derived1 : Base {
    override fun f() {}
    fun g() {}
}

class Derived2 : Base {
    override fun f() {}
    fun h() {}
}

fun main() {
    val b1: Base = Derived1() // Апкастинг
    b1.f() // Часть Base
    // b1.g() // Не часть Base
    val b2: Base = Derived2() // Апкастинг
    b2.f() // Часть Base
    // b2.h() // Не часть Base
}

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

Умные приведения Link to heading

Умные приведения в Kotlin — это автоматические понижающие приведения. Ключевое слово is проверяет, является ли объект определённым типом. Любой код в пределах области этого проверки предполагает, что он является этим типом:

// DownCasting/IsKeyword.kt
import downcasting.*

fun main() {
    val b1: Base = Derived1() // Подъем
    if (b1 is Derived1)
        b1.g() // В пределах области проверки "is"
    
    val b2: Base = Derived2() // Подъем
    if (b2 is Derived2)
        b2.h() // В пределах области проверки "is"
}

Если b1 является типом Derived1, вы можете вызвать g(). Если b2 является типом Derived2, вы можете вызвать h().

Умные приведения особенно полезны внутри выражений when, которые используют is для поиска типа аргумента when. В main(), каждый конкретный тип сначала поднимается до Creature, а затем передаётся в what():

// DownCasting/Creature.kt
package downcasting

import atomictest.eq

interface Creature

class Human : Creature {
    fun greeting() = "I'm Human"
}

class Dog : Creature {
    fun bark() = "Yip!"
}

class Alien : Creature {
    fun mobility() = "Three legs"
}

fun what(c: Creature): String =
    when (c) {
        is Human -> c.greeting()
        is Dog -> c.bark()
        is Alien -> c.mobility()
        else -> "Something else"
    }

fun main() {
    val c: Creature = Human()
    what(c) eq "I'm Human"
    what(Dog()) eq "Yip!"
    what(Alien()) eq "Three legs"
    
    class Who : Creature
    what(Who()) eq "Something else"
}

В main(), подъем происходит при присвоении Human к Creature, передаче Dog в what(), передаче Alien в what(), и передаче Who в what().

Иерархии классов традиционно изображаются с базовым классом вверху и производными классами, расходящимися вниз. what() принимает ранее поднятый объект Creature и определяет его точный тип, тем самым понижая этот объект Creature по иерархии наследования, от более общего базового класса к более специфическому производному классу.

Выражение when, которое производит значение, требует ветвь else, чтобы захватить все оставшиеся возможности. В main(), ветвь else проверяется с использованием экземпляра локального класса Who.

Каждая ветвь when использует c, как если бы это был тип, который мы проверяли: вызывая greeting(), если c — это Human, bark(), если это Dog, и mobility(), если это Alien.

Модифицируемая ссылка Link to heading

Автоматические приведения типов подлежат особому ограничению. Если ссылка на объект базового класса модифицируема (var), то существует вероятность, что эта ссылка может быть присвоена другому объекту между моментом, когда тип определяется, и моментом, когда вы вызываете конкретные функции на объекте с приведением типа. То есть, конкретный тип объекта может измениться между определением типа и его использованием.

В следующем примере c является аргументом для when, и Kotlin настаивает на том, чтобы этот аргумент был неизменяемым, чтобы он не мог измениться между выражением is и вызовом, сделанным после ->:

// DownCasting/MutableSmartCast.kt
package downcasting

class SmartCast1(val c: Creature) {
    fun contact() {
        when (c) {
            is Human -> c.greeting()
            is Dog -> c.bark()
            is Alien -> c.mobility()
        }
    }
}

class SmartCast2(var c: Creature) {
    fun contact() {
        when (val c = c) { // [1]
            is Human -> c.greeting() // [2]
            is Dog -> c.bark()
            is Alien -> c.mobility()
        }
    }
}

Аргумент конструктора c является val в SmartCast1 и var в SmartCast2. В обоих случаях c передается в выражение when, которое использует серию умных приведений типов.

В [1] выражение val c = c выглядит странно и используется здесь только для удобства — мы не рекомендуем “затенять” имена идентификаторов в обычном коде. val c создает новый локальный идентификатор c, который захватывает значение свойства c. Однако свойство c является var, в то время как локальный (затененный) c — это val. Попробуйте удалить val c =. Это означает, что c теперь будет свойством, которое является var. Это приведет к сообщению об ошибке для строки [2]:

  • Умное приведение к ‘Human’ невозможно, потому что ‘c’ является изменяемым свойством, которое могло быть изменено к этому времени.

Аналогичные сообщения возникают для is Dog и is Alien. Это не ограничивается выражениями when; есть и другие ситуации, которые могут привести к тому же сообщению об ошибке.

Изменение, описанное в сообщении об ошибке, обычно происходит через конкурентность, когда несколько независимых задач имеют возможность изменять c в непредсказуемые моменты времени. (Конкурентность — это продвинутая тема, которую мы не рассматриваем в этой книге).

Kotlin заставляет нас убедиться, что c не изменится с момента выполнения проверки is и до момента, когда c используется как тип с приведением. SmartCast1 делает это, делая свойство c val, а SmartCast2 — вводя локальный val c.

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

Ключевое слово as Link to heading

Ключевое слово as принудительно преобразует общий тип в конкретный тип: AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Понижающее преобразование 371

// DownCasting/Unsafe.kt
**package downcasting**
**import atomictest.***
**fun** dogBarkUnsafe(c: Creature) =
(c **as** Dog).bark()
**fun** dogBarkUnsafe2(c: Creature): **String** {
    c **as** Dog
    c.bark()
    **return** c.bark() + c.bark()
}
**fun** main() {
    dogBarkUnsafe(Dog()) eq "Yip!"
    dogBarkUnsafe2(Dog()) eq "Yip!Yip!"
    (capture {
        dogBarkUnsafe(Human())
    }) contains listOf("ClassCastException")
}

Функция dogBarkUnsafe2() демонстрирует вторую форму as: если вы пишете c as Dog, то c рассматривается как Dog на протяжении всего оставшегося объема видимости.
Неудачное преобразование as вызывает исключение ClassCastException. Простое as называется небезопасным преобразованием.
Когда безопасное преобразование as? не удается, оно не вызывает исключение, а вместо этого возвращает null. Вы должны сделать что-то разумное с этим null, чтобы предотвратить последующее исключение NullPointerException. Оператор Эльвиса (описанный в разделе Безопасные вызовы и оператор Эльвиса) обычно является самым простым подходом:

// DownCasting/Safe.kt
**package downcasting**
**import atomictest.eq**
**fun** dogBarkSafe(c: Creature) =
(c **as?** Dog)?.bark() ?: "Not a Dog"
**fun** main() {
    dogBarkSafe(Dog()) eq "Yip!"
    dogBarkSafe(Human()) eq "Not a Dog"
}

Если c не является Dog, as? возвращает null. Таким образом, (c as? Dog) является выражением, допускающим null, и мы должны использовать оператор безопасного вызова ?. для вызова bark(). Если as? возвращает null, то всё выражение (c as? Dog)?.bark() также вернет null, что оператор Эльвиса обработает, вернув “Not a Dog”.

Обнаружение типов в списках Link to heading

При использовании в предикате is находит объекты заданного типа в List или любом итерируемом объекте (чем-то, через что можно итерироваться):

// DownCasting/FindType.kt
package downcasting
import atomictest.eq

val group: List<Creature> = listOf(
    Human(), Human(), Dog(), Alien(), Dog()
)

fun main() {
    val dog = group
        .find { it is Dog } as Dog? // [1]
    dog?.bark() eq "Yip!" // [2]
}

Поскольку group содержит объекты типа Creature, find() возвращает Creature. Мы хотим рассматривать его как Dog, поэтому мы явно приводим его к этому типу в конце строки [1]. В group может не быть ни одного объекта типа Dog, в этом случае find() вернет null, поэтому мы должны привести результат к nullable Dog?. Поскольку dog является nullable, мы используем оператор безопасного вызова в строке [2].

Обычно вы можете избежать кода в строке [1], используя filterIsInstance(), который возвращает все элементы определенного типа:

// DownCasting/FilterIsInstance.kt
import downcasting.*
import atomictest.eq

fun main() {
    val humans1: List<Creature> =
        group.filter { it is Human }
    humans1.size eq 2

    val humans2: List<Human> =
        group.filterIsInstance<Human>()
    humans2 eq humans1
}

filterIsInstance() — это более читаемый способ получить тот же результат, что и filter(). Однако типы результатов различаются: в то время как filter() возвращает List объектов Creature (даже если все результирующие элементы являются Human), filterIsInstance() возвращает список целевого типа Human. Мы также устранили проблемы с null, которые были в FindType.kt.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Запечатанные классы Link to heading

Чтобы ограничить иерархию классов, объявите суперкласс как sealed. Рассмотрим поездку путешественников, использующих разные виды транспорта: // SealedClasses/UnSealed.kt package withoutsealedclasses
import atomictest.eq
open class Transport
data class Train (
val line: String
): Transport()
data class Bus (
val number: String ,
val capacity: Int
): Transport()
fun travel(transport: Transport) =
when (transport) {
is Train ->
“Поезд ${ transport.line } "
is Bus ->
“Автобус ${ transport.number } : " +
“вместимость ${ transport.capacity } "
else -> " $ transport находится в неопределенности!”
}
fun main() {
listOf(Train(“S1”), Bus(“11”, 90))
.map(::travel) eq
“[Поезд S1, Автобус 11: вместимость 90]”
}
Запечатанные классы 375
Поезд и автобус содержат разные детали о своем виде транспорта.
Функция travel() содержит выражение when, которое определяет точный тип параметра transport. Kotlin требует наличия ветки else по умолчанию, потому что могут существовать другие подклассы Transport.
Функция travel() показывает inherent проблему, связанную с приведением типов. Предположим, вы наследуете Tram как новый тип Transport. Если вы это сделаете, travel() продолжит компилироваться и выполняться, не давая вам никаких подсказок о том, что вам следует изменить его для обнаружения Tram. Если у вас много экземпляров приведения типов, разбросанных по вашему коду, это становится проблемой для обслуживания.
Мы можем улучшить ситуацию, используя ключевое слово sealed. При определении Transport, замените open class на sealed class :
// SealedClasses/SealedClasses.kt
package sealedclasses
import atomictest.eq
sealed class Transport
data class Train (
val line: String
) : Transport()
data class Bus (
val number: String ,
val capacity: Int
) : Transport()
fun travel(transport: Transport) =
when (transport) {
is Train ->
“Поезд ${ transport.line } "
is Bus ->
“Автобус ${ transport.number } : " +
“вместимость ${ transport.capacity } "
}
fun main() {
listOf(Train(“S1”), Bus(“11”, 90))
.map(::travel) eq
“[Поезд S1, Автобус 11: вместимость 90]”
}
Все прямые подклассы запечатанного класса должны находиться в том же файле, что и базовый класс.
Хотя Kotlin заставляет вас исчерпывающе проверять все возможные типы в выражении when, when в travel() больше не требует ветки else. Поскольку Transport запечатан, Kotlin знает, что нет дополнительных подклассов Transport, кроме тех, что присутствуют в этом файле. Выражение when теперь исчерпывающее без ветки else.
Запечатанные иерархии обнаруживают ошибки при добавлении новых подклассов. Когда вы вводите новый подкласс, вы должны обновить весь код, который использует существующую иерархию. Функция travel() в UnSealed.kt продолжит работать, потому что ветка else выдает “$transport находится в неопределенности!” для неизвестных типов транспорта. Однако, вероятно, это не то поведение, которое вы хотите.
Запечатанный класс показывает все места, которые нужно изменить, когда мы добавляем новый подкласс, такой как Tram. Функция travel() в SealedClasses.kt не скомпилируется, если мы введем класс Tram, не внося дополнительных изменений. Ключевое слово sealed делает невозможным игнорирование проблемы, потому что вы получаете ошибку компиляции.
Ключевое слово sealed делает приведение типов более приемлемым, но вы все равно должны быть осторожны с проектами, которые чрезмерно используют приведение типов. Часто существует лучший и более чистый способ написать этот код, используя полиморфизм.

sealed vs. abstract Link to heading

Здесь мы показываем, что как абстрактные, так и запечатанные классы допускают идентичные типы функций, свойств и конструкторов: AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
SealedClasses 377

// SealedClasses/SealedVsAbstract.kt
package sealedclasses

abstract class Abstract(val av: String) {
    open fun concreteFunction() {}
    open val concreteProperty = ""
    abstract fun abstractFunction(): String
    abstract val abstractProperty: String
    init {}
    constructor(c: Char) : this(c.toString())
}

open class Concrete() : Abstract("") {
    override fun concreteFunction() {}
    override val concreteProperty = ""
    override fun abstractFunction() = ""
    override val abstractProperty = ""
}

sealed class Sealed(val av: String) {
    open fun concreteFunction() {}
    open val concreteProperty = ""
    abstract fun abstractFunction(): String
    abstract val abstractProperty: String
    init {}
    constructor(c: Char) : this(c.toString())
}

open class SealedSubclass() : Sealed("") {
    override fun concreteFunction() {}
    override val concreteProperty = ""
    override fun abstractFunction() = ""
    override val abstractProperty = ""
}

fun main() {
    Concrete()
    SealedSubclass()
}

Запечатанный класс по сути является абстрактным классом с дополнительным ограничением, что все прямые подклассы должны быть определены в одном и том же файле.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
SealedClasses 378
Непрямые подклассы запечатанного класса могут быть определены в отдельном файле:

// SealedClasses/ThirdLevelSealed.kt
package sealedclasses

class ThirdLevel : SealedSubclass()

ThirdLevel не наследует напрямую от Sealed, поэтому ему не нужно находиться в SealedVsAbstract.kt.
Хотя запечатанный интерфейс кажется полезной конструкцией, Kotlin не предоставляет его, потому что классы Java не могут быть предотвращены от реализации одного и того же интерфейса.

Перечисление подклассов Link to heading

Когда класс является запечатанным (sealed), вы можете легко перебирать его подклассы: // SealedClasses/SealedSubclasses.kt package sealedclasses
import atomictest.eq
sealed class Top
class Middle1 : Top()
class Middle2 : Top()
open class Middle3 : Top()
class Bottom3 : Middle3()
fun main() {
Top::class.sealedSubclasses
.map { it.simpleName } eq
“[Middle1, Middle2, Middle3]”
}

Создание класса генерирует объект класса. Вы можете получить доступ к свойствам и членам функций этого объекта класса, чтобы узнать информацию, а также создавать и манипулировать объектами этого класса. ::class производит объект класса, поэтому Top::class производит объект класса для Top.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Одним из свойств объектов класса является sealedSubclasses, который ожидает, что Top является запечатанным классом (в противном случае он возвращает пустой список). sealedSubclasses производит все объекты классов этих подклассов. Обратите внимание, что только непосредственные подклассы Top появляются в результате.

Метод toString() для объекта класса немного многословен. Мы получаем только имя класса, используя свойство simpleName.

sealedSubclasses использует рефлексию, что требует, чтобы зависимость kotlin-reflection.jar была в classpath. Рефлексия — это способ динамически обнаруживать и использовать характеристики класса.

sealedSubclasses может быть важным инструментом при построении полиморфных систем. Он может гарантировать, что новые классы будут автоматически включены во все соответствующие операции. Однако, поскольку он обнаруживает подклассы во время выполнения, это может повлиять на производительность вашей системы. Если у вас возникают проблемы со скоростью, обязательно используйте профайлер, чтобы выяснить, может ли sealedSubclasses быть проблемой (по мере того как вы учитесь использовать профайлер, вы обнаружите, что проблемы с производительностью обычно не там, где вы их ожидаете).

Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Проверка типов Link to heading

В Kotlin вы можете легко действовать на объекте в зависимости от его типа. Обычно эта деятельность относится к области полиморфизма, поэтому проверка типов позволяет делать интересные выборы в дизайне. Традиционно проверка типов используется для особых случаев. Например, большинство насекомых могут летать, но есть небольшое количество, которые не могут. Не имеет смысла нагружать интерфейс Insect теми немногими насекомыми, которые не могут летать, поэтому в функции basic() мы используем проверку типов, чтобы их отобрать:

// TypeChecking/Insects.kt
package typechecking
import atomictest.eq

interface Insect {
    fun walk() = "$name: walk"
    fun fly() = "$name: fly"
}

class HouseFly : Insect
class Flea : Insect {
    override fun fly() = throw Exception("Flea cannot fly")
    fun crawl() = "Flea: crawl"
}

fun Insect.basic() =
    walk() + " " +
    if (this is Flea) crawl() else fly()

interface SwimmingInsect : Insect {
    fun swim() = "$name: swim"
}

interface WaterWalker : Insect {
    fun walkWater() = "$name: walk on water"
}

class WaterBeetle : SwimmingInsect
class WaterStrider : WaterWalker
class WhirligigBeetle : SwimmingInsect, WaterWalker

fun Insect.water() =
    when (this) {
        is SwimmingInsect -> swim()
        is WaterWalker -> walkWater()
        else -> "$name: drown"
    }

fun main() {
    val insects = listOf(
        HouseFly(), Flea(), WaterStrider(),
        WaterBeetle(), WhirligigBeetle()
    )
    insects.map { it.basic() } eq
        "[HouseFly: walk HouseFly: fly, " +
        "Flea: walk Flea: crawl, " +
        "WaterStrider: walk WaterStrider: fly, " +
        "WaterBeetle: walk WaterBeetle: fly, " +
        "WhirligigBeetle: walk " +
        "WhirligigBeetle: fly]"
    insects.map { it.water() } eq
        "[HouseFly: drown, Flea: drown, " +
        "WaterStrider: walk on water, " +
        "WaterBeetle: swim, " +
        "WhirligigBeetle: swim]"
}

Существует также очень небольшое количество насекомых, которые могут ходить по воде или плавать под водой. Снова, не имеет смысла помещать эти особые поведения в базовый класс, чтобы поддерживать такую малую долю типов. Вместо этого функция Insect.water() содержит выражение when, которое выбирает эти подтипы для специального поведения и предполагает стандартное поведение для всего остального.

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

Чтобы упростить код, свойство name возвращает тип объекта, на который указывает this:

// TypeChecking/AnyName.kt
package typechecking

val Any.name
    get() = this::class.simpleName

Свойство name принимает Any и получает связанную ссылку на класс с помощью ::class, затем возвращает simpleName этого класса.

Теперь рассмотрим вариацию примера с “формой”:

// TypeChecking/TypeCheck1.kt
package typechecking
import atomictest.eq

interface Shape {
    fun draw(): String
}

class Circle : Shape {
    override fun draw() = "Circle: Draw"
}

class Square : Shape {
    override fun draw() = "Square: Draw"
    fun rotate() = "Square: Rotate"
}

fun turn(s: Shape) = when (s) {
    is Square -> s.rotate()
    else -> ""
}

fun main() {
    val shapes = listOf(Circle(), Square())
    shapes.map { it.draw() } eq
        "[Circle: Draw, Square: Draw]"
    shapes.map { turn(it) } eq
        "[, Square: Rotate]"
}

Существует несколько причин, по которым вы можете добавить rotate() в Square вместо Shape:

  • Интерфейс Shape вне вашего контроля, поэтому вы не можете его изменить.
  • Поворот Square кажется особым случаем, который не должен нагружать и/или усложнять интерфейс Shape.
  • Вы просто пытаетесь быстро решить проблему, добавив Square, и не хотите утруждать себя добавлением rotate() в Shape и реализацией его во всех подтипах.

Существуют, безусловно, ситуации, когда это решение не негативно сказывается на вашем дизайне, и конструкция when в Kotlin производит чистый и понятный код. Однако, если вам необходимо развивать вашу систему, добавляя больше типов, это начинает становиться запутанным:

// TypeChecking/TypeCheck2.kt
package typechecking
import atomictest.eq

class Triangle : Shape {
    override fun draw() = "Triangle: Draw"
    fun rotate() = "Triangle: Rotate"
}

fun turn2(s: Shape) = when (s) {
    is Square -> s.rotate()
    is Triangle -> s.rotate()
    else -> ""
}

fun main() {
    val shapes = listOf(Circle(), Square(), Triangle())
    shapes.map { it.draw() } eq
        "[Circle: Draw, Square: Draw, Triangle: Draw]"
    shapes.map { turn(it) } eq
        "[, Square: Rotate, ]"
    shapes.map { turn2(it) } eq
        "[, Square: Rotate, Triangle: Rotate]"
}

Полиморфный вызов в shapes.map { it.draw() } адаптируется к новому классу Triangle без каких-либо изменений или ошибок. Также Kotlin не допускает Triangle, если он не реализует draw().

Исходная функция turn() не ломается, когда мы добавляем Triangle, но она также не производит желаемый результат. Функция turn() должна стать turn2(), чтобы сгенерировать желаемое поведение.

Предположим, ваша система начинает накапливать больше функций, подобных turn(). Логика Shape теперь распределена по всем этим функциям, а не централизована в иерархии Shape. Если вы добавите больше новых типов Shape, вам придется искать каждую функцию, содержащую when, которая переключается по типу Shape, и модифицировать ее, чтобы включить новый случай. Если вы пропустите любую из этих функций, компилятор не поймает это.

Функции turn() и turn2() демонстрируют то, что часто называют кодированием проверки типов, что означает тестирование для каждого типа в вашей системе. (Если вы ищете только один или несколько специальных типов, это обычно не считается кодированием проверки типов).

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

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

Мы увидим, что Kotlin значительно смягчает эту проблему с помощью использования запечатанных классов. Решение не идеально, но проверка типов становится гораздо более разумным выбором дизайна.

Проверка типов в вспомогательных функциях Link to heading

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

// TypeChecking/BeverageContainer.kt
package typechecking
import atomictest.eq

interface BeverageContainer {
    fun open(): String
    fun pour(): String
}

class Can : BeverageContainer {
    override fun open() = "Pop Top"
    override fun pour() = "Can: Pour"
}

open class Bottle : BeverageContainer {
    override fun open() = "Remove Cap"
    override fun pour() = "Bottle: Pour"
}

class GlassBottle : Bottle()
class PlasticBottle : Bottle()

fun BeverageContainer.recycle() =
    when (this) {
        is Can -> "Recycle Can"
        is GlassBottle -> "Recycle Glass"
        else -> "Landfill"
    }

fun main() {
    val refrigerator = listOf(
        Can(), GlassBottle(), PlasticBottle()
    )
    refrigerator.map { it.open() } eq
        "[Pop Top, Remove Cap, Remove Cap]"
    refrigerator.map { it.recycle() } eq
        "[Recycle Can, Recycle Glass, " +
        "Landfill]"
}

Определяя recycle() как вспомогательную функцию, мы собираем различные поведения переработки в одном месте, а не распределяем их по иерархии BeverageContainer, делая recycle() членом класса. Действие на типы с помощью when является чистым и простым, но дизайн все еще проблематичен. Когда вы добавляете новый тип, recycle() тихо использует ветвь else. Из-за этого необходимые изменения в функциях проверки типов, таких как recycle(), могут быть упущены. Мы хотели бы, чтобы компилятор сообщал нам, что мы забыли проверить тип, так же как он делает это, когда мы реализуем интерфейс или наследуем абстрактный класс и он сообщает нам, что мы забыли переопределить функцию.

Запечатанные классы предоставляют значительное улучшение в этом отношении. Сделав Shape запечатанным классом, мы заставляем when в turn() (после удаления else) требовать, чтобы каждый тип был проверен. Интерфейсы не могут быть запечатанными, поэтому нам нужно переписать Shape в класс:

// TypeChecking/TypeCheck3.kt
package typechecking3
import atomictest.eq
import typechecking.name

sealed class Shape {
    fun draw() = "$name: Draw"
}

class Circle : Shape()
class Square : Shape() {
    fun rotate() = "Square: Rotate"
}

class Triangle : Shape() {
    fun rotate() = "Triangle: Rotate"
}

fun turn(s: Shape) = when (s) {
    is Circle -> ""
    is Square -> s.rotate()
    is Triangle -> s.rotate()
}

fun main() {
    val shapes = listOf(Circle(), Square())
    shapes.map { it.draw() } eq
        "[Circle: Draw, Square: Draw]"
    shapes.map { turn(it) } eq
        "[, Square: Rotate]"
}

Если мы добавим новый Shape, компилятор скажет нам добавить новый путь проверки типов в turn(). Но давайте посмотрим, что происходит, когда мы пытаемся применить запечатанные классы к проблеме BeverageContainer. В процессе мы создаем дополнительные подтипы Can и Bottle:

// TypeChecking/BeverageContainer2.kt
package typechecking2
import atomictest.eq

sealed class BeverageContainer {
    abstract fun open(): String
    abstract fun pour(): String
}

sealed class Can : BeverageContainer() {
    override fun open() = "Pop Top"
    override fun pour() = "Can: Pour"
}

class SteelCan : Can()
class AluminumCan : Can()

sealed class Bottle : BeverageContainer() {
    override fun open() = "Remove Cap"
    override fun pour() = "Bottle: Pour"
}

class GlassBottle : Bottle()
sealed class PlasticBottle : Bottle()
class PETBottle : PlasticBottle()
class HDPEBottle : PlasticBottle()

fun BeverageContainer.recycle() =
    when (this) {
        is Can -> "Recycle Can"
        is Bottle -> "Recycle Bottle"
    }

fun BeverageContainer.recycle2() =
    when (this) {
        is Can -> when (this) {
            is SteelCan -> "Recycle Steel"
            is AluminumCan -> "Recycle Aluminum"
        }
        is Bottle -> when (this) {
            is GlassBottle -> "Recycle Glass"
            is PlasticBottle -> when (this) {
                is PETBottle -> "Recycle PET"
                is HDPEBottle -> "Recycle HDPE"
            }
        }
    }

fun main() {
    val refrigerator = listOf(
        SteelCan(), AluminumCan(),
        GlassBottle(),
        PETBottle(), HDPEBottle()
    )
    refrigerator.map { it.open() } eq
        "[Pop Top, Pop Top, Remove Cap, " +
        "Remove Cap, Remove Cap]"
    refrigerator.map { it.recycle() } eq
        "[Recycle Can, Recycle Can, " +
        "Recycle Bottle, Recycle Bottle, " +
        "Recycle Bottle]"
    refrigerator.map { it.recycle2() } eq
        "[Recycle Steel, Recycle Aluminum, " +
        "Recycle Glass, " +
        "Recycle PET, Recycle HDPE]"
}

Промежуточные классы Can и Bottle также должны быть запечатанными, чтобы этот подход работал. Пока классы являются прямыми подклассами BeverageContainer, компилятор гарантирует, что when в recycle() является исчерпывающим. Но подклассы, такие как GlassBottle и AluminumCan, не проверяются. Чтобы решить проблему, мы должны явно включить вложенные выражения when, которые видны в recycle2(), в этом случае компилятор требует исчерпывающих проверок типов (попробуйте закомментировать один из конкретных типов Can или Bottle, чтобы проверить это).

Чтобы создать надежное решение для проверки типов, вы должны строго использовать запечатанные классы на каждом промежуточном уровне иерархии классов, при этом обеспечивая, чтобы каждый уровень подклассов имел соответствующий вложенный when. В этом случае, если вы добавите новый подтип Can или Bottle, компилятор гарантирует, что recycle2() проверяет каждый подтип. Хотя это не так чисто, как полиморфизм, это значительное улучшение по сравнению с предыдущими объектно-ориентированными языками и позволяет вам выбирать, писать ли полиморфную член-функцию или вспомогательную функцию. Обратите внимание, что эта проблема возникает только тогда, когда у вас есть несколько уровней наследования.

Для сравнения, давайте перепишем BeverageContainer2.kt, переместив recycle() в BeverageContainer, который снова может быть интерфейсом:

// TypeChecking/BeverageContainer3.kt
package typechecking3
import atomictest.eq
import typechecking.name

interface BeverageContainer {
    fun open(): String
    fun pour() = "$name: Pour"
    fun recycle(): String
}

abstract class Can : BeverageContainer {
    override fun open() = "Pop Top"
}

class SteelCan : Can() {
    override fun recycle() = "Recycle Steel"
}

class AluminumCan : Can() {
    override fun recycle() = "Recycle Aluminum"
}

abstract class Bottle : BeverageContainer {
    override fun open() = "Remove Cap"
}

class GlassBottle : Bottle() {
    override fun recycle() = "Recycle Glass"
}

abstract class PlasticBottle : Bottle()
class PETBottle : PlasticBottle() {
    override fun recycle() = "Recycle PET"
}

class HDPEBottle : PlasticBottle() {
    override fun recycle() = "Recycle HDPE"
}

fun main() {
    val refrigerator = listOf(
        SteelCan(), AluminumCan(),
        GlassBottle(),
        PETBottle(), HDPEBottle()
    )
    refrigerator.map { it.open() } eq
        "[Pop Top, Pop Top, Remove Cap, " +
        "Remove Cap, Remove Cap]"
    refrigerator.map { it.recycle() } eq
        "[Recycle Steel, Recycle Aluminum, " +
        "Recycle Glass, " +
        "Recycle PET, Recycle HDPE]"
}

Сделав Can и Bottle абстрактными классами, мы заставляем их подклассы переопределять recycle() так же, как компилятор заставляет проверять каждый тип внутри recycle2() в BeverageContainer2.kt. Теперь поведение recycle() распределено между классами, что может быть приемлемо — это дизайнерское решение. Если вы решите, что поведение переработки часто меняется и вы хотите, чтобы оно было сосредоточено в одном месте, то использование вспомогательной функции с проверкой типов recycle2() из BeverageContainer2.kt может быть лучшим выбором для ваших нужд, и возможности Kotlin делают это разумным.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Вложенные классы Link to heading

Вложенные классы позволяют создавать более изящные структуры внутри ваших объектов. Вложенный класс — это просто класс в пространстве имен внешнего класса. Это подразумевает, что внешний класс «владеет» вложенным классом. Эта функция не является обязательной, но вложение класса может прояснить ваш код. Здесь класс Plane вложен в класс Airport:

// NestedClasses/Airport.kt
package nestedclasses
import atomictest.eq
import nestedclasses.Airport.Plane

class Airport(private val code: String) {
    open class Plane {
        // Может получать доступ к приватным свойствам:
        fun contact(airport: Airport) = "Contacting ${airport.code} "
    }

    private class PrivatePlane : Plane()
    
    fun privatePlane(): Plane = PrivatePlane()
}

fun main() {
    val denver = Airport("DEN")
    var plane = Plane() // [1]
    plane.contact(denver) eq "Contacting DEN"
    // Нельзя сделать это:
    // val privatePlane = Airport.PrivatePlane()
    
    val frankfurt = Airport("FRA")
    plane = frankfurt.privatePlane()
    // Нельзя сделать это:
    // val p = plane as PrivatePlane // [2]
    
    plane.contact(frankfurt) eq "Contacting FRA"
}

В методе contact() вложенный класс Plane имеет доступ к приватному свойству code в аргументе airport, тогда как обычный класс не имел бы такого доступа. Кроме того, Plane просто является классом внутри пространства имен Airport. Создание объекта Plane не требует объекта Airport, но если вы создаете его вне тела класса Airport, вам обычно нужно квалифицировать вызов конструктора в [1]. Импортируя nestedclasses.Airport.Plane, мы избегаем этой квалификации.

Вложенный класс может быть приватным, как в случае с PrivatePlane. Сделав его приватным, вы делаете так, что PrivatePlane полностью невидим вне тела Airport, поэтому вы не можете вызвать конструктор PrivatePlane вне Airport. Если вы определяете и возвращаете PrivatePlane из функции-члена, как это видно в privatePlane(), результат должен быть приведен к публичному типу (при условии, что он расширяет публичный тип) и не может быть приведен к приватному типу, как видно в [2].

Вот пример вложенности, где Cleanable является базовым классом как для внешнего класса House, так и для всех вложенных классов. Метод clean() проходит по списку частей и вызывает clean() для каждой из них, создавая своего рода рекурсию:

// NestedClasses/NestedHouse.kt
package nestedclasses
import atomictest.*

abstract class Cleanable(val id: String) {
    open val parts: List<Cleanable> = listOf()
    
    fun clean(): String {
        val text = " $id clean"
        if (parts.isEmpty()) return text
        return " ${parts.joinToString(" ", "(", ")", transform = Cleanable::clean)} $text\n"
    }
}

class House : Cleanable("House") {
    override val parts = listOf(
        Bedroom("Master Bedroom"),
        Bedroom("Guest Bedroom")
    )
    
    class Bedroom(id: String) : Cleanable(id) {
        override val parts = listOf(Closet(), Bathroom())
        
        class Closet : Cleanable("Closet") {
            override val parts = listOf(Shelf(), Shelf())
            
            class Shelf : Cleanable("Shelf")
        }
        
        class Bathroom : Cleanable("Bathroom") {
            override val parts = listOf(Toilet(), Sink())
            
            class Toilet : Cleanable("Toilet")
            class Sink : Cleanable("Sink")
        }
    }
}

fun main() {
    House().clean() eq """
    (((Shelf clean Shelf clean) Closet clean
    (Toilet clean Sink clean) Bathroom clean
    ) Master Bedroom clean
    ((Shelf clean Shelf clean) Closet clean
    (Toilet clean Sink clean) Bathroom clean
    ) Guest Bedroom clean
    ) House clean
    """
}

Обратите внимание на множество уровней вложенности. Например, класс Bedroom содержит Bathroom, который содержит Toilet и Sink.

Локальные классы Link to heading

Классы, которые вложены внутри функций, называются локальными классами: AtomicKotlin (www.AtomicKotlin.com) авторов Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
NestedClasses 395

// NestedClasses/LocalClasses.kt
package nestedclasses

fun localClasses() {
    open class Amphibian
    class Frog : Amphibian()
    val amphibian: Amphibian = Frog()
}

Amphibian выглядит как кандидат на интерфейс, а не на открытый класс. Однако локальные интерфейсы не допускаются.
Локальные открытые классы должны быть редкими; если вам нужен один, то, вероятно, то, что вы пытаетесь создать, достаточно значимо, чтобы создать обычный класс.
Amphibian и Frog невидимы за пределами localClasses(), поэтому вы не можете вернуть их из функции. Чтобы вернуть объекты локальных классов, вы должны привести их к классу или интерфейсу, определенному вне функции:

// NestedClasses/ReturnLocal.kt
package nestedclasses

interface Amphibian

fun createAmphibian(): Amphibian {
    class Frog : Amphibian
    return Frog()
}

fun main() {
    val amphibian = createAmphibian()
    // amphibian as Frog
}

Frog по-прежнему невидим за пределами createAmphibian() — в main() вы не можете привести amphibian к Frog, потому что Frog недоступен, поэтому Kotlin сообщает о попытке использовать Frog как “неразрешенную ссылку”.

Классы внутри интерфейсов Link to heading

Классы могут быть вложенными в интерфейсы:
AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Вложенные классы 396

// NestedClasses/WithinInterface.kt
package nestedclasses
import atomictest.eq

interface Item {
    val type: Type
    data class Type(val type: String)
}

class Bolt(type: String) : Item {
    override val type = Item.Type(type)
}

fun main() {
    val items = listOf(
        Bolt("Slotted"), Bolt("Hex")
    )
    items.map(Item::type) eq
    "[Type(type=Slotted), Type(type=Hex)]"
}

В классе Bolt свойство val type должно быть переопределено и присвоено с использованием квалифицированного имени класса Item.Type.

Вложенные перечисления Link to heading

Перечисления — это классы, поэтому их можно вкладывать в другие классы: // NestedClasses/Ticket.kt package nestedclasses
import atomictest.eq
import nestedclasses.Ticket.Seat.*
class Ticket (
val name: String,
val seat: Seat = Coach
) {
enum class Seat {
Coach,
Premium,
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
NestedClasses 397
Business,
First
}
fun upgrade(): Ticket {
val newSeat = values()[
(seat.ordinal + 1)
.coerceAtMost(First.ordinal)
]
return Ticket(name, newSeat)
}
fun meal() = when (seat) {
Coach -> “Багажный обед”
Premium -> “Багажный обед с печеньем”
Business -> “Горячий обед”
First -> “Личный шеф-повар”
}
override fun toString() = " $ seat”
}
fun main() {
val tickets = listOf(
Ticket(“Джерри”),
Ticket(“Лето”, Premium),
Ticket(“Скванчи”, Business),
Ticket(“Бет”, First)
)
tickets.map(Ticket::meal) eq
“[Багажный обед, Багажный обед с печеньем, " +
“Горячий обед, Личный шеф-повар]”
tickets.map(Ticket::upgrade) eq
“[Premium, Business, First, First]”
tickets eq
“[Coach, Premium, Business, First]”
tickets.map(Ticket::meal) eq
“[Багажный обед, Багажный обед с печеньем, " +
“Горячий обед, Личный шеф-повар]”
}
upgrade() добавляет единицу к порядковому значению места, затем использует библиотечную
функцию coerceAtMost(), чтобы гарантировать, что новое значение не превышает First.ordinal
перед индексированием в values(), чтобы получить новый тип Seat. Следуя принципам
функционального программирования, обновление билета создает новый билет, а не
модифицирует старый.
meal() использует when для проверки каждого типа Seat, и это предполагает, что мы могли бы использовать
полиморфизм вместо этого.
Перечисления не могут быть вложены в функции и не могут наследоваться от других
классов (включая другие перечисления).
Интерфейсы могут содержать вложенные перечисления. FillIt — это игра-подобная симуляция, которая
заполняет квадратную сетку случайно выбранными знаками X и O: // NestedClasses/FillIt.kt package nestedclasses
import nestedclasses.Game.State.*
import nestedclasses.Game.Mark.*
import kotlin.random.Random
import atomictest.*
interface Game {
enum class State { Playing, Finished }
enum class Mark { Blank, X, O }
}
class FillIt (
val side: Int = 3, randomSeed: Int = 0
): Game {
val rand = Random(randomSeed)
private var state = Playing
private val grid =
MutableList(side * side) { Blank }
private var player = X
fun turn() {
val blanks = grid.withIndex()
.filter { it.value == Blank }
if (blanks.isEmpty()) {
state = Finished
} else {
grid[blanks.random(rand).index] = player
player = if (player == X) O else X
}
}
fun play() {
while (state != Finished)
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
NestedClasses 399
turn()
}
override fun toString() =
grid.chunked(side).joinToString("\n”)
}
fun main() {
val game = FillIt(8, 17)
game.play()
game eq "””
[O, X, O, X, O, X, X, X]
[X, O, O, O, O, O, X, X]
[O, O, X, O, O, O, X, X]
[X, O, O, O, O, O, X, O]
[X, X, O, O, X, X, X, O]
[X, X, O, O, X, X, O, X]
[O, X, X, O, O, O, X, O]
[X, O, X, X, X, O, X, X]
"””
}
Для тестируемости мы инициализируем объект Random с randomSeed, чтобы получить одинаковый
вывод каждый раз, когда программа запускается. Каждый элемент grid инициализируется значением Blank.
В turn() мы сначала находим все ячейки, содержащие Blank, вместе с их индексами. Если
больше нет пустых ячеек, то симуляция завершена. В противном случае мы используем
random() с нашим инициализированным генератором, чтобы выбрать одну из пустых ячеек. Поскольку мы
использовали withIndex() ранее, мы должны выбрать свойство index, чтобы получить местоположение
ячейки, которую мы хотим изменить.
Чтобы отобразить список в виде двумерной сетки, toString() использует
библиотечную функцию chunked(), чтобы разбить список на части, каждая длиной side, а затем
объединяет их с помощью переводов строки.
Попробуйте поэкспериментировать с FillIt, используя разные значения side и randomSeed.
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC

Объекты Link to heading

Ключевое слово object определяет нечто, что выглядит примерно как класс. Однако вы не можете создать экземпляры объекта — их только один. Это иногда называется паттерном Singleton. Объект — это способ объединить функции и свойства, которые логически принадлежат друг другу, но это объединение либо не требует нескольких экземпляров, либо вы хотите явно предотвратить создание нескольких экземпляров. Вы никогда не создаете экземпляр объекта — их только один, и он доступен после определения объекта: // Objects/ObjectKeyword.kt package objects
import atomictest.eq
object JustOne {
val n = 2
fun f() = n * 10
fun g() = this.n * 20 // [1]
}
fun main() {
// val x = JustOne() // Ошибка
JustOne.n eq 2
JustOne.f() eq 20
JustOne.g() eq 40
}
Здесь вы не можете сказать JustOne(), чтобы создать новый экземпляр класса JustOne. Это происходит потому, что ключевое слово object определяет структуру и создает объект одновременно. Кроме того, оно помещает элементы в пространство имен объекта. Если вы хотите, чтобы объект был виден только в текущем файле, вы можете сделать его private.
• [1] Ключевое слово this ссылается на единственный экземпляр объекта.
Вы не можете предоставить список параметров для объекта.
Конвенции именования немного отличаются при использовании object. Обычно, когда мы создаем экземпляр класса, мы пишем с маленькой буквы первую букву имени экземпляра. Однако, когда вы создаете объект, Kotlin определяет класс и создает единственный экземпляр этого класса. Мы пишем с заглавной буквы первую букву имени объекта, потому что он также представляет собой класс.
Объект может наследоваться от обычного класса или интерфейса: // Objects/ObjectInheritance.kt package objects
import atomictest.eq
open class Paint (val color: String) {
open fun apply() = “Наносим $ color”
}
object Acrylic : Paint(“Синий”) {
override fun apply() =
“Акрил, ${super.apply() } "
}
interface PaintPreparation {
fun prepare(): String
}
object Prepare : PaintPreparation {
override fun prepare() = “Соскоблить”
}
fun main() {
Prepare.prepare() eq “Соскоблить”
Paint(“Зеленый”).apply() eq “Наносим Зеленый”
Acrylic.apply() eq “Акрил, Наносим Синий”
}
Существует только один экземпляр объекта, поэтому этот экземпляр разделяется между всем кодом, который его использует. Вот объект в своем собственном пакете:
AtomicKotlin(www.AtomicKotlin.com)byBruceEckel&SvetlanaIsakova,©2021MindViewLLC
Объекты 402
// Objects/GlobalSharing.kt
package objectsharing
object Shared {
var i: Int = 0
}
Теперь мы можем использовать Shared в другом пакете:
// Objects/Share1.kt
package objectshare1
import objectsharing.Shared
fun f() {
Shared.i += 5
}
А в третьем пакете:
// Objects/Share2.kt
package objectshare2
import objectsharing.Shared
import objectshare1.f
import atomictest.eq
fun g() {
Shared.i += 7
}
fun main() {
f()
g()
Shared.i eq 12
}
Вы можете видеть по результатам, что Shared — это один и тот же объект во всех пакетах, что имеет смысл, потому что object создает единственный экземпляр. Если вы сделаете Shared private, он не будет доступен в других файлах.
Объекты не могут быть размещены внутри функций, но они могут быть вложены внутри других объектов или классов (при условии, что эти классы не вложены внутри других классов):
AtomicKotlin(www.AtomicKotlin.com)byBruceEckel&SvetlanaIsakova,©2021MindViewLLC
Объекты 403
// Objects/ObjectNesting.kt
package objects
import atomictest.eq
object Outer {
object Nested {
val a = “Outer.Nested.a”
}
}
class HasObject {
object Nested {
val a = “HasObject.Nested.a”
}
}
fun main() {
Outer.Nested.a eq “Outer.Nested.a”
HasObject.Nested.a eq “HasObject.Nested.a”
}
Существует еще один способ поместить объект внутри класса: это companion object, который вы увидите в разделе Companion Objects.
Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin(www.AtomicKotlin.com)byBruceEckel&SvetlanaIsakova,©2021MindViewLLC

Внутренние классы Link to heading

Внутренние классы похожи на вложенные классы, но объект внутреннего класса сохраняет ссылку на внешний класс. Внутренний класс имеет неявную связь с внешним классом. В следующем примере класс Hotel похож на класс Airport из Вложенных классов, но использует внутренние классы. Обратите внимание, что reception является частью Hotel, но метод callReception(), который является членом вложенного класса Room, получает доступ к reception без квалификации:

// InnerClasses/Hotel.kt
package innerclasses
import atomictest.eq

class Hotel(private val reception: String) {
    open inner class Room(val id: Int = 0) {
        // Использует 'reception' из внешнего класса:
        fun callReception() = "Room $id Calling $reception"
    }

    private inner class Closet : Room()
    fun closet(): Room = Closet()
}

fun main() {
    val nycHotel = Hotel("311")
    // Вам нужен внешний объект для
    // создания экземпляра внутреннего класса:
    val room = nycHotel.Room(319)
    room.callReception() eq "Room 319 Calling 311"

    val sfHotel = Hotel("0")
    val closet = sfHotel.closet()
    closet.callReception() eq "Room 0 Calling 0"
}

Поскольку Closet наследует внутренний класс Room, Closet также должен быть внутренним классом. Вложенные классы не могут наследоваться от внутренних классов. Closet является приватным, поэтому он виден только в пределах класса Hotel. Внутренний объект сохраняет ссылку на связанный внешний объект. Таким образом, при создании внутреннего объекта вы сначала должны иметь внешний объект. Вы не можете создать объект Room без объекта Hotel, как вы видите с nycHotel.Room(). Внутренние классы данных не допускаются.

Квалифицированный this Link to heading

Одним из преимуществ классов является ссылка this. Вам не нужно явно указывать “текущий объект”, когда вы обращаетесь к свойству или члену функции. В простом классе значение this очевидно, но в случае вложенного класса this может ссылаться как на внутренний объект, так и на внешний объект. Чтобы разрешить эту проблему, Kotlin предоставляет синтаксис квалифицированного this: this, за которым следует @ и имя целевого класса.

Рассмотрим три уровня классов: внешний класс Fruit, содержащий вложенный класс Seed, который, в свою очередь, содержит вложенный класс DNA:

// InnerClasses/QualifiedThis.kt
package innerclasses
import atomictest.eq
import typechecking.name

class Fruit { // Неявная метка @Fruit
    fun changeColor(color: String) =
        "Fruit $color"

    fun absorbWater(amount: Int) {}

    inner class Seed { // Неявная метка @Seed
        fun changeColor(color: String) =
            "Seed $color"

        fun germinate() {}

        fun whichThis() {
            // По умолчанию ссылается на текущий класс:
            this.name eq "Seed"
            // Для ясности вы можете избыточно
            // квалифицировать стандартный this:
            this@Seed.name eq "Seed"
            // Необходимо явно обращаться к Fruit:
            this@Fruit.name eq "Fruit"
            // Нельзя получить доступ к более внутреннему классу:
            // this@DNA.name
        }
    }

    inner class DNA { // Неявная метка @DNA
        fun changeColor(color: String) {
            // changeColor(color) // Рекурсивный вызов
            this@Seed.changeColor(color)
            this@Fruit.changeColor(color)
        }

        fun plant() {
            // Вызов функций внешнего класса
            // Без квалификации:
            germinate()
            absorbWater(10)
        }

        // Функция расширения:
        fun Int.grow() { // Неявная метка @grow
            // По умолчанию это получатель Int.grow():
            this.name eq "Int"
            // Избыточная квалификация:
            this@grow.name eq "Int"
            // Вы все еще можете получить доступ ко всему:
            this@DNA.name eq "DNA"
            this@Seed.name eq "Seed"
            this@Fruit.name eq "Fruit"
        }

        // Функции расширения для внешних классов:
        fun Seed.plant() {}
        fun Fruit.plant() {}

        fun whichThis() {
            // По умолчанию ссылается на текущий класс:
            this.name eq "DNA"
            // Избыточная квалификация:
            this@DNA.name eq "DNA"
            // Остальные должны быть явными:
            this@Seed.name eq "Seed"
            this@Fruit.name eq "Fruit"
        }
    }
}

// Функция расширения:
fun Fruit.grow(amount: Int) {
    absorbWater(amount)
    // Вызывает версию changeColor() класса Fruit:
    changeColor("Red") eq "Fruit Red"
}

// Функция расширения для вложенного класса:
fun Fruit.Seed.grow(n: Int) {
    germinate()
    // Вызывает версию changeColor() класса Seed:
    changeColor("Green") eq "Seed Green"
}

// Функция расширения для вложенного класса:
fun Fruit.Seed.DNA.grow(n: Int) = n.grow()

fun main() {
    val fruit = Fruit()
    fruit.grow(4)
    val seed = fruit.Seed()
    seed.grow(9)
    seed.whichThis()
    val dna = seed.DNA()
    dna.plant()
    dna.grow(5)
    dna.whichThis()
    dna.changeColor("Purple")
}

Fruit, Seed и DNA все имеют функции с именем changeColor(), но здесь нет переопределения — это не является отношением наследования. Поскольку у них одинаковые имена и сигнатуры, единственный способ их различить — это квалифицированный this, как вы видите в changeColor() класса DNA. Внутри plant() функции ни одного из двух внешних классов могут быть вызваны без квалификации, если нет конфликтов имен.

Несмотря на то, что это функция расширения, grow() все еще может получить доступ ко всем объектам во внешнем классе. grow() может быть вызвана в любом месте, где доступен неявный получатель Fruit.Seed.DNA; например, внутри функции расширения для DNA.

Наследование Внутренних Классов Link to heading

Внутренний класс может наследовать другой внутренний класс из другого внешнего класса. Здесь Yolk в BigEgg происходит от Yolk в Egg:

// InnerClasses/InnerClassInheritance.kt
package innerclasses
import atomictest.*

open class Egg {
    private var yolk = Yolk()
    open inner class Yolk {
        init { trace("Egg.Yolk()") }
        open fun f() { trace("Egg.Yolk.f()") }
    }
    init { trace("New Egg()") }
    fun insertYolk(y: Yolk) { yolk = y }
    fun g() { yolk.f() }
}

class BigEgg : Egg() {
    inner class Yolk : Egg.Yolk() {
        init { trace("BigEgg.Yolk()") }
        override fun f() {
            trace("BigEgg.Yolk.f()")
        }
    }
    init { insertYolk(Yolk()) }
}

fun main() {
    BigEgg().g()
    trace eq """
    Egg.Yolk()
    New Egg()
    Egg.Yolk()
    BigEgg.Yolk()
    BigEgg.Yolk.f()
    """
}

BigEgg.Yolk явно указывает на Egg.Yolk как на свой базовый класс и переопределяет его функцию-член f(). Функция insertYolk() позволяет BigEgg выполнить восходящее преобразование одного из своих объектов Yolk в ссылку yolk в Egg, так что когда g() вызывает yolk.f(), используется переопределенная версия f(). Второй вызов Egg.Yolk() — это вызов конструктора базового класса конструктора BigEgg.Yolk. Вы можете увидеть, что переопределенная версия f() используется, когда вызывается g().

В качестве обзора конструкции объектов изучите вывод trace, пока он не станет понятным.

Локальные и анонимные внутренние классы Link to heading

Классы, определенные внутри член-функций, называются локальными внутренними классами. Их также можно создавать анонимно, используя объектное выражение или преобразование SAM. В любом случае ключевое слово inner не используется, но подразумевается:

// InnerClasses/LocalInnerClasses.kt
package innerclasses
import atomictest.eq

fun interface Pet {
    fun speak(): String
}

object CreatePet {
    fun home() = " home!"

    fun dog(): Pet {
        val say = "Bark"
        // Локальный внутренний класс:
        class Dog : Pet {
            override fun speak() = say + home()
        }
        return Dog()
    }

    fun cat(): Pet {
        val emit = "Meow"
        // Анонимный внутренний класс:
        return object : Pet {
            override fun speak() = emit + home()
        }
    }

    fun hamster(): Pet {
        val squeak = "Squeak"
        // Преобразование SAM:
        return Pet { squeak + home() }
    }
}

fun main() {
    CreatePet.dog().speak() eq "Bark home!"
    CreatePet.cat().speak() eq "Meow home!"
    CreatePet.hamster().speak() eq "Squeak home!"
}

Локальный внутренний класс имеет доступ к другим элементам функции, а также к элементам объекта внешнего класса, таким образом, say, emit, squeak и home() доступны внутри speak(). Вы можете идентифицировать анонимный внутренний класс, потому что он использует объектное выражение, что вы видите в cat(). Он возвращает объект класса, унаследованного от Pet, который переопределяет speak(). Анонимные внутренние классы меньше и проще и не создают именованный класс, который будет использоваться только в одном месте. Еще более компактным является преобразование SAM, как видно в hamster().

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

// InnerClasses/CounterFactory.kt
package innerclasses
import atomictest.*

fun interface Counter {
    fun next(): Int
}

object CounterFactory {
    private var count = 0

    fun new(name: String): Counter {
        // Локальный внутренний класс:
        class Local : Counter {
            init { trace("Local()") }
            override fun next(): Int {
                // Доступ к локальным идентификаторам:
                trace("$name $count")
                return count++
            }
        }
        return Local()
    }

    fun new2(name: String): Counter {
        // Экземпляр анонимного внутреннего класса:
        return object : Counter {
            init { trace("Counter()") }
            override fun next(): Int {
                trace("$name $count")
                return count++
            }
        }
    }

    fun new3(name: String): Counter {
        trace("Counter()")
        return Counter { // Преобразование SAM
            trace("$name $count")
            count++
        }
    }
}

fun main() {
    fun test(counter: Counter) {
        (0..3).forEach { counter.next() }
    }

    test(CounterFactory.new("Local"))
    test(CounterFactory.new2("Anon"))
    test(CounterFactory.new3("SAM"))
    trace eq """
    Local() Local 0 Local 1 Local 2 Local 3
    Counter() Anon 4 Anon 5 Anon 6 Anon 7
    Counter() SAM 8 SAM 9 SAM 10 SAM 11
    """
}

Счетчик отслеживает количество и возвращает следующее значение типа Int. Методы new(), new2() и new3() создают разные реализации интерфейса Counter. Метод new() возвращает экземпляр именованного внутреннего класса, new2() возвращает экземпляр анонимного внутреннего класса, а new3() использует преобразование SAM для создания анонимного объекта. Все полученные объекты Counter имеют неявный доступ к элементам внешнего объекта, таким образом, они являются внутренними классами, а не просто вложенными классами. Вы можете видеть из вывода, что count в CounterFactory разделяется всеми объектами Counter.

Преобразования SAM ограничены — например, они не поддерживают блоки init.

В Kotlin файлы могут содержать несколько классов и функций верхнего уровня. Из-за этого редко возникает необходимость в локальных классах, поэтому, если они вам нужны, они должны быть простыми и понятными. Например, разумно создать простой класс данных, который используется только внутри функции. Если локальный класс становится сложным, вам, вероятно, следует вынести его из функции и сделать его обычным классом.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Сопровождающие объекты Link to heading

Членские функции действуют на конкретные экземпляры класса. Некоторые функции не «относятся» к объекту, поэтому их не нужно связывать с этим объектом. Функции и поля внутри сопровождающего объекта относятся к классу. Обычные элементы класса могут получать доступ к элементам сопровождающего объекта, но элементы сопровождающего объекта не могут получать доступ к элементам обычного класса. Как вы видели в разделе «Объекты», возможно определить обычный объект внутри класса, но это не создает ассоциацию между объектом и классом. В частности, вам придется явно называть вложенный объект, когда вы ссылаетесь на его члены. Если вы определяете сопровождающий объект внутри класса, его элементы становятся прозрачно доступными для этого класса:

// CompanionObjects/CompanionObject.kt
package companionobjects
import atomictest.eq

class WithCompanion {
    companion object {
        val i = 3
        fun f() = i * 3
    }
    
    fun g() = i + f()
}

fun WithCompanion.Companion.h() = f() * i

fun main() {
    val wc = WithCompanion()
    wc.g() eq 12
    WithCompanion.i eq 3
    WithCompanion.f() eq 9
    WithCompanion.h() eq 27
}

Снаружи класса вы получаете доступ к членам сопровождающего объекта, используя имя класса, как в WithCompanion.i и WithCompanion.f(). Другие члены класса могут получать доступ к элементам сопровождающего объекта без квалификации, как вы видите в определении g(). h() — это функция-расширение для сопровождающего объекта. Если функция не требует доступа к приватным членам класса, вы можете выбрать определение ее на уровне файла, а не помещать в сопровождающий объект. В классе допускается только один сопровождающий объект. Для ясности вы можете дать сопровождающему объекту имя:

// CompanionObjects/NamingCompanionObjects.kt
package companionobjects
import atomictest.eq

class WithNamed {
    companion object Named {
        fun s() = "from Named"
    }
}

class WithDefault {
    companion object {
        fun s() = "from Default"
    }
}

fun main() {
    WithNamed.s() eq "from Named"
    WithNamed.Named.s() eq "from Named"
    WithDefault.s() eq "from Default"
    // Имя по умолчанию — "Companion":
    WithDefault.Companion.s() eq "from Default"
}

Даже когда вы даете сопровождающему объекту имя, вы все равно можете получить доступ к его элементам без использования имени. Если вы не дадите сопровождающему объекту имя, Kotlin присвоит ему имя Companion. Если вы создаете свойство внутри сопровождающего объекта, оно создает единственное хранилище для этого поля, общее для всех экземпляров связанного класса:

// CompanionObjects/ObjectProperty.kt
package companionobjects
import atomictest.eq

class WithObjectProperty {
    companion object {
        private var n: Int = 0 // Только одно
    }
    
    fun increment() = ++n
}

fun main() {
    val a = WithObjectProperty()
    val b = WithObjectProperty()
    a.increment() eq 1
    b.increment() eq 2
    a.increment() eq 3
}

Тесты в main() показывают, что n имеет только одно хранилище, независимо от того, сколько экземпляров WithObjectProperty создано. a и b оба получают доступ к одной и той же памяти для n. increment() показывает, что вы можете получить доступ к приватным членам сопровождающего объекта из окружающего класса. Когда функция только получает доступ к свойствам в сопровождающем объекте, имеет смысл переместить эту функцию внутрь сопровождающего объекта:

// CompanionObjects/ObjectFunctions.kt
package companionobjects
import atomictest.eq

class CompanionObjectFunction {
    companion object {
        private var n: Int = 0
        fun increment() = ++n
    }
}

fun main() {
    CompanionObjectFunction.increment() eq 1
    CompanionObjectFunction.increment() eq 2
}

Теперь вам больше не нужно создавать экземпляр CompanionObjectFunction, чтобы вызвать increment(). Предположим, вы хотите вести учет каждого создаваемого вами объекта, чтобы дать каждому уникальный читаемый идентификатор:

// CompanionObjects/ObjectCounter.kt
package companionobjects
import atomictest.eq

class Counted {
    companion object {
        private var count = 0
    }
    
    private val id = count++
    
    override fun toString() = "# $id"
}

fun main() {
    List(4) { Counted() } eq "[#0, #1, #2, #3]"
}

Сопровождающий объект может быть экземпляром класса, определенного в другом месте:

// CompanionObjects/CompanionInstance.kt
package companionobjects
import atomictest.*

interface ZI {
    fun f(): String
    fun g(): String
}

open class ZIOpen : ZI {
    override fun f() = "ZIOpen.f()"
    override fun g() = "ZIOpen.g()"
}

class ZICompanion {
    companion object: ZIOpen()
    fun u() = trace(" ${f()} ${g()} ")
}

class ZICompanionInheritance {
    companion object: ZIOpen() {
        override fun g() = "ZICompanionInheritance.g()"
        fun h() = "ZICompanionInheritance.h()"
    }
    
    fun u() = trace(" ${f()} ${g()} ${h()} ")
}

class ZIClass {
    companion object: ZI {
        override fun f() = "ZIClass.f()"
        override fun g() = "ZIClass.g()"
    }
    
    fun u() = trace(" ${f()} ${g()} ")
}

fun main() {
    ZIClass.f()
    ZIClass.g()
    ZIClass().u()
    ZICompanion.f()
    ZICompanion.g()
    ZICompanion().u()
    ZICompanionInheritance.f()
    ZICompanionInheritance.g()
    ZICompanionInheritance().u()
    trace eq """
    ZIClass.f() ZIClass.g()
    ZIOpen.f() ZIOpen.g()
    ZIOpen.f()
    ZICompanionInheritance.g()
    ZICompanionInheritance.h()
    """
}

ZICompanion использует объект ZIOpen в качестве своего сопровождающего объекта, а ZICompanionInheritance создает объект ZIOpen, переопределяя и расширяя ZIOpen. ZIClass показывает, что вы можете реализовать интерфейс, создавая сопровождающий объект. Если класс, который вы хотите использовать в качестве сопровождающего объекта, не является открытым, вы не можете использовать его напрямую, как мы сделали выше. Однако, если этот класс реализует интерфейс, вы все равно можете использовать его через делегирование класса:

// CompanionObjects/CompanionDelegation.kt
package companionobjects
import atomictest.*

class ZIClosed : ZI {
    override fun f() = "ZIClosed.f()"
    override fun g() = "ZIClosed.g()"
}

class ZIDelegation {
    companion object: ZI by ZIClosed()
    fun u() = trace(" ${f()} ${g()} ")
}

class ZIDelegationInheritance {
    companion object: ZI by ZIClosed() {
        override fun g() = "ZIDelegationInheritance.g()"
        fun h() = "ZIDelegationInheritance.h()"
    }
    
    fun u() = trace(" ${f()} ${g()} ${h()} ")
}

fun main() {
    ZIDelegation.f()
    ZIDelegation.g()
    ZIDelegation().u()
    ZIDelegationInheritance.f()
    ZIDelegationInheritance.g()
    ZIDelegationInheritance().u()
    trace eq """
    ZIClosed.f() ZIClosed.g()
    ZIClosed.f()
    ZIDelegationInheritance.g()
    ZIDelegationInheritance.h()
    """
}

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

// CompanionObjects/DelegateAndExtend.kt
package companionobjects
import atomictest.eq

interface Extended : ZI {
    fun u(): String
}

class Extend : ZI by Companion, Extended {
    companion object: ZI {
        override fun f() = "Extend.f()"
        override fun g() = "Extend.g()"
    }
    
    override fun u() = " ${f()} ${g()} "
}

private fun test(e: Extended): String {
    e.f()
    e.g()
    return e.u()
}

fun main() {
    test(Extend()) eq "Extend.f() Extend.g()"
}

В Extend интерфейс ZI реализуется с использованием собственного сопровождающего объекта, который имеет имя по умолчанию Companion. Но мы также реализуем интерфейс Extended, который является интерфейсом ZI плюс дополнительная функция u(). Часть ZI интерфейса Extended уже реализована через Companion, поэтому нам нужно только переопределить дополнительную функцию u(), чтобы завершить Extend. Теперь объект Extend может быть приведен к Extended в качестве аргумента для test().

Распространенное использование сопровождающего объекта — это контроль создания объектов — это паттерн «Фабричный метод». Предположим, вы хотите разрешить создание только списков объектов Numbered2, а не отдельных объектов Numbered2:

// CompanionObjects/CompanionFactory.kt
package companionobjects
import atomictest.eq

class Numbered2 private constructor(private val id: Int) {
    override fun toString() = "# $id"
    
    companion object Factory {
        fun create(size: Int) = List(size) { Numbered2(it) }
    }
}

fun main() {
    Numbered2.create(0) eq "[]"
    Numbered2.create(5) eq "[#0, #1, #2, #3, #4]"
}

Конструктор Numbered2 является приватным. Это означает, что есть только один способ создать экземпляр — через фабричную функцию create(). Фабричная функция иногда может решить проблемы, которые обычные конструкторы не могут. Конструкторы в сопровождающих объектах инициализируются, когда окружающий класс впервые создается в программе:

// CompanionObjects/Initialization.kt
package companionobjects
import atomictest.*

class CompanionInit {
    companion object {
        init {
            trace("Companion Constructor")
        }
    }
}

fun main() {
    trace("Before")
    CompanionInit()
    trace("After 1")
    CompanionInit()
    trace("After 2")
    CompanionInit()
    trace("After 3")
    trace eq """
    Before
    Companion Constructor
    After 1
    After 2
    After 3
    """
}

Вы можете видеть из вывода, что сопровождающий объект создается только один раз, в первый раз, когда создается объект CompanionInit. Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Раздел VI: Профилактика Link to heading

Сбой Link to heading

Если отладка — это процесс устранения программных ошибок, то программирование должно быть процессом их внесения. — Эдсгер Дейкстра

Обработка Исключений Link to heading

Неудача всегда является возможностью.
Kotlin находит основные ошибки, когда анализирует вашу программу. Ошибки, которые не могут быть обнаружены на этапе компиляции, должны обрабатываться во время выполнения. В разделе “Исключения” вы узнали, как выбрасывать исключения. В этом разделе мы поймаем исключения.
Исторически неудачи часто были катастрофическими. Например, программы, написанные на языке C, просто переставали работать, теряли свои данные и потенциально могли привести к сбою операционной системы.
Улучшенная обработка ошибок — это мощный способ повысить надежность кода. Обработка ошибок особенно важна при создании повторно используемых компонентов программы. Чтобы создать надежную систему, каждый компонент должен быть надежным. С последовательной обработкой ошибок компоненты могут надежно сообщать о проблемах клиентскому коду.
Современные приложения часто используют параллелизм, и параллельная программа должна выживать при некритических исключениях. Сервер, например, должен восстанавливаться, когда открытая сессия завершается через исключение.
Исключения объединяют три действия:

  1. Сообщение об ошибке
  2. Восстановление
  3. Очистка ресурсов
    Рассмотрим каждое из них.

Отчетность Link to heading

Стандартные исключения библиотеки часто бывают достаточными. Для более специфической обработки исключений вы можете наследовать новые типы исключений от Exception или его подтипов:

ExceptionHandling 424 // ExceptionHandling/DefiningExceptions.kt package exceptionhandling import atomictest.* class Exception1 ( val value: Int ): Exception(“неверное значение: $ value”) open class Exception2 ( description: String ): Exception(description) class Exception3 ( description: String ): Exception2(description) fun main() { capture { throw Exception1(13) } eq “Exception1: неверное значение: 13” capture { throw Exception3(“ошибка”) } eq “Exception3: ошибка” } Выражение throw, как в main(), требует экземпляр подтипа Throwable. Чтобы определить новые типы исключений, наследуйте Exception (который расширяет Throwable). Оба Exception1 и Exception2 наследуют Exception, в то время как Exception3 наследует Exception2.

Восстановление Link to heading

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

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

Когда выбрасывается исключение, механизм обработки исключений ищет подходящее место для продолжения выполнения. Исключение продолжает подниматься на более высокие уровни, от функции function1(), которая выбросила исключение, к функции function2(), которая вызывает function1(), к функции function3(), которая вызывает function2(), и так далее, пока не достигнет main(). Соответствующий обработчик ловит исключение. Это останавливает поиск и выполняет этот обработчик. Если программа никогда не находит соответствующий обработчик, она завершает работу с трассировкой стека в консоли.

// ExceptionHandling/Stacktrace.kt
package stacktrace
import exceptionhandling.Exception1

fun function1(): Int =
    throw Exception1(-52)

fun function2() = function1()
fun function3() = function2()

fun main() {
    // function3()
}

Раскомментирование вызова function3() приводит к следующей трассировке стека:

Exception in thread "main" exceptionhandling.Exception1: wrong value: -52
    at stacktrace.StacktraceKt.function1(Stacktrace.kt:6)
    at stacktrace.StacktraceKt.function2(Stacktrace.kt:8)
    at stacktrace.StacktraceKt.function3(Stacktrace.kt:10)
    at stacktrace.StacktraceKt.main(Stacktrace.kt:13)
    at stacktrace.StacktraceKt.main(Stacktrace.kt)

Любая из функций function1(), function2() или function3() может поймать исключение и обработать его, предотвращая завершение программы из-за исключения.

Обработчик исключений — это ключевое слово catch, за которым следует список параметров, содержащий исключение, которое вы обрабатываете. За ним следует блок кода, реализующий восстановление.

В следующем примере функция toss() генерирует разные исключения для аргументов 1-3, в противном случае она возвращает “OK”. Функция test() содержит полный набор обработчиков для функции toss():

// ExceptionHandling/Handlers.kt
package exceptionhandling
import atomictest.eq

fun toss(which: Int) = when (which) {
    1 -> throw Exception1(1)
    2 -> throw Exception2("Exception 2")
    3 -> throw Exception3("Exception 3")
    else -> "OK"
}

fun test(which: Int): Any? =
    try {
        toss(which)
    } catch (e: Exception1) {
        e.value
    } catch (e: Exception3) {
        e.message
    } catch (e: Exception2) {
        e.message
    }

fun main() {
    test(0) eq "OK"
    test(1) eq 1
    test(2) eq "Exception 2"
    test(3) eq "Exception 3"
}

Когда вы вызываете toss(), вы должны поймать все соответствующие исключения toss(), позволяя несущественным исключениям “всплывать” и обрабатываться в другом месте.

Вся конструкция try - catch в test() является единым выражением: она возвращает либо последнее выражение тела try, либо последнее выражение блока catch, соответствующего исключению. Если ни один обработчик не обрабатывает исключение, это исключение выбрасывается дальше по стеку. Если оно не поймано, оно генерирует трассировку стека.

Поскольку Exception3 является подклассом Exception2, Exception3 обрабатывается как Exception2, если обработчик Exception2 появляется в последовательности обработчиков перед обработчиком Exception3:

// ExceptionHandling/Hierarchy.kt
package exceptionhandling
import atomictest.eq

fun testCatchOrder(which: Int) =
    try {
        toss(which)
    } catch (e: Exception2) { // [1]
        "Handler for Exception2 got ${e.message} "
    } catch (e: Exception3) { // [2]
        "Handler for Exception3 got ${e.message} "
    }

fun main() {
    testCatchOrder(2) eq "Handler for Exception2 got Exception 2"
    testCatchOrder(3) eq "Handler for Exception2 got Exception 3"
}

Порядок блоков catch означает, что Exception3 будет пойман в строке [1], несмотря на то, что более специфичный тип обработчика исключений находится в строке [2].

Подтипы исключений Link to heading

В функции testCode() неверный аргумент code вызывает исключение IllegalArgumentException: // ExceptionHandling/LibraryException.kt package exceptionhandling
import atomictest.*
fun testCode(code: Int) {
if (code <= 1000) {
throw IllegalArgumentException(
“‘code’ must be > 1000: $ code”)
}
}
fun main() {
try {
// A1 в шестнадцатеричном (hex) представлении — это 161:
testCode(“A1”.toInt(16))
} catch (e: IllegalArgumentException) {
e.message eq
“‘code’ must be > 1000: 161”
}
try {
testCode(“0”.toInt(1))
} catch (e: IllegalArgumentException) {
e.message eq
“radix 1 was not in valid range 2..36”
}
}
Исключение IllegalArgumentException выбрасывается как в testCode(), так и в библиотечной функции toInt(radix). Это приводит к несколько запутанным сообщениям об ошибках в main(). Проблема в том, что мы используем одно и то же исключение для представления двух различных проблем. Мы решаем это, выбрасывая новый тип исключения, называемый IncorrectInputException, для нашей ошибки: // ExceptionHandling/NewException.kt package exceptionhandling
import atomictest.eq
class IncorrectInputException (
message: String
): Exception(message)
fun checkCode(code: Int) {
if (code <= 1000) {
throw IncorrectInputException(
“Code must be > 1000: $ code”)
}
}
fun main() {
try {
checkCode(“A1”.toInt(16))
} catch (e: IncorrectInputException) {
e.message eq “Code must be > 1000: 161”
} catch (e: IllegalArgumentException) {
“Produces error” eq “if it gets here”
}
try {
checkCode(“1”.toInt(1))
} catch (e: IncorrectInputException) {
“Produces error” eq “if it gets here”
} catch (e: IllegalArgumentException) {
e.message eq
“radix 1 was not in valid range 2..36”
}
}
Теперь каждая проблема имеет свой собственный обработчик.
Старайтесь не создавать слишком много типов исключений. В качестве правила, используйте разные типы исключений для различения различных схем обработки и используйте разные параметры конструктора для предоставления деталей для конкретной схемы обработки.

Очистка ресурсов Link to heading

Когда неудача неизбежна, автоматическая очистка ресурсов помогает другим частям программы продолжать работать безопасно. finally обеспечивает очистку ресурсов во время обработки исключений. Блок finally всегда выполняется, независимо от того, покидаете ли вы блок try нормально или с исключением:

// ExceptionHandling/TryFinally.kt
package exceptionhandling
import atomictest.*

fun checkValue(value: Int) {
    try {
        trace(value)
        if (value <= 0)
            throw IllegalArgumentException("value must be positive: $value")
    } finally {
        trace("In finally clause for $value")
    }
}

AtomicKotlin (www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC

fun main() {
    listOf(10, -10).forEach {
        try {
            checkValue(it)
        } catch (e: IllegalArgumentException) {
            trace("In catch clause for main()")
            trace(e.message)
        }
    }
    trace eq """
    10
    In finally clause for 10
    -10
    In finally clause for -10
    In catch clause for main()
    value must be positive: -10
    """
}

Блок finally работает даже с промежуточными блоками catch. Например, предположим, что переключатель должен быть выключен, когда вы с ним закончили:

// ExceptionHandling/GuaranteedCleanup.kt
package exceptionhandling
import atomictest.eq

data class Switch(
    var on: Boolean = false,
    var result: String = "OK"
)

fun testFinally(i: Int): Switch {
    val sw = Switch()
    try {
        sw.on = true
        when (i) {
            0 -> throw IllegalStateException()
            1 -> return sw // [1]
        }
    } catch (e: IllegalStateException) {
        sw.result = "exception"
    } finally {
        sw.on = false
    }
    return sw
}

fun main() {
    testFinally(0) eq "Switch(on=false, result=exception)"
    testFinally(1) eq "Switch(on=false, result=OK)" // [2]
    testFinally(2) eq "Switch(on=false, result=OK)"
}

Даже если мы возвращаемся внутри блока try ([1]), блок finally все равно выполняется ([2]). Независимо от того, завершится ли testFinally() нормально или с исключением, блок finally всегда выполняется.

Обработка Исключений в AtomicTest Link to heading

Эта книга использует метод capture() из AtomicTest для обеспечения того, что ожидаемые исключения выбрасываются. Метод capture() принимает функцию в качестве аргумента и возвращает объект CapturedException, содержащий класс исключения и сообщение об ошибке:

// ExceptionHandling/CaptureImplementation.kt
package exceptionhandling
import atomictest.CapturedException

fun capture(f: () -> Unit): CapturedException =
    try { // [1]
        f()
        CapturedException(null, "<Error>: Ожидалось исключение") // [2]
    } catch (e: Throwable) { // [3]
        CapturedException(e::class, // [4]
            if (e.message != null) ": ${e.message} "
            else "")
    }

AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
Обработка Исключений 432

fun main() {
    capture {
        throw Exception("!!!")
    } eq "Exception: !!!" // [5]
    capture {
        1
    } eq "<Error>: Ожидалось исключение"
}

Метод capture() вызывает свою функцию-аргумент f внутри блока try ([1]), обрабатывая все возможные исключения, перехватывая Throwable ([3]). Если исключение не выбрасывается, сообщение CapturedException указывает на то, что ожидалось исключение ([2]). Если исключение перехвачено, возвращаемый CapturedException содержит класс исключения и сообщение ([4]). CapturedException можно сравнивать со строкой, используя eq ([5]).

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

Руководство Link to heading

Восстановление после исключений оказывается удивительно редким, учитывая, что восстановление было изначальным намерением. Основная цель исключений в Kotlin — выявление ошибок в программе, а не восстановление. Поймать исключения в обычном коде Kotlin — это, следовательно, «запах кода».

Вот рекомендации по работе с исключениями в Kotlin:

  1. Логические ошибки: Это ошибки в вашем коде. Либо не ловите их вообще (и производите трассировку стека), либо ловите их на верхнем уровне вашего приложения и сообщайте об ошибках, возможно, перезапуская затронутую операцию.

  2. Ошибки данных: Это ошибки, вызванные некорректными данными, которые программист не может контролировать. Приложение должно каким-то образом справляться с проблемой, не обвиняя в этом логику программы. Например, мы использовали String.toInt() в этом атоме, который выбрасывает исключение для неподходящей строки. У него также есть компаньон String.toIntOrNull(), который возвращает null при неудаче, так что вы можете использовать его в выражении, таком как val n = string.toIntOrNull() ?: default. Библиотека Kotlin разработана с учетом обработки плохого результата, возвращая null вместо выбрасывания исключения. Операции, которые могут иногда завершаться неудачей, обычно имеют версию с «OrNull», которую вы можете использовать вместо версии с исключением.

  3. Проверочные инструкции тестируют на логические ошибки. Они вызывают исключения, когда находят ошибку, но выглядят как вызовы функций, так что вы не явно выбрасываете исключения в вашем коде.

  4. Ошибки ввода/вывода: Это внешние условия, которые вы не можете контролировать и не можете игнорировать. Однако использование подхода «OrNull» быстро затуманивает понимание кода. Более важно, что вы часто можете восстановиться после ошибок ввода/вывода, обычно повторяя операцию. Таким образом, операции ввода/вывода в Kotlin выбрасывают исключения, поэтому у вас будет код в ваших приложениях, который обрабатывает их и пытается восстановиться.

Упражнения и решения можно найти на www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Проверка инструкций Link to heading

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

require() Link to heading

Проектирование по контракту (Design By Contract) гарантирует выполнение предусловий и ограничений инициализации. Функция require() в Kotlin обычно используется для проверки аргументов функции, поэтому она обычно появляется в начале тел функций. Эти проверки не могут быть выполнены на этапе компиляции. Предусловия относительно легко включить в ваш код, но иногда их можно превратить в модульные тесты.

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

// CheckInstructions/JulianMonth.kt
package checkinstructions
import atomictest.*

data class Month(val monthNumber: Int) {
    init {
        require(monthNumber in 1..12) {
            "Month out of range: $monthNumber"
        }
    }
}

fun main() {
    Month(1) eq "Month(monthNumber=1)"
    capture { Month(13) } eq
        "IllegalArgumentException: " +
        "Month out of range: 13"
}

Мы выполняем require() внутри конструктора. require() выбрасывает IllegalArgumentException, если его условие не выполняется. Вы всегда можете использовать require() вместо того, чтобы выбрасывать IllegalArgumentException.

Второй параметр для require() — это лямбда, которая возвращает строку. Если строка требует конструирования, то эта накладная работа не происходит, если require() не завершится неудачей. Когда аргументы для Quadratic.kt из Резюме 2 неподходящие, он выбрасывает IllegalArgumentException. Мы можем упростить код, используя require():

// CheckInstructions/QuadraticRequire.kt
package checkinstructions
import kotlin.math.sqrt
import atomictest.*

class Roots(
    val root1: Double,
    val root2: Double
)

fun quadraticZeroes(
    a: Double,
    b: Double,
    c: Double
): Roots {
    require(a != 0.0) { "a is zero" }
    val underRadical = b * b - 4 * a * c
    require(underRadical >= 0) {
        "Negative underRadical: $underRadical"
    }
    val squareRoot = sqrt(underRadical)
    val root1 = (-b - squareRoot) / (2 * a)
    val root2 = (-b + squareRoot) / (2 * a)
    return Roots(root1, root2)
}

fun main() {
    capture {
        quadraticZeroes(0.0, 4.0, 5.0)
    } eq "IllegalArgumentException: " +
        "a is zero"
    capture {
        quadraticZeroes(3.0, 4.0, 5.0)
    } eq "IllegalArgumentException: " +
        "Negative underRadical: -44.0"
    val roots = quadraticZeroes(1.0, 2.0, -8.0)
    roots.root1 eq -4.0
    roots.root2 eq 2.0
}

Этот код гораздо яснее и чище, чем оригинальный Quadratic.kt.

Следующий класс DataFile позволяет нам работать с файлами независимо от того, выполняются ли примеры в IDE через курс AtomicKotlin или в отдельной сборке для книги. Все объекты DataFile хранят свои файлы в подкаталоге targetDir:

// CheckInstructions/DataFile.kt
package checkinstructions
import atomictest.eq
import java.io.File
import java.nio.file.Paths

val targetDir = File("DataFiles")

class DataFile(val fileName: String) : File(targetDir, fileName) {
    init {
        if (!targetDir.exists())
            targetDir.mkdir()
    }

    fun erase() { if (exists()) delete() }
    fun reset(): File {
        erase()
        createNewFile()
        return this
    }
}

fun main() {
    DataFile("Test.txt").reset() eq
        Paths.get("DataFiles", "Test.txt").toString()
}

DataFile управляет соответствующим файлом в операционной системе для записи и чтения этого файла. Базовым классом для DataFile является java.io.File, который является одним из старейших классов в библиотеке Java; он появился в первой версии языка, когда считали, что это отличная идея использовать один и тот же класс (File) для представления как файлов, так и директорий. Kotlin без труда наследует File, несмотря на его древность.

Во время создания мы создаем targetDir, если он не существует. Функция erase() удаляет файл, в то время как reset() удаляет файл и создает новый, пустой файл.

Стандартная библиотека Java содержит только перегруженный метод get() в классе Paths. Версия get(), которая нам нужна, принимает любое количество строк и создает объект Path, представляющий путь к директории, который независим от операционной системы.

Открытие файла часто имеет ряд предусловий, обычно связанных с путями к файлам, именами и содержимым. Рассмотрим функцию, которая открывает и читает файл с именем, начинающимся с file_. Используя require(), мы проверяем, что имя файла корректно, что файл существует и не пустой:

// CheckInstructions/GetTrace.kt
package checkinstructions
import atomictest.*

fun getTrace(fileName: String): List<String> {
    require(fileName.startsWith("file_")) {
        "$fileName must start with 'file_'"
    }
    val file = DataFile(fileName)
    require(file.exists()) {
        "$fileName doesn't exist"
    }
    val lines = file.readLines()
    require(lines.isNotEmpty()) {
        "$fileName is empty"
    }
    return lines
}

fun main() {
    DataFile("file_empty.txt").writeText("")
    DataFile("file_wubba.txt").writeText("wubba lubba dub dub")
    capture {
        getTrace("wrong_name.txt")
    } eq "IllegalArgumentException: " +
        "wrong_name.txt must start with 'file_'"
    capture {
        getTrace("file_nonexistent.txt")
    } eq "IllegalArgumentException: " +
        "file_nonexistent.txt doesn't exist"
    capture {
        getTrace("file_empty.txt")
    } eq "IllegalArgumentException: " +
        "file_empty.txt is empty"
    getTrace("file_wubba.txt") eq
        "[wubba lubba dub dub]"
}

Мы использовали версию require() с двумя параметрами, но также есть версия с одним параметром, которая производит сообщение по умолчанию:

// CheckInstructions/SingleArgRequire.kt
package checkinstructions
import atomictest.*

fun singleArgRequire(arg: Int): Int {
    require(arg > 5)
    return arg
}

fun main() {
    capture {
        singleArgRequire(5)
    } eq "IllegalArgumentException: " +
        "Failed requirement."
    singleArgRequire(6) eq 6
}

Сообщение об ошибке не так явно, как в версии с двумя параметрами, но в некоторых случаях оно достаточно.

requireNotNull() Link to heading

Функция requireNotNull() проверяет свой первый аргумент и возвращает его, если он не равен null. В противном случае она вызывает IllegalArgumentException. При успешном выполнении аргумент requireNotNull() автоматически преобразуется в ненулевой тип. Таким образом, вам обычно не нужно использовать возвращаемое значение requireNotNull():
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC
CheckInstructions 440
// CheckInstructions/RequireNotNull.kt
package checkinstructions
import atomictest.*
fun notNull(n: Int? ): Int {
requireNotNull(n) { // [1]
“Аргумент notNull() не может быть null”
}
return n * 9 // [2]
}
fun main() {
val n: Int? = null
capture {
notNull(n)
} eq “IllegalArgumentException: " +
“Аргумент notNull() не может быть null”
capture {
requireNotNull(n) // [3]
} eq “IllegalArgumentException: " +
“Требуемое значение было null.”
notNull(11) eq 99
}
• [2] Обратите внимание, что n больше не требует проверки на null, потому что вызов requireNotNull() сделал его ненулевым.
Как и в случае с require(), существует версия с двумя параметрами, в которой вы можете создать собственное сообщение ([1]), и версия с одним параметром с сообщением по умолчанию ([3]). Поскольку requireNotNull() проверяет на конкретную проблему (null), версия с одним параметром более полезна, чем в случае с require().

check() Link to heading

Постусловие, основанное на принципе “дизайн через контракт”, проверяет результаты функции. Постусловия важны для длинных и сложных функций, где вы можете не доверять результатам. Всякий раз, когда вы можете описать ограничения на результаты функции, разумно выразить их как постусловие.

check() идентичен require(), за исключением того, что он выбрасывает IllegalStateException. Обычно он используется в конце функции, чтобы проверить, что результаты (или поля в объекте функции) действительны — что все не попало в плохое состояние.

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

// CheckInstructions/Postconditions.kt
package checkinstructions
import atomictest.*

val resultFile = DataFile("Results.txt")

fun createResultFile(create: Boolean) {
    if (create)
        resultFile.writeText("Results\n# ok")
    // ... другие пути выполнения
    check(resultFile.exists()) {
        "${resultFile.name} doesn't exist!"
    }
}

fun main() {
    resultFile.erase()
    capture {
        createResultFile(false)
    } eq "IllegalStateException: " +
        "Results.txt doesn't exist!"
    createResultFile(true)
}

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

assert() Link to heading

Чтобы избежать комментирования и раскомментирования операторов check(), assert() позволяет вам включать и отключать проверки assert().
assert() происходит из Java. Проверки отключены по умолчанию и активируются только в том случае, если вы явно включаете их с помощью флага командной строки. В Kotlin этот флаг -ea.
Мы рекомендуем использовать require() и check(), которые всегда доступны без специальной конфигурации.
Упражнения и решения можно найти на сайте www.AtomicKotlin.com.
AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Тип Nothing Link to heading

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

// NothingType/InfiniteLoop.kt
package nothingtype

fun infinite(): Nothing {
    while (true) {}
}

Nothing — это встроенный тип Kotlin, у которого нет экземпляров. Практическим примером является встроенная функция TODO(), которая имеет тип возврата Nothing и выбрасывает NotImplementedError:

// NothingType/Todo.kt
package nothingtype

import atomictest.*

fun later(s: String): String = TODO("later()")
fun later2(s: String): Int = TODO()

fun main() {
    capture {
        later("Hello")
    } eq "NotImplementedError: " +
    "An operation is not implemented: later()"
    
    capture {
        later2("Hello!")
    } eq "NotImplementedError: " +
    "An operation is not implemented."
}

Обе функции later() и later2() возвращают типы, отличные от Nothing, хотя TODO() возвращает Nothing. Nothing совместим с любым типом. Функции later() и later2() компилируются успешно. Если вы вызовете любую из них, будет выброшено исключение, напоминающее вам о необходимости написать реализации. TODO() — это полезный инструмент для “эскизирования” структуры кода, чтобы убедиться, что все работает вместе, прежде чем заполнять детали.

В следующем примере функция fail() всегда выбрасывает исключение, поэтому она возвращает Nothing. Обратите внимание, что вызов fail() более читаем и компактен, чем явное выбрасывание исключения:

// NothingType/Fail.kt
package nothingtype

import atomictest.*

fun fail(i: Int): Nothing =
    throw Exception("fail($i)")

fun main() {
    capture {
        fail(1)
    } eq "Exception: fail(1)"
    
    capture {
        fail(2)
    } eq "Exception: fail(2)"
}

Функция fail() позволяет вам легко изменить стратегию обработки ошибок. Например, вы можете изменить тип исключения или добавить дополнительное сообщение перед выбрасыванием исключения. Это выбрасывает исключение BadData, если аргумент не является строкой:

// NothingType/CheckObject.kt
package nothingtype

import atomictest.*

class BadData(m: String) : Exception(m)

fun checkObject(obj: Any?): String =
    if (obj is String) obj
    else throw BadData("Needs String, got $obj")

fun test(checkObj: (obj: Any?) -> String) {
    checkObj("abc") eq "abc"
    capture {
        checkObj(null)
    } eq "BadData: Needs String, got null"
    
    capture {
        checkObj(123)
    } eq "BadData: Needs String, got 123"
}

fun main() {
    test(::checkObject)
}

Тип возврата checkObject() — это тип возврата выражения if. Kotlin рассматривает throw как тип Nothing, и Nothing может быть присвоен любому типу. В checkObject() тип String имеет приоритет над Nothing, поэтому тип выражения if — это String.

Мы можем переписать checkObject() с использованием безопасного приведения типов и оператора Эльвиса. Функция checkObject2() приводит obj к типу String, если это возможно, в противном случае выбрасывает исключение:

// NothingType/CheckObject2.kt
package nothingtype

fun failWithBadData(obj: Any?): Nothing =
    throw BadData("Needs String, got $obj")

fun checkObject2(obj: Any?): String =
    (obj as? String) ?: failWithBadData(obj)

fun main() {
    test(::checkObject2)
}

Когда компилятор получает простой null без дополнительной информации о типе, он выводит nullable Nothing:

// NothingType/ListOfNothing.kt
import atomictest.eq

fun main() {
    val none: Nothing? = null
    var nullableString: String? = null // [1]
    nullableString = "abc"
    nullableString = none // [2]
    nullableString eq null
    
    val nullableInt: Int? = none // [3]
    nullableInt eq null
    
    val listNone: List<Nothing?> = listOf(null)
    val ints: List<Int?> = listOf(null) // [4]
    ints eq listNone
}

Вы можете присвоить как null, так и none переменной или значению типа, допускающего null, таким как nullableString или nullableInt. Это разрешено, потому что типы как null, так и none — это Nothing? (nullable Nothing). Таким образом, выражение типа Nothing (например, fail()) может быть интерпретировано как “любой тип”, а выражение типа Nothing? (например, null) может быть интерпретировано как “любой допускающий null тип”. Присвоения различным nullable типам показаны в строках [1], [2] и [3].

listNone инициализируется списком, содержащим только значение null. Компилятор выводит это как List<Nothing?>. По этой причине вы должны явно указать тип элемента ([4]), который хотите хранить в списке, когда инициализируете его только с null.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Очистка ресурсов Link to heading

Использование блоков try - finally для очистки ресурсов утомительно и подвержено ошибкам. Функции библиотеки Kotlin управляют очисткой за вас. Как вы узнали в разделе об обработке исключений, блок finally очищает ресурсы независимо от того, как выходит блок try. Но что, если исключение может произойти при закрытии ресурса? В итоге вы получаете еще один try внутри блока finally. Более того, если одно исключение выбрасывается внутри try, а другое — при закрытии ресурса, последнее не должно скрывать первое. Обеспечение правильной очистки становится очень запутанным.

Чтобы уменьшить эту сложность, функция use() в Kotlin гарантирует правильную очистку закрываемых ресурсов, освобождая вас от написания кода для очистки вручную. Функция use() работает с любым объектом, который реализует интерфейс AutoCloseable в Java. Она выполняет код внутри блока, а затем вызывает close() на объекте, независимо от того, как вы выходите из блока — нормально (включая через return) или через исключение.

Функция use() повторно выбрасывает все исключения, поэтому вам все равно нужно иметь дело с этими исключениями. Предопределенные классы, которые работают с use(), можно найти в документации Java для AutoCloseable. Например, чтобы прочитать строки из файла, мы применяем use() к BufferedReader. Класс DataFile из CheckInstructions наследует java.io.File:

// ResourceCleanup/AutoCloseable.kt
import atomictest.eq
import checkinstructions.DataFile

fun main() {
    DataFile("Results.txt")
        .bufferedReader()
        .use { it.readLines().first() } eq "Results"
}

Функция useLines() открывает объект File, извлекает все его строки и передает эти строки целевой функции (обычно лямбде):

// ResourceCleanup/UseLines.kt
import atomictest.eq
import checkinstructions.DataFile

fun main() {
    DataFile("Results.txt").useLines {
        it.filter { "#" in it }.first() // [1]
    } eq "# ok"

    DataFile("Results.txt").useLines { lines ->
        lines.filter { line -> // [2]
            "#" in line
        }.first()
    } eq "# ok"
}
  • [1] Левый it ссылается на коллекцию строк в файле, в то время как правый it ссылается на каждую отдельную строку. Чтобы избежать путаницы, старайтесь не писать код с двумя разными близкими it.
  • [2] Именованные аргументы предотвращают путаницу от слишком большого количества it.

Все происходит внутри лямбды useLines(); за пределами лямбды содержимое файла недоступно, если вы явно не вернете его. Когда useLines() закрывает файл, она возвращает результат лямбды.

Функция forEachLine() упрощает применение действия к каждой строке в файле:

// ResourceCleanup/ForEachLine.kt
import checkinstructions.DataFile
import atomictest.*

fun main() {
    DataFile("Results.txt").forEachLine {
        if (it.startsWith("#"))
            trace(" **$** it")
    }
    trace eq "# ok"
}

Лямбда в forEachLine() возвращает Unit, что означает, что все, что вы делаете со строками, должно быть достигнуто через побочные эффекты. В функциональном программировании мы предпочитаем возвращать результаты, а не полагаться на побочные эффекты, и поэтому useLines() является более функциональным подходом, чем forEachLine(). Тем не менее, forEachLine() — это быстрое решение для простых утилит.

Вы можете создать свой собственный класс, который работает с use(), реализовав интерфейс AutoCloseable, который содержит только функцию close():

// ResourceCleanup/Usable.kt
package resourcecleanup

import atomictest.*

class Usable() : AutoCloseable {
    fun func() = trace("func()")
    override fun close() = trace("close()")
}

fun main() {
    Usable().use { it.func() }
    trace eq "func() close()"
}

Функция use() обеспечивает очистку ресурсов в момент их создания, а не заставляет вас писать код для очистки, когда вы закончили с ресурсом. Упражнения и решения можно найти на www.AtomicKotlin.com.

Логирование Link to heading

Логирование фиксирует информацию из работающей программы. Например, программа установки может записывать:

  • Шаги, предпринятые во время настройки.
  • Директории для хранения файлов.
  • Начальные значения для программы.

Веб-сервер может записывать адрес источника и статус каждого запроса. Логирование также полезно во время отладки. Без логирования вам, возможно, придется расшифровывать поведение программы с помощью операторов println(). Это может быть полезно при отсутствии отладчика (например, встроенного в IntelliJ IDEA). Однако, как только вы решите, что программа работает правильно, вы, вероятно, уберете операторы println(). Позже, если вы столкнетесь с новыми ошибками, вы можете вернуть их обратно. В отличие от этого, логирование можно динамически включать, когда это необходимо, и отключать в противном случае.

Для некоторых сбоев вы можете только сообщить о проблеме. Программа, которая восстанавливается от некоторых типов ошибок (как показано в разделе “Обработка исключений”), может записывать детали этих ошибок для последующего анализа. Например, в веб-приложении вы не завершаете программу, если что-то идет не так. Логирование фиксирует эти события, предоставляя программистам и администраторам способ обнаружить проблемы. Тем временем приложение продолжает работать.

Мы используем пакет логирования с открытым исходным кодом, разработанный для Kotlin, под названием Kotlin-logging, который имеет ощущение и простоту Kotlin. Существуют и другие пакеты логирования на выбор.

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

// Logging/BasicLogging.kt
package logging
import mu.KLogging

private val log = KLogging().logger

fun main() {
    val msg = "Hello, Kotlin Logging!"
    log.trace(msg)
    log.debug(msg)
    log.info(msg)
    log.warn(msg)
    log.error(msg)
}

В функции main() показаны различные уровни логирования: trace(), debug() и info() фиксируют поведенческую информацию, в то время как warn() и error() указывают на проблемы. Конфигурация запуска определяет уровни логирования, которые фактически сообщаются. Это можно изменить во время выполнения. Операторы долгосрочных приложений могут изменять уровень логирования без перезапуска программы (что часто неприемлемо).

Библиотеки логирования имеют довольно странную историю. Люди были недовольны оригинальной библиотекой логирования, распространяемой с Java, поэтому они создали другие библиотеки. В попытке унифицировать логирование, разработчики начали разрабатывать общие интерфейсы логирования. Признавая, что организации могут быть заинтересованы в существующих библиотеках логирования, эти интерфейсы были созданы как фасады для нескольких различных библиотек логирования. Позже другие программисты создали (предположительно улучшенные) фасады поверх этих фасадов. Использование системы логирования часто означает выбор фасада, а затем выбор одной или нескольких базовых реализаций.

Библиотека Kotlin-logging является фасадом для Simple Logging Facade for Java (SLF4J), которая является абстракцией над несколькими фреймворками логирования. Вы выбираете фреймворк, который соответствует вашим потребностям — хотя более вероятно, что это решение примет группа операций в вашей компании, так как именно они обычно управляют логированием и анализируют полученные журналы.

В этом примере мы используем slf4j-simple в качестве нашей реализации. Это входит в состав SLF4J, и, таким образом, нам не требуется устанавливать или настраивать дополнительную библиотеку — некоторые библиотеки имеют раздражающее количество сложности настройки. slf4j-simple отправляет свой вывод в поток ошибок консоли. Когда вы запускаете программу, вы видите:

[main] INFO mu.KLogging - Hello, Kotlin Logging!
[main] WARN mu.KLogging - Hello, Kotlin Logging!
[main] ERROR mu.KLogging - Hello, Kotlin Logging!

trace() и debug() не производят вывода, потому что конфигурация по умолчанию не сообщает о этих уровнях. Чтобы получить разные уровни отчетности, измените вашу конфигурацию логирования. Конфигурация логирования варьируется в зависимости от используемого пакета логирования, поэтому мы не будем обсуждать это здесь.

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

Для базовых проблем работа по установке, настройке и использованию системы логирования может соблазнить вас вернуться к операторам println(). К счастью, существуют более простые стратегии.

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

// Logging/SimpleLoggingStrategy.kt
package logging
import checkinstructions.DataFile

val logFile = // Reset ensures an empty file:
DataFile("simpleLogFile.txt").reset()

fun debug(msg: String) = System.err.println("Debug: $msg")
// Чтобы отключить:
// fun debug(msg: String) = Unit

fun trace(msg: String) = logFile.appendText("Trace: $msg\n")

fun main() {
    debug("Simple Logging Strategy")
    trace("Line 1")
    trace("Line 2")
    println(logFile.readText())
}

/* Пример вывода: Debug: Simple Logging Strategy Trace: Line 1 Trace: Line 2 */

debug() отправляет свой вывод в поток ошибок консоли. trace() отправляет свой вывод в файл журнала.

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

// Logging/AtomicLog.kt
package atomiclog
import checkinstructions.DataFile

class Logger(fileName: String) {
    val logFile = DataFile(fileName).reset()

    private fun log(type: String, msg: String) = logFile.appendText("$type: $msg\n")

    fun trace(msg: String) = log("Trace", msg)
    fun debug(msg: String) = log("Debug", msg)
    fun info(msg: String) = log("Info", msg)
    fun warn(msg: String) = log("Warn", msg)
    fun error(msg: String) = log("Error", msg)

    // Для базового тестирования:
    fun report(msg: String) {
        trace(msg)
        debug(msg)
        info(msg)
        warn(msg)
        error(msg)
    }
}

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

// Logging/UseAtomicLog.kt
package useatomiclog
import atomiclog.Logger
import atomictest.eq

private val logger = Logger("AtomicLog.txt")

fun main() {
    logger.report("Hello, Atomic Log!")
    logger.logFile.readText() eq """
    Trace: Hello, Atomic Log!
    Debug: Hello, Atomic Log!
    Info: Hello, Atomic Log!
    Warn: Hello, Atomic Log!
    Error: Hello, Atomic Log!
    """
}

Соблазнительно создать еще одну библиотеку логирования. Это, вероятно, не лучшее использование времени.

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

Упражнения и решения можно найти на www.AtomicKotlin.com.

Модульное тестирование Link to heading

Модульное тестирование — это практика создания теста на корректность для каждого аспекта функции. Модульные тесты быстро выявляют сломанный код, ускоряя темп разработки.
В тестировании гораздо больше, чем мы можем охватить в этой книге, поэтому этот атом является лишь базовым введением.
“Модуль” в “модульном тестировании” описывает небольшой фрагмент кода, обычно функцию, которая тестируется отдельно и независимо. Это не следует путать с несвязанным типом Unit в Kotlin.
Модульные тесты обычно пишутся программистом и выполняются каждый раз, когда вы собираете проект. Поскольку модульные тесты запускаются так часто, они должны выполняться быстро.
Вы уже изучали модульное тестирование, читая эту книгу, через библиотеку AtomicTest, которую мы используем для проверки кода книги. AtomicTest использует лаконичную функцию eq для наиболее распространенного паттерна в модульном тестировании: сравнения ожидаемого результата с сгенерированным результатом.
Из многочисленных фреймворков для модульного тестирования JUnit является самым популярным для Java. Существуют также фреймворки, созданные специально для Kotlin. Стандартная библиотека Kotlin включает kotlin.test, которая предоставляет фасад для различных тестовых библиотек. Таким образом, вы не ограничены использованием конкретной библиотеки. kotlin.test также содержит обертки для базовых функций утверждений.
Чтобы использовать kotlin.test, вам нужно изменить раздел зависимостей в файле build.gradle вашего проекта, добавив:

testImplementation "org.jetbrains.kotlin:kotlin-test-common"

Внутри модульного теста программист вызывает различные функции утверждений, которые проверяют ожидаемое поведение тестируемой функции. Функции утверждений включают assertEquals(), которая сравнивает фактическое значение с ожидаемым, и assertTrue(), которая проверяет свой первый аргумент, логическое выражение. В этом примере модульные тесты — это функции с именами, начинающимися со слова test:

// UnitTesting/NoFramework.kt
package unittesting
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import atomictest.*

fun fortyTwo() = 42

fun testFortyTwo(n: Int = 42) {
    assertEquals(
        expected = n,
        actual = fortyTwo(),
        message = "Некорректно,")
}

fun allGood(b: Boolean = true) = b

fun testAllGood(b: Boolean = true) {
    assertTrue(allGood(b), "Не хорошо")
}

fun main() {
    testFortyTwo()
    testAllGood()
    capture {
        testFortyTwo(43)
    } contains
        listOf("expected:", "<43>",
                "but was", "<42>")
    capture {
        testAllGood(false)
    } contains listOf("Error", "Не хорошо")
}

В main() вы можете увидеть, что неудачная функция утверждения вызывает AssertionError — это означает, что модульный тест не прошел, сигнализируя о проблеме программисту.
kotlin.test содержит множество функций, названия которых начинаются с assert:

  • assertEquals(), assertNotEquals()
  • assertTrue(), assertFalse()
  • assertNull(), assertNotNull()
  • assertFails(), assertFailsWith()
    Аналогичные функции обычно включаются в каждый фреймворк для модульного тестирования, но названия и порядок параметров могут отличаться. Например, параметр message в assertEquals() может быть первым или последним. Также легко перепутать ожидаемое и фактическое — использование именованных аргументов помогает избежать этой проблемы.
    Функция expect() в kotlin.test выполняет блок кода и сравнивает этот результат с ожидаемым значением:
fun <T> expect(
    expected: T,
    message: String?,
    block: () -> T
) {
    assertEquals(expected, block(), message)
}

Вот testFortyTwo(), переписанный с использованием expect():

// UnitTesting/UsingExpect.kt
package unittesting
import atomictest.*
import kotlin.test.*

fun testFortyTwo2(n: Int = 42) {
    expect(n, "Некорректно,") { fortyTwo() }
}

fun main() {
    testFortyTwo2()
    capture {
        testFortyTwo2(43)
    } contains
        listOf("expected:",
                "<43> but was:", "<42>")
    assertFails { testFortyTwo2(43) }
    capture {
        assertFails { testFortyTwo2() }
    } contains
        listOf("Expected an exception",
                "to be thrown",
                "but was completed successfully.")
    assertFailsWith<AssertionError> {
        testFortyTwo2(43)
    }
    capture {
        assertFailsWith<AssertionError> {
            testFortyTwo2()
        }
    } contains
        listOf("Expected an exception",
                "to be thrown",
                "but was completed successfully.")
}

Важно добавлять тесты для крайних случаев. Если функция вызывает ошибку при определенных условиях, это должно быть проверено с помощью модульного теста (как это делает capture() в AtomicTest). assertFails() и assertFailsWith() гарантируют, что исключение выбрасывается. assertFailsWith() также проверяет тип исключения.

Тестовые фреймворки Link to heading

Типичный тестовый фреймворк содержит набор функций утверждений и механизм для запуска тестов и отображения результатов. Большинство тестовых раннеров показывают результаты с зеленым цветом для успешных тестов и красным для неудачных.

Этот атом использует JUnit5 в качестве основной библиотеки для kotlin.test. Чтобы включить его в проект, секция зависимостей вашего build.gradle должна выглядеть следующим образом:

testImplementation "org.jetbrains.kotlin:kotlin-test"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit5"
testImplementation "org.junit.jupiter:junit-jupiter:$junit_version"

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

kotlin.test предоставляет фасады для наиболее часто используемых функций. Утверждения делегируются соответствующим функциям в основном тестовом фреймворке. Например, в классе org.junit.jupiter.api.Assertions, функция assertEquals() вызывает Assertions.assertEquals().

Kotlin поддерживает аннотации для определений и выражений. Аннотация — это знак @, за которым следует имя аннотации, и она указывает на специальное обращение с аннотированным элементом. Аннотация @Test преобразует обычную функцию в тестовую функцию. Мы можем протестировать fortyTwo() и allGood() с помощью аннотации @Test:

// Tests/unittesting/SampleTest.kt
package unittesting

import kotlin.test.*

class SampleTest {
    @Test
    fun testFortyTwo() {
        expect(42, "Некорректно,") { fortyTwo() }
    }

    @Test
    fun testAllGood() {
        assertTrue(allGood(), "Не хорошо")
    }
}

kotlin.test использует typealias для создания фасада для аннотации @Test:

typealias Test = org.junit.jupiter.api.Test

Это говорит компилятору подменить аннотацию @org.junit.jupiter.api.Test на @Test.

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

Функции @Test могут быть запущены:

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

IntelliJ IDEA позволяет вам повторно запускать только неудавшиеся тесты.

Рассмотрим простую конечную машину состояний с тремя состояниями: Включено, Выключено и Приостановлено. Функции start(), pause(), resume() и finish() управляют конечной машиной состояний. resume() ценен, потому что возобновление приостановленной машины значительно дешевле и/или быстрее, чем запуск машины.

// UnitTesting/StateMachine.kt
package unittesting

import unittesting.State.*

enum class State { On, Off, Paused }

class StateMachine {
    var state: State = Off
        private set

    private fun transition(new: State, current: State = On) {
        if (new == Off && state != Off)
            state = Off
        else if (state == current)
            state = new
    }

    fun start() = transition(On, Off)
    fun pause() = transition(Paused, On)
    fun resume() = transition(On, Paused)
    fun finish() = transition(Off)
}

Эти операции игнорируются:

  • resume() или finish() на машине, которая выключена.
  • pause() или start() на приостановленной машине.

Чтобы протестировать StateMachine, мы создаем свойство sm внутри тестового класса. Тестовый раннер создает новый объект StateMachineTest для каждого отдельного теста:

// Tests/unittesting/StateMachineTest.kt
package unittesting

import kotlin.test.*

class StateMachineTest {
    val sm = StateMachine()

    @Test
    fun start() {
        sm.start()
        assertEquals(State.On, sm.state)
    }

    @Test
    fun `pause and resume`() {
        sm.start()
        sm.pause()
        assertEquals(State.Paused, sm.state)
        sm.resume()
        assertEquals(State.On, sm.state)
        sm.pause()
        assertEquals(State.Paused, sm.state)
    }
    // ...
}

Обычно Kotlin позволяет использовать только буквы и цифры для имен функций. Однако, если вы поместите имя функции в обратные кавычки, вы можете использовать любые символы (включая пробелы). Это означает, что вы можете создавать имена функций, которые представляют собой предложения, описывающие их тесты, такие как pause and resume. Это дает более полезную информацию об ошибках.

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

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

Если вы используете сервер непрерывной интеграции (CI), такой как TeamCity, все доступные тесты запускаются автоматически, и вы получаете уведомление, если что-то ломается.

Рассмотрим класс с несколькими свойствами:

// UnitTesting/Learner.kt
package unittesting

enum class Language {
    Kotlin, Java, Go, Python, Rust, Scala
}

data class Learner(
    val id: Int,
    val name: String,
    val surname: String,
    val language: Language
)

Часто полезно добавлять утилитные функции для создания тестовых данных, особенно когда вам нужно создать много объектов с одинаковыми значениями по умолчанию во время тестирования. Здесь makeLearner() создает объекты с значениями по умолчанию:

// Tests/unittesting/LearnerTest.kt
package unittesting

import unittesting.Language.*
import kotlin.test.*

fun makeLearner(
    id: Int,
    language: Language = Kotlin, // [1]
    name: String = "Test Name $id",
    surname: String = "Test Surname $id"
) = Learner(id, name, surname, language)

class LearnerTest {
    @Test
    fun `single Learner`() {
        val learner = makeLearner(10, Java)
        assertEquals("Test Name 10", learner.name)
    }

    @Test
    fun `multiple Learners`() {
        val learners = (1..9).map(::makeLearner)
        assertTrue(learners.all { it.language == Kotlin })
    }
}

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

Порядок параметров makeLearner() упрощает его использование. В данном случае мы ожидаем, что будем указывать язык, отличный от значения по умолчанию, чаще, чем изменять значения по умолчанию для имени и фамилии, поэтому параметр language стоит на втором месте ([1]).

Мокирование и интеграционные тесты Link to heading

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

Крайне важно тестировать отдельные части функциональности независимо — именно это делают юнит-тесты. Также необходимо убедиться, что разные части системы работают вместе — именно это делают интеграционные тесты. Юнит-тесты направлены “внутрь”, в то время как интеграционные тесты направлены “наружу”.

Тестирование в IntelliJ IDEA Link to heading

IntelliJ IDEA и Android Studio поддерживают создание и выполнение модульных тестов. Чтобы создать тест, щелкните правой кнопкой мыши (или control-клик на Mac) на классе или функции, которую вы хотите протестировать, и выберите «Сгенерировать…» из всплывающего меню. В меню «Сгенерировать» выберите «Тест…». Второй способ — открыть список «интерактивных действий» и выбрать «Создать тест».

Выберите JUnit 5 в качестве «Библиотеки для тестирования». Если появится сообщение о том, что «Библиотека JUnit 5 не найдена в модуле», нажмите кнопку «Исправить» рядом с сообщением. «Пакет назначения» должен быть unittesting. Результат окажется в другой директории (всегда отделяйте тесты от основного кода). По умолчанию Gradle использует папку src/test/kotlin, но вы можете выбрать другое место назначения.

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

После генерации кода тестового фреймворка вы можете изменить его в соответствии с вашими потребностями. Для примеров и упражнений в этом атоме замените: import org.junit.Test
import org.junit.Assert.*
на:
import kotlin.test.*

При запуске тестов в IntelliJ IDEA вы можете получить сообщение об ошибке, например «события теста не были получены». Это происходит потому, что конфигурация по умолчанию в IDEA предполагает, что вы запускаете свои тесты внешне, используя Gradle. Чтобы исправить это и запустить тесты внутри IDEA, начните с меню файла:
Файл | Настройки | Сборка, выполнение, развертывание | Инструменты сборки | Gradle
На этой странице вы увидите выпадающий список с заголовком «Запуск тестов с использованием:», который установлен на «Gradle (по умолчанию)». Измените это на «IntelliJ IDEA», и ваши тесты будут выполняться корректно.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Раздел VII: Электроинструменты Link to heading

Любой дурак может написать код, который понимает компьютер. Хорошие программисты пишут код, который понимают люди. — Мартин Фаулер

Расширяющие Лямбды Link to heading

Расширяющая лямбда похожа на расширяющую функцию. Она определяет лямбду вместо функции. Здесь va и vb дают одинаковый результат:

// ExtensionLambdas/Vanbo.kt
package extensionlambdas
import atomictest.eq

val va: (String, Int) -> String = { str, n ->
    str.repeat(n) + str.repeat(n)
}

val vb: String.(Int) -> String = {
    this.repeat(it) + repeat(it)
}

fun main() {
    va("Vanbo", 2) eq "VanboVanboVanboVanbo"
    "Vanbo".vb(2) eq "VanboVanboVanboVanbo"
    vb("Vanbo", 2) eq "VanboVanboVanboVanbo"
    // "Vanbo".va(2) // Не компилируется
}

va — это обычная лямбда, как те, что вы видели на протяжении всей этой книги. Она принимает два параметра: String и Int, и возвращает String. Тело лямбды также имеет два параметра, за которыми следует необходимая стрелка: str, n ->.

vb перемещает параметр String за пределы скобок и использует синтаксис расширяющей функции: String.(Int). Как и в случае с расширяющей функцией, объект типа, который расширяется (String в данном случае), становится получателем и может быть доступен с помощью this.

Первый вызов в vb использует явную форму this.repeat(it). Второй вызов опускает this, чтобы получить repeat(it). Как и любая лямбда, если у вас есть только один параметр (Int в этом случае), он ссылается на этот параметр.

В main() вызов va() — это именно то, что вы ожидаете от объявления типа лямбды (String, Int) -> String — два аргумента в традиционном вызове функции. vb() — это расширение, поэтому его можно вызывать с использованием расширяющей формы "Vanbo".vb(2). vb() также может быть вызван с использованием традиционной формы vb("Vanbo", 2). va() не может быть вызван с использованием расширяющей формы.

Когда вы впервые видите расширяющую лямбду, может показаться, что часть String.(Int) — это то, на что следует сосредоточиться. Но String не расширяется списком параметров (Int) — он расширяется всей лямбдой: String.(Int) -> String.

Документация Kotlin обычно называет расширяющие лямбды литералами функций с получателем. Термин “литерал функции” охватывает как лямбды, так и анонимные функции. Термин “лямбда с получателем” часто используется как синоним для расширяющей лямбды, чтобы подчеркнуть, что это лямбда с получателем в качестве дополнительного неявного параметра.

Как и расширяющая функция, расширяющая лямбда может иметь несколько параметров:

// ExtensionLambdas/Parameters.kt
package extensionlambdas
import atomictest.eq

val zero: Int.() -> Boolean = {
    this == 0
}

val one: Int.(Int) -> Boolean = {
    this % it == 0
}

val two: Int.(Int, Int) -> Boolean = { arg1, arg2 ->
    this % (arg1 + arg2) == 0
}

val three: Int.(Int, Int, Int) -> Boolean = { arg1, arg2, arg3 ->
    this % (arg1 + arg2 + arg3) == 0
}

fun main() {
    0.zero() eq true
    10.one(10) eq true
    20.two(10, 10) eq true
    30.three(10, 10, 10) eq true
}

В one() используется вместо именования параметра. Если это приводит к неясному синтаксису, лучше использовать явные имена параметров.

Мы демонстрировали расширяющие лямбды, определяя val s, но они чаще встречаются в качестве параметров функции, как в f2():

// ExtensionLambdas/FunctionParameters.kt
package extensionlambdas

class A {
    fun af() = 1
}

class B {
    fun bf() = 2
}

fun f1(lambda: (A, B) -> Int) =
    lambda(A(), B())

fun f2(lambda: A.(B) -> Int) =
    A().lambda(B())

fun lambdas() {
    f1 { aa, bb -> aa.af() + bb.bf() }
    f2 { af() + it.bf() }
}

В main() обратите внимание на более лаконичный синтаксис в лямбде, переданной в f2().

Если ваша расширяющая лямбда возвращает Unit, результат, производимый телом лямбды, игнорируется:

// ExtensionLambdas/LambdaUnitReturn.kt
package extensionlambdas

fun unitReturn(lambda: A.() -> Unit) =
    A().lambda()

fun nonUnitReturn(lambda: A.() -> String) =
    A().lambda()

fun lambdaUnitReturn() {
    unitReturn {
        "Unit игнорирует возвращаемое значение" +
        "Так что это может быть чем угодно ..."
    }
    unitReturn { 1 } // ... любого типа ...
    unitReturn { } // ... или ничего
    nonUnitReturn {
        "Должен возвращать правильный тип"
    }
    // nonUnitReturn { } // Не вариант
}

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

// ExtensionLambdas/Transform.kt
package extensionlambdas
import atomictest.eq

fun String.transform1(
    n: Int, lambda: (String, Int) -> String
) = lambda(this, n)

fun String.transform2(
    n: Int, lambda: String.(Int) -> String
) = lambda(this, n)

val duplicate: String.(Int) -> String = {
    repeat(it)
}

val alternate: String.(Int) -> String = {
    toCharArray()
        .filterIndexed { i, _ -> i % it == 0 }
        .joinToString("")
}

fun main() {
    "hello".transform1(5, duplicate)
        .transform2(3, alternate) eq "hleolhleo"
    "hello".transform2(5, duplicate)
        .transform1(3, alternate) eq "hleolhleo"
}

transform1() ожидает обычную лямбду, в то время как transform2() ожидает расширяющую лямбду. В main() расширяющие лямбды duplicate и alternate передаются как в transform1(), так и в transform2(). Получатель this внутри расширяющих лямбд duplicate и alternate становится первым аргументом String, когда любая из лямбд передается в transform1().

Используя ::, мы можем передать ссылку на функцию, когда ожидается расширяющая лямбда:

// ExtensionLambdas/FuncReferences.kt
package extensionlambdas
import atomictest.eq

fun Int.d1(f: (Int) -> Int) = f(this) * 10
fun Int.d2(f: Int.() -> Int) = f() * 10

fun f1(n: Int) = n + 3
fun Int.f2() = this + 3

fun main() {
    74.d1(::f1) eq 770
    74.d2(::f1) eq 770
    74.d1(Int::f2) eq 770
    74.d2(Int::f2) eq 770
}

Ссылка на расширяющую функцию имеет такой же тип, как и расширяющая лямбда: Int::f2 имеет тип Int.() -> Int.

В вызове 74.d1(Int::f2) мы передаем расширяющую функцию в d1(), которая не объявляет параметр расширяющей лямбды.

Полиморфизм работает как с обычными расширяющими функциями (Base.g()), так и с расширяющими лямбдами (параметр Base.h()):

// ExtensionLambdas/ExtensionPolymorphism.kt
package extensionlambdas
import atomictest.eq

open class Base {
    open fun f() = 1
}

class Derived : Base() {
    override fun f() = 99
}

fun Base.g() = f()
fun Base.h(xl: Base.() -> Int) = xl()

fun main() {
    val b: Base = Derived() // Восходящее преобразование
    b.g() eq 99
    b.h { f() } eq 99
}

Вы бы не ожидали, что это не сработает, но всегда стоит проверить предположение, создав пример.

Вы можете использовать синтаксис анонимной функции (описанный в разделе “Локальные функции”) вместо расширяющих лямбд. Здесь мы используем анонимную расширяющую функцию:

// ExtensionLambdas/AnonymousFunction.kt
package extensionlambdas
import atomictest.eq

fun exec(
    arg1: Int, arg2: Int,
    f: Int.(Int) -> Boolean
) = arg1.f(arg2)

fun main() {
    exec(10, 2, fun Int.(d: Int): Boolean {
        return this % d == 0
    }) eq true
}

В main() вызов exec() показывает, что анонимная расширяющая функция принимается как расширяющая лямбда.

Стандартная библиотека Kotlin содержит множество функций, которые работают с расширяющими лямбдами. Например, StringBuilder — это изменяемый объект, который производит неизменяемую строку, когда вы вызываете toString(). В отличие от этого, более современный buildString() принимает расширяющую лямбду. Он создает свой собственный объект StringBuilder, применяет к этому объекту расширяющую лямбду, а затем вызывает toString(), чтобы получить результат:

// ExtensionLambdas/StringCreation.kt
package extensionlambdas
import atomictest.eq

private fun messy(): String {
    val built = StringBuilder() // [1]
    built.append("ABCs: ")
    ('a'..'x').forEach { built.append(it) }
    return built.toString() // [2]
}

private fun clean() = buildString {
    append("ABCs: ")
    ('a'..'x').forEach { append(it) }
}

private fun cleaner() =
    ('a'..'x').joinToString("", "ABCs: ")

fun main() {
    messy() eq "ABCs: abcdefghijklmnopqrstuvwx"
    messy() eq clean()
    clean() eq cleaner()
}

В messy() мы повторяем имя built несколько раз. Мы также должны создать StringBuilder ([1]) и получить результат ([2]). Используя buildString() в clean(), вам не нужно создавать и управлять получателем для вызовов append(), что делает все гораздо более лаконичным.

cleaner() показывает, что, если вы посмотрите, вы иногда можете найти более прямое решение, которое полностью обходится без билдера.

Существуют функции стандартной библиотеки, аналогичные buildString(), которые используют расширяющие лямбды для создания инициализированных, только для чтения List и Map:

// ExtensionLambdas/ListsAndMaps.kt
@file:OptIn(ExperimentalStdlibApi::class)
package extensionlambdas
import atomictest.eq

val characters: List<String> = buildList {
    add("Chars:")
    ('a'..'d').forEach { add(" $it") }
}

val charmap: Map<Char, Int> = buildMap {
    ('A'..'F').forEachIndexed { n, ch ->
        put(ch, n)
    }
}

fun main() {
    characters eq "[Chars:, a, b, c, d]"
    // characters eq characters2
    charmap eq "{A=0, B=1, C=2, D=3, E=4, F=5}"
}

Внутри расширяющих лямбд List и Map являются изменяемыми, но результаты buildList и buildMap являются только для чтения List и Map.

Создание строителей с использованием расширяющих лямбд Link to heading

Гипотетически, вы можете создать конструкторы для создания всех необходимых конфигураций объектов. Иногда количество возможностей делает это неаккуратным и непрактичным. У паттерна Строитель есть несколько преимуществ: AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC

  1. Он создает объекты в многоступенчатом процессе. Это может быть полезно, когда создание объекта является сложным.
  2. Он производит различные вариации объектов, используя один и тот же базовый код создания.
  3. Он отделяет общий код создания от специализированного кода, что упрощает написание и чтение кода для отдельных вариаций объектов.

Реализация строителей с использованием расширяющих лямбд предоставляет дополнительное преимущество — создание языка, специфичного для предметной области (DSL). Цель DSL — синтаксис, который удобен и понятен пользователю, являющемуся экспертом в своей области, а не экспертом в программировании. Это позволяет пользователю создавать рабочие решения, зная лишь небольшую часть окружающего языка, при этом извлекая выгоду из структуры и безопасности этого языка.

Например, рассмотрим систему, которая фиксирует действия и ингредиенты для приготовления различных видов сэндвичей. Мы можем использовать классы для моделирования частей Рецепта:

// ExtensionLambdas/Sandwich.kt
package sandwich
import atomictest.eq

open class Recipe : ArrayList<RecipeUnit>()
open class RecipeUnit {
    override fun toString() = " ${this::class.simpleName} "
}
open class Operation : RecipeUnit()
class Toast : Operation()
class Grill : Operation()
class Cut : Operation()
open class Ingredient : RecipeUnit()
class Bread : Ingredient()
class PeanutButter : Ingredient()
class GrapeJelly : Ingredient()
class Ham : Ingredient()
class Swiss : Ingredient()
class Mustard : Ingredient()
open class Sandwich : Recipe() {
    fun action(op: Operation): Sandwich {
        add(op)
        return this
    }
    fun grill() = action(Grill())
    fun toast() = action(Toast())
    fun cut() = action(Cut())
}

fun sandwich(fillings: Sandwich.() -> Unit): Sandwich {
    val sandwich = Sandwich()
    sandwich.add(Bread())
    sandwich.toast()
    sandwich.fillings()
    sandwich.cut()
    return sandwich
}

fun main() {
    val pbj = sandwich {
        add(PeanutButter())
        add(GrapeJelly())
    }
    val hamAndSwiss = sandwich {
        add(Ham())
        add(Swiss())
        add(Mustard())
        grill()
    }
    pbj eq "[Bread, Toast, PeanutButter, GrapeJelly, Cut]"
    hamAndSwiss eq "[Bread, Toast, Ham, Swiss, Mustard, Grill, Cut]"
}

Функция sandwich() фиксирует основные ингредиенты и операции для производства любого сэндвича (здесь мы предполагаем, что все сэндвичи поджарены, но в упражнениях вы увидите, как сделать это необязательным). Расширяющая лямбда fillings позволяет вызывающему настраивать сэндвич различными способами, но без необходимости создавать конструктор для каждой конфигурации.

Синтаксис, представленный в main(), показывает, как эта система может использоваться как DSL — пользователю нужно лишь понять синтаксис создания сэндвича, вызывая sandwich() и предоставляя ингредиенты и операции внутри фигурных скобок.

Упражнения и решения можно найти на www.AtomicKotlin.com. AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC

Функции области видимости Link to heading

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

Существует пять функций области видимости: let(), run(), with(), apply() и also(). Они предназначены для работы с лямбда-выражениями и не требуют импорта. Они различаются по способу доступа к контекстному объекту, используя либо it, либо this, и по тому, что они возвращают. with() использует другой синтаксис вызова, чем остальные. Вот вы можете увидеть различия:

// ScopeFunctions/Differences.kt
package scopefunctions
import atomictest.eq

data class Tag(var n: Int = 0) {
    var s: String = ""
    fun increment() = ++n
}

fun main() {
    // let(): Доступ к объекту с помощью 'it'
    // Возвращает последнее выражение в лямбда-выражении
    Tag(1).let {
        it.s = "let: ${it.n} "
        it.increment()
    } eq 2

    // let() с именованным аргументом лямбда-выражения:
    Tag(2).let { tag ->
        tag.s = "let: ${tag.n} "
        tag.increment()
    } eq 3

    // run(): Доступ к объекту с помощью 'this'
    // Возвращает последнее выражение в лямбда-выражении
    Tag(3).run {
        s = "run: $n" // Неявный 'this'
        increment() // Неявный 'this'
    } eq 4

    // with(): Доступ к объекту с помощью 'this'
    // Возвращает последнее выражение в лямбда-выражении
    with(Tag(4)) {
        s = "with: $n"
        increment()
    } eq 5

    // apply(): Доступ к объекту с помощью 'this'
    // Возвращает измененный объект
    Tag(5).apply {
        s = "apply: $n"
        increment()
    } eq "Tag(n=6)"

    // also(): Доступ к объекту с помощью 'it'
    // Возвращает измененный объект
    Tag(6).also {
        it.s = "also: ${it.n} "
        it.increment()
    } eq "Tag(n=7)"

    // also() с именованным аргументом лямбда-выражения:
    Tag(7).also { tag ->
        tag.s = "also: ${tag.n} "
        tag.increment()
    } eq "Tag(n=8)"
}

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

  • Функции области видимости, которые получают доступ к контекстному объекту с помощью this (run(), with() и apply()), производят самый чистый синтаксис в своем блоке области.
  • Функции области видимости, которые получают доступ к контекстному объекту с помощью it (let() и also()), позволяют вам предоставить именованный аргумент лямбда-выражения.
  • Функции области видимости, которые производят последнее выражение в своем лямбда-выражении (let(), run() и with()), предназначены для создания результатов.
  • Функции области видимости, которые возвращают измененный контекстный объект (apply() и also()), предназначены для объединения выражений.

with() является обычной функцией, а run() — функцией-расширением; в остальном они идентичны. Предпочитайте run() для цепочек вызовов и когда получатель может быть нулевым.

Вот сводка характеристик функций области видимости:

this Контекстit Контекст
Производит последнее выражениеwith, run
Производит получательapply, also

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

// ScopeFunctions/AndNullability.kt
package scopefunctions
import atomictest.eq
import kotlin.random.Random

fun gets(): String? =
    if (Random.nextBoolean()) "str!" else null

fun main() {
    gets()?.let {
        it.removeSuffix("!") + it.length
    }?.eq("str4")
}

В main(), если gets() возвращает ненулевой результат, то вызывается let. Ненулевой получатель let становится ненулевым it внутри лямбда-выражения. Применение оператора безопасного доступа к контекстному объекту проверяет весь контекст на null, как видно в [1] - [4] в следующем примере. В противном случае каждый вызов внутри области должен быть индивидуально проверен на null:

// ScopeFunctions/Gnome.kt
package scopefunctions

class Gnome(val name: String) {
    fun who() = "Gnome: $name"
}

fun whatGnome(gnome: Gnome?) {
    gnome?.let { it.who() } // [1]
    gnome.let { it?.who() }
    gnome?.run { who() } // [2]
    gnome.run { this?.who() }
    gnome?.apply { who() } // [3]
    gnome.apply { this?.who() }
    gnome?.also { it.who() } // [4]
    gnome.also { it?.who() }
    // Никакой помощи для нулевости:
    with(gnome) { this?.who() }
}

Когда вы используете оператор безопасного доступа на let(), run(), apply() или also(), вся область игнорируется для нулевого контекстного объекта:

// ScopeFunctions/NullGnome.kt
package scopefunctions
import atomictest.*

fun whichGnome(gnome: Gnome?) {
    trace(gnome?.name)
    gnome?.let { trace(it.who()) }
    gnome?.run { trace(who()) }
    gnome?.apply { trace(who()) }
    gnome?.also { trace(it.who()) }
}

fun main() {
    whichGnome(Gnome("Bob"))
    whichGnome(null)
    trace eq """
    Bob
    Gnome: Bob
    Gnome: Bob
    Gnome: Bob
    null
    """
}

Трассировка показывает, что когда whichGnome() получает нулевой аргумент, никакие функции области видимости не выполняются. Попытка получить объект из Map имеет нулевой результат, потому что нет гарантии, что он найдет запись для этого ключа. Здесь мы показываем различные функции области видимости, примененные к результату поиска в Map:

// ScopeFunctions/MapLookup.kt
package scopefunctions
import atomictest.*

data class Plumbus(var id: Int)

fun display(map: Map<String, Plumbus>) {
    trace("displaying $map")
    val pb1: Plumbus = map["main"]?.let {
        it.id += 10
        it
    } ?: return
    trace(pb1)

    val pb2: Plumbus? = map["main"]?.run {
        id += 9
        this
    }
    trace(pb2)

    val pb3: Plumbus? = map["main"]?.apply {
        id += 8
    }
    trace(pb3)

    val pb4: Plumbus? = map["main"]?.also {
        it.id += 7
    }
    trace(pb4)
}

fun main() {
    display(mapOf("main" to Plumbus(1)))
    display(mapOf("none" to Plumbus(2)))
    trace eq """
    displaying {main=Plumbus(id=1)}
    Plumbus(id=11)
    Plumbus(id=20)
    Plumbus(id=28)
    Plumbus(id=35)
    displaying {none=Plumbus(id=2)}
    """
}

Хотя with() может быть использован в этом примере, результаты слишком неаккуратные, чтобы их рассматривать. В трассировке вы видите, что каждый объект Plumbus создается во время первого вызова display() в main(), но ни один не создается во время второго вызова. Посмотрите на определение pb1 и вспомните оператор Эльвиса. Если выражение слева от ?: не равно null, оно становится результатом и присваивается pb1. Но если это выражение равно null, правая сторона ?: становится результатом, который равен return, так что display() возвращает до завершения инициализации pb1, и, следовательно, ни одно из значений pb1 - pb4 не создается.

Функции области видимости работают с нулевыми типами в цепочках вызовов:

// ScopeFunctions/NameTag.kt
package scopefunctions
import atomictest.trace

val functions = listOf(
    fun(name: String?) {
        name
            ?.takeUnless { it.isBlank() }
            ?.let { trace(" $it in let") }
    },
    fun(name: String?) {
        name
            ?.takeUnless { it.isBlank() }
            ?.run { trace(" $this in run") }
    },
    fun(name: String?) {
        name
            ?.takeUnless { it.isBlank() }
            ?.apply { trace(" $this in apply") }
    },
    fun(name: String?) {
        name
            ?.takeUnless { it.isBlank() }
            ?.also { trace(" $it in also") }
    },
)

fun main() {
    functions.forEach { it(null) }
    functions.forEach { it(" ") }
    functions.forEach { it("Yumyulack") }
    trace eq """
    Yumyulack in let
    Yumyulack in run
    Yumyulack in apply
    Yumyulack in also
    """
}

functions — это список ссылок на функции, которые применяются вызовами forEach в main(), используя его вместе с синтаксисом вызова функции. Каждая функция в functions использует различную функцию области видимости. Вызовы forEach к it(null) и it(" ") фактически игнорируются, поэтому мы отображаем только ненулевой, непустой ввод.

При вложении функций области видимости может быть доступно несколько объектов this или it в данном контексте. Иногда трудно понять, какой объект выбран:

// ScopeFunctions/Nesting.kt
package scopefunctions
import atomictest.eq

fun nesting(s: String, i: Int): String =
    with(s) {
        with(i) {
            toString()
        }
    } +
    s.let {
        i.let {
            it.toString()
        }
    } +
    s.run {
        i.run {
            toString()
        }
    } +
    s.apply {
        i.apply {
            toString()
        }
    } +
    s.also {
        i.also {
            it.toString()
        }
    }

fun main() {
    nesting("X", 7) eq "777XX"
}

Во всех случаях вызов toString() применяется к Int, потому что “ближайший” this или it является неявным получателем Int. apply() и also() возвращают измененный объект s, а не результат вычисления. Поскольку функции области видимости предназначены для улучшения читаемости, вложение функций области видимости является сомнительной практикой.

Ни одна из функций области видимости не обеспечивает очистку ресурсов так, как это делает use():

// ScopeFunctions/Blob.kt
package scopefunctions
import atomictest.*

data class Blob(val id: Int) : AutoCloseable {
    override fun toString() = "Blob($id)"
    fun show() { trace("$this") }
    override fun close() = trace("Close $this")
}

fun main() {
    Blob(1).let { it.show() }
    Blob(2).run { show() }
    with(Blob(3)) { show() }
    Blob(4).apply { show() }
    Blob(5).also { it.show() }
    Blob(6).use { it.show() }
    Blob(7).use { it.run { show() } }
    Blob(8).apply { show() }.also { it.close() }
    Blob(9).also { it.show() }.apply { close() }
    Blob(10).apply { show() }.use { }
    trace eq """
    Blob(1)
    Blob(2)
    Blob(3)
    Blob(4)
    Blob(5)
    Blob(6)
    Close Blob(6)
    Blob(7)
    Close Blob(7)
    Blob(8)
    Close Blob(8)
    Blob(9)
    Close Blob(9)
    Blob(10)
    Close Blob(10)
    """
}

Хотя use() выглядит похоже на let() и also(), use() не позволяет ничего возвращать из своего лямбда-выражения. Это предотвращает цепочку выражений или создание результатов. Без use(), close() не вызывается для ни одной из функций области видимости. Чтобы использовать функцию области видимости и гарантировать очистку, поместите функцию области видимости внутрь лямбда-выражения use(), как в Blob(7). Blob(8) и Blob(9) показывают, как явно вызвать close(), и как использовать apply() и also() взаимозаменяемо. Blob(10) использует apply(), и результат передается в use(), который вызывает close() в конце своего лямбда-выражения.

Функции области видимости встроены Link to heading

Обычно передача лямбда-выражения в качестве аргумента сохраняет код лямбда-выражения в вспомогательном объекте, добавляя небольшую накладную нагрузку во время выполнения по сравнению с обычным вызовом функции. Эта накладная нагрузка обычно не вызывает беспокойства, учитывая преимущества лямбда-выражений (читаемость и структуру кода). Кроме того, JVM содержит множество оптимизаций, которые часто компенсируют эту накладную нагрузку.

Любая стоимость производительности, независимо от того, насколько она мала, приводит к рекомендациям «использовать функцию с осторожностью». Вся накладная нагрузка во время выполнения устраняется путем определения функций области видимости как встроенных. Таким образом, функции области видимости могут использоваться без колебаний.

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

Когда встроенная функция принимает лямбда-аргумент, компилятор встраивает тело лямбда-выражения вместе с телом функции. Таким образом, не создаются дополнительные классы или объекты для передачи лямбда-выражения функции. (Это работает только в том случае, если лямбда-выражение вызывается напрямую или передается другой встроенной функции).

Хотя вы можете применять это к любой функции, встроенные функции предназначены либо для встраивания тел лямбда-выражений, либо для создания реифицированных обобщений. Вы можете найти дополнительную информацию о AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Создание обобщений Link to heading

Обобщенный код работает с типами, которые “указываются позже”. Обычные классы и функции работают с конкретными типами. Если вы хотите, чтобы код работал с большим количеством типов, такая жесткость может быть чрезмерно ограничительной. Полиморфизм — это инструмент обобщения в объектно-ориентированном программировании. Вы пишете функцию, которая принимает объект базового класса в качестве параметра, а затем вызываете эту функцию с объектом любого класса, производного от этого базового класса, включая классы, которые еще не были созданы. Теперь ваша функция более общая и полезна в большем количестве мест.

Единая иерархия может быть слишком ограничивающей, потому что вы должны наследовать от этой иерархии, чтобы создать объект, который подходит вашему параметру функции. Если параметр функции является интерфейсом вместо класса, ограничения ослабляются, чтобы включить все, что реализует этот интерфейс. Это дает программисту-клиенту возможность реализовать интерфейс в сочетании с существующим классом, то есть адаптировать существующий класс под функцию. Используемые таким образом, интерфейсы могут пересекаться с иерархиями классов.

Иногда даже интерфейс слишком ограничителен, потому что заставляет вас работать только с этим интерфейсом. Ваш код может быть еще более общим, если он работает с “некоторым неуточненным типом”, а не с конкретным интерфейсом или классом. Этот “неуточненный тип” является обобщенным параметром типа.

Создание обобщенных типов и функций — это довольно сложная тема, большая часть которой выходит за рамки этой книги. Этот раздел пытается дать вам достаточно информации, чтобы вы не удивились, когда столкнетесь с обобщенными концепциями и ключевыми словами. Если вы хотите серьезно заняться написанием обобщенных типов и функций, вам нужно будет изучить более продвинутые ресурсы.

Any Link to heading

Any является корнем иерархии классов Kotlin. Каждый класс Kotlin имеет Any в качестве суперкласса. Один из способов работы с неуточненными типами — это передача аргументов типа Any, и это может иногда запутать вопрос о том, когда использовать обобщения. Если Any работает, это более простое решение, а простота, как правило, лучше.

Существует два способа использования Any. Первый и самый простой подход — это когда вам нужно работать только с Any и ничем больше. Это крайне ограничивает возможности — у Any всего три функции-члена: equals(), hashCode() и toString(). Также существуют функции-расширения, но они не могут выполнять никаких прямых операций с типом. Например, apply() просто применяет свой аргумент функции к Any.

Если вы знаете тип Any, вы можете привести его к нужному типу и выполнять операции, специфичные для этого типа. Поскольку это связано с информацией о типе во время выполнения (как показано в Downcasting), вы рискуете получить ошибку во время выполнения, если передадите неправильный тип в вашу функцию (также это может немного повлиять на производительность). Иногда это оправдано, чтобы избежать дублирования кода.

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

// CreatingGenerics/Speakers.kt
package creatinggenerics
import atomictest.eq

class Person {
    fun speak() = "Hi!"
}

class Dog {
    fun bark() = "Ruff!"
}

class Robot {
    fun communicate() = "Beep!"
}

fun talk(speaker: Any) = when (speaker) {
    is Person -> speaker.speak()
    is Dog -> speaker.bark()
    is Robot -> speaker.communicate()
    else -> "Not a talker" // Или исключение
}
fun main() {
    talk(Person()) eq "Hi!"
    talk(Dog()) eq "Ruff!"
    talk(Robot()) eq "Beep!"
    talk(11) eq "Not a talker"
}

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

Определение обобщений Link to heading

Дублированный код является кандидатом для преобразования в обобщенную функцию или тип. Вы делаете это, добавляя угловые скобки ( <> ), содержащие один или несколько обобщенных заполнителей. Здесь обобщенный заполнитель T представляет неизвестный тип:
// CreatingGenerics/DefiningGenerics.kt
package creatinggenerics
fun gFunction(arg: T): T = arg
class GClass ( val x: T) {
fun f(): T = x
}
class GMemberFunction {
fun f(arg: T): T = arg
}
interface GInterface {
val x: T
fun f(): T
}
class GImplementation (
AtomicKotlin(www.AtomicKotlin.com) от Bruce Eckel и Светланы Исаковой, ©2021 MindView LLC
Создание обобщений 492
override val x: T
) : GInterface {
override fun f(): T = x
}
class ConcreteImplementation
GInterface< String > { override val x: String get () = “x” override fun f() = “f()” } fun basicGenerics() { gFunction(“Желтый”) gFunction(1) gFunction(Dog()).bark() // [1] gFunction(Dog()).bark() GClass(“Циан”).f() GClass(11).f() GClass(Dog()).f().bark() // [2] GClass(Dog()).f().bark() GMemberFunction().f(“Янтарь”) GMemberFunction().f(111) GMemberFunction().f(Dog()).bark() // [3] GMemberFunction().f(Dog()).bark() GImplementation(“Циан”).f() GImplementation(11).f() GImplementation(Dog()).f().bark() ConcreteImplementation().f() ConcreteImplementation().x } basicGenerics() показывает, что каждое обобщение обрабатывает разные типы: • gFunction() принимает параметр типа T и возвращает результат типа T. • GClass хранит значение типа T. Его член-функция f() возвращает значение типа T. • GMemberFunction параметризует член-функцию внутри класса, а не параметризует весь класс. AtomicKotlin(www.AtomicKotlin.com) от Bruce Eckel и Светланы Исаковой, ©2021 MindView LLC Создание обобщений 493 • Вы также можете определить интерфейс с обобщенными параметрами, как показано в GInterface. Реализация GInterface может либо переопределить параметр типа, как в GImplementation, либо предоставить конкретный аргумент типа, как в ConcreteImplementation. Обратите внимание в [1], [2] и [3], что мы можем вызвать bark() на результате, потому что этот результат имеет тип Dog. Рассмотрите [1], [2] и [3], а также строки, которые следуют за ними. Тип T определяется выводом типов для [1], [2] и [3]. Иногда это невозможно, если обобщение или его вызов слишком сложны для разбора компилятором. В этом случае вы должны указать тип(ы), используя синтаксис, показанный в строках, непосредственно следующих за [1], [2] и [3].

Сохранение информации о типах Link to heading

Как вы увидите позже в этом разделе, код внутри обобщенных классов и функций не может знать тип T — это называется стиранием типов. Обобщения можно рассматривать как способ сохранить информацию о типах для возвращаемого значения. Таким образом, вам не нужно писать код для явной проверки и приведения возвращаемого значения к желаемому типу.

Распространенное использование обобщенного кода — это контейнеры, которые хранят другие объекты. Рассмотрим класс CarCrate, который действует как тривиальная коллекция, хранящая и производящая один элемент типа Car:

// CreatingGenerics/CarCrate.kt
package creatinggenerics
import atomictest.eq

class Car {
    override fun toString() = "Car"
}

class CarCrate(private var c: Car) {
    fun put(car: Car) { c = car }
    fun get(): Car = c
}

fun main() {
    val cc = CarCrate(Car())
    val car: Car = cc.get()
    car eq "Car"
}

Когда мы вызываем cc.get(), результат возвращается как тип Car. Мы хотели бы сделать этот инструмент доступным для большего количества объектов, чем просто Car, поэтому мы обобщаем этот класс как Crate:

// CreatingGenerics/Crate.kt
package creatinggenerics
import atomictest.eq

open class Crate<T>(private var contents: T) {
    fun put(item: T) { contents = item }
    fun get(): T = contents
}

fun main() {
    val cc = Crate(Car())
    val car: Car = cc.get()
    car eq "Car"
}

Crate гарантирует, что вы можете помещать только T в Crate, и когда вы вызываете get() на этом Crate, результат возвращается как тип T.

Мы можем создать версию map() для Crate, определив обобщенную функцию-расширение:

// CreatingGenerics/MapCrate.kt
package creatinggenerics
import atomictest.eq

fun <T, R> Crate<T>.map(f: (T) -> R): List<R> =
    listOf(f(get()))

fun main() {
    Crate(Car()).map { it.toString() + "x" } eq "[Carx]"
}

map() возвращает список результатов, полученных путем применения f() к каждому элементу во входной последовательности. Поскольку Crate содержит только один элемент, результат всегда будет списком из одного элемента. Существует два обобщенных аргумента: T для входного значения и R для результата, что позволяет f() производить тип результата, отличающийся от типа входного значения.

Ограничения параметров типа Link to heading

Ограничение параметра типа указывает, что тип аргумента обобщения должен наследоваться от ограничения. <T: Base> означает, что T должен быть типа Base или чем-то, производным от Base. Этот раздел показывает, что использование ограничений отличается от не обобщенного типа, который наследует Base.

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

// CreatingGenerics/Disposable.kt
package creatinggenerics
import atomictest.eq

interface Disposable {
    val name: String
    fun action(): String
}

class Compost(override val name: String) : Disposable {
    override fun action() = "Добавить в компостер"
}

interface Transport : Disposable

class Donation(override val name: String) : Transport {
    override fun action() = "Вызвать для забирания"
}

class Recyclable(override val name: String) : Transport {
    override fun action() = "Положить в контейнер"
}

class Landfill(override val name: String) : Transport {
    override fun action() = "Положить в мусорный бак"
}

val items = listOf(
    Compost("Цедра апельсина"),
    Compost("Ядро яблока"),
    Donation("Диван"),
    Donation("Одежда"),
    Recyclable("Пластик"),
    Recyclable("Металл"),
    Recyclable("Картон"),
    Landfill("Мусор"),
)

val recyclables = items.filterIsInstance<Recyclable>()

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

// CreatingGenerics/Constrained.kt
package creatinggenerics
import atomictest.eq

fun <T: Disposable> nameOf(disposable: T) = disposable.name

// В качестве расширения:
fun <T: Disposable> T.name() = name

fun main() {
    recyclables.map { nameOf(it) } eq "[Пластик, Металл, Картон]"
    recyclables.map { it.name() } eq "[Пластик, Металл, Картон]"
}

Мы не можем получить доступ к name без ограничения. Это достигает того же результата без обобщений:

// CreatingGenerics/NonGenericConstraint.kt
package creatinggenerics
import atomictest.eq

fun nameOf2(disposable: Disposable) = disposable.name

fun Disposable.name2() = name

fun main() {
    recyclables.map { nameOf2(it) } eq "[Пластик, Металл, Картон]"
    recyclables.map { it.name2() } eq "[Пластик, Металл, Картон]"
}

Почему использовать ограничение вместо обычного полиморфизма? Ответ заключается в возвращаемом типе. С обобщениями возвращаемый тип может быть точным, а не быть приведенным к базовому типу:

// CreatingGenerics/SameReturnType.kt
package creatinggenerics
import kotlin.random.Random

private val rnd = Random(47)

fun List<Disposable>.aRandom(): Disposable = this[rnd.nextInt(size)]

fun <T: Disposable> List<T>.bRandom(): T = this[rnd.nextInt(size)]

fun <T> List<T>.cRandom(): T = this[rnd.nextInt(size)]

fun sameReturnType() {
    val a: Disposable = recyclables.aRandom()
    val b: Recyclable = recyclables.bRandom()
    val c: Recyclable = recyclables.cRandom()
}

Без обобщений aRandom() может производить только базовый класс Disposable, в то время как и bRandom(), и cRandom() производят Recyclable. bRandom() никогда не получает доступ к элементам T, поэтому его ограничение бессмысленно, и в итоге оно оказывается таким же, как и cRandom(), который не использует ограничение.

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

  1. Получить доступ к функции или свойству.
  2. Сохранить тип при его возврате.
// CreatingGenerics/Constraints.kt
package creatinggenerics
import kotlin.random.Random

private val rnd = Random(47)

// Получает доступ к action(), но не может вернуть точный тип:
fun List<Disposable>.inexact(): Disposable {
    val d: Disposable = this[rnd.nextInt(size)]
    d.action()
    return d
}

// Не может получить доступ к action() без ограничения:
fun <T> List<T>.noAccess(): T {
    val d: T = this[rnd.nextInt(size)]
    // d.action()
    return d
}

// Получает доступ к action() и возвращает точный тип:
fun <T: Disposable> List<T>.both(): T {
    val d: T = this[rnd.nextInt(size)]
    d.action()
    return d
}

fun constraints() {
    val i: Disposable = recyclables.inexact()
    val n: Recyclable = recyclables.noAccess()
    val b: Recyclable = recyclables.both()
}

inexact() является расширением для List<Disposable>, которое позволяет ему получить доступ к action(), но оно не является обобщенным, поэтому может вернуть только базовый тип Disposable. Как обобщенное, noAccess() может вернуть точный тип T, но без ограничения не может получить доступ к action(). Только когда вы добавляете ограничение на T в both(), вы можете получить доступ к action() и вернуть точный тип T.

Стирание типов Link to heading

Совместимость с Java является важной частью Kotlin. В Java обобщения не были частью оригинального языка — они были добавлены много лет спустя, после того как было написано большое количество кода. Внедрение обобщений в Java без нарушения существующего кода потребовало важного компромисса: обобщенные типы доступны только во время компиляции, но не сохраняются во время выполнения — типы стираются. Это стирание затрагивает и Kotlin.

Давайте представим, что стирание не происходит:

// CreatingGenerics/Erasure.kt
package creatinggenerics

fun main() {
    val strings = listOf("a", "b", "c")
    val all: List<Any> = listOf(1, 2, "x")
    useList(strings)
    useList(all)
}

fun useList(list: List<Any>) {
    // if (list is List<String>) {} // [1]
}

РаспCommentируйте строку [1], и вы увидите следующую ошибку: “Невозможно проверить экземпляр стертого типа: List”. Вы не можете проверить обобщенный тип во время выполнения, потому что информация о типе была стерта.

Если бы стирание не происходило, список мог бы выглядеть так, предполагая, что дополнительная информация о типе помещена в конец списка (это не работает таким образом!):

AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Создание обобщений Link to heading

Стертые обобщения Link to heading

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

Стертые обобщения Link to heading

Вы не можете угадать информацию о типе из содержимого List, не анализируя все элементы. Проверка только первого элемента из второго списка приводит к неправильному предположению, что это List.

Дизайнеры Kotlin решили следовать Java и использовать стирание по двум причинам:

  1. Совместимость с Java.
  2. Нагрузка. Хранение информации о типе обобщений значительно увеличивает объем памяти, занимаемой обобщенным List или Map. Например, стандартный Map состоит из множества объектов Map.Entry, а Map.Entry является обобщенным классом. Таким образом, если бы обобщения по умолчанию были реифицированы повсюду, каждый ключ и значение каждого Map.Entry содержали бы дополнительную информацию о типе.

Реализация аргументов типа функции Link to heading

Информация о типах также стирается для вызовов обобщенных функций, что означает, что вы не можете сделать много с обобщенным параметром внутри функции. Чтобы сохранить информацию о типах для аргументов функции, добавьте ключевое слово reified. Рассмотрим функцию a(), которая требует информации о классе для выполнения своей задачи:

AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Создание обобщений 501

// CreatingGenerics/ReificationA.kt
package creatinggenerics
import kotlin.reflect.KClass

fun <T: Any> a(kClass: KClass<T>) {
    // Использует KClass<T>
}

Когда мы вызываем a() внутри второй обобщенной функции b(), мы хотели бы использовать информацию о типе для обобщенного аргумента:

// CreatingGenerics/ReificationB.kt
package creatinggenerics
// Не компилируется из-за стирания:
// fun <T: Any> b() = a(T::class)

Информация о типе для T стирается, когда этот код выполняется, поэтому b() не будет компилироваться. Вы не можете получить доступ к классу обобщенного параметра типа внутри тела функции.

Решение на Java заключается в том, чтобы передать информацию о типе в функцию вручную:

// CreatingGenerics/ReificationC.kt
package creatinggenerics
import kotlin.reflect.KClass

fun <T: Any> c(kClass: KClass<T>) = a(kClass)
class K
val kc = c(K::class)

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

Чтобы использовать reified, функция также должна быть inline:

AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Создание обобщений 502

// CreatingGenerics/ReificationD.kt
package creatinggenerics
inline fun <reified T: Any> d() = a(T::class)
val kd = d<K>()

d() производит тот же эффект, что и c(), но d() не требует ссылки на класс в качестве аргумента.

reified говорит компилятору сохранить информацию о соответствующем аргументе типа. Теперь информация о типе доступна во время выполнения, так что вы можете получить к ней доступ внутри тела функции.

Реализация позволяет использовать is с обобщенным параметром типа:

// CreatingGenerics/CheckType.kt
package creatinggenerics
import atomictest.eq

inline fun <reified T> check(t: Any) = t is T
// fun <T> check1(t: Any) = t is T // [1]

fun main() {
    check<String>("1") eq true
    check<Int>("1") eq false
}

• [1] Без reified информация о типе стирается, поэтому вы не можете проверить, является ли данный элемент экземпляром T.

В следующем примере select() производит имена каждого объекта Disposable определенного подтипа. Он использует reified в сочетании с ограничением:

AtomicKotlin (www.AtomicKotlin.com) от Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Создание обобщений 503

// CreatingGenerics/Select.kt
package creatinggenerics
import atomictest.eq

inline fun <reified T : Disposable> select() =
    items.filterIsInstance<T>().map { it.name }

fun main() {
    select<Compost>() eq "[Orange Peel, Apple Core]"
    select<Donation>() eq "[Couch, Clothing]"
    select<Recyclable>() eq "[Plastic, Metal, Cardboard]"
    select<Landfill>() eq "[Trash]"
}

Функция библиотеки filterIsInstance() сама определена с использованием ключевого слова reified.

Вариация Link to heading

Сочетание обобщений и наследования создает два измерения изменений. Если у вас есть Container<T>, и вы хотите присвоить его Container<U>, где T и U имеют отношение наследования, вы должны установить ограничения для Container, используя аннотации вариации in или out, в зависимости от того, как вы хотите использовать Container. Вот три версии контейнера Box: базовый Box<T>, один с использованием <in T> и один с использованием <out T>:

// CreatingGenerics/InAndOutBoxes.kt
package variance

class Box<T>(private var contents: T) {
    fun put(item: T) { contents = item }
    fun get(): T = contents
}

class InBox<in T>(private var contents: T) {
    fun put(item: T) { contents = item }
}

class OutBox<out T>(private var contents: T) {
    fun get(): T = contents
}

in T означает, что методы класса могут принимать только аргументы типа T, но не могут возвращать значения типа T. То есть, объекты типа T могут быть помещены в InBox, но не могут оттуда выйти. out T означает, что методы класса могут возвращать объекты типа T, но не могут принимать аргументы типа T — вы не можете помещать объекты типа T в OutBox.

Почему нам нужны эти ограничения? Рассмотрим эту иерархию:

// CreatingGenerics/Pets.kt
package variance

open class Pet
class Cat : Pet()
class Dog : Pet()

Коты и собаки являются подтипами Pet. Существует ли отношение подтипов между Box<Cat> и Box<Pet>? Кажется, что мы должны иметь возможность присвоить, например, Box<Cat> в Box<Pet> или в Box<Any> (поскольку Any является суперклассом всего):

// CreatingGenerics/BoxAssignment.kt
package variance

val catBox = Box<Cat>(Cat())
// val petBox: Box<Pet> = catBox
// val anyBox: Box<Any> = catBox

Если бы Kotlin это разрешал, petBox имел бы put(item: Pet). Собака также является Pet, поэтому это позволило бы вам поместить собаку в catBox, нарушая “кошачью природу” этого контейнера. Более того, anyBox имел бы put(item: Any), так что вы могли бы поместить любой объект в catBox — контейнер не имел бы типобезопасности вообще.

Если мы предотвратим использование put(), присвоения будут безопасными, потому что никто не сможет поместить собаку в OutBox<Cat>. Компилятор позволяет нам присвоить OutBox<Cat> в OutBox<Pet> или в OutBox<Any>, потому что аннотация out предотвращает наличие функций put():

// CreatingGenerics/OutBoxAssignment.kt
package variance

val outCatBox: OutBox<Cat> = OutBox(Cat())
val outPetBox: OutBox<Pet> = outCatBox
val outAnyBox: OutBox<Any> = outCatBox

fun getting() {
    val cat: Cat = outCatBox.get()
    val pet: Pet = outPetBox.get()
    val any: Any = outAnyBox.get()
}

Без put() мы не можем поместить собаку в OutBox<Cat>, так что ее “кошачья природа” сохраняется.

Без get() InBox<Any> может быть присвоен InBox<Pet>, InBox<Cat> или InBox<Dog>:

// CreatingGenerics/InBoxAssignment.kt
package variance

val inBoxAny: InBox<Any> = InBox(Any())
val inBoxPet: InBox<Pet> = inBoxAny
val inBoxCat: InBox<Cat> = inBoxAny
val inBoxDog: InBox<Dog> = inBoxAny

fun main() {
    inBoxAny.put(Any())
    inBoxAny.put(Pet())
    inBoxAny.put(Cat())
    inBoxAny.put(Dog())
    inBoxPet.put(Pet())
    inBoxPet.put(Cat())
    inBoxPet.put(Dog())
    inBoxCat.put(Cat())
    inBoxDog.put(Dog())
}

Безопасно помещать Any, Pet, Cat или Dog в InBox<Any>, в то время как вы можете помещать только Pet, Cat или Dog в InBox<Pet>. inBoxCat и inBoxDog будут принимать только Cat и Dog соответственно. Это те поведения, которые мы ожидаем от контейнеров с такими параметрами типов, и компилятор это обеспечивает.

Вот краткое резюме отношений подтипов для Box, OutBox и InBox:

Вариация

  • Box<T> является инвариантным. Это означает, что ни Box<Cat>, ни Box<Pet> не являются подтипами друг друга, поэтому ни один из них не может быть присвоен другому.
  • OutBox<out T> является ковариантным. Это означает, что OutBox<Cat> является подтипом OutBox<Pet>. Когда вы поднимаете OutBox<Cat> до OutBox<Pet>, он изменяется так же, как и подъем Cat до Pet.
  • InBox<in T> является контравариантным. Это означает, что InBox<Pet> является подтипом InBox<Cat>. Когда вы поднимаете InBox<Pet> до InBox<Cat>, он изменяется противоположным образом, как подъем Cat до Pet.

Читаемый список из стандартной библиотеки Kotlin является ковариантным. Вы можете присвоить List<Cat> List<Pet>. MutableList является инвариантным, потому что он содержит add():

// CreatingGenerics/CovariantList.kt
package variance

fun main() {
    val catList: List<Cat> = listOf(Cat())
    val petList: List<Pet> = catList
    var mutablePetList: MutableList<Pet> = mutableListOf(Cat())
    mutablePetList.add(Dog())
    // Несоответствие типов:
    // mutablePetList =
    // mutableListOf<Cat>(Cat()) // [1]
}

• [1] Если это присвоение сработает, мы могли бы нарушить “кошачью природу” mutableListOf<Cat>, добавив собаку.

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

// CreatingGenerics/CovariantReturnTypes.kt
package variance

interface Parent
interface Child : Parent
interface X {
    fun f(): Parent
}
interface Y : X {
    override fun f(): Child
}

Обратите внимание, что переопределенный f() в Y возвращает Child, в то время как f() в X возвращает Parent. Этот раздел был лишь легким введением в тему вариации. Повторяющийся код является кандидатом на обобщенные типы или функции. Этот атом лишь предоставляет базовое понимание идей — если вам нужно более глубокое понимание, вам следует найти его в более продвинутом изложении. Упражнения и решения можно найти на www.AtomicKotlin.com.

Перегрузка операторов Link to heading

В контексте программирования перегрузка означает «добавление дополнительного значения чему-то, что уже существует». Перегрузка операторов позволяет вам взять оператор, такой как +, и придать ему значение для вашего нового типа или дополнительное значение для существующего типа.

Перегрузка операторов имеет бурное прошлое. Она была популяризирована в C++, но поскольку в C++ не было сборки мусора, написание перегруженных операторов было затруднительным. В результате ранние разработчики Java сочли перегрузку операторов «плохой» и не разрешили ее в Java, хотя сборка мусора в Java сделала бы это относительно простым. Простота перегрузки операторов при поддержке сборки мусора была продемонстрирована в языке Python, который ограничивал вас знакомым (ограниченным) набором операторов, как и C++. Scala затем экспериментировала с возможностью изобретать собственные операторы, что привело к тому, что некоторые программисты злоупотребляли этой функцией и создавали непонятный код. Kotlin извлек уроки из этих языков и упростил процесс перегрузки операторов, но ограничил ваш выбор разумным и знакомым набором операторов. Кроме того, правила приоритета операторов не могут быть изменены.

Мы создадим небольшой класс Num и добавим перегруженный + в качестве функции расширения. Чтобы перегрузить оператор, вы используете ключевое слово operator перед fun, за которым следует специальное предопределенное имя функции для этого оператора. Например, специальное имя функции для оператора + — это plus():

// OperatorOverloading/Num.kt
package operatoroverloading
import atomictest.eq

data class Num(val n: Int)

operator fun Num.plus(rval: Num) =
    Num(n + rval.n)

fun main() {
    Num(4) + Num(5) eq Num(9)
    Num(4).plus(Num(5)) eq Num(9)
}

Если бы вы определяли обычную (не операторную) функцию для использования между двумя операндами, вы бы использовали ключевое слово infix, но операторы уже являются инфиксными. Поскольку plus() — это обычная функция, вы также можете вызывать ее обычным способом.

Когда вы определяете оператор как член функции, вы можете получить доступ к приватным элементам в классе, к которым функция расширения не может получить доступ:

// OperatorOverloading/MemberOperator.kt
package operatoroverloading
import atomictest.eq

data class Num2(private val n: Int) {
    operator fun plus(rval: Num2) =
        Num2(n + rval.n)
}

// Невозможно получить доступ к 'n': он приватен в 'Num2':
// operator fun Num2.minus(rval: Num2) =
//     Num2(n - rval.n)

fun main() {
    Num2(4) + Num2(5) eq Num2(9)
}

В некоторых контекстах полезно создать специальное значение для оператора. Здесь мы моделируем молекулу с +, которая прикрепляет ее к другой молекуле. Свойство attached является связующим звеном между молекулами:

// OperatorOverloading/Molecule.kt
package operatoroverloading
import atomictest.eq

data class Molecule(
    val id: Int = idCount++,
    var attached: Molecule? = null
) {
    companion object {
        private var idCount = 0
    }

    operator fun plus(other: Molecule) {
        attached = other
    }
}

fun main() {
    val m1 = Molecule()
    val m2 = Molecule()
    m1 + m2 // [1]
    m1 eq "Molecule(id=0, attached=Molecule(id=1, attached=null))"
}

• [1] Читается как знакомое математическое выражение, но для человека, использующего модель, это может быть особенно значимым синтаксисом.

Этот пример неполный; если вы добавите строку m2 + m1, то при попытке отобразить m2 вы получите переполнение стека (можете ли вы исправить проблему?).

Равенство Link to heading

Вызов оператора == (равенство) или != (неравенство) вызывает функцию-член equals(). Классы данных автоматически переопределяют equals() для сравнения хранимых данных, но если вы не переопределите equals() для не-данных классов, версия по умолчанию сравнивает ссылки, а не содержимое:

AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Перегрузка операторов 512

// OperatorOverloading/DefaultEquality.kt
package operatoroverloading
import atomictest.eq

class A(val i: Int)
data class D(val i: Int)

fun main() {
    // Обычный класс:
    val a = A(1)
    val b = A(1)
    val c = a
    (a == b) eq false
    (a == c) eq true

    // Класс данных:
    val d = D(1)
    val e = D(1)
    (d == e) eq true
}

a и b ссылаются на разные объекты в памяти, поэтому ссылки разные, и a == b возвращает false, даже несмотря на то, что два объекта хранят идентичные данные. a и c ссылаются на один и тот же объект в памяти, поэтому их сравнение возвращает true. Поскольку класс данных D автоматически генерирует equals(), который смотрит на содержимое D, d == e возвращает true.

equals() — это единственный оператор, который не может быть функцией расширения; он должен быть переопределен как функция-член. При определении собственного equals() вы переопределяете стандартный equals(other: Any?). Обратите внимание, что тип other — это Any?, а не конкретный тип вашего класса. Это позволяет вам сравнивать ваш тип с другими типами, что означает, что вы должны выбрать типы, разрешенные для сравнения:

AtomicKotlin (www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC
Перегрузка операторов 513

// OperatorOverloading/DefiningEquality.kt
package operatoroverloading
import atomictest.eq

class E(var v: Int) {
    override fun equals(other: Any?) = when {
        this === other -> true // [1]
        other !is E -> false // [2]
        else -> v == other.v // [3]
    }

    override fun hashCode(): Int = v
    override fun toString() = "E($v)"
}

fun main() {
    val a = E(1)
    val b = E(2)
    (a == b) eq false // a.equals(b)
    (a != b) eq true // !a.equals(b)

    // Сравнение по ссылке:
    (E(1) === E(1)) eq false
}
  • [1] Это оптимизация: если other ссылается на тот же объект в памяти, результат автоматически true. Тройной символ равенства === проверяет равенство ссылок.
  • [2] Это определяет, что тип other должен быть таким же, как текущий тип. Чтобы E можно было сравнивать с другими типами, добавьте дополнительные выражения соответствия.
  • [3] Это сравнивает хранимые данные. На этом этапе компилятор знает, что other имеет тип E, поэтому мы можем получить доступ к other.v без приведения типа.

При переопределении equals() вы также должны переопределить hashCode(). Это сложная тема, но основное правило заключается в том, что если два объекта равны, они должны возвращать одно и то же значение hashCode(). Стандартные структуры данных, такие как Map и Set, не будут работать без этого правила. Все становится еще сложнее с открытым классом, потому что вам нужно сравнивать экземпляр со всеми возможными подклассами. Вы можете узнать больше о концепции хеширования в Википедии.

Определение правильных equals() и hashCode() выходит за рамки этой книги — то, что мы делаем здесь, иллюстрирует концепцию и работает для нашего простого примера, но не будет работать для более сложных случаев. Эта сложность является причиной того, что классы данных создают свои собственные equals() и hashCode(). Если вам необходимо определить свои собственные equals() и hashCode(), мы рекомендуем автоматически генерировать их с помощью IntelliJ IDEA или Android Studio с помощью действия Generate -> equals и hashCode.

Когда вы сравниваете объекты, допускающие значение null, с помощью ==, Kotlin требует проверки на null. Это можно сделать с помощью if или оператора Эльвиса:

// OperatorOverloading/EqualsForNullable.kt
package operatoroverloading
import atomictest.eq

fun equalsWithIf(a: E?, b: E?) =
    if (a === null) b === null
    else a == b

fun equalsWithElvis(a: E?, b: E?) =
    a?.equals(b) ?: (b === null)

fun main() {
    val x: E? = null
    val y = E(0)
    val z: E? = null
    (x == y) eq false
    (x == z) eq true
    equalsWithIf(x, y) eq false
    equalsWithIf(x, z) eq true
    equalsWithElvis(x, y) eq false
    equalsWithElvis(x, z) eq true
}

equalsWithIf() сначала проверяет, является ли ссылка a равной null, в этом случае единственный способ, чтобы два объекта были равны, — это если ссылка b также равна null. Если a не является ссылкой null, используется член equals() для сравнения двух объектов. equalsWithElvis() достигает того же эффекта, но более лаконично, используя как ?. так и ?: .

Арифметические операторы Link to heading

Мы можем определить базовые арифметические операторы как расширения для класса E : // OperatorOverloading/ArithmeticOperators.kt package operatoroverloading
import atomictest.eq
// Унарные операторы:
operator fun E.unaryPlus() = E(v)
operator fun E.unaryMinus() = E(-v)
operator fun E.not() = this
// Инкремент/декремент:
operator fun E.inc() = E(v + 1)
operator fun E.dec() = E(v - 1)
fun unary(a: E) {
+a // unaryPlus()
-a // unaryMinus()
!a // not()
var b = a
b++ // inc() (должен быть var)
b– // dec() (должен быть var)
}
// Бинарные операторы:
operator fun E.plus(e: E) = E(v + e.v)
operator fun E.minus(e: E) = E(v - e.v)
operator fun E.times(e: E) = E(v * e.v)
operator fun E.div(e: E) = E(v / e.v)
operator fun E.rem(e: E) = E(v % e.v)
fun binary(a: E, b: E) {
a + b // a.plus(b)
a - b // a.minus(b)
a * b // a.times(b)
a / b // a.div(b)
a % b // a.rem(b)
}
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
OperatorOverloading 516
// Увеличенное присваивание:
operator fun E.plusAssign(e: E) { v += e.v }
operator fun E.minusAssign(e: E) { v -= e.v }
operator fun E.timesAssign(e: E) { v *= e.v }
operator fun E.divAssign(e: E) { v /= e.v }
operator fun E.remAssign(e: E) { v %= e.v }
fun assignment(a: E, b: E) {
a += b // a.plusAssign(b)
a -= b // a.minusAssign(b)
a *= b // a.timesAssign(b)
a /= b // a.divAssign(b)
a %= b // a.remAssign(b)
}
fun main() {
val two = E(2)
val three = E(3)
two + three eq E(5)
two * three eq E(6)
val thirteen = E(13)
thirteen / three eq E(4)
thirteen % three eq E(1)
val one = E(1)
one += three * three
one eq E(10)
}
При написании расширения помните, что свойства и функции расширяемого типа доступны неявно. В определении unaryPlus(), например, v в E(v) — это свойство v из E, которое расширяется.
Обратите внимание, что x += e может быть разрешено как x = x.plus(e), если x является var, или как x.plusAssign(e), если x является val и соответствующий член plusAssign() доступен. Если оба варианта работают, компилятор выдает ошибку, указывая, что не может выбрать.
Параметр может быть другого типа, чем тип, который расширяет оператор. Здесь расширение оператора + для E принимает параметр типа Int:
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
OperatorOverloading 517
// OperatorOverloading/DifferentTypes.kt
package operatoroverloading
import atomictest.eq
operator fun E.plus(i: Int) = E(v + i)
fun main() {
E(1) + 10 eq E(11)
}
Приоритет операторов фиксирован и идентичен как для встроенных типов, так и для пользовательских типов. Например, умножение имеет более высокий приоритет, чем сложение, и оба имеют более высокий приоритет, чем равенство; таким образом, 1 + 2 * 3 == 7 — это правда. Вы можете найти таблицу приоритета операторов в документации.
Иногда, когда вы смешиваете арифметические и программные операторы, результат не очевиден. Здесь мы комбинируем + и оператор Эльвиса:
// OperatorOverloading/ConfusingPrecedence.kt
package operatoroverloading
import atomictest.eq
fun main() {
val x: Int? = 1
val y: Int = 2
val sum = x ?: 0 + y
sum eq 1
(x ?: 0) + y eq 3 // [1]
x ?: (0 + y) eq 1 // [2]
}
В sum, + имеет более высокий приоритет, чем оператор Эльвиса ?:, поэтому результат 1 ?: (0 + 2) == 1. Это может быть не то, что имел в виду программист. При смешивании различных операций, где приоритет не очевиден, мы рекомендуем добавлять скобки, как в строках [1] и [2].

Сравнение Link to heading

Все операции сравнения < , > , <= , >= автоматически доступны, когда вы определяете compareTo() : 47 https://kotlinlang.org/docs/reference/grammar.html#expressions AtomicKotlin(www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC Перегрузка операторов 518 // OperatorOverloading/Comparison.kt package operatoroverloading import atomictest.eq operator fun E.compareTo(e: E): Int = v.compareTo(e.v) fun main() { val a = E(2) val b = E(3) (a < b) eq true // a.compareTo(b) < 0 (a > b) eq false // a.compareTo(b) > 0 (a <= b) eq true // a.compareTo(b) <= 0 (a >= b) eq false // a.compareTo(b) >= 0 } compareTo() должен возвращать Int, указывающий: • 0, если элементы равны. • Положительное значение, если первый элемент (приемник) больше второго (аргумента). • Отрицательное значение, если первый элемент меньше второго.

Диапазоны и Контейнеры Link to heading

rangeTo() перегружает оператор .. для создания диапазонов, в то время как contains() указывает, находится ли значение в диапазоне: // OperatorOverloading/Ranges.kt package operatoroverloading
import atomictest.eq
data class R ( val r: IntRange) { // Диапазон
override fun toString() = “R( $ r)”
}
operator fun E.rangeTo(e: E) = R(v..e.v)
operator fun R.contains(e: E): Boolean =
e.v in r
fun main() {
val a = E(2)
val b = E(3)
val r = a..b // a.rangeTo(b)
(a in r) eq true // r.contains(a)
(a !in r) eq false // !r.contains(a)
r eq R(2..3)
}

Доступ к контейнеру Link to heading

Перегрузка функции contains() позволяет проверить, содержится ли значение в контейнере, в то время как get() и set() поддерживают чтение и присвоение элементов в контейнере с использованием квадратных скобок: // OperatorOverloading/ContainerAccess.kt package operatoroverloading
import atomictest.eq
data class C ( val c: MutableList< Int >) {
override fun toString() = “C( $ c)”
}
operator fun C.contains(e: E) = e.v in c
operator fun C.get(i: Int ): E = E(c[i])
operator fun C.set(i: Int , e: E) {
c[i] = e.v
}
fun main() {
val c = C(mutableListOf(2, 3))
(E(2) in c) eq true // c.contains(E(2))
(E(4) in c) eq false // c.contains(E(4))
c[1] eq E(3) // c.get(1)
c[1] = E(4) // c.set(2, E(4))
AtomicKotlin(www.AtomicKotlin.com) by Bruce Eckel & Svetlana Isakova, ©2021 MindView LLC
OperatorOverloading 520
c eq C(mutableListOf(2, 4))
}
В IntelliJ IDEA или Android Studio вы можете перейти к объявлению функции или класса из его использования. Это также работает с операторами: вы можете установить курсор на .., а затем перейти к его определению, чтобы увидеть, какая операторная функция вызывается.

Вызов Link to heading

Размещение круглых скобок после объекта генерирует вызов invoke(), поэтому оператор invoke() делает объект похожим на функцию. Вы можете определить invoke() с любым количеством параметров:

// OperatorOverloading/Invoke.kt
package operatoroverloading
import atomictest.eq

class Func {
    operator fun invoke() = "invoke()"
    operator fun invoke(i: Int) = "invoke($i)"
    operator fun invoke(i: Int, j: String) = "invoke($i, $j)"
    operator fun invoke(i: Int, j: String, k: Double) = "invoke($i, $j, $k)"
}

fun main() {
    val f = Func()
    f() eq "invoke()"
    f(22) eq "invoke(22)"
    f(22, "Hi") eq "invoke(22, Hi)"
    f(22, "Three", 3.1416) eq "invoke(22, Three, 3.1416)"
}

Вы также можете определить invoke() с vararg, чтобы работать с любым количеством аргументов одного типа (см. Списки переменных аргументов).

invoke() может быть определен как функция-расширение. Здесь это расширение для String, принимающее функцию в качестве параметра и вызывающее эту функцию на строке:

// OperatorOverloading/StringInvoke.kt
package operatoroverloading
import atomictest.eq

operator fun String.invoke(f: (s: String) -> String) = f(this)

fun main() {
    "mumbling" { it.uppercase() } eq "MUMBLING"
}

Поскольку лямбда является последним аргументом invoke(), ее можно вызывать без скобок.

Если у вас есть ссылка на функцию, вы можете использовать ее для прямого вызова функции, используя скобки или через invoke():

// OperatorOverloading/InvokeFunctionType.kt
package operatoroverloading
import atomictest.eq

fun main() {
    val func: (String) -> Int = { it.length }
    func("abc") eq 3
    func.invoke("abc") eq 3

    val nullableFunc: ((String) -> Int)? = null
    if (nullableFunc != null) {
        nullableFunc("abc")
    }
    nullableFunc?.invoke("abc") // [1]
}

• [1] Если ссылка на функцию может быть нулевой, вы можете комбинировать invoke() и безопасный доступ.

Наиболее распространенное использование пользовательского invoke() — это создание DSL (Domain-Specific Language).

Имена функций в обратных кавычках Link to heading

Kotlin позволяет использовать пробелы, определенные нестандартные символы и зарезервированные слова в имени функции, помещая это имя функции в обратные кавычки:

// OperatorOverloading/Backticks.kt
**package operatoroverloading**
**fun** `A long name with spaces`() = **Unit**
**fun** `*how* **is this** working?`() = **Unit**
**fun** `'when' **is** a keyword`() = **Unit**
// fun `Illegal characters :<>`() = Unit
**fun** main() {
    `A long name with spaces`()
    `*how* **is this** working?`()
    `'*when*' **is** a keyword`()
}

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

Вы можете легко создать непонятный код:

// OperatorOverloading/Swearing.kt
**package operatoroverloading**
**import atomictest.eq**
**infix fun** String.`#!%`(s: **String**) =
    " **$** this Rowzafrazaca **$** s"
**fun** main() {
    "howdy" `#!%` "Ma'am!" eq
    "howdy Rowzafrazaca Ma'am!"
}

Kotlin принимает этот код, но что он означает для читателя? Поскольку код читается гораздо чаще, чем пишется, вы должны делать свои программы как можно более понятными.

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

Все это синтаксический сахар. Туалетная бумага — это синтаксический сахар, и я все равно хочу ее. — Барри Хокинс

Упражнения и решения можно найти на www.AtomicKotlin.com.

Использование операторов Link to heading

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

// UsingOperators/NewAngle.kt
import atomictest.eq

fun main() {
    val list = MutableList(10) { 'a' + it }
    list[7] eq 'h' // оператор get()
    list.get(8) eq 'i' // Явный вызов
    list[9] = 'x' // оператор set()
    list.set(9, 'x') // Явный вызов
    list[9] eq 'x'
    ('d' in list) eq true // оператор contains()
    list.contains('e') eq true // Явный вызов
}

Доступ к элементам списка с помощью квадратных скобок вызывает перегруженные операторы get() и set(), в то время как in вызывает contains(). Вызов += на изменяемой коллекции модифицирует её, в то время как вызов + возвращает новую коллекцию, содержащую старые элементы вместе с новым элементом:

// UsingOperators/OperatorPlus.kt
import atomictest.eq

fun main() {
    val mutableList = mutableListOf(1, 2, 3)
    mutableList += 4 // оператор plusAssign()
    mutableList.plusAssign(5) // Явный вызов
    mutableList eq "[1, 2, 3, 4, 5]"
    mutableList + 99 eq "[1, 2, 3, 4, 5, 99]"
    mutableList eq "[1, 2, 3, 4, 5]"
    val list = listOf(1) // Только для чтения
    val newList = list + 2 // оператор plus()
    list eq "[1]"
    newList eq "[1, 2]"
    val another = list.plus(3) // Явный вызов
    another eq "[1, 3]"
}

Вызов += на коллекции только для чтения, вероятно, не даст ожидаемого результата:

// UsingOperators/Unexpected.kt
import atomictest.eq

fun main() {
    var list = listOf(1, 2)
    list += 3 // Вероятно, неожиданно
    list eq "[1, 2, 3]"
}

В неизменяемой коллекции a += b вызывает plusAssign() для модификации a. Однако plusAssign() недоступен для коллекций только для чтения, поэтому Kotlin переписывает a += b в a = a + b. Это вызывает plus(), который не изменяет коллекцию, а создает новую и присваивает результат ссылке var list. Таким образом, a += b все еще дает ожидаемый результат для a — по крайней мере, для простых типов, таких как Int`.

// UsingOperators/ReadOnlyAndPlus.kt
import atomictest.eq

fun main() {
    var list = listOf(1, 2)
    val initial = list
    list += 3
    list eq "[1, 2, 3]"
    list = list.plus(4)
    list eq "[1, 2, 3, 4]"
    initial eq "[1, 2]"
}

Последние строки показывают, что начальная коллекция остается неизменной. Создание новой коллекции для каждого добавленного элемента, вероятно, не является вашей целью. Проблема не возникает, если вы используете val для list вместо var, потому что вызов += не скомпилируется. Это одна из причин использовать val по умолчанию — используйте var только когда это необходимо.

compareTo() был введен как отдельная функция расширения в перегрузке операторов. Однако вы получите больше преимуществ, если ваш класс реализует интерфейс Comparable и переопределяет его compareTo():

// UsingOperators/CompareTo.kt
package usingoperators

import atomictest.eq

data class Contact(
    val name: String,
    val mobile: String
) : Comparable<Contact> {
    override fun compareTo(other: Contact): Int = name.compareTo(other.name)
}

fun main() {
    val alice = Contact("Alice", "0123456789")
    val bob = Contact("Bob", "9876543210")
    val carl = Contact("Carl", "5678901234")
    (alice < bob) eq true
    (alice <= bob) eq true
    (alice > bob) eq false
    (alice >= bob) eq false
    val contacts = listOf(bob, carl, alice)
    contacts.sorted() eq listOf(alice, bob, carl)
    contacts.sortedDescending() eq listOf(carl, bob, alice)
}

Любые два Comparable могут быть сравнены с использованием <, <=, > и >= (обратите внимание, что == и != не включены). Kotlin не требует модификатор оператора при переопределении compareTo(), потому что он уже был определен как оператор в интерфейсе Comparable.

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

// UsingOperators/ComparableRange.kt
package usingoperators

import atomictest.eq

class F(val i: Int) : Comparable<F> {
    override fun compareTo(other: F) = i.compareTo(other.i)
}

fun main() {
    val range = F(1)..F(7)
    (F(3) in range) eq true
    (F(9) in range) eq false
}

Предпочитайте реализовывать Comparable. Определяйте compareTo() как функцию расширения только при использовании класса, над которым у вас нет контроля.

Операторы деструктурирования Link to heading

Еще одна группа операторов, которые вы обычно не определяете, — это функции componentN() (component1(), component2() и т. д.), используемые для деструктурирующих объявлений. В функции main() Kotlin тихо генерирует вызовы component1() и component2() для деструктурирующего присваивания:

// UsingOperators/DestructuringDuo.kt
package usingoperators
import atomictest.*

class Duo(val x: Int, val y: Int) {
    operator fun component1(): Int {
        trace("component1()")
        return x
    }
    operator fun component2(): Int {
        trace("component2()")
        return y
    }
}

fun main() {
    val (a, b) = Duo(1, 2)
    a eq 1
    b eq 2
    trace eq "component1() component2()"
}

Тот же подход работает с Map, которые используют тип Entry, содержащий функции-члены component1() и component2():

// UsingOperators/DestructuringMap.kt
import atomictest.eq

fun main() {
    val map = mapOf("a" to 1)
    for ((key, value) in map) {
        key eq "a"
        value eq 1
    }
    // Деструктурирующее присваивание становится:
    for (entry in map) {
        val key = entry.component1()
        val value = entry.component2()
        key eq "a"
        value eq 1
    }
}

Вы можете использовать деструктурирующие объявления с любым классом данных, потому что функции componentN() генерируются автоматически:

// UsingOperators/DestructuringData.kt
package usingoperators
import atomictest.eq

data class Person(
    val name: String,
    val age: Int
) {
    // Компилятор генерирует:
    // fun component1() = name
    // fun component2() = age
}

fun main() {
    val person = Person("Alice", 29)
    val (name, age) = person
    // Деструктурирующее присваивание становится:
    val name_ = person.component1()
    val age_ = person.component2()
    name eq "Alice"
    age eq 29
    name_ eq "Alice"
    age_ eq 29
}

Kotlin генерирует функцию componentN() для каждого свойства. Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Делегирование свойств Link to heading

Свойство может делегировать свою логику доступа. Вы связываете свойство с делегатом с помощью ключевого слова by:

val/var property by delegate

Класс делегата должен содержать функцию getValue(), если свойство является val (только для чтения), или функции getValue() и setValue(), если свойство является var (для чтения/записи).

Сначала рассмотрим случай только для чтения:

// PropertyDelegation/BasicRead.kt
package propertydelegation
import atomictest.eq
import kotlin.reflect.KProperty

class Readable(val i: Int) {
    val value: String by BasicRead()
}

class BasicRead {
    operator fun getValue(
        r: Readable,
        property: KProperty<*>
    ) = "getValue: ${r.i} "
}

fun main() {
    val x = Readable(11)
    val y = Readable(17)
    x.value eq "getValue: 11"
    y.value eq "getValue: 17"
}

Значение в Readable делегируется объекту BasicRead. Функция getValue() принимает параметр Readable, что позволяет ей получить доступ к Readable — когда вы говорите by, это связывает BasicRead с целым объектом Readable. Обратите внимание, что getValue() получает доступ к i в Readable. Поскольку getValue() возвращает строку, тип value также должен быть строкой. Второй параметр getValue() property имеет специальный тип KProperty, который предоставляет рефлексивную информацию о делегированном свойстве.

Если делегированное свойство является var, оно должно обрабатывать как чтение, так и запись, поэтому класс делегата требует как getValue(), так и setValue():

// PropertyDelegation/BasicReadWrite.kt
package propertydelegation
import atomictest.eq
import kotlin.reflect.KProperty

class ReadWriteable(var i: Int) {
    var msg = ""
    var value: String by BasicReadWrite()
}

class BasicReadWrite {
    operator fun getValue(
        rw: ReadWriteable,
        property: KProperty<*>
    ) = "getValue: ${rw.i} "

    operator fun setValue(
        rw: ReadWriteable,
        property: KProperty<*>,
        s: String
    ) {
        rw.i = s.toIntOrNull() ?: 0
        rw.msg = "setValue to ${rw.i} "
    }
}

fun main() {
    val x = ReadWriteable(11)
    x.value eq "getValue: 11"
    x.value = "99"
    x.msg eq "setValue to 99"
    x.value eq "getValue: 99"
}

Первые два параметра setValue() такие же, как и у getValue(), а третий — это значение справа от =, которое мы хотим установить. Оба getValue() и setValue() должны согласовываться по типу, который читается и записывается, который в этом случае является строкой (тип value в ReadWriteable). Обратите внимание, что setValue() получает доступ к i в ReadWriteable, а также к msg.

BasicRead.kt и BasicReadWrite.kt не реализуют интерфейс. Класс может использоваться в качестве делегата, если он просто соответствует соглашению о наличии необходимых функций с необходимыми сигнатурами. Однако вы также можете реализовать интерфейс ReadOnlyProperty, как показано здесь в BasicRead2:

// PropertyDelegation/BasicRead2.kt
package propertydelegation
import atomictest.eq
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

class Readable2(val i: Int) {
    val value: String by BasicRead2()
    // SAM преобразование:
    val value2: String by ReadOnlyProperty { _, _ -> "getValue: $i" }
}

class BasicRead2 : ReadOnlyProperty<Readable2, String> {
    override operator fun getValue(
        thisRef: Readable2,
        property: KProperty<*>
    ) = "getValue: ${thisRef.i} "
}

fun main() {
    val x = Readable2(11)
    val y = Readable2(17)
    x.value eq "getValue: 11"
    x.value2 eq "getValue: 11"
    y.value eq "getValue: 17"
    y.value2 eq "getValue: 17"
}

Реализация ReadOnlyProperty сообщает читателю, что BasicRead2 может использоваться в качестве делегата и обеспечивает правильное определение getValue(). Поскольку ReadOnlyProperty имеет только одну член-функцию (и она определена как функциональный интерфейс в стандартной библиотеке), value2 определяется гораздо более лаконично с использованием SAM-преобразования.

BasicReadWrite.kt можно изменить, чтобы реализовать ReadWriteProperty, обеспечивая правильные определения getValue() и setValue():

// PropertyDelegation/BasicReadWrite2.kt
package propertydelegation
import atomictest.eq
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class ReadWriteable2(var i: Int) {
    var msg = ""
    var value: String by BasicReadWrite2()
}

class BasicReadWrite2 : ReadWriteProperty<ReadWriteable2, String> {
    override operator fun getValue(
        rw: ReadWriteable2,
        property: KProperty<*>
    ) = "getValue: ${rw.i} "

    override operator fun setValue(
        rw: ReadWriteable2,
        property: KProperty<*>,
        s: String
    ) {
        rw.i = s.toIntOrNull() ?: 0
        rw.msg = "setValue to ${rw.i} "
    }
}

fun main() {
    val x = ReadWriteable2(11)
    x.value eq "getValue: 11"
    x.value = "99"
    x.msg eq "setValue to 99"
    x.value eq "getValue: 99"
}

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

  1. Для чтения:
operator fun getValue(thisRef: T, property: KProperty<*>): V
  1. Для записи:
operator fun setValue(thisRef: T, property: KProperty<*>, value: V)

Если делегированное свойство является val, требуется только первая функция, и ReadOnlyProperty может быть реализован с использованием SAM-преобразования.

Параметры:

  • thisRef: T указывает на объект делегата, где T — это тип этого делегата. Если вы не хотите использовать thisRef в функции, вы можете эффективно отключить его, используя Any? для T.
  • property: KProperty<*> предоставляет информацию о самом свойстве. Наиболее часто используемым является name, который возвращает имя поля делегированного свойства.
  • value — это значение, хранящееся в делегированном свойстве, которое устанавливается с помощью setValue(). V — это тип этого свойства.

getValue() и setValue() могут быть определены либо по соглашению, либо написаны как реализации ReadOnlyProperty или ReadWriteProperty.

Чтобы обеспечить доступ к приватным элементам, вложите класс делегата:

// PropertyDelegation/Accessibility.kt
package propertydelegation
import atomictest.eq
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

class Person(
    private val first: String,
    private val last: String
) {
    val name by // SAM преобразование:
    ReadOnlyProperty<Person, String> { _, _ ->
        "$first $last"
    }
}

fun main() {
    val alien = Person("Floopy", "Noopers")
    alien.name eq "Floopy Noopers"
}

Предполагая достаточный доступ к элементам в делегирующем классе, getValue() и setValue() могут быть написаны как функции-расширения:

// PropertyDelegation/Add.kt
package propertydelegation2
import atomictest.eq
import kotlin.reflect.KProperty

class Add(val a: Int, val b: Int) {
    val sum by Sum()
}

class Sum {
    operator fun Sum.getValue(
        thisRef: Add,
        property: KProperty<*>
    ) = thisRef.a + thisRef.b
}

fun main() {
    val addition = Add(144, 12)
    addition.sum eq 156
}

Таким образом, вы можете использовать существующий класс, который вы не можете изменить или отнаследовать, и все равно делегировать свойство с его помощью.

Здесь, когда вы устанавливаете значение свойства, хранимое число является числом Фибоначчи для этого значения, используя функцию fibonacci() из модуля Recursion:

// PropertyDelegation/FibonacciProperty.kt
package propertydelegation
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import recursion.fibonacci
import atomictest.eq

class Fibonacci : ReadWriteProperty<Any?, Long> {
    private var current: Long = 0

    override operator fun getValue(
        thisRef: Any?,
        property: KProperty<*>
    ) = current

    override operator fun setValue(
        thisRef: Any?,
        property: KProperty<*>,
        value: Long
    ) {
        current = fibonacci(value.toInt())
    }
}

fun main() {
    var fib by Fibonacci()
    fib eq 0L
    fib = 22L
    fib eq 17711L
    fib = 90L
    fib eq 2880067194370816120L
}

fib в main() является локальным делегированным свойством — оно определено внутри функции, а не класса. Делегированное свойство также может быть определено на уровне файла.

Первый обобщенный аргумент ReadWriteProperty может быть Any?, потому что мы никогда не используем его для доступа к чему-либо внутри Fibonacci, что потребовало бы конкретной информации о типе. Вместо этого мы манипулируем свойством current, как можем в любой член-функции.

Во многих примерах, которые мы видели до сих пор, первый параметр getValue() и setValue() имеет конкретный тип. Эти делегаты были привязаны к этому конкретному типу. Иногда возможно создать универсальный делегат, игнорируя первый тип как Any?. Например, предположим, что мы хотим хранить каждое делегированное строковое свойство в текстовом файле с именем, соответствующим этому свойству:

// PropertyDelegation/FileDelegate.kt
package propertydelegation
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import checkinstructions.DataFile

class FileDelegate : ReadWriteProperty<Any?, String> {
    override fun getValue(
        thisRef: Any?,
        property: KProperty<*>
    ): String {
        val file = DataFile(property.name + ".txt")
        return if (file.exists())
            file.readText()
        else ""
    }

    override fun setValue(
        thisRef: Any?,
        property: KProperty<*>,
        value: String
    ) {
        DataFile(property.name + ".txt").writeText(value)
    }
}

Этот делегат только должен взаимодействовать с файлом и не нуждается в чем-либо через thisRef. Мы игнорируем thisRef, типизируя его как Any?, потому что Any? не имеет интересных операций. Нас интересует property.name, которое является именем поля.

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

// PropertyDelegation/Configuration.kt
package propertydelegation
import checkinstructions.DataFile
import atomictest.eq

class Configuration {
    var user by FileDelegate()
    var id by FileDelegate()
    var project by FileDelegate()
}

fun main() {
    val config = Configuration()
    config.user = "Luciano"
    config.id = "Ramalho47"
    config.project = "MyLittlePython"
    DataFile("user.txt").readText() eq "Luciano"
    DataFile("id.txt").readText() eq "Ramalho47"
    DataFile("project.txt").readText() eq "MyLittlePython"
}

Поскольку он может игнорировать окружающий тип, FileDelegate является повторно используемым.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Инструменты делегирования свойств Link to heading

Стандартная библиотека содержит специальные операции делегирования свойств. Map — один из немногих типов в библиотеке Kotlin, который предварительно настроен для использования в качестве делегированного свойства. Один Map может использоваться для хранения всех свойств в классе. Каждый идентификатор свойства становится строковым ключом для карты, а тип свойства захватывается в связанном значении:

// DelegationTools/CarService.kt
package propertydelegation
import atomictest.eq

class Driver(
    map: MutableMap<String, Any?>
) {
    var name: String by map
    var age: Int by map
    var id: String by map
    var available: Boolean by map
    var coord: Pair<Double, Double> by map
}

fun main() {
    val info = mutableMapOf<String, Any?>(
        "name" to "Bruno Fiat",
        "age" to 22,
        "id" to "X97C111",
        "available" to false,
        "coord" to Pair(111.93, 1231.12)
    )
    val driver = Driver(info)
    driver.available eq false
    driver.available = true
    info eq "{name=Bruno Fiat, age=22, " +
        "id=X97C111, available=true, " +
        "coord=(111.93, 1231.12)}"
}

Обратите внимание, что оригинальный Map info изменяется при установке driver.available = true. Это работает, потому что стандартная библиотека Kotlin содержит функции расширения Map getValue() и setValue(), которые позволяют делегировать свойства. Эти упрощенные версии показывают, как они работают:

// DelegationTools/MapAccessors.kt
package delegationtools
import kotlin.reflect.KProperty

operator fun MutableMap<String, Any>.getValue(
    thisRef: Any?, property: KProperty<*>
): Any? {
    return this[property.name]
}

operator fun MutableMap<String, Any>.setValue(
    thisRef: Any?, property: KProperty<*>,
    value: Any
) {
    this[property.name] = value
}

Чтобы увидеть фактические определения библиотеки, поместите курсор на ключевое слово by в IntelliJ IDEA или Android Studio и вызовите “Перейти к объявлению”.

Delegates.observable() отслеживает изменения изменяемого свойства. Здесь мы отслеживаем старые и новые значения:

// DelegationTools/Team.kt
package delegationtools
import kotlin.properties.Delegates.observable
import atomictest.eq

class Team {
    var msg = ""
    var captain: String by observable("<0>") { prop, old, new ->
        msg += " ${prop.name} $old to $new "
    }
}

fun main() {
    val team = Team()
    team.captain = "Adam"
    team.captain = "Amanda"
    team.msg eq "captain <0> to Adam captain Adam to Amanda"
}

observable() принимает два аргумента:

  1. Начальное значение для свойства; в данном случае это “<0>”.
  2. Функцию, которая выполняется при изменении свойства. Здесь мы используем лямбду. Аргументы функции — это изменяемое свойство, текущее значение этого свойства и значение, на которое оно изменяется.

Delegates.vetoable() позволяет предотвратить изменение свойства, если новое значение свойства не удовлетворяет заданному предикату. Здесь aName() настаивает на том, чтобы имя капитана команды начиналось с буквы “A”:

// DelegationTools/TeamWithTraditions.kt
package delegationtools
import atomictest.*
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

fun aName(
    property: KProperty<*>,
    old: String,
    new: String
) = if (new.startsWith("A")) {
    trace(" $old -> $new")
    true
} else {
    trace("Имя должно начинаться с 'A'")
    false
}

interface Captain {
    var captain: String
}

class TeamWithTraditions : Captain {
    override var captain: String by Delegates.vetoable("Adam", ::aName)
}

class TeamWithTraditions2 : Captain {
    override var captain: String by Delegates.vetoable("Adam") { _, old, new ->
        if (new.startsWith("A")) {
            trace(" $old -> $new")
            true
        } else {
            trace("Имя должно начинаться с 'A'")
            false
        }
    }
}

fun main() {
    listOf(
        TeamWithTraditions(),
        TeamWithTraditions2()
    ).forEach {
        it.captain = "Amanda"
        it.captain = "Bill"
        it.captain eq "Amanda"
    }
    trace eq """
    Adam -> Amanda
    Имя должно начинаться с 'A'
    Adam -> Amanda
    Имя должно начинаться с 'A'
    """
}

Delegates.vetoable() принимает два аргумента: начальное значение для свойства и функцию onChange(), которая в этом примере является ::aName. onChange() принимает три аргумента: property: KProperty<*>, старое значение, которое в данный момент хранится в свойстве, и новое значение, которое помещается в свойство. Функция возвращает логическое значение, указывающее, было ли изменение успешным или предотвращенным.

TeamWithTraditions2 определяет Delegates.vetoable() с использованием лямбды вместо функции aName().

Оставшимся инструментом в properties.Delegates является notNull(), который создает свойство, которое должно быть инициализировано перед тем, как его можно будет прочитать:

// DelegationTools/NeverNull.kt
package delegationtools
import atomictest.*
import kotlin.properties.Delegates

class NeverNull {
    var nn: Int by Delegates.notNull()
}

fun main() {
    val non = NeverNull()
    capture {
        non.nn
    } eq "IllegalStateException: Property " +
        "nn should be initialized before get."
    non.nn = 11
    non.nn eq 11
}

Попытка прочитать non.nn до того, как nn было присвоено значение, вызывает исключение. После того, как nn было присвоено, вы можете успешно его прочитать.

Упражнения и решения можно найти на www.AtomicKotlin.com.

Ленивая инициализация Link to heading

На данный момент вы узнали два способа инициализации свойств:

  1. Хранить начальное значение в момент определения или в конструкторе.
  2. Определить пользовательский геттер, который вычисляет свойство при каждом доступе.

Этот атом исследует третий случай использования: дорогостоящая инициализация, которая может вам не понадобиться сразу или вообще. Например:

  • Сложные и времязатратные вычисления
  • Сетевые запросы
  • Доступ к базе данных

Это может привести к двум проблемам:

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

Это происходит достаточно часто, что Kotlin включает встроенное решение. Ленивая переменная инициализируется, когда она впервые используется, а не когда создается. Если мы никогда не используем ленивую переменную, она никогда не выполняет эту дорогостоящую инициализацию.

Концепция ленивых свойств не уникальна для Kotlin. Лень может быть реализована в других языках, независимо от того, предоставляют ли они прямую поддержку. Kotlin предлагает последовательный, узнаваемый идиом для таких свойств, используя делегирование свойств. С ленивой переменной используется следующий синтаксис:

val lazyProperty by lazy { initializer }

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

// LazyInitialization/LazySyntax.kt
package lazyinitialization
import atomictest.*

val idle: String by lazy {
    trace("Инициализация 'idle'")
    "Я никогда не используюсь"
}

val helpful: String by lazy {
    trace("Инициализация 'helpful'")
    "Я помогаю!"
}

fun main() {
    trace(helpful)
    trace eq """
    Инициализация 'helpful'
    Я помогаю!
    """
}

Свойство idle не инициализируется, потому что оно никогда не запрашивается. Обратите внимание, что как helpful, так и idle являются val. Без ленивой инициализации вам пришлось бы сделать их var, что привело бы к менее надежному коду.

Мы можем увидеть всю работу, которую выполняет ленивая инициализация, реализовав поведение для свойства Int без нее:

// LazyInitialization/LazyInt.kt
package lazyinitialization
import atomictest.*

class LazyInt(val init: () -> Int) {
    private var helper: Int? = null
    val value: Int
        get() {
            if (helper == null)
                helper = init()
            return helper!!
        }
}

fun main() {
    val later = LazyInt {
        trace("Инициализация 'later'")
        5
    }
    trace("Первый доступ к 'value':")
    trace(later.value)
    trace("Второй доступ к 'value':")
    trace(later.value)
    trace eq """
    Первый доступ к 'value':
    Инициализация 'later'
    5
    Второй доступ к 'value':
    5
    """
}

Свойство value не хранит значение, а вместо этого имеет геттер, который извлекает значение из свойства helper. Это похоже на код, который Kotlin генерирует для lazy.

Теперь мы можем сравнить три способа инициализации свойства — в момент определения, с использованием геттера и с использованием ленивой инициализации:

// LazyInitialization/PropertyOptions.kt
package lazyinitialization
import atomictest.trace

fun compute(i: Int): Int {
    trace("Вычисление $i")
    return i
}

object Properties {
    val atDefinition = compute(1)
    val getter
        get() = compute(2)
    val lazyInit by lazy { compute(3) }
    val never by lazy { compute(4) }
}

fun main() {
    listOf(
        Properties::atDefinition,
        Properties::getter,
        Properties::lazyInit
    ).forEach {
        trace(" ${it.name} :")
        trace(" ${it.get()} ")
        trace(" ${it.get()} ")
    }
    trace eq """
    Вычисление 1
    atDefinition:
    1
    1
    getter:
    Вычисление 2
    2
    Вычисление 2
    2
    lazyInit:
    Вычисление 3
    3
    3
    """
}
  • atDefinition инициализируется, когда вы создаете экземпляр Properties.
  • “Вычисление 1” появляется перед “atDefinition:”, что показывает, что инициализация происходит до любых обращений.
  • getter вычисляется каждый раз, когда вы к нему обращаетесь. “Вычисление 2” появляется дважды, по одному разу для каждого доступа к свойству.
  • Инициализационное значение для lazyInit вычисляется только в первый раз, когда оно запрашивается. Инициализация никогда не происходит, если вы не обращаетесь к этому свойству — обратите внимание, что “Вычисление 4” никогда не появляется в трассировке.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Поздняя инициализация Link to heading

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

Рассмотрим интерфейс Bag с методом setUp(), который инициализирует экземпляры:

// LateInitialization/Bag.kt
package lateinitialization

interface Bag {
    fun setUp()
}

Предположим, мы хотим повторно использовать библиотеку, которая создает и манипулирует Bag и гарантирует, что метод setUp() будет вызван. Эта библиотека требует инициализации подкласса в методе setUp(), а не в конструкторе:

// LateInitialization/Suitcase.kt
package lateinitialization

import atomictest.eq

class Suitcase : Bag {
    private var items: String? = null

    override fun setUp() {
        items = "носки, куртка, ноутбук"
    }

    fun checkSocks(): Boolean =
        items?.contains("носки") ?: false
}

fun main() {
    val suitcase = Suitcase()
    suitcase.setUp()
    suitcase.checkSocks() eq true
}

Suitcase инициализирует items, переопределяя метод setUp(). Однако мы не можем просто определить items как String — если мы это сделаем, мы должны предоставить ненулевую инициализацию в конструкторе. Использование заглушечного значения, такого как пустая строка, является плохой практикой, потому что вы никогда не знаете, было ли оно действительно инициализировано. null указывает на то, что оно не инициализировано.

Определение items как nullable String? означает, что мы должны проверять на null во всех членах функций, как в checkSocks(). Однако мы знаем, что библиотека, которую мы повторно используем, инициализирует items, вызывая setUp(), поэтому проверки на null не должны быть необходимы. Модификатор свойства lateinit решает эту проблему — здесь мы инициализируем items после создания экземпляра BetterSuitcase:

// LateInitialization/BetterSuitcase.kt
package lateinitialization

import atomictest.eq

class BetterSuitcase : Bag {
    lateinit var items: String

    override fun setUp() {
        items = "носки, куртка, ноутбук"
    }

    fun checkSocks() = "носки" in items
}

fun main() {
    val suitcase = BetterSuitcase()
    suitcase.setUp()
    suitcase.checkSocks() eq true
}

Сравните эту версию checkSocks() с той, что в Suitcase.kt. Модификатор lateinit означает, что items безопасно определено как ненулевое свойство.

lateinit может использоваться для свойства внутри тела класса, для свойства верхнего уровня или локальной переменной.

Ограничения:

  • lateinit может использоваться только для var свойства, а не для val.
  • Свойство должно быть ненулевым типом.
  • Свойство не может быть примитивным типом.
  • lateinit не разрешен для абстрактных свойств в абстрактном классе или интерфейсе.
  • lateinit не разрешен для свойств с пользовательскими get() или set().

Что произойдет, если вы забудете инициализировать такое свойство? Вы не получите ошибок или предупреждений на этапе компиляции, потому что логика инициализации может быть сложной и зависеть от других свойств, которые Kotlin не может отслеживать:

// LateInitialization/FaultySuitcase.kt
package lateinitialization

import atomictest.*

class FaultySuitcase : Bag {
    lateinit var items: String

    override fun setUp() {}
    
    fun checkSocks() = "носки" in items
}

fun main() {
    val suitcase = FaultySuitcase()
    suitcase.setUp()
    capture {
        suitcase.checkSocks()
    } eq "UninitializedPropertyAccessException: lateinit property items has not been initialized"
}

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

isInitialized сообщает вам, было ли свойство lateinit инициализировано. Свойство должно находиться в вашей текущей области видимости и доступаться с помощью оператора :::

// LateInitialization/IsInitialized.kt
package lateinitialization

import atomictest.*

class WithLate {
    lateinit var x: String

    fun status() = " ${::x.isInitialized} "
}

lateinit var y: String

fun main() {
    trace(" ${::y.isInitialized} ")
    y = "Готово"
    trace(" ${::y.isInitialized} ")
    val withlate = WithLate()
    trace(withlate.status())
    withlate.x = "Установлено"
    trace(withlate.status())
    trace eq "false true false true"
}

Хотя вы можете создать локальную переменную lateinit var, вы не можете вызвать isInitialized для нее, потому что ссылки на локальные var или val не поддерживаются.

Упражнения и решения можно найти на сайте www.AtomicKotlin.com.

Приложения Link to heading

Приложение A: AtomicTest Link to heading

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

  • Тестирование вводит фреймворк и описывает функции eq и neq, а также объект trace.
  • Исключения вводят функцию capture().
  • Обработка исключений описывает реализацию функции capture().
  • Юнит-тестирование использует AtomicTest, чтобы помочь ввести концепцию юнит-тестирования.

// AtomicTest/AtomicTest.kt package atomictest import kotlin.math.abs import kotlin.reflect.KClass const val ERROR_TAG = “[Ошибка]: " private fun <L, R> test( actual: L, expected: R, checkEquals: Boolean = true, predicate: () -> Boolean ) { println(actual) if (!predicate()) { print(ERROR_TAG) println(” $ actual " + ( if (checkEquals) “!=” else “==”) + " $ expected”) } }

/** Приложение A: AtomicTest 555

  • Сравнивает строковое представление
  • этого объекта со строкой rval. */ infix fun Any.eq(rval: String) { test( this, rval) { toString().trim() == rval.trimIndent() } }

/**

  • Проверяет, что этот объект равен rval. */ infix fun T.eq(rval: T) { test( this, rval) { this == rval } }

/**

  • Проверяет, что этот объект не равен rval. */ infix fun T.neq(rval: T) { test( this, rval, checkEquals = false) { this != rval } }

/**

  • Проверяет, что число Double равно
  • rval с учетом положительного дельты. */ infix fun Double.eq(rval: Double) { test( this, rval) { abs( this - rval) < 0.0000001 } }

/**

  • Содержит информацию о захваченном исключении: / class CapturedException ( private val exceptionClass: KClass<>?, private val actualMessage: String ) { private val fullMessage: String get() { val className = exceptionClass?.simpleName ?: "” return className + actualMessage }

    infix fun eq(message: String) { fullMessage eq message }

    infix fun contains(parts: List< String >) { if (parts.any { it !in fullMessage }) { print(ERROR_TAG) println(“Фактическое сообщение: $ fullMessage”) println(“Ожидаемые части: $ parts”) } }

    override fun toString() = fullMessage }

/**

  • Захватывает исключение и производит
  • информацию о нем. Использование:
  • capture {
  • // Код, который вызывает ошибку
  • } eq “FailureException: сообщение” */ fun capture(f: () -> Unit): CapturedException = try { f() CapturedException( null, " $ ERROR_TAG Ожидалось исключение”) } catch (e: Throwable) { CapturedException(e::class, (e.message?.let { “: $ it” } ?: “”)) }

/**

  • Накопливает вывод, когда вызывается так:

  • trace(“информация”)

  • trace(объект)

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

  • trace eq “ожидаемый вывод” */ object trace { private val trc = mutableListOf< String >()

    operator fun invoke(obj: Any?) { trc += obj.toString() }

    /**

    • Сравнивает содержимое trc с многострочной
    • String, игнорируя пробелы. */ infix fun eq(multiline: String) { val trace = trc.joinToString("\n”) val expected = multiline.trimIndent() .replace("\n”, " “) test(trace, multiline) { trace.replace("\n”, " “) == expected } trc.clear() } } AtomicKotlin(www.AtomicKotlin.com) авторы Брюс Эккель и Светлана Исакова, ©2021 MindView LLC

Приложение B: Java Link to heading

Совместимость Link to heading

Вы можете легко вызывать Java-код из Kotlin и код Kotlin из Java. Основной целью дизайна Kotlin является создание бесшовного опыта для программистов на Java. Если вы хотите постепенно перейти на Kotlin, вы можете легко начать, добавляя небольшие фрагменты Kotlin в ваш существующий Java-проект. Таким образом, вы можете писать новый код на Kotlin поверх вашей Java-базы, извлекая выгоду из возможностей языка Kotlin, не будучи вынужденным переписывать Java-код, когда это не имеет смысла. Этот раздел рассматривает вопросы и техники взаимодействия между Kotlin и Java.

Вызов Java из Kotlin Link to heading

Чтобы использовать класс Java из Kotlin, импортируйте его, создайте экземпляр и вызовите функцию, так же как вы бы сделали это в Java. Здесь мы используем java.util.Random():

// interoperability/Random.kt
import atomictest.eq
import java.util.Random

fun main() {
    val rand = Random(47)
    rand.nextInt(100) eq 58
}

Как и при создании любого экземпляра в Kotlin, вам не нужно использовать new, как в Java. Класс из Java-библиотеки работает как нативный класс Kotlin.

Геттеры и сеттеры в стиле JavaBean в классе Java становятся свойствами в Kotlin:

// interoperability/Chameleon.java
package interoperability;

import java.io.Serializable;

public class Chameleon implements Serializable {
    private int size;
    private String color;

    public int getSize() {
        return size;
    }

    public void setSize(int newSize) {
        size = newSize;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String newColor) {
        color = newColor;
    }
}

При работе с Java имя пакета должно быть идентично (включая регистр) имени директории. Имена пакетов Java обычно содержат только строчные буквы. Чтобы соответствовать этой конвенции, в этом приложении используются только строчные буквы в имени подкаталога примеров взаимодействия.

Импортированный класс Chameleon работает как класс Kotlin со свойствами:

// interoperability/UseBeanClass.kt
import interoperability.Chameleon
import atomictest.eq

fun main() {
    val chameleon = Chameleon()
    chameleon.size = 1
    chameleon.size eq 1
    chameleon.color = "green"
    chameleon.color eq "green"
    chameleon.color = "turquoise"
    chameleon.color eq "turquoise"
}

Atomic Kotlin (www.AtomicKotlin.com) Брюса Эккеля и Светланы Исаковой, ©2021 MindView LLC

Расширения функций особенно полезны, когда вы используете существующую Java-библиотеку, которой не хватает необходимых член-функций. Например, мы можем добавить операцию adjustToTemperature() в Chameleon:

// interoperability/ExtensionsToJavaClass.kt
package interop

import interoperability.Chameleon
import atomictest.eq

fun Chameleon.adjustToTemperature(isHot: Boolean) {
    color = if (isHot) "grey" else "black"
}

fun main() {
    val chameleon = Chameleon()
    chameleon.size = 2
    chameleon.size eq 2
    chameleon.adjustToTemperature(isHot = true)
    chameleon.color eq "grey"
}

Стандартная библиотека Kotlin содержит множество расширений для классов стандартной библиотеки Java, таких как List и String.

Вызов Kotlin из Java Link to heading

Kotlin создает библиотеки, которые могут быть использованы из Java. Для программиста на Java библиотека Kotlin выглядит как обычная библиотека Java. Поскольку все в Java является классом, начнем с класса Kotlin, содержащего свойство и функцию:

// interoperability/KotlinClass.kt
package interop

class Basic {
    var property1 = 1
    fun value() = property1 * 10
}

Если вы импортируете этот класс в Java, он будет выглядеть как обычный класс Java:

// interoperability/UsingKotlinClass.java
package interoperability;

import interop.Basic;
import static atomictest.AtomicTestKt.eq;

public class UsingKotlinClass {
    public static void main(String[] args) {
        Basic b = new Basic();
        eq(b.getProperty1(), 1);
        b.setProperty1(12);
        eq(b.value(), 120);
    }
}

Свойство property1 становится приватным полем, содержащим геттеры и сеттеры в стиле JavaBean. Член-функция value() становится методом Java с тем же именем. Мы также импортировали AtomicTest, что требует дополнительных действий в Java: мы должны импортировать его с использованием ключевого слова static и указать имя пакета. eq() может быть вызван только как обычная функция, потому что Java не поддерживает инфиксную нотацию.

Если класс Kotlin находится в том же пакете, что и код Java, вам не нужно его импортировать:

// interoperability/KotlinDataClass.kt
package interoperability

data class Staff(
    var name: String,
    var role: String
)

Классы данных генерируют дополнительные функции-члены, такие как equals(), hashCode() и toString(), которые работают без проблем в Java. В конце main() мы проверяем реализации equals() и hashCode(), помещая объект Data в HashMap, а затем извлекая его:

// interoperability/UseDataClass.java
package interoperability;

import java.util.HashMap;
import static atomictest.AtomicTestKt.eq;

public class UseDataClass {
    public static void main(String[] args) {
        Staff e = new Staff("Fluffy", "Office Manager");
        eq(e.getRole(), "Office Manager");
        e.setName("Uranus");
        e.setRole("Assistant");
        eq(e, "Staff(name=Uranus, role=Assistant)");
        
        // Вызов copy() из класса данных:
        Staff cf = e.copy("Cornfed", "Sidekick");
        eq(cf, "Staff(name=Cornfed, role=Sidekick)");
        
        HashMap<Staff, String> hm = new HashMap<>();
        // Сотрудники работают как ключи хеширования:
        hm.put(e, "Cheerful");
        eq(hm.get(e), "Cheerful");
    }
}

Если вы используете командную строку для запуска Java-кода, который включает код Kotlin, вы должны включить kotlin-runtime.jar в качестве зависимости, в противном случае вы получите исключения времени выполнения, сообщающие о том, что некоторые классы утилит библиотеки не найдены. IntelliJ IDEA автоматически включает kotlin-runtime.jar.

Функции верхнего уровня Kotlin отображаются на статические методы в классе Java, который получает свое имя от файла Kotlin:

// interoperability/TopLevelFunction.kt
package interop

fun hi() = "Hello!"

Чтобы импортировать, укажите имя класса, сгенерированное Kotlin. Это имя также должно использоваться при вызове статического метода:

// interoperability/CallTopLevelFunction.java
package interoperability;

import interop.TopLevelFunctionKt;
import static atomictest.AtomicTestKt.eq;

public class CallTopLevelFunction {
    public static void main(String[] args) {
        eq(TopLevelFunctionKt.hi(), "Hello!");
    }
}

Если вы не хотите квалифицировать hi() именем пакета, используйте import static, как мы делаем с AtomicTest:

// interoperability/CallTopLevelFunction2.java
package interoperability;

import static interop.TopLevelFunctionKt.hi;
import static atomictest.AtomicTestKt.eq;

public class CallTopLevelFunction2 {
    public static void main(String[] args) {
        eq(hi(), "Hello!");
    }
}

Если вам не нравится имя класса, сгенерированное Kotlin, вы можете изменить его с помощью аннотации @JvmName:

// interoperability/ChangeName.kt
@file:JvmName("Utils")
package interop

fun salad() = "Lettuce!"

Теперь вместо ChangeNameKt мы используем Utils:

// interoperability/MakeSalad.java
package interoperability;

import interop.Utils;
import static atomictest.AtomicTestKt.eq;

public class MakeSalad {
    public static void main(String[] args) {
        eq(Utils.salad(), "Lettuce!");
    }
}

Вы можете найти дополнительные детали в документации.

Адаптация Java к Kotlin Link to heading

Одной из целей дизайна Kotlin является возможность взять существующий тип Java и адаптировать его под ваши нужды. Эта способность не ограничивается только дизайнерами библиотек — ту же логику можно применить к любому внешнему коду.

В Recursion мы создали Fibonacci.kt для эффективного вычисления чисел Фибоначчи. Эта реализация ограничена размером Long, который она возвращает. Если вы хотите возвращать более крупные значения, стандартная библиотека Java включает класс BigInteger. Несколько строк кода преобразуют BigInteger во что-то, что ощущается как нативный класс Kotlin:

// interoperability/BigInt.kt
package biginteger

import java.math.BigInteger

fun Int.toBigInteger(): BigInteger =
    BigInteger.valueOf(toLong())

fun String.toBigInteger(): BigInteger =
    BigInteger(this)

operator fun BigInteger.plus(other: BigInteger): BigInteger = add(other)

Функции расширения toBigInteger() преобразуют любой Int или String в BigInteger, вызывая конструктор BigInteger и передавая строку-приемник в качестве аргумента. Перегрузка оператора BigInteger.plus() позволяет вам писать number + other. Это делает работу с BigInteger более приятной по сравнению с громоздким number.plus(other) в Java.

Используя BigInteger, Recursion/Fibonacci.kt легко преобразуется для получения гораздо больших результатов:

// interoperability/BigFibonacci.kt
package interop

import atomictest.eq
import java.math.BigInteger
import java.math.BigInteger.ONE
import java.math.BigInteger.ZERO

fun fibonacci(n: Int): BigInteger {
    tailrec fun fibonacci(n: Int, current: BigInteger, next: BigInteger): BigInteger {
        if (n == 0) return current
        return fibonacci(n - 1, next, current + next) // [1]
    }
    return fibonacci(n, ZERO, ONE)
}

fun main() {
    (0..7).map { fibonacci(it) } eq "[0, 1, 1, 2, 3, 5, 8, 13]"
    fibonacci(22) eq 17711.toBigInteger()
    fibonacci(150) eq "9969216677189303386214405760200".toBigInteger()
}

Все Long были заменены на BigInteger. В main() вы видите, как Int и String преобразуются в BigInteger с использованием различных свойств расширения toBigInteger(). В строке [1] мы используем оператор plus для нахождения суммы current + next; это идентично оригинальной версии с использованием Long.

Вызов fibonacci(150) вызывает переполнение в версии Recursion/Fibonacci.kt, но работает нормально после преобразования в BigInteger.

Проверяемые исключения в Java и Kotlin Link to heading

Java была в значительной степени спроектирована по образцу языка C++, который позволял указывать исключения, которые может выбрасывать функция. Дизайнеры Java решили сделать шаг дальше и заставить каждого, кто вызывает эту функцию, обрабатывать каждое указанное исключение. Это казалось хорошей идеей в то время, и так родились проверяемые исключения — эксперимент, который, насколько нам известно, не был повторен в последующих языках программирования.

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

// interoperability/JavaChecked.java
package interoperability;
import java.io.*;
import java.nio.file.*;
import static atomictest.AtomicTestKt.eq;

public class JavaChecked {
    // Путь к текущему исходному файлу, основанный на директории, где вызывается Gradle:
    static Path thisFile = Paths.get("DataFiles", "file_wubba.txt");

    public static void main(String[] args) {
        BufferedReader source = null;
        try {
            source = new BufferedReader(new FileReader(thisFile.toFile()));
        } catch (FileNotFoundException e) {
            // Восстановление после ошибки открытия файла
        }
        try {
            String first = source.readLine();
            eq(first, "wubba lubba dub dub");
        } catch (IOException e) {
            // Восстановление после ошибки чтения
        }
        try {
            source.close();
        } catch (IOException e) {
            // Восстановление после ошибки закрытия
        }
    }
}

Каждая из вышеуказанных операций включает проверяемые исключения и должна быть помещена внутри блока try, иначе Java выдаст ошибки компиляции за необработанные исключения. Единственная причина для обработки исключения — это если вы можете каким-то образом восстановиться от проблемы. Если это не то, что вы можете исправить, нет смысла писать блок catch для этого исключения — просто позвольте ему стать отчетом об ошибке. В приведенных выше примерах восстановление от ошибок кажется сомнительным, но вас все равно заставляют писать блоки try - catch.

Давайте перепишем этот пример на Kotlin:

// interoperability/KotlinChecked.kt
import atomictest.eq
import java.io.File

fun main() {
    File("DataFiles/file_wubba.txt")
        .readLines()[0] eq "wubba lubba dub dub"
}

Kotlin позволяет нам сократить операцию до одной строки кода, потому что он добавляет функции расширения к классу File в Java. В то же время Kotlin устраняет проверяемые исключения. Если бы мы захотели, мы могли бы окружить промежуточные операции блоками try - catch, но Kotlin не требует проверяемых исключений. Это обеспечивает отчетность об ошибках, не заставляя вас писать дополнительный шумный код.

Библиотеки Java часто используют проверяемые исключения в ситуациях, которые находятся вне контроля программиста и обычно не поддаются восстановлению. В этих случаях лучше всего обрабатывать исключение на верхнем уровне и перезапустить процесс, если это возможно. Требование, чтобы все промежуточные уровни передавали исключение, только добавляет когнитивную нагрузку при попытке понять код.

Если вы пишете код на Kotlin, который вызывается из Java, и вам нужно указать проверяемое исключение, Kotlin предоставляет аннотацию @Throws, чтобы передать эту информацию вызывающему Java:

// interoperability/AnnotateThrows.kt
package interop
import java.io.IOException

@Throws(IOException::class)
fun hasCheckedException() {
    throw IOException()
}

Вот как hasCheckedException() вызывается из Java:

// interoperability/CatchChecked.java
package interoperability;
import interop.AnnotateThrowsKt;
import java.io.IOException;
import static atomictest.AtomicTestKt.eq;

public class CatchChecked {
    public static void main(String[] args) {
        try {
            AnnotateThrowsKt.hasCheckedException();
        } catch (IOException e) {
            eq(e, "java.io.IOException");
        }
    }
}

Если вы не обработаете исключение, компилятор Java выдаст сообщение об ошибке. Хотя Kotlin включает поддержку языка для обработки исключений, он склонен подчеркивать отчетность об ошибках и оставляет обработку исключений для тех редких случаев, когда вы действительно можете восстановиться от проблемы (почти исключительно операции ввода-вывода).

Nullable Types и Java Link to heading

Kotlin гарантирует, что чистый код на Kotlin не содержит ошибок, связанных с null, но когда вы обращаетесь к Java, таких гарантий нет. В следующем коде на Java метод get() иногда возвращает null:

// interoperability/JTool.java
package interoperability;

public class JTool {
    public static JTool get(String s) {
        if (s == null) return null;
        return new JTool();
    }

    public String method() {
        return "Success";
    }
}

Чтобы использовать JTool в Kotlin, вы должны знать, как работает метод get(). У вас есть три варианта, показанные здесь в определениях a, b и c:

// interoperability/PlatformTypes.kt
package interop

import interoperability.JTool
import atomictest.eq

object KotlinCode {
    val a: JTool? = JTool.get("") // [1]
    val b: JTool = JTool.get("") // [2]
    val c = JTool.get("") // [3]
}

fun main() {
    with(KotlinCode) {
        a?.method() eq "Success" // [4]
        b.method() eq "Success"
        c.method() eq "Success" // [5]
        ::a.returnType eq "interoperability.JTool?"
        ::b.returnType eq "interoperability.JTool"
        ::c.returnType eq "interoperability.JTool!" // [6]
    }
}
  • [1] Укажите тип как nullable.
  • [2] Укажите тип как non-nullable.
  • [3] Используйте вывод типов.

Функция with() в main() позволяет нам ссылаться на a, b и c без квалификации KotlinCode. Поскольку идентификаторы находятся внутри объекта, мы можем использовать синтаксис ссылок на члены и свойство returnType для определения их типов. Чтобы инициализировать a, b и c, мы передаем ненулевую строку в get(), поэтому a, b и c все получают ненулевые ссылки, и каждый из них может успешно вызвать метод().

  • [4] Поскольку a является nullable, он должен использовать ?. при вызовах методов.
  • [5] c ведет себя как ненулевая ссылка и может быть разыменован без дополнительных проверок.
  • [6] Обратите внимание, что c не возвращает ни nullable тип, ни non-nullable тип, а нечто совершенно другое: JTool!.

Тип! является платформенным типом Kotlin и не имеет обозначения — вы не можете записать его в свой код. Он используется всякий раз, когда Kotlin должен вывести тип за пределами своей области. Если тип приходит из Java, его доступ может привести к исключению NullPointerException (NPE). Вот что происходит, когда JTool.get() возвращает нулевую ссылку:

// interoperability/NPEOnPlatformType.kt
import interoperability.JTool
import atomictest.*

fun main() {
    val xn: JTool? = JTool.get(null) // [1]
    xn?.method() eq null
    val yn = JTool.get(null) // [2]
    yn?.method() eq null // [3]
    capture {
        yn.method() // [4]
    } contains listOf("NullPointerException")
    capture {
        val zn: JTool = JTool.get(null) // [5]
    } eq "NullPointerException: get(null) must not be null"
}

Когда вы вызываете метод Java, такой как JTool.get(), внутри Kotlin, его возвращаемое значение (если не аннотировано, как объяснено в следующем разделе) является платформенным типом, который в данном случае — JTool!.

  • [1] Поскольку xn является nullable типом JTool?, он может успешно получить null. Присваивание nullable типу безопасно, потому что Kotlin заставляет вас проверять на null, используя ?. при вызове метода().
  • [2] На момент определения yn успешно получает null без жалоб, потому что Kotlin выводит его как платформенный тип JTool!.
  • [3] Вы можете разыменовать yn, используя безопасный доступ ?. , который в данном случае возвращает null.
  • [4] Однако использование ?. не обязательно. Вы можете просто разыменовать yn. В этом случае вы получите NullPointerException без какого-либо полезного сообщения.
  • [5] Присваивание ненулевому типу может привести к NPE. Kotlin проверяет на null в момент присваивания. Инициализация zn завершается неудачей, потому что объявленный тип JTool обещает, что zn не является nullable, но он получает null, что приводит к NullPointerException, на этот раз с полезным сообщением.

Сообщение об исключении содержит подробную информацию о выражении, которое вызвало null: NullPointerException: get(null) must not be null. Хотя это исключение времени выполнения, исчерпывающее сообщение об ошибке делает проблему гораздо проще, чем исправление обычного NPE.

Платформенный тип содержит наименьшее количество информации, доступной для этого типа. В данном случае он только говорит вам, что тип — JTool. Он может быть как nullable, так и non-nullable — при использовании выведенного платформенного типа вы просто не знаете.

Вы не можете явно объявить платформенный тип (например, JTool!). Вы можете только наблюдать платформенный тип в сообщениях об ошибках или когда вы отображаете выведенный тип, как в PlatformTypes.kt, или проверяя тип в IDE.

Когда вы работаете над смешанным проектом на Kotlin и Java, у вас может не быть контроля над кодовой базой Java. При использовании внешней Java-библиотеки вы не можете изменить исходный код, поэтому вам нужно работать с платформенными типами.

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

Аннотации Нулевости Link to heading

Если вы контролируете кодовую базу Java, вы можете добавить аннотации нулевости к коду Java и избежать тонких ошибок NPE. @Nullable и @NotNull указывают Kotlin рассматривать тип Java как нулевой или ненулевой соответственно. Здесь мы добавляем аннотации нулевости Kotlin в JTool.java:

AtomicKotlin(www.AtomicKotlin.com) авторы Bruce Eckel и Svetlana Isakova, ©2021 MindView LLC
Приложение B: Java Интероперабельность 573

// interoperability/AnnotatedJTool.java
package interoperability;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class AnnotatedJTool {
    @Nullable
    public static JTool getUnsafe(@Nullable String s) {
        if (s == null) return null;
        return getSafe(s);
    }

    @NotNull
    public static JTool getSafe(@NotNull String s) {
        return new JTool();
    }

    public String method() {
        return "Success";
    }
}

Применение аннотации к параметру Java влияет только на этот параметр. Применение аннотации перед методом Java изменяет возвращаемый тип.

Когда вы вызываете getUnsafe() и getSafe() в Kotlin, Kotlin рассматривает функции-члены AnnotatedJTool как нативные Kotlin нулевые или ненулевые:

// interoperability/AnnotatedJava.kt
package interop

import interoperability.AnnotatedJTool
import atomictest.eq

object KotlinCode2 {
    val a = AnnotatedJTool.getSafe("")
    // Не компилируется:
    // val b = AnnotatedJTool.getSafe(null)
    val c = AnnotatedJTool.getUnsafe("")
    val d = AnnotatedJTool.getUnsafe(null)
}

fun main() {
    AtomicKotlin(www.AtomicKotlin.com) авторы Bruce Eckel и Svetlana Isakova, ©2021 MindView LLC  
    Приложение B: Java Интероперабельность 574
    with(KotlinCode2) {
        ::a.returnType eq "interoperability.JTool"
        ::c.returnType eq "interoperability.JTool?"
        ::d.returnType eq "interoperability.JTool?"
    }
}

@NotNull JTool преобразуется в ненулевой тип Kotlin JTool, а аннотированный @Nullable JTool преобразуется в JTool? Kotlin. Вы можете увидеть это в типах, показанных в main() для a, c и d.

Вы не можете передать нулевой аргумент, когда ожидается ненулевой аргумент, даже если это тип Java, аннотированный @NotNull, поэтому Kotlin не скомпилирует AnnotatedJTool.getSafe(null).

Поддерживаются различные виды аннотаций нулевости с использованием различных имен:

  • @Nullable и @CheckForNull указаны стандартом JSR-305.
  • @Nullable и @NonNull используются в Android.
  • @Nullable и @NotNull поддерживаются инструментами JetBrains.
  • Есть и другие. Вы можете найти полный список в документации Kotlin.

Kotlin обнаруживает аннотации нулевости по умолчанию для пакета или класса Java, как указано в стандарте JSR-305. Если по умолчанию это @NotNull, вы должны явно указывать только аннотации @Nullable. Если по умолчанию это @Nullable, вы должны явно указывать только аннотации @NotNull. Документация содержит технические детали для выбора аннотации по умолчанию.

Если вы разрабатываете смешанные проекты на Kotlin и Java, ваши приложения будут безопаснее, если вы используете аннотации нулевости в вашем коде Java.

Коллекции и Java Link to heading

Эта книга не требует знаний Java. Однако, когда вы пишете код на Kotlin для Java Virtual Machine (JVM), полезно быть знакомым со стандартной библиотекой коллекций Java, потому что Kotlin использует её для создания своих собственных коллекций.

Библиотека коллекций Java представляет собой набор классов и интерфейсов, которые реализуют структуры данных коллекций, такие как списки, множества и карты. Эти структуры данных обычно имеют четкие и простые интерфейсы, но для повышения производительности могут иметь сложные реализации. Новые языки обычно создают свои собственные библиотеки коллекций с нуля. Например, язык Scala имеет свою собственную библиотеку коллекций, которая во многом превосходит библиотеку коллекций Java, но также делает более сложным смешивание Scala и Java.

Библиотека коллекций Kotlin намеренно не переписывается с нуля. Вместо этого она состоит из улучшений, основанных на библиотеке коллекций Java. Например, когда вы создаете изменяемый List, вы на самом деле используете ArrayList из Java:

// interoperability/HiddenArrayList.kt
import atomictest.eq

fun main() {
    val list = mutableListOf(1, 2, 3)
    list.javaClass.name eq "java.util.ArrayList"
}

Для бесшовной совместимости с кодом Java Kotlin использует интерфейсы из стандартной библиотеки Java, а часто и те же реализации. Это дает три преимущества:

  1. Код на Kotlin может легко смешиваться с кодом на Java. Не требуется дополнительная конвертация при передаче коллекций Kotlin в код Java.
  2. Годы оптимизации производительности в стандартной библиотеке Java автоматически доступны программистам на Kotlin.
  3. Стандартная библиотека, включенная в приложение на Kotlin, мала, потому что она использует коллекции Java, а не определяет свои собственные. Стандартная библиотека Kotlin в основном состоит из функций расширения, которые улучшают коллекции Java.

Kotlin также исправляет проблему проектирования. В Java все интерфейсы коллекций изменяемые. Например, java.util.List имеет методы add() и remove(), которые изменяют List. Как мы показали на протяжении всей этой книги, изменяемость является источником значительного количества проблем программирования. Таким образом, в Kotlin тип коллекции по умолчанию является только для чтения:

// interoperability/ReadOnlyByDefault.kt
package interop

data class Animal(val name: String)

interface Zoo {
    fun viewAnimals(): Collection<Animal>
}

fun visitZoo(zoo: Zoo) {
    val animals = zoo.viewAnimals()
    // Ошибка компиляции:
    // animals.add(Animal("Grumpy Cat"))
}

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

Java предоставляет частичное решение для неизменяемости коллекций: при возврате коллекции вы можете поместить её в специальную обертку, которая выбрасывает исключение при любой попытке изменить основную коллекцию. Это не обеспечивает статическую проверку типов, но может предотвратить тонкие ошибки. Однако вы должны помнить о необходимости обернуть коллекцию, чтобы сделать её только для чтения, тогда как в Kotlin вы должны быть явными, когда хотите изменяемую коллекцию.

Kotlin имеет отдельные интерфейсы для изменяемых и только для чтения коллекций:

  • Collection / MutableCollection
  • List / MutableList
  • Set / MutableSet
  • Map / MutableMap

Эти интерфейсы дублируют интерфейсы из стандартной библиотеки Java:

  • java.util.Collection
  • java.util.List
  • java.util.Set
  • java.util.Map

В Kotlin, как и в Java, Collection является суперклассом для List и Set. MutableCollection расширяет Collection и является суперклассом для MutableList и MutableSet. Вот базовая структура:

// interoperability/CollectionStructure.kt
package collectionstructure

interface Collection<E>
interface List<E> : Collection<E>
interface Set<E> : Collection<E>
interface Map<K, V>
interface MutableCollection<E>
interface MutableList<E> : List<E>, MutableCollection<E>
interface MutableSet<E> : Set<E>, MutableCollection<E>
interface MutableMap<K, V> : Map<K, V>

Для простоты мы показываем только имена, а не полные объявления из стандартной библиотеки Kotlin.

Изменяемые коллекции Kotlin соответствуют их аналогам в Java. Если вы сравните MutableCollection из kotlin.collections с java.util.List, вы увидите, что они объявляют одни и те же функции-члены (методы, в терминологии Java). Collection, List, Set и Map в Kotlin также дублируют интерфейсы Java, но без каких-либо методов мутации.

Обе kotlin.collections.List и kotlin.collections.MutableList видимы из Java как java.util.List. Эти интерфейсы особенные: они существуют только в Kotlin, но на уровне байт-кода они оба заменяются на List Java.

Список Kotlin может быть приведен к списку Java:

// interoperability/JavaList.kt
import atomictest.eq

fun main() {
    val list = listOf(1, 2, 3)
    (list is java.util.List<*>) eq true
}

Этот код выдает предупреждение:

  • Этот класс не должен использоваться в Kotlin.
  • Используйте kotlin.collections.List или kotlin.collections.MutableList вместо этого.

Это напоминание использовать интерфейсы Kotlin, а не Java, при программировании на Kotlin. Имейте в виду, что “только для чтения” не то же самое, что “неизменяемый”. Коллекция не может быть изменена с помощью ссылки только для чтения, но она все равно может измениться:

// interoperability/ReadOnlyCollections.kt
import atomictest.eq

fun main() {
    val mutable = mutableListOf(1, 2, 3)
    // Ссылка только для чтения на изменяемый список:
    val list: List<Int> = mutable
    mutable += 4
    // список изменился:
    list eq "[1, 2, 3, 4]"
}

Здесь список только для чтения ссылается на MutableList, который затем может быть изменен путем манипуляции с mutable. Поскольку все коллекции Java изменяемы, код Java может изменять коллекцию Kotlin только для чтения, даже если вы передаете её через ссылку только для чтения.

Коллекции Kotlin не обеспечивают полной безопасности, но предоставляют хорошую компромиссу между наличием лучшей библиотеки и поддержанием совместимости с Java.

Примитивные типы Java Link to heading

В Kotlin вы вызываете конструктор для создания объекта, но в Java вы должны использовать new, чтобы создать объект. new помещает создаваемый объект в кучу. Такие типы называются ссылочными типами. Создание объектов в куче может быть неэффективным для базовых типов, таких как числа. Для этих типов Java использует подход, принятый в C и C++: вместо создания переменной с помощью new, создается “автоматическая” переменная, которая хранит значение непосредственно. Автоматические переменные размещаются в стеке, что делает их гораздо более эффективными. Такие типы получают специальное обращение со стороны JVM и называются примитивными типами. Существует фиксированное количество примитивных типов: boolean, int, long, char, byte, short, float и double. Примитивные типы всегда содержат ненулевое значение, и их нельзя использовать в качестве обобщенных аргументов. Если вам нужно хранить null или использовать такие типы в качестве обобщенных аргументов, вы можете использовать соответствующий ссылочный тип, определенный в стандартной библиотеке Java, такой как java.lang.Boolean или java.lang.Integer. Эти типы часто называют обертками или упакованными типами, чтобы подчеркнуть, что они только оборачивают примитивное значение и хранят его в куче.

// interoperability/JavaWrapper.java
package interoperability;

import java.util.*;

public class JavaWrapper {
    public static void main(String[] args) {
        // Примитивный тип
        int i = 10;
        // Обертки
        Integer iOrNull = null;
        List<Integer> list = new ArrayList<>();
    }
}

Java различает ссылочные типы и примитивные типы, но Kotlin — нет. Вы используете один и тот же тип Int как для определения целочисленной переменной var / val, так и для использования его в качестве обобщенного аргумента. На уровне JVM Kotlin использует ту же поддержку примитивных типов. Когда это возможно, Kotlin заменяет Int на примитивный int в байт-коде. Nullable Int? или Int, используемый в качестве обобщенного аргумента, может быть представлен только с помощью обертки:

// interoperability/KotlinWrapper.kt
package interop

fun main() {
    // Генерирует примитивный int:
    val i = 10
    // Генерирует обертки:
    val iOrNull: Int? = null
    val list: List<Int> = listOf(1, 2, 3)
}

Обычно вам не нужно много думать о том, генерируются ли примитивы или обертки компилятором Kotlin, но полезно знать, как это реализовано на JVM.

Документация объясняет больше о нюансах совместимости Kotlin/Java.
Документация