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

177

Если у меня есть обнуляемый тип 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.

Джейсон Минард
источник

Ответы:

296

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

В Kotlin вы не можете получить доступ к значению NULL, не будучи уверенным, что это не так null( Проверка на NULL в условиях ), или утверждая, что он, безусловно, не nullиспользует !!оператор sure , обращаясь к нему с помощью ?.безопасного вызова или, наконец, давая что-то, что, возможно 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

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

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

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

Подробнее об !!уверенном операторе

!!Оператор утверждает , что значение не является 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()
}

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


Подробнее об ?.операторе Safe Call

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

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

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

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

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

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

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

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

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

Джейсон Минард
источник
1

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

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

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

Джон Фаррелл
источник
12
val что-то: Xyz = createPossblyNullXyz () !! будет бросать NPE, когда createPossblyNullXyz () возвращает ноль. Это проще и следует соглашениям для работы со значением, которое, как вы знаете, не равно нулю
Стивен Уотерман