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

Например, этот код ошибочный:

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

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

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?. Следовательно, как лучше всего заставить тип значения не допускать значения NULL?

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

181
Jayson Minard 28 Дек 2015 в 21:13

2 ответа

Лучший ответ

Во-первых, вы должны прочитать все о Null Safety в Kotlin, которая охватывает дела тщательно.

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

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

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() 
}

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

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

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

Или в другом случае, когда карта МОЖЕТ вернуть значение NULL, но вы можете указать значение по умолчанию, тогда сам 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

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

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

Подробнее об операторе sure !!

Оператор !! утверждает, что значение не равно 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, с помощью проверки null, компилятор будет smart cast, чтобы значение в теле оператора не допускало значения 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 на тип, не допускающий значения NULL:

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

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

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

// or

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

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

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 не может быть smart cast в значение, не допускающее значения 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()
}

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


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

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

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, но есть шанс, что другой поток изменил значение, и поэтому нет smart cast. Мы можем изменить этот пример, чтобы использовать безопасный оператор вызова вместе с функцией 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, предоставляют тип результата успеха или отказа, который также может разветвлять ваш код. И библиотека Promises для Kotlin под названием Kovenant делает то же самое в форме обещаний.

  • для коллекций в качестве возвращаемых типов всегда возвращается пустая коллекция вместо null, если вам не нужно третье состояние «отсутствует». Kotlin имеет вспомогательные функции, такие как emptyList() или {{X2 }} для создания этих пустых значений.

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

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

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

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

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

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

301
Gastón Saillén 5 Май 2019 в 14:42

Предыдущий ответ - трудный поступок, но вот один быстрый и простой способ:

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

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

3
John Farrell 10 Май 2017 в 05:22