У Котліна, який ідіоматичний спосіб поводитися з незмінними значеннями, посилаючись на них або перетворюючи їх


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?. Отже, який найкращий спосіб змусити тип значення не бути нульовим?

Примітка: це запитання навмисно написано та відповіло автором ( запитання із самовідказами ), так що ідіоматичні відповіді на поширені теми Котліна присутні в ТА. Також для уточнення деяких дійсно старих відповідей, написаних для алфавітів Котліна, не точних для поточного Котліна.

Відповіді:


296

По-перше, ви повинні прочитати все про Null Safety в Котліні, яке ретельно висвітлює справи.

У Котлін, ви не можете отримати доступ до обнуляє значення , не будучи впевненим , що він не null( Перевірка нульовий в умовах ), або стверджувати , що це, безумовно , НЕ nullвикористовуючи !!вірний оператор , доступ до нього з ?.безпечного виклику , або , нарешті , даючи що - то , можливо , nullзначення за замовчуванням за допомогою ?:оператора Elvis .

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

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

У розділі "Чому це працює, коли нульова перевірка" читайте довідкову інформацію нижче на смарт-ролях .

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

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

або в іншому випадку, коли карта COULD повертає нуль, але ви можете надати значення за замовчуванням, тоді 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()
}

читати більше: Перевірка на наявність нуля в умовах


Більше про ?.оператора безпечного дзвінка

Оператор безпечного виклику повертає 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перевіряли, але маємо шанс інший потік змінити значення і, отже, не мати розумного відтворення . Ми можемо змінити цей зразок, щоб використовувати оператор безпечного виклику разом з 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
    }
}

читати далі: Безпечні дзвінки


Більше про ?:Оператора Елвіса

Оператор Elvis дозволяє надати альтернативне значення, коли вираз зліва від оператора 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

Пов'язані теми

У Котліні більшість програм намагаються уникати nullзначень, але це не завжди можливо. І іноді nullмає ідеальний сенс. Деякі вказівки для роздумів:

  • в деяких випадках він гарантує різні типи повернення, які включають статус виклику методу та результат у разі успіху. Бібліотеки, такі як Результат, дають вам результат результату успіху чи відмови, який також може розгалужувати ваш код. І бібліотека Обіцянь для Котліна під назвою Ковенант робить те саме у вигляді обіцянок.

  • для колекцій як типів повернення завжди повертайте порожню колекцію замість а null, якщо вам не потрібен третій стан "немає". Kotlin має допоміжні функції, такі як emptyList()абоemptySet() створювати ці порожні значення.

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

  • коли переважні методи з Java, де Котлін не впевнений у нікчемності коду Java, ви завжди можете відмовитись ?від зміни, якщо ви впевнені, якою має бути підпис та функціональність. Тому ваш перекритий метод є більш nullбезпечним. Те саме, що реалізувати інтерфейси Java в Котліні, змінити нульовість таким, яким ви знаєте, що є дійсним.

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

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

  • допоміжні функції, як-от filterNotNull()видалення нулів із колекцій, або listOfNotNull()для повернення нуля або списку окремих елементів з можливого nullзначення.

  • є також безпечний (нульовий) оператор лиття , який дозволяє, якщо це неможливо, повернути нульовий тип повернення до нульового типу. Але у мене немає вагомого випадку використання, який не вирішується іншими методами, згаданими вище.


1

Попередня відповідь - важкий вчинок, який слід виконати, але ось один швидкий і простий спосіб:

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

Якщо це дійсно ніколи не буде нульовим, виняток не відбудеться, але якщо він є, ви побачите, що пішло не так.


12
вал щось: Xyz = createPossblyNullXyz () !! кине NPE, коли createPossblyNullXyz () поверне null. Це простіше, і дотримуватися умов для поводження з цінністю, яке ви знаєте, не є нульовим
Стівен Уотерман
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.