Основы Kotlin
Повседневный слой синтаксиса — val против var, выражения if и when, строковые шаблоны, Unit, иерархия Any, аргументы по умолчанию и именованные аргументы, функции верхнего уровня и scope-функции let/run/with/apply/also.
12 вопросов
JuniorТеорияОчень частоПочему if и when в Kotlin — выражения, а не инструкции?
Почему if и when в Kotlin — выражения, а не инструкции?
Они дают значение, поэтому их можно присвоить напрямую: val max = if (a > b) a else b. Значением становится последнее выражение выбранной ветки. Поэтому в Kotlin нет тернарного оператора и поэтому when в позиции выражения обязан покрыть все случаи.
Типичные ошибки
- ✗Искать тернарный оператор вместо использования
ifкак выражения - ✗Забывать, что
whenв позиции выражения обязан быть исчерпывающим - ✗Думать, что значением становится вся ветка, а не её последнее выражение
Уточняющие вопросы
- →Когда ветка
elseвwhenобязательна, а когда её можно опустить? - →Какой тип у
if, ветки которого возвращают разные типы?
JuniorТеорияОчень частоВ чём разница между val и var при объявлении переменной?
В чём разница между val и var при объявлении переменной?
var объявляет переменную, которую можно переприсваивать; val — переменную, присваиваемую ровно один раз. val фиксирует ссылку, а не объект за ней: в val с MutableList по-прежнему проходит add. По умолчанию берите val, а var — только когда нужно.
Типичные ошибки
- ✗Считать, что
valделает неизменяемым сам объект, а не только ссылку - ✗Думать, что
val— подсказка стиля, которую компилятор не проверяет - ✗Путать
valс константой времени компиляции — этоconst val
Уточняющие вопросы
- →Почему
val-свойство с собственным getter может возвращать разное значение при каждом чтении? - →Что
const valдобавляет кval, и где его можно объявлять?
JuniorТеорияЧастоЧто дают аргументы по умолчанию и именованные аргументы вместо перегрузок?
Что дают аргументы по умолчанию и именованные аргументы вместо перегрузок?
Значение по умолчанию в сигнатуре позволяет одной функции заменить целую телескопическую цепочку перегрузок. Именованные аргументы дают вызывающему пропустить средние умолчания и передать только нужное — send(msg, retries = 3) — и делают читаемым голый флаг Boolean.
Типичные ошибки
- ✗Считать, что значение по умолчанию вычисляется один раз и общее для вызовов
- ✗Ожидать, что из Java перегрузки видны без
@JvmOverloads - ✗Думать, что умолчание обязано быть константой времени компиляции, а не любым выражением
Уточняющие вопросы
- →Что генерирует аннотация для interop
@JvmOverloadsдля функции с умолчаниями? - →Почему вставка нового параметра в середину сигнатуры ломает существующие вызовы?
JuniorТеорияЧастоЧто такое Unit, и чем он отличается от void в Java?
Что такое Unit, и чем он отличается от void в Java?
Unit — настоящий тип ровно с одним значением, объектом Unit, который возвращает функция без осмысленного результата. void в Java типом не является и значения не имеет, поэтому не годится в аргументы дженерика; Unit годится — отсюда тип () -> Unit у лямбды.
Типичные ошибки
- ✗Считать
Unitключевым словом вродеvoid, а не типом со значением - ✗Путать
UnitсNothing— типом, у которого значений нет вовсе - ✗Писать
Voidв типе лямбды там, где Kotlin ожидаетUnit
Уточняющие вопросы
- →Чем
Unitотличается отNothing— типа без значений? - →Как функция Kotlin, возвращающая
Unit, выглядит из Java?
MiddleТеорияЧастоЧем различаются scope-функции let, run, with, apply и also?
Чем различаются scope-функции let, run, with, apply и also?
Они различаются по двум осям. Объект контекста — либо получатель this (run, with, apply), либо аргумент лямбды it (let, also). Результат — либо собственное значение лямбды (let, run, with), либо сам объект контекста (apply, also).
Типичные ошибки
- ✗Брать
apply, когда обратно нужно вычисленное значение лямбды - ✗Считать, что все scope-функции выдают объект одинаково
- ✗Вкладывать scope-функции так, что
itиthisперекрывают друг друга
Уточняющие вопросы
- →Какая scope-функция подходит для null-безопасного преобразования nullable-значения и почему?
- →Почему
withпринимает объект контекста аргументом, аrun— нет?
JuniorТеорияИногдаЧто дают строковые шаблоны по сравнению с конкатенацией или String.format?
Что дают строковые шаблоны по сравнению с конкатенацией или String.format?
Шаблон подставляет значения прямо в литерал: $name для простого имени и ${age + 1} для любого выражения. Компилятор разворачивает его в цепочку StringBuilder, то есть цена та же, что у ручной конкатенации, а в отличие от String.format строке формата нечем разойтись.
Типичные ошибки
- ✗Думать, что шаблон медленнее
+, раз он выглядит как интерполяция - ✗Забывать
${}вокруг выражения и подставлять только первое имя - ✗Не экранировать литеральный знак доллара, который компилятор читает как плейсхолдер
Уточняющие вопросы
- →Как вставить литеральный символ
$в строку с шаблоном? - →Что добавляет raw string literal поверх обычной строки с шаблоном?
JuniorТеорияИногдаЗачем Kotlin разрешает функции верхнего уровня вместо класса-утилиты со статикой?
Зачем Kotlin разрешает функции верхнего уровня вместо класса-утилиты со статикой?
Функция может жить прямо в файле, поэтому хелперу больше не нужен класс, существующий только чтобы его держать. Класс всё равно генерируется: Utils.kt становится классом UtilsKt со статическими членами, поэтому из Java пишут UtilsKt.foo().
Типичные ошибки
- ✗Считать, что JVM-класс не генерируется и из Java функцию не вызвать
- ✗По привычке заворачивать хелперы в
object, когда хватает функции в файле - ✗Ожидать, что Java увидит функцию в классе, названном по пакету
Уточняющие вопросы
- →Как изменить имя генерируемого JVM-класса для файла на Kotlin?
- →Когда extension-функция уместнее функции верхнего уровня?
MiddleТеорияИногдаГде Any и Any? стоят в иерархии типов Kotlin по сравнению с Object из Java?
Где Any и Any? стоят в иерархии типов Kotlin по сравнению с Object из Java?
Any — корень всех non-nullable типов; Any? — настоящая вершина, ведь допускает и null. У Any объявлены только equals, hashCode и toString — никаких wait/notify, как у Object. А Unit — обычный тип под Any, поэтому возврат «ничего» тоже даёт значение.
Типичные ошибки
- ✗Ожидать, что
Anyпредоставляетwait,notifyи прочее изObject - ✗Считать, что переменная типа
Anyможет хранитьnullбез? - ✗Помещать
Unitвне иерархии вместо места подAny
Уточняющие вопросы
- →Как вызвать
waitилиnotifyдля значения, статический тип которогоAny? - →Какой тип стоит в самом низу иерархии, ниже всех остальных?
MiddleКодИногдаЧто выведет функция, у которой аргумент по умолчанию создаёт новый список?
Что выведет функция, у которой аргумент по умолчанию создаёт новый список?
Печатается [a], затем [b], затем [x, c]. Значение по умолчанию — выражение, вычисляемое заново при каждом вызове с опущенным аргументом, поэтому каждый такой вызов строит свой список. Явная передача shared дописывает в этот список.
Типичные ошибки
- ✗Переносить привычку Python, где умолчание вычисляется один раз при определении
- ✗Считать, что явно переданный аргумент копируется, а не разделяется
- ✗Думать, что умолчание обязано быть константой, а не любым выражением
Уточняющие вопросы
- →Может ли умолчание ссылаться на более ранний параметр той же функции?
- →Что происходит с умолчаниями, когда функцию вызывают из Java?
SeniorТеорияИногдаЧто именно гарантирует val, и где эта гарантия заканчивается?
Что именно гарантирует val, и где эта гарантия заканчивается?
val гарантирует лишь то, что ссылка присваивается один раз. Объект за ней по-прежнему может меняться: в val с MutableList спокойно проходит add. А val-свойство с собственным getter пересчитывается при каждом чтении, поэтому два чтения законно различаются.
Типичные ошибки
- ✗Отдавать наружу
valсMutableListи называть такой API неизменяемым - ✗Кэшировать результат
valс собственным getter, считая, что он не меняется - ✗Приравнивать
valкfinalиз Java применительно к объекту, а не к ссылке
Уточняющие вопросы
- →Как отдать поле-коллекцию наружу так, чтобы вызывающие действительно не могли её менять?
- →Почему
valс собственным getter не годится для умного приведения?
SeniorТеорияИногдаЧем when в Kotlin отличается от switch с сопоставлением с образцом в Java?
Чем when в Kotlin отличается от switch с сопоставлением с образцом в Java?
Оба сопоставляют по типу и значению, и оба — выражения. Но when вообще не требует субъекта: голый when { a > b -> … } цепляет произвольные условия и сопоставляет диапазоны через in. switch требует селектор; а в when проверка типа умно приводит субъект.
Типичные ошибки
- ✗Считать, что
whenпринимает только константы, как классическийswitch - ✗Упускать, что проверка типа в
whenумно приводит субъект внутри ветки - ✗Забывать, что
whenбез субъекта полностью заменяет цепочку if/else-if
Уточняющие вопросы
- →Когда компилятор разрешает опустить
elseвwhenпо sealed-иерархии? - →Что даёт
whenбез субъекта по сравнению с цепочкойif/else if?
SeniorДизайнРедкоВы ревьюите pull request в backend-сервисе на Kotlin. Автор завернул почти каждую инструкцию в scope-функцию. Обращение к репозиторию выглядит как repo.find(id)?.let { it.also { log(it) } }?.run { toDto() }. Билдер конфигурации вкладывает apply в apply на три уровня, из-за чего this во внутренней лямбде читателю неоднозначен. Хелпер валидации использует also только чтобы менять переменную, захваченную из внешней функции. Заявленная цель команды — код, который новичок читает с первого прохода. Дайте отзыв на ревью: скажите, какие из этих применений оправданы, а какие нет, объясните, что именно теряет читатель, когда it и this перекрывают друг друга во вложенных scope-функциях, и сформулируйте конкретные правила для style guide команды, чтобы у let, run, apply и also осталась одна понятная роль у каждой. Не отвечайте просто «используйте их реже» — назовите критерий, который вы применяете в каждом месте вызова.
Вы ревьюите pull request в backend-сервисе на Kotlin. Автор завернул почти каждую инструкцию в scope-функцию. Обращение к репозиторию выглядит как repo.find(id)?.let { it.also { log(it) } }?.run { toDto() }. Билдер конфигурации вкладывает apply в apply на три уровня, из-за чего this во внутренней лямбде читателю неоднозначен. Хелпер валидации использует also только чтобы менять переменную, захваченную из внешней функции. Заявленная цель команды — код, который новичок читает с первого прохода. Дайте отзыв на ревью: скажите, какие из этих применений оправданы, а какие нет, объясните, что именно теряет читатель, когда it и this перекрывают друг друга во вложенных scope-функциях, и сформулируйте конкретные правила для style guide команды, чтобы у let, run, apply и also осталась одна понятная роль у каждой. Не отвечайте просто «используйте их реже» — назовите критерий, который вы применяете в каждом месте вызова.
Выбирайте по тому, что каждая отдаёт: let преобразует, run вычисляет значение, apply настраивает объект, который вернёте, also даёт побочный эффект. Вложенность перекрывает it/this — это цена читаемости, а не скорости. Одна scope-функция на выражение.
Типичные ошибки
- ✗Оценивать злоупотребление scope-функциями по цене аллокаций, а не по читаемости
- ✗Выбирать scope-функцию по nullability получателя, а не по тому, что она возвращает
- ✗Использовать
alsoкак универсальный блок, меняющий переменные снаружи
Уточняющие вопросы
- →Как переписать цепочку обращения к репозиторию, чтобы у каждого шага была одна очевидная цель?
- →Когда именование параметра лямбды лучше опоры на неявный
it?