Di Kotlin, apa cara idiomatis untuk berurusan dengan nilai yang dapat dibatalkan, merujuk atau mengubahnya

177

Jika saya memiliki tipe yang dapat dibatalkan Xyz? , saya ingin referensi atau mengubahnya ke jenis yang tidak dapat dibatalkan Xyz. Apa cara idiomatis melakukannya di Kotlin?

Misalnya, kode ini salah:

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

Tetapi jika saya memeriksa null terlebih dahulu itu diizinkan, mengapa?

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

Bagaimana cara saya mengubah atau memperlakukan nilai sebagai bukan nulltanpa memerlukan ifcek, dengan asumsi saya tahu pasti itu tidak pernah null? Sebagai contoh, di sini saya mengambil nilai dari peta yang saya jamin ada dan hasilnya get()tidak null. Tapi saya punya kesalahan:

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

Metode get()berpikir mungkin item itu hilang dan kembali tipe Int?. Oleh karena itu, apa cara terbaik untuk memaksa jenis nilai menjadi tidak dapat dibatalkan?

Catatan: pertanyaan ini sengaja ditulis dan dijawab oleh penulis ( Pertanyaan yang Dijawab Sendiri ), sehingga jawaban idiomatis untuk topik Kotlin yang umum diajukan hadir dalam SO. Juga untuk memperjelas beberapa jawaban lama yang ditulis untuk huruf-huruf Kotlin yang tidak akurat untuk Kotlin masa kini.

Jayson Minard
sumber

Jawaban:

296

Pertama, Anda harus membaca semua tentang Null Safety di Kotlin yang mencakup kasus secara menyeluruh.

Di Kotlin, Anda tidak dapat mengakses nilai nullable tanpa yakin itu tidak null( Memeriksa null dalam kondisi ), atau menyatakan bahwa itu pasti tidak nullmenggunakan !!operator yang pasti , mengaksesnya dengan ?.Panggilan Aman , atau terakhir memberikan sesuatu yang mungkin nullmerupakan nilai default menggunakan ?:Operator Elvis .

Untuk kasus pertama Anda dalam pertanyaan, Anda memiliki opsi tergantung pada maksud kode yang akan Anda gunakan salah satunya, dan semuanya idiomatis tetapi memiliki hasil yang berbeda:

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

Untuk "Mengapa itu bekerja ketika null diperiksa" baca informasi latar belakang di bawah ini pada smart gips .

Untuk kasus ke-2 Anda dalam pertanyaan Anda dalam pertanyaan dengan Map, jika Anda sebagai pengembang yakin hasilnya tidak pernah ada null, gunakan !!operator tertentu sebagai pernyataan:

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

atau dalam kasus lain, ketika peta BISA mengembalikan nol tetapi Anda dapat memberikan nilai default, maka Mapitu sendiri memiliki getOrElsemetode :

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

Informasi latar belakang:

Catatan: dalam contoh di bawah ini saya menggunakan tipe eksplisit untuk membuat perilaku jelas. Dengan inferensi tipe, biasanya tipe dapat dihilangkan untuk variabel lokal dan anggota pribadi.

Lebih lanjut tentang !!operator pasti

The !!Operator menegaskan bahwa nilai tersebut tidak nullatau melempar NPE. Ini harus digunakan dalam kasus di mana pengembang menjamin bahwa nilainya tidak akan pernah null. Anggap saja sebagai penegasan diikuti oleh pemain cerdas .

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

baca lebih lanjut: !! Tentu Operator


Lebih lanjut tentang nullMemeriksa dan Pemain Cerdas

Jika Anda melindungi akses ke tipe nullable dengan nullcek, kompiler akan dengan cerdas memasukkan nilai di dalam tubuh pernyataan menjadi non-nullable. Ada beberapa aliran rumit di mana ini tidak dapat terjadi, tetapi untuk kasus umum berfungsi dengan baik.

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

Atau jika Anda melakukan ispemeriksaan untuk jenis yang tidak dapat dibatalkan:

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

Dan hal yang sama untuk ekspresi 'kapan' yang juga aman dilemparkan:

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

// or

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

Beberapa hal tidak memungkinkan nullpemeriksaan untuk pemeran cerdas untuk penggunaan variabel nanti. Contoh di atas menggunakan variabel lokal yang sama sekali tidak bisa bermutasi dalam aliran aplikasi, apakah valatau varvariabel ini tidak punya kesempatan untuk bermutasi menjadi null. Namun, dalam kasus lain di mana kompiler tidak dapat menjamin analisis aliran, ini akan menjadi kesalahan:

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

Siklus hidup variabel nullableInttidak sepenuhnya terlihat dan dapat ditugaskan dari utas lainnya, nullpemeriksaan tidak dapat dilakukan dengan cerdas ke nilai yang tidak dapat dibatalkan. Lihat topik "Panggilan Aman" di bawah ini untuk solusinya.

Kasus lain yang tidak dapat dipercaya oleh pemain cerdas untuk tidak bermutasi adalah valproperti pada objek yang memiliki pengambil kustom. Dalam hal ini, kompiler tidak memiliki visibilitas ke apa yang mengubah nilai dan oleh karena itu Anda akan mendapatkan pesan kesalahan:

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

baca selengkapnya: Memeriksa null dalam kondisi


Lebih lanjut tentang ?.operator Panggilan Aman

Operator panggilan aman mengembalikan nol jika nilai ke kiri adalah nol, jika tidak terus mengevaluasi ekspresi ke kanan.

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

Contoh lain di mana Anda ingin mengulang daftar tetapi hanya jika tidak nulldan tidak kosong, sekali lagi operator panggilan aman sangat berguna:

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

Dalam salah satu contoh di atas kami memiliki kasus di mana kami melakukan ifpemeriksaan tetapi memiliki kesempatan utas lain bermutasi nilai dan karena itu tidak ada pemain pintar . Kami dapat mengubah sampel ini untuk menggunakan operator panggilan aman bersama dengan letfungsi untuk menyelesaikan ini:

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

baca selengkapnya: Panggilan Aman


Lebih lanjut tentang ?:Operator Elvis

Operator Elvis memungkinkan Anda untuk memberikan nilai alternatif ketika ekspresi di sebelah kiri operator adalah null:

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

Ini memiliki beberapa kegunaan kreatif juga, misalnya melempar pengecualian ketika ada sesuatu null:

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

atau untuk kembali lebih awal dari suatu fungsi:

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

baca selengkapnya: Operator Elvis


Operator Null dengan Fungsi Terkait

Kotlin stdlib memiliki serangkaian fungsi yang bekerja sangat baik dengan operator yang disebutkan di atas. Sebagai contoh:

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

Topik-topik terkait

Di Kotlin, sebagian besar aplikasi mencoba menghindari nullnilai, tetapi itu tidak selalu mungkin. Dan terkadang nullmasuk akal. Beberapa pedoman untuk dipikirkan:

  • dalam beberapa kasus, ia menjamin jenis pengembalian yang berbeda yang mencakup status pemanggilan metode dan hasilnya jika berhasil. Perpustakaan seperti Hasil memberi Anda jenis hasil keberhasilan atau kegagalan yang juga dapat bercabang kode Anda. Dan perpustakaan Janji untuk Kotlin yang disebut Kovenan melakukan hal yang sama dalam bentuk janji.

  • untuk koleksi sebagai tipe pengembalian selalu mengembalikan koleksi kosong alih-alih null, kecuali Anda memerlukan status ketiga "tidak ada". Kotlin memiliki fungsi pembantu seperti emptyList()atauemptySet() untuk membuat nilai-nilai kosong ini.

  • saat menggunakan metode yang mengembalikan nilai nullable yang Anda miliki default atau alternatif, gunakan operator Elvis untuk memberikan nilai default. Dalam hal Mappenggunaan getOrElse()yang memungkinkan nilai default dihasilkan, bukan Mapmetode get()yang mengembalikan nilai nullable. Sama untukgetOrPut()

  • saat mengganti metode dari Java di mana Kotlin tidak yakin tentang nullability dari kode Java, Anda selalu dapat menghapus ?nullability dari penggantian Anda jika Anda yakin apa tanda tangan dan fungsionalitasnya. Karena itu, metode yang Anda timpa lebih nullaman. Sama untuk mengimplementasikan antarmuka Java di Kotlin, ubah nullability menjadi apa yang Anda ketahui valid.

  • lihat fungsi yang sudah bisa membantu, seperti untuk String?.isNullOrEmpty()dan String?.isNullOrBlank()yang dapat beroperasi pada nilai yang dapat dibatalkan dengan aman dan melakukan apa yang Anda harapkan. Bahkan, Anda dapat menambahkan ekstensi Anda sendiri untuk mengisi celah di perpustakaan standar.

  • fungsi pernyataan seperti checkNotNull()dan requireNotNull()di perpustakaan standar.

  • fungsi pembantu seperti filterNotNull()yang menghapus null dari koleksi, atau listOfNotNull()untuk mengembalikan daftar nol atau item tunggal dari nullnilai yang mungkin .

  • ada operator cast Aman (nullable) juga yang memungkinkan pemain untuk non-nullable kembali nol jika tidak mungkin. Tapi saya tidak punya kasus penggunaan yang valid untuk ini yang tidak diselesaikan dengan metode lain yang disebutkan di atas.

Jayson Minard
sumber
1

Jawaban sebelumnya adalah tindakan yang sulit untuk diikuti, tetapi inilah satu cara cepat dan mudah:

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

Jika benar-benar tidak pernah nol, pengecualian tidak akan terjadi, tetapi jika pernah Anda akan melihat apa yang salah.

John Farrell
sumber
12
val something: Xyz = createPossiblyNullXyz () !! akan melempar NPE saat createPossiblyNullXyz () mengembalikan nol. Ini lebih sederhana dan mengikuti konvensi untuk berurusan dengan nilai yang Anda tahu bukan nol
Steven Waterman