Jetbrains . У него есть
хорошая документация, поэтому сегодня я остановлюсь на крошечной части —
нулевой безопасности .
Существует как минимум пара подходов к
nullобработке на языках JVM:
- Java не намного дальше C — каждая ссылка («указатель») может быть
null, нравится вам это или нет. Если это не примитив, каждое поле, параметр или возвращаемое значение могут бытьnull. - Groovy имеет схожий фон, но добавляет немного синтаксического сахара , а именно Elvis Operator (
?:) и Safe Navigation Operator (?.). - Clojure переименовывается
nullвnil, дополнительно обрабатывая его какfalseв логических выражениях.NullPointerExceptionвсе еще возможно. - Scala первым принимает систематическую, безопасную от типов
Option[T]монаду (Java 8 также будет иметьOptional<T>!). Идиоматический код Scala не должен содержатьnulls, но при взаимодействии с Java вы должны иногда оборачивать пустые значения.
Котлин использует еще один подход. Ссылки, которые могут
null иметь различный тип, таким образом, нулевая безопасность кодируется в системе типов и применяется только во время компиляции. Мы получаем
NullPointerExceptionбесплатный код и никаких накладных расходов из-за дополнительной
Optionоболочки.
На уровне синтаксиса каждый тип
T имеет супер тип,
T? который позволяет
null. Посмотрите на эти тривиальные примеры:
fun hello(name: String) {
println("Hello, ${name}")
}
fun main(args: Array<String>) {
val str = "Kotlin"
hello(str)
val maybeStr: String? = "Maybe?"
hello(maybeStr) //doesn't COMPILE
if(maybeStr != null) {
hello(maybeStr)
}
}
Тип str выводится String. Функция hello() принимает String так hello(str) хорошо. Однако мы явно объявляем maybeStr как String? тип (nullable String). Компилятор позволяет нам вызов hello() с String? из — за несовместимый тип.
Однако, если компилятор может доказать, что вызов безопасен, например, потому что мы только что проверили null, компиляция завершится успешно. Чтобы быть точным, компилятор может доказать, что даунктинг от String? до String безопасен. Точно так же я всегда находил раздражающим в Java то, что после использования instanceof оператора (который сам по себе раздражает) мне все равно приходится бросать объект вниз:
Object obj = "Any object"
if(obj instanceof String) {
hello((String)obj)
}
Не в Котлине:
val obj: Any = "Any object"
if(obj is String) {
hello(obj)
}
Увидеть? obj имеет тип Any ( Object в терминах Java), поэтому вызов hello(obj) обречен на неудачу, верно? Не совсем. Компилятор может доказать, что obj он на самом деле является типом, String поэтому он выполняет автоматический безопасный переход для нас. Ухоженная! Но вернемся к null обработке.
Я много говорил о даункинге, вспоминая, что у любого ненулевого типа T есть супер-тип обнуляемый T?. Как и в любом другом полиморфном языке, апкастинг неявен. Другими словами, мы можем передать тип, T когда T? требуется — что вполне очевидно:
val str: String = "Hello" //String type can be inferred here
unsafeHello(str)
fun unsafeHello(name: String?) {
}
Интересно, что примитивы также могут быть обнуляемыми:
fun safePositive(x: Int) = x > 0 fun unsafePositive(x: Int?): Boolean = x != null && x > 0
В сгенерированном байт-коде первый метод занимает int время, а второй java.lang.Integer. Пока мы это делаем, компилируются первые два выражения, но не последнее:
if(unsafePositive(maybeInt)) {
//...
}
if(maybeInt != null && safePositive(maybeInt)) {
//...
}
if(safePositive(maybeInt)) {
//...
}
Первое выражение имеет идеальное совпадение типов ( Int? против Int?). Во втором случае компилятор может доказать, что maybeInt может быть понижен до Intтребуемого safePositive(). В последнем случае это невозможно доказать, что приведет к ошибке компиляции несоответствия типов.
Пока это выглядит великолепно — нулевая безопасность без дополнительных затрат времени выполнения. Однако совместимость Java является ахиллесовой пятой Котлина. В Scala Option[T] оболочка реализована поверх языка, а сама Scala допускает null взаимодействие с Java. Вы не увидите null в идиоматическом коде Scala, но он иногда появляется при взаимодействии с коллекциями и библиотеками Java. Обычно Option(javaMethod()) требуется дополнительное делегирование.
Однако Kotlin использует гораздо более агрессивный подход: каждый параметр каждого метода Java считается обнуляемым (что нас не волнует), но также каждое возвращаемое значение обнуляется — если не указано иное. Оказывается, компилятор Kotlin обладает некоторыми знаниями о JDK:
val formatted: String = String.format("Kotlin-is-%s", "cool")
val joined: String = String.join("-", "Kotlin", "is", "cool")
Первая строка компилируется просто отлично, Котлин знает, что String.format() никогда не вернется null. Однако этого нельзя сказать о String.join()новом для Java 8. Таким образом, даже если он String.join()никогда не вернется null , вы все равно получите String? предполагаемый тип. То же самое относится к любой библиотеке или вашему пользовательскому коду Java. К сожалению, @javax.validation.constraints.NotNull аннотации не помогают, не говоря уже о том, что вы не можете добавлять аннотации в код библиотеки / JDK.
Ну … вы вроде как можете … IntelliJ IDEA имеет скрытую функцию под названием Внешние аннотации, которая позволяет вам аннотировать произвольный метод, даже во внешних JAR-файлах. Вы не можете изменить внешний код, поэтому такие аннотации хранятся в специальном annotations.xml файле:
<root> <item name='java.lang.String java.lang.String join(java.lang.CharSequence, java.lang.CharSequence...)'> <annotation name='org.jetbrains.annotations.NotNull'/> </item> </root>
Это объявление (конечно, IntelliJ управляет им для вас) сообщает компилятору Kotlin, что он String.join() не может вернуться null. Поскольку наш код не будет компилироваться без него, он должен быть проверен в системе контроля версий и станет частью вашей базы кода.
Не похоже, что эта проблема скоро исчезнет. Всегда будут библиотеки без @NotNull аннотаций, и компилятор не сможет определить, является ли метод Java обнуляемым или нет (особенно принимая динамический характер загрузки классов и CLASSPATH). Более портативное решение — просто прекратить приведение к нулевому типу
String.join("-", "Kotlin", "is", "cool") as String
… но это кажется излишним.
Подводя итог: null обработка в Kotlin является одновременно радикальной (безопасность типов и проверка во время компиляции) и консервативной ( null все еще здесь, без функционального монадического стиля). Я только надеюсь, что грубые грани в совместимости с Java со временем исчезнут.