W Kotlinie, jaki jest idiomatyczny sposób radzenia sobie z wartościami zerowymi, odwoływania się do nich lub konwertowania

177

Jeśli mam typ dopuszczający wartość null Xyz?, chcę odwołać się do niego lub przekonwertować go na typ dopuszczający wartość null Xyz. Jaki jest idiomatyczny sposób na zrobienie tego w Kotlinie?

Na przykład ten kod zawiera błąd:

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

Ale jeśli najpierw sprawdzę null, to jest dozwolone, dlaczego?

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

Jak zmienić lub traktować wartość jako nie nullbez wymagania ifsprawdzenia, zakładając, że wiem na pewno, że tak naprawdę nigdy nie jest null? Na przykład tutaj pobieram wartość z mapy, co do której mogę zagwarantować, że istnieje, a wynik get()nie null. Ale mam błąd:

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?"

Metoda get()uważa, że ​​jest możliwe, że brakuje elementu i zwraca typ Int?. Jaki jest zatem najlepszy sposób na wymuszenie, aby typ wartości nie dopuszczał wartości null?

Uwaga: to pytanie jest celowo napisane i udzielone przez autora ( Pytania z własną odpowiedzią), tak aby idiomatyczne odpowiedzi na często zadawane tematy Kotlin były obecne w SO. Również wyjaśnienie niektórych naprawdę starych odpowiedzi napisanych dla alfabetu Kotlin, które nie są dokładne dla obecnego Kotlina.

Jayson Minard
źródło

Odpowiedzi:

296

Najpierw powinieneś przeczytać wszystko o Null Safety w Kotlinie, które dokładnie omawia sprawy.

W Kotlin, nie można uzyskać dostępu do wartości zerowalne nie będąc pewna, że to nie jest null( Sprawdzanie wartości null w warunkach ) lub twierdząc, że jest on na pewno nie nullza pomocą !!pewny operatora , dostępu do niego z ?.awaryjnym Wezwania , albo wreszcie dając coś, co jest prawdopodobnie nullwartość domyślna przy użyciu ?:operatora Elvisa .

W pierwszym przypadku w Twoim pytaniu masz opcje w zależności od przeznaczenia kodu, którego użyłbyś jednej z nich, i wszystkie są idiomatyczne, ale mają różne wyniki:

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

W sekcji „Dlaczego to działa, gdy zaznaczono wartość null”, przeczytaj poniższe informacje na temat inteligentnych rzutów .

W drugim przypadku w pytaniu z pytaniem Map, jeśli jako programista jesteś pewien, że wynik nigdy nie będzie null, użyj !!pewnego operatora jako potwierdzenia:

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

lub w innym przypadku, gdy mapa MOŻE zwrócić wartość null, ale możesz podać wartość domyślną, wtedy Mapsama ma getOrElsemetodę :

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

Informacje ogólne:

Uwaga: w poniższych przykładach używam jawnych typów, aby wyjaśnić zachowanie. W przypadku wnioskowania o typie typy można zwykle pominąć w przypadku zmiennych lokalnych i prywatnych elementów członkowskich.

Więcej o !!pewnym operatorze

!!Operator stwierdza, że wartość ta nie jest nulllub generuje NPE. Powinno to być używane w przypadkach, gdy programista gwarantuje, że wartość nigdy nie będzie null. Pomyśl o tym jako o asercie, po którym następuje sprytna obsada .

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

czytaj więcej: !! Pewnie operator


Więcej informacji o nullsprawdzaniu i inteligentnych rzutach

Jeśli chronisz dostęp do typu dopuszczającego wartość null za pomocą nullczeku, kompilator inteligentnie rzutuje wartość w treści instrukcji, aby nie dopuszczała wartości null. Istnieje kilka skomplikowanych przepływów, w których nie może się to zdarzyć, ale w typowych przypadkach działa dobrze.

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

Lub jeśli issprawdzisz typ nie dopuszczający wartości null:

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

To samo dotyczy wyrażeń „kiedy”, które również są bezpieczne:

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

// or

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

Niektóre rzeczy nie pozwalają na nullsprawdzenie inteligentnego rzutowania w celu późniejszego użycia zmiennej. Powyższy przykład wykorzystuje zmienną lokalną, która w żaden sposób nie mogło być zmutowany w przepływie aplikacji, czy valczy varta zmienna nie miał okazji do mutacji w null. Ale w innych przypadkach, gdy kompilator nie może zagwarantować analizy przepływu, byłby to błąd:

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

Cykl życia zmiennej nullableIntnie jest w pełni widoczny i może być przypisany z innych wątków, nullsprawdzenie nie może być inteligentnie rzutowane na wartość nieprzekraczającą wartości null. Zobacz temat „Bezpieczne połączenia” poniżej, aby poznać obejście tego problemu.

Innym przypadkiem, któremu inteligentne rzutowanie nie może zaufać, aby nie ulegać mutacji, jest valwłaściwość obiektu, który ma niestandardowy moduł pobierający. W takim przypadku kompilator nie ma wglądu w to, co zmienia wartość, i dlatego zostanie wyświetlony komunikat o błędzie:

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

czytaj więcej: Sprawdzanie null w warunkach


Więcej o ?.operatorze Safe Call

Operator bezpiecznego wywołania zwraca wartość null, jeśli wartość po lewej stronie jest równa null, w przeciwnym razie kontynuuje obliczanie wyrażenia po prawej stronie.

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

Kolejny przykład, w którym chcesz iterować listę, ale tylko wtedy, gdy nie jest nulli nie jest pusta, ponownie przydaje się bezpieczny operator połączeń:

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

W jednym z powyższych przykładów mieliśmy przypadek, w którym ifsprawdziliśmy, ale mieliśmy szansę, że inny wątek zmutował wartość, a zatem nie sprytnie rzucał . Możemy zmienić ten przykład, aby użyć bezpiecznego operatora połączenia wraz z letfunkcją rozwiązującą ten problem:

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

czytaj więcej: Bezpieczne połączenia


Więcej o ?:Elvis Operator

Operator Elvis umożliwia podanie alternatywnej wartości, gdy wyrażenie po lewej stronie operatora to null:

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

Ma też kilka kreatywnych zastosowań, na przykład rzuca wyjątek, gdy coś jest null:

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

lub aby wrócić wcześniej z funkcji:

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

czytaj więcej: Elvis Operator


Operatory zerowe z powiązanymi funkcjami

Kotlin stdlib ma szereg funkcji, które bardzo dobrze współpracują z wyżej wymienionymi operatorami. Na przykład:

// 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

Powiązane tematy

W Kotlinie większość aplikacji stara się unikać nullwartości, ale nie zawsze jest to możliwe. Czasami nullma to sens. Kilka wskazówek do przemyślenia:

  • w niektórych przypadkach gwarantuje różne typy zwracane, które obejmują stan wywołania metody i wynik, jeśli się powiedzie. Biblioteki, takie jak Result, podają typ wyniku sukcesu lub niepowodzenia, który może również rozgałęzić kod. A biblioteka Promises dla Kotlina o nazwie Kovenant robi to samo w formie obietnic.

  • w przypadku kolekcji jako typy zwracane zawsze zwracają pustą kolekcję zamiast a null, chyba że potrzebny jest trzeci stan „nieobecny”. Kotlin ma funkcje pomocnicze, takie jak emptyList()lubemptySet() do tworzenia tych pustych wartości.

  • gdy używasz metod, które zwracają wartość dopuszczającą wartość null, dla której masz wartość domyślną lub alternatywną, użyj operatora Elvis, aby podać wartość domyślną. W przypadku Mapużycia, getOrElse()która pozwala na wygenerowanie wartości domyślnej zamiast Mapmetody, get()która zwraca wartość dopuszczającą wartość null. To samo dotyczygetOrPut()

  • podczas zastępowania metod z języka Java, w przypadku których Kotlin nie jest pewien, czy kod Java nie może mieć ?wartości null, zawsze możesz usunąć wartość null z zastąpienia, jeśli masz pewność, jaka powinna być sygnatura i funkcjonalność. Dlatego Twoja nadpisana metoda jest nullbezpieczniejsza. To samo dotyczy implementowania interfejsów Java w Kotlin, zmień wartość null na to, co wiesz, że jest prawidłowe.

  • spójrz na funkcje, które mogą już pomóc, takie jak for String?.isNullOrEmpty()i String?.isNullOrBlank()które mogą bezpiecznie operować na wartości zerowej i zrób to, czego oczekujesz. W rzeczywistości możesz dodać własne rozszerzenia, aby wypełnić wszelkie luki w standardowej bibliotece.

  • funkcje asercji, takie jak checkNotNull()iw requireNotNull()bibliotece standardowej.

  • funkcje pomocnicze, takie jak filterNotNull()usuwanie wartości null z kolekcji lub listOfNotNull()zwracanie listy zerowej lub pojedynczej pozycji z możliwej nullwartości.

  • istnieje również operator rzutowania Safe (dopuszczający wartość null), który umożliwia rzutowanie na typ, który nie dopuszcza wartości null, zwraca wartość null, jeśli nie jest to możliwe. Ale nie mam dla tego ważnego przypadku użycia, który nie zostałby rozwiązany innymi metodami wymienionymi powyżej.

Jayson Minard
źródło
1

Poprzednia odpowiedź jest trudna do naśladowania, ale oto jeden szybki i łatwy sposób:

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

Jeśli to naprawdę nigdy nie jest zerowe, wyjątek się nie wydarzy, ale jeśli kiedykolwiek się zdarzy, zobaczysz, co poszło nie tak.

John Farrell
źródło
12
val coś: Xyz = createPossablyNullXyz () !! wyrzuci NPE, gdy createPossablyNullXyz () zwraca null. Jest prostszy i zgodny z konwencjami dotyczącymi wartości, o których wiesz, że nie są zerowe
Steven Waterman