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 не должен содержатьnull
s, но при взаимодействии с 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 со временем исчезнут.