In Kotlin, qual è il modo idiomatico di gestire valori nullable, fare riferimento o convertirli


177

Se ho un tipo nullable Xyz?, voglio fare riferimento a esso o convertirlo in un tipo non nullable Xyz. Qual è il modo idiomatico di farlo in Kotlin?

Ad esempio, questo codice è in errore:

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

Ma se controllo prima null è permesso, perché?

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

Come posso modificare o trattare un valore come no nullsenza richiedere il ifcontrollo, supponendo che lo sappia per certo che non lo è mai null? Ad esempio, qui sto recuperando un valore da una mappa che posso garantire esiste e il risultato get()non lo è null. Ma ho un errore:

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

Il metodo get()ritiene che l'articolo sia mancante e restituisce il tipo Int?. Pertanto, qual è il modo migliore per forzare il tipo di valore a essere non annullabile?

Nota: questa domanda è stata scritta e risposta intenzionalmente dall'autore ( domande con risposta autonoma ), in modo che le risposte idiomatiche agli argomenti di Kotlin più comuni siano presenti in SO. Anche per chiarire alcune risposte molto vecchie scritte per gli alfa di Kotlin che non sono accurate per l'attuale Kotlin.

Risposte:


296

Innanzitutto, dovresti leggere tutto sulla sicurezza nulla in Kotlin che copre a fondo i casi.

In Kotlin, non puoi accedere a un valore nullable senza essere sicuro che non lo sia null( Verifica di null in condizioni ), o affermando che sicuramente non sta nullusando l' !!operatore sicuro , accedendo con una ?.chiamata sicura o dando infine qualcosa che è probabilmente nullun valore predefinito utilizzando ?:Elvis Operator .

Per il tuo primo caso nella tua domanda hai opzioni che dipendono dall'intento del codice che useresti uno di questi, e tutti sono idiomatici ma hanno risultati diversi:

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

Per "Perché funziona quando è selezionato null" leggi le informazioni di base qui sotto su smart cast .

Per il tuo secondo caso nella tua domanda nella domanda con Map, se come sviluppatore sei sicuro che il risultato non sia mai null, usa l' !!operatore sicuro come un'asserzione:

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

o in un altro caso, quando la mappa POTREBBE restituire un valore nullo ma è possibile fornire un valore predefinito, allora Mapstesso ha un getOrElsemetodo :

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

Informazioni di base:

Nota: negli esempi seguenti sto usando tipi espliciti per chiarire il comportamento. Con l'inferenza di tipo, normalmente i tipi possono essere omessi per variabili locali e membri privati.

Maggiori informazioni !!sull'operatore sicuro

L' !!operatore afferma che il valore non è nullo genera un NPE. Questo dovrebbe essere usato nei casi in cui lo sviluppatore garantisce che il valore non sarà mai null. Pensalo come un'affermazione seguita da un cast intelligente .

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

leggi di più: !! Sicuro operatore


Ulteriori informazioni su nullVerifica e Smart Cast

Se si protegge l'accesso a un tipo nullable con un nullsegno di spunta, il compilatore eseguirà il cast intelligente del valore all'interno del corpo dell'istruzione come non annullabile. Ci sono alcuni flussi complicati in cui ciò non può accadere, ma per casi comuni funziona bene.

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

O se si esegue un iscontrollo per un tipo non annullabile:

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

E lo stesso per le espressioni "quando" che trasmettono anche in sicurezza:

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

// or

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

Alcune cose non consentono al nullcontrollo di eseguire il cast intelligente per l'uso successivo della variabile. L'esempio precedente utilizza una variabile locale che non avrebbe potuto mutare in alcun modo nel flusso dell'applicazione, indipendentemente dal fatto valche varquesta variabile non avesse l'opportunità di mutare in a null. Ma, in altri casi in cui il compilatore non può garantire l'analisi del flusso, questo sarebbe un errore:

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

Il ciclo di vita della variabile nullableIntnon è completamente visibile e può essere assegnato da altri thread, il nullcontrollo non può essere trasmesso in modo intelligente in un valore non annullabile. Vedere l'argomento "Chiamate sicure" di seguito per una soluzione alternativa.

Un altro caso che non può essere considerato attendibile da un cast intelligente per non mutare è una valproprietà su un oggetto che ha un getter personalizzato. In questo caso, il compilatore non ha visibilità su ciò che muta il valore e quindi riceverai un messaggio di errore:

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

leggi di più: Verifica di null in condizioni


Ulteriori informazioni ?.sull'operatore Safe Call

L'operatore di chiamata sicura restituisce null se il valore a sinistra è null, altrimenti continua a valutare l'espressione a destra.

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

Un altro esempio in cui si desidera iterare un elenco, ma solo in caso contrario nulle non vuoto, l'operatore di chiamata sicura può tornare utile:

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

In uno degli esempi sopra abbiamo avuto un caso in cui abbiamo fatto un ifcontrollo ma abbiamo la possibilità che un altro thread abbia mutato il valore e quindi nessun cast intelligente . Possiamo modificare questo esempio per utilizzare l'operatore di chiamata sicura insieme alla letfunzione per risolvere questo:

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

leggi di più: Chiamate sicure


Ulteriori informazioni su ?:Elvis Operator

L'operatore Elvis consente di fornire un valore alternativo quando un'espressione a sinistra dell'operatore è null:

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

Ha anche alcuni usi creativi, ad esempio generare un'eccezione quando qualcosa è null:

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

o per tornare presto da una funzione:

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

leggi di più: Elvis Operator


Operatori null con funzioni correlate

Kotlin stdlib ha una serie di funzioni che funzionano davvero bene con gli operatori sopra menzionati. Per esempio:

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

Argomenti correlati

In Kotlin, la maggior parte delle applicazioni cerca di evitare i nullvalori, ma non è sempre possibile. E a volte nullha perfettamente senso. Alcune linee guida a cui pensare:

  • in alcuni casi, garantisce diversi tipi di restituzione che includono lo stato della chiamata del metodo e il risultato in caso di successo. Le librerie come Risultato forniscono un tipo di risultato di esito positivo o negativo che può anche ramificare il codice. E la biblioteca Promesse per Kotlin chiamata Kovenant fa lo stesso sotto forma di promesse.

  • per le raccolte come tipi di restituzione restituisce sempre una raccolta vuota anziché una null, a meno che non sia necessario un terzo stato di "non presente". Kotlin ha funzioni di supporto come emptyList()oemptySet() per creare questi valori vuoti.

  • quando si utilizzano metodi che restituiscono un valore nullable per il quale si dispone di un valore predefinito o alternativo, utilizzare l'operatore Elvis per fornire un valore predefinito. Nel caso di un Maputilizzo, getOrElse()ciò consente di generare un valore predefinito invece del Mapmetodo get()che restituisce un valore nullable. Lo stesso pergetOrPut()

  • quando si sovrascrivono metodi da Java in cui Kotlin non è sicuro della nullità del codice Java, è sempre possibile eliminare la ?nullità dalla sostituzione se si è sicuri di quale firma e funzionalità dovrebbero essere. Pertanto il tuo metodo ignorato è più nullsicuro. Lo stesso per l'implementazione delle interfacce Java in Kotlin, modifica la nullità per essere ciò che sai essere valido.

  • guarda le funzioni che possono già aiutare, come for String?.isNullOrEmpty()e String?.isNullOrBlank()che possono operare su un valore nullable in modo sicuro e fare ciò che ti aspetti. In effetti, puoi aggiungere le tue estensioni per colmare eventuali lacune nella libreria standard.

  • l'asserzione funziona come checkNotNull()e requireNotNull()nella libreria standard.

  • funzioni di supporto come quelle filterNotNull()che rimuovono i null dagli insiemi o listOfNotNull()per restituire uno zero o un singolo elenco di elementi da un possibile nullvalore.

  • esiste anche un operatore di cast sicuro (nullable) che consente a un cast di restituire un tipo non nullable null se non è possibile. Ma non ho un caso d'uso valido per questo che non è risolto dagli altri metodi sopra menzionati.


1

La risposta precedente è un atto difficile da seguire, ma ecco un modo semplice e veloce:

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

Se davvero non è mai nullo, l'eccezione non accadrà, ma se mai lo è vedrai cosa è andato storto.


12
val qualcosa: Xyz = createPossiblyNullXyz () !! genererà un NPE quando createPossiblyNullXyz () restituisce null. È più semplice e segue le convenzioni per gestire un valore che sai non è nullo
Steven Waterman,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.