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

Если у меня есть обнуляемый тип Xyz? Я хочу сослаться на него или преобразовать его в необнуляемый тип Xyz, Какой идиоматический способ сделать это в Котлине?

Например, этот код содержит ошибку:

val something: Xyz? = createPossiblyNullXyz()
something.foo() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Xyz?"

Но если я сначала проверю ноль, то это разрешено, почему?

val something: Xyz? = createPossiblyNullXyz()
if (something != null) {
    something.foo() 
}

Как я могу изменить или обработать значение как не null не требуя if проверить, если я знаю наверняка, что это действительно никогда null? Например, здесь я извлекаю значение из карты, которое, как я могу гарантировать, существует, и результат get() не является null, Но у меня есть ошибка:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")
something.toLong() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?"

Метод get() считает, что возможно, что элемент отсутствует и возвращает тип Int?, Следовательно, каков наилучший способ заставить тип значения не обнуляться?

Примечание: этот вопрос намеренно написан и получен ответ от автора ( Вопросы с самоотвечением), так что идиоматические ответы на часто задаваемые темы Kotlin присутствуют в SO. Также, чтобы прояснить некоторые действительно старые ответы, написанные для альф Kotlin, которые не являются точными для сегодняшнего Kotlin.

4 ответа

Решение

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

В Kotlin вы не можете получить доступ к значению NULL, не будучи уверенным, что это не null ( Проверка на ноль в условиях), или утверждение, что это, безусловно, нет null с использованием !! уверен оператор, доступ к нему с ?. Безопасный вызов, или, наконец, дать что-то, что возможно null значение по умолчанию с использованием ?: Элвис Оператор.

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

val something: Xyz? = createPossiblyNullXyz()

// access it as non-null asserting that with a sure call
val result1 = something!!.foo()

// access it only if it is not null using safe operator, 
// returning null otherwise
val result2 = something?.foo()

// access it only if it is not null using safe operator, 
// otherwise a default value using the elvis operator
val result3 = something?.foo() ?: differentValue

// null check it with `if` expression and then use the value, 
// similar to result3 but for more complex cases harder to do in one expression
val result4 = if (something != null) {
                   something.foo() 
              } else { 
                   ...
                   differentValue 
              }

// null check it with `if` statement doing a different action
if (something != null) { 
    something.foo() 
} else { 
    someOtherAction() 
}

Чтобы узнать, "Почему это работает, если выбрано значение NULL", прочитайте приведенную ниже справочную информацию о смарт-бросках.

Для вашего 2-го случая в вашем вопросе в вопросе с Map, если вы как разработчик уверены, что результат никогда не будет null использовать !! верный оператор как утверждение:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")!!
something.toLong() // now valid

или в другом случае, когда карта МОЖЕТ вернуть ноль, но вы можете указать значение по умолчанию, тогда Map сам по себе имеет getOrElse метод:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.getOrElse("z") { 0 } // provide default value in lambda
something.toLong() // now valid

Исходная информация:

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

Подробнее о !! верный оператор

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

val possibleXyz: Xyz? = ...
// assert it is not null, but if it is throw an exception:
val surelyXyz: Xyz = possibleXyz!! 
// same thing but access members after the assertion is made:
possibleXyz!!.foo()

читать дальше: !! Конечно оператор


Больше о null Проверка и умное приведение

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

val possibleXyz: Xyz? = ...
if (possibleXyz != null) {
   // allowed to reference members:
   possiblyXyz.foo()
   // or also assign as non-nullable type:
   val surelyXyz: Xyz = possibleXyz
}

Или если вы делаете is проверить для необнуляемого типа:

if (possibleXyz is Xyz) {
   // allowed to reference members:
   possiblyXyz.foo()
}

И то же самое для выражений "когда", которые также безопасны:

when (possibleXyz) {
    null -> doSomething()
    else -> possibleXyz.foo()
}

// or

when (possibleXyz) {
    is Xyz -> possibleXyz.foo()
    is Alpha -> possibleXyz.dominate()
    is Fish -> possibleXyz.swim() 
}

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

var nullableInt: Int? = ...

public fun foo() {
    if (nullableInt != null) {
        // Error: "Smart cast to 'kotlin.Int' is impossible, because 'nullableInt' is a mutable property that could have been changed by this time"
        val nonNullableInt: Int = nullableInt
    }
}

Жизненный цикл переменной nullableInt не полностью виден и может быть назначен из других потоков, null проверка не может быть умно приведена к ненулевому значению. См. Тему "Безопасные звонки" ниже для обхода проблемы.

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

class MyThing {
    val possibleXyz: Xyz? 
        get() { ... }
}

// now when referencing this class...

val thing = MyThing()
if (thing.possibleXyz != null) {
   // error: "Kotlin: Smart cast to 'kotlin.Int' is impossible, because 'p.x' is a property that has open or custom getter"
   thing.possiblyXyz.foo()
}

Подробнее: Проверка на ноль в условиях


Подробнее о ?. Оператор безопасного звонка

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

val possibleXyz: Xyz? = makeMeSomethingButMaybeNullable()
// "answer" will be null if any step of the chain is null
val answer = possibleXyz?.foo()?.goo()?.boo()

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

val things: List? = makeMeAListOrDont()
things?.forEach {
    // this loops only if not null (due to safe call) nor empty (0 items loop 0 times):
}

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

var possibleXyz: Xyz? = 1

public fun foo() {
    possibleXyz?.let { value ->
        // only called if not null, and the value is captured by the lambda
        val surelyXyz: Xyz = value
    }
}

Подробнее: Безопасные звонки


Подробнее о ?: Элвис Оператор

Оператор Элвиса позволяет указать альтернативное значение, когда выражение слева от оператора null:

val surelyXyz: Xyz = makeXyzOrNull() ?: DefaultXyz()

Он также имеет некоторые творческие применения, например, создает исключение, когда что-то null:

val currentUser = session.user ?: throw Http401Error("Unauthorized")

или вернуться рано из функции:

fun foo(key: String): Int {
   val startingCode: String = codes.findKey(key) ?: return 0
   // ...
   return endingValue
}

подробнее: Элвис Оператор


Нулевые операторы со связанными функциями

Kotlin stdlib имеет ряд функций, которые очень хорошо работают с операторами, упомянутыми выше. Например:

// use ?.let() to change a not null value, and ?: to provide a default
val something = possibleNull?.let { it.transform() } ?: defaultSomething

// use ?.apply() to operate further on a value that is not null
possibleNull?.apply {
    func1()
    func2()
}

// use .takeIf or .takeUnless to turn a value null if it meets a predicate
val something = name.takeIf { it.isNotBlank() } ?: defaultName

val something = name.takeUnless { it.isBlank() } ?: defaultName

Похожие темы

В Kotlin большинство приложений стараются избегать null ценности, но это не всегда возможно. И иногда null имеет смысл. Некоторые рекомендации для размышления:

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

  • для коллекций в качестве возвращаемых типов всегда возвращать пустую коллекцию вместо nullЕсли только вам не нужно третье состояние "нет". Котлин имеет вспомогательные функции, такие как emptyList() или же emptySet() создать эти пустые значения.

  • при использовании методов, которые возвращают обнуляемое значение, для которого у вас есть значение по умолчанию или альтернатива, используйте оператор Elvis для предоставления значения по умолчанию. В случае Map использовать getOrElse() который позволяет генерировать значение по умолчанию вместо Map метод get() который возвращает обнуляемое значение. То же самое для getOrPut()

  • при переопределении методов из Java, когда Kotlin не уверен в обнуляемости кода Java, вы всегда можете удалить ? обнуляемость из вашего переопределения, если вы уверены, что подпись и функциональность должны быть. Поэтому ваш переопределенный метод более null безопасный. То же самое для реализации интерфейсов Java в Kotlin, измените обнуляемость на то, что, как вы знаете, является действительным.

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

  • Утверждение функций, таких как checkNotNull() а также requireNotNull() в стандартной библиотеке.

  • вспомогательные функции, такие как filterNotNull() которые удаляют нули из коллекций, или listOfNotNull() для возврата нулевого или единственного списка элементов из возможно null значение.

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

Предыдущий ответ - трудный шаг для подражания, но вот один быстрый и простой способ:

val something: Xyz = createPossiblyNullXyz() ?: throw RuntimeError("no it shouldn't be null")
something.foo() 

Если оно действительно никогда не равно нулю, исключение не произойдет, но если оно когда-нибудь произойдет, вы увидите, что пошло не так.

Хочу добавить, что теперь существует библиотека Konad , которая решает более сложные ситуации для обнуляемой композиции. Здесь следует пример использования:

      val foo: Int? = 1
val bar: String? = "2"
val baz: Float? = 3.0f

fun useThem(x: Int, y: String, z: Float): Int = x + y.toInt() + z.toInt()

val result: Int? = ::useThem.curry() 
   .on(foo.maybe) 
   .on(bar.maybe) 
   .on(baz.maybe)
   .nullable

если вы хотите, чтобы его можно было обнулить, или

      val result: Result<Int> = ::useThem.curry() 
   .on(foo.ifNull("Foo should not be null")) 
   .on(bar.ifNull("Bar should not be null")) 
   .on(baz.ifNull("Baz should not be null"))
   .result

если вы хотите накапливать ошибки. См. возможно раздел

Принятый ответ содержит полную информацию, здесь я добавляю резюме

Как вызывать функции для переменной типа nullable

      val str: String? = "HELLO"

// 1. Safe call (?), makes sure you don't get NPE
val lowerCaseStr = str?.toLowerCase()   // same as str == null ? null : str.toLowerCase()

// 2. non-null asserted call (!!), only use if you are sure that value is non-null
val upperCaseStr = str!!.toUpperCase()  // same as str.toUpperCase() in java, NPE if str is null

Как преобразовать переменную типа, допускающую значение NULL, в тип, не допускающий значения NULL

Учитывая, что вы на 100% уверены, что переменная с нулевым значением содержит ненулевое значение

      // use non-null assertion, will cause NPE if str is null
val nonNullableStr = str!!      // type of nonNullableStr is String(non-nullable)

Почему безопасное (?) или ненулевое (!!) утверждение не требуется внутри нулевой проверки, если блок

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

      if(str != null){
   val upperCaseStr = str.toUpperCase()   // str can't possibly be null, no need of ? or !!
}
Другие вопросы по тегам