Was ist in Kotlin die idiomatische Art, mit nullbaren Werten umzugehen, sie zu referenzieren oder zu konvertieren?

176

Wenn ich einen nullbaren Typ habe Xyz?, möchte ich ihn referenzieren oder in einen nicht nullbaren Typ konvertieren Xyz. Was ist die idiomatische Art, dies in Kotlin zu tun?

Dieser Code ist beispielsweise fehlerhaft:

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

Aber wenn ich zuerst null überprüfe, ist es erlaubt, warum?

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

Wie ändere oder behandle ich einen Wert als nicht, nullohne dass die ifPrüfung erforderlich ist , vorausgesetzt, ich weiß, dass dies wirklich niemals der Fall ist null? Zum Beispiel rufe ich hier einen Wert von einer Karte ab, von dem ich garantieren kann, dass er existiert und das Ergebnis von get()ist nicht null. Aber ich habe einen Fehler:

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

Die Methode get()hält es für möglich, dass das Element fehlt, und gibt den Typ zurück Int?. Was ist daher der beste Weg, um zu erzwingen, dass der Werttyp nicht nullwertfähig ist?

Hinweis: Diese Frage wurde absichtlich vom Autor geschrieben und beantwortet ( Selbst beantwortete Fragen ), sodass die idiomatischen Antworten auf häufig gestellte Kotlin-Themen in SO vorhanden sind. Auch um einige wirklich alte Antworten zu klären, die für Alphas von Kotlin geschrieben wurden und für das heutige Kotlin nicht korrekt sind.

Jayson Minard
quelle

Antworten:

295

Zunächst sollten Sie alles über Null Safety in Kotlin lesen, das die Fälle gründlich abdeckt.

In Kotlin können Sie nicht auf einen nullbaren Wert zugreifen, ohne sicher zu sein, dass dies nicht der Fall ist null(unter Bedingungen auf null prüfen ) oder zu behaupten, dass nullder !!sichere Operator sicher nicht verwendet wird , mit einem ?.sicheren Aufruf darauf zugegriffen wird oder zuletzt etwas angegeben wird, das möglicherweise nullein Wert ist Standardwert mit dem ?:Elvis-Operator .

Für Ihren ersten Fall in Ihrer Frage haben Sie Optionen, abhängig von der Absicht des Codes, den Sie verwenden würden. Eine davon ist idiomatisch, hat jedoch unterschiedliche Ergebnisse:

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

Lesen Sie für "Warum funktioniert es, wenn null aktiviert ist" die folgenden Hintergrundinformationen zu Smart Casts .

Verwenden MapSie für Ihren zweiten Fall in Ihrer Frage in der Frage mit , wenn Sie als Entwickler sicher sind, dass das Ergebnis niemals eintreten wird null, einen !!sicheren Operator als Aussage:

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

oder in einem anderen Fall, wenn die Karte eine Null zurückgeben KÖNNTE, Sie aber einen Standardwert angeben können, hat sie Mapselbst eine getOrElseMethode :

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

Hintergrundinformation:

Hinweis: In den folgenden Beispielen verwende ich explizite Typen, um das Verhalten zu verdeutlichen. Bei der Typinferenz können die Typen normalerweise für lokale Variablen und private Mitglieder weggelassen werden.

Mehr über den !!sicheren Operator

Der !!Operator behauptet, dass der Wert kein nullNPE ist oder wirft. Dies sollte in Fällen verwendet werden, in denen der Entwickler garantiert, dass der Wert niemals erreicht wird null. Betrachten Sie es als eine Behauptung, gefolgt von einer klugen Besetzung .

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

Lesen Sie mehr: !! Sicherer Betreiber


Weitere nullInformationen zu Checking und Smart Casts

Wenn Sie Zugriff auf einen Nullable Type mit einem Schutz zu nullüberprüfen, wird der Compiler SMART wirft den Wert innerhalb des Körpers der Erklärung als nicht-nullable. Es gibt einige komplizierte Abläufe, in denen dies nicht möglich ist, aber in häufigen Fällen funktioniert dies einwandfrei.

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

Oder wenn Sie isnach einem nicht nullbaren Typ suchen:

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

Und das Gleiche gilt für 'Wann'-Ausdrücke, die auch sicher besetzt sind:

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

// or

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

Einige Dinge erlauben es nicht, die nullPrüfung für die spätere Verwendung der Variablen intelligent zu machen . Das obige Beispiel verwendet eine lokale Variable, die in keiner Weise in den Fluss der Anwendung mutiert sein können, ob valoder vardiese Variable keine Gelegenheit hatte , in eine mutieren null. In anderen Fällen, in denen der Compiler die Flussanalyse nicht garantieren kann, ist dies ein Fehler:

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

Der Lebenszyklus der Variablen nullableIntist nicht vollständig sichtbar und kann von anderen Threads zugewiesen werden. Die nullPrüfung kann nicht intelligent in einen nicht nullbaren Wert umgewandelt werden. Eine Problemumgehung finden Sie im Thema "Sichere Anrufe".

Ein weiterer Fall, dem ein Smart Cast nicht vertrauen kann, um nicht zu mutieren, ist eine valEigenschaft für ein Objekt, das über einen benutzerdefinierten Getter verfügt. In diesem Fall hat der Compiler keine Sichtbarkeit darüber, was den Wert mutiert, und daher wird eine Fehlermeldung angezeigt:

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

Lesen Sie mehr: Überprüfen auf Null in Bedingungen


Weitere Informationen zum ?.Safe Call-Betreiber

Der Operator für sichere Aufrufe gibt null zurück, wenn der Wert links null ist, andernfalls wird der Ausdruck rechts weiter ausgewertet.

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

Ein weiteres Beispiel, bei dem Sie eine Liste iterieren möchten, aber nur, wenn sie nullnicht leer ist, ist der Operator für sichere Anrufe wieder nützlich:

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

In einem der obigen Beispiele hatten wir einen Fall, in dem wir eine ifÜberprüfung durchgeführt haben, aber die Möglichkeit hatten, dass ein anderer Thread den Wert mutierte und daher keine intelligente Besetzung . Wir können dieses Beispiel ändern, um den Operator für sichere Anrufe zusammen mit der letFunktion zur Lösung dieses Problems zu verwenden:

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

Lesen Sie mehr: Sichere Anrufe


Mehr über die ?: Elvis Operator

Mit dem Elvis-Operator können Sie einen alternativen Wert angeben, wenn ein Ausdruck links vom Operator lautet null:

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

Es hat auch einige kreative Verwendungszwecke, zum Beispiel eine Ausnahme auslösen, wenn etwas ist null:

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

oder früh von einer Funktion zurückzukehren:

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

Lesen Sie mehr: Elvis Operator


Nulloperatoren mit verwandten Funktionen

Kotlin stdlib hat eine Reihe von Funktionen, die mit den oben genannten Operatoren sehr gut funktionieren. Beispielsweise:

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

Verwandte Themen

In Kotlin versuchen die meisten Anwendungen, nullWerte zu vermeiden , dies ist jedoch nicht immer möglich. Und manchmal nullmacht es vollkommen Sinn. Einige Richtlinien zum Nachdenken:

  • In einigen Fällen werden verschiedene Rückgabetypen garantiert, die den Status des Methodenaufrufs und das Ergebnis bei Erfolg enthalten. Bibliotheken wie Result geben Ihnen einen Erfolgs- oder Fehlerergebnistyp, der auch Ihren Code verzweigen kann. Und die Promises-Bibliothek für Kotlin namens Kovenant macht dasselbe in Form von Versprechungen.

  • Für Sammlungen als Rückgabetypen wird immer eine leere Sammlung anstelle von a zurückgegeben null, es sei denn, Sie benötigen den dritten Status "nicht vorhanden". Kotlin verfügt über Hilfsfunktionen wie emptyList()oderemptySet() zum Erstellen dieser leeren Werte.

  • Wenn Sie Methoden verwenden, die einen nullbaren Wert zurückgeben, für den Sie einen Standardwert oder eine Alternative haben, verwenden Sie den Elvis-Operator, um einen Standardwert anzugeben. Im Fall einer MapVerwendung getOrElse(), bei der anstelle einer MapMethode, get()die einen nullbaren Wert zurückgibt, ein Standardwert generiert werden kann. Gleiches gilt fürgetOrPut()

  • Wenn Sie Methoden aus Java überschreiben, bei denen Kotlin nicht sicher ist, ob der Java-Code nullbar ist, können Sie die ?Nullbarkeit jederzeit aus Ihrer Überschreibung entfernen, wenn Sie sicher sind, wie die Signatur und Funktionalität lauten soll. Daher ist Ihre überschriebene Methode nullsicherer. Ändern Sie die Nullbarkeit so, wie Sie es für die Implementierung von Java-Schnittstellen in Kotlin für gültig halten.

  • Schauen Sie sich Funktionen an, die bereits hilfreich sein können, z. B. für String?.isNullOrEmpty()und String?.isNullOrBlank()die einen nullbaren Wert sicher verarbeiten können, und tun Sie, was Sie erwarten. Tatsächlich können Sie Ihre eigenen Erweiterungen hinzufügen, um Lücken in der Standardbibliothek zu schließen.

  • Assertion funktioniert wie checkNotNull()und requireNotNull()in der Standardbibliothek.

  • filterNotNull()Hilfsfunktionen wie das Entfernen von Nullen aus Sammlungen oder das listOfNotNull()Zurückgeben einer Null- oder Einzelelementliste von einem möglichen nullWert.

  • Es gibt auch einen sicheren (nullbaren) Umwandlungsoperator , mit dem eine Umwandlung in einen nicht nullbaren Typ null zurückgeben kann, wenn dies nicht möglich ist. Ich habe jedoch keinen gültigen Anwendungsfall dafür, der mit den anderen oben genannten Methoden nicht gelöst werden kann.

Jayson Minard
quelle
1

Die vorherige Antwort ist schwer zu befolgen, aber hier ist ein schneller und einfacher Weg:

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

Wenn es wirklich nie null ist, wird die Ausnahme nicht auftreten, aber wenn es jemals ist, werden Sie sehen, was schief gelaufen ist.

John Farrell
quelle
12
val etwas: Xyz = createPossiblyNullXyz () !! löst eine NPE aus, wenn createPossiblyNullXyz () null zurückgibt. Es ist einfacher und folgt den Konventionen für den Umgang mit einem Wert, von dem Sie wissen, dass er nicht null ist
Steven Waterman,