ใน Kotlin เป็นวิธีสำนวนที่จะจัดการกับค่า nullable การอ้างอิงหรือการแปลงพวกเขา


177

หากฉันมีประเภทที่ไม่สามารถใช้Xyz?งานได้ฉันต้องการอ้างอิงหรือแปลงเป็นประเภทที่ไม่สามารถยกเลิกXyzได้ การทำเช่นนั้นใน Kotlin เป็นอย่างไร?

ตัวอย่างเช่นรหัสนี้มีข้อผิดพลาด:

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?คิดว่ามันเป็นไปได้ว่ารายการจะหายไปและประเภทของผลตอบแทนดังนั้นวิธีที่ดีที่สุดในการบังคับประเภทของค่าที่ไม่เป็นโมฆะคืออะไร?

หมายเหตุ: คำถามนี้เขียนโดยเจตนาและตอบโดยผู้เขียน ( คำถามที่ตอบด้วยตนเอง ) ดังนั้นคำตอบที่เป็นไปตามหัวข้อ Kotlin ที่ถูกถามบ่อยนั้นมีอยู่ใน SO นอกจากนี้เพื่อชี้แจงคำตอบเก่า ๆ ที่เขียนขึ้นสำหรับอัลฟาของ Kotlin ที่ไม่ถูกต้องสำหรับ Kotlin วันปัจจุบัน

คำตอบ:


296

ก่อนอื่นคุณควรอ่านข้อมูลทั้งหมดเกี่ยวกับNull Safetyใน Kotlin ซึ่งครอบคลุมทุกกรณีอย่างละเอียด

ใน Kotlin คุณไม่สามารถเข้าถึงค่า nullable โดยไม่แน่ใจว่าไม่ใช่null(การตรวจสอบ null ในเงื่อนไข ) หรือยืนยันว่ามันไม่ได้nullใช้ตัว!!ดำเนินการที่แน่นอนเข้าถึงโดยใช้?.Safe Callหรือให้สิ่งที่อาจnullเป็น ค่าเริ่มต้นโดยใช้?:Elvis Operatorเอลวิสผู้ประกอบการ

สำหรับกรณีที่ 1 ของคุณในคำถามของคุณคุณมีตัวเลือกขึ้นอยู่กับความตั้งใจของรหัสที่คุณจะใช้อย่างใดอย่างหนึ่งเหล่านี้และทั้งหมดเป็นสำนวน แต่มีผลลัพธ์ที่แตกต่างกัน:

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

หรือในกรณีอื่นเมื่อ map COULD ส่งคืนค่า null แต่คุณสามารถระบุค่าเริ่มต้น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ตรวจสอบและการส่งข้อมูลอย่างชาญฉลาด

หากคุณป้องกันการเข้าถึงประเภท nullable ด้วยการnullตรวจสอบคอมไพเลอร์จะส่งค่าภายในเนื้อความของคำสั่งให้เป็น non-nullable มีบางกระแสที่ซับซ้อนซึ่งสิ่งนี้ไม่สามารถเกิดขึ้นได้ แต่สำหรับกรณีทั่วไปก็ใช้งานได้ดี

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 Operator

ผู้ประกอบการ 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
}

อ่านเพิ่มเติม: Elvis Operator


ผู้ประกอบการที่ไม่มีฟังก์ชั่นที่เกี่ยวข้อง

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

หัวข้อที่เกี่ยวข้อง

ใน Kotlin แอปพลิเคชันส่วนใหญ่พยายามหลีกเลี่ยงnullค่าต่างๆ แต่ไม่สามารถทำได้เสมอไป และบางครั้งก็nullสมเหตุสมผลดี แนวทางที่ควรพิจารณา:

  • ในบางกรณีมันรับประกันประเภทผลตอบแทนที่แตกต่างกันซึ่งรวมถึงสถานะของการเรียกวิธีการและผลถ้าประสบความสำเร็จ ไลบรารี่เช่นผลลัพธ์จะให้ผลลัพธ์ที่สำเร็จหรือล้มเหลวซึ่งสามารถแยกรหัสของคุณได้ และห้องสมุดสัญญาสำหรับ Kotlin ที่ชื่อว่าKovenantก็ทำเช่นเดียวกันในรูปแบบของสัญญา

  • สำหรับคอลเลกชันที่เป็นประเภทส่งคืนจะส่งคืนคอลเลกชันที่ว่างเปล่าแทนที่จะเป็นnullเว้นแต่คุณจะต้องการสถานะที่สามของ "ไม่แสดง" Kotlin มีฟังก์ชันผู้ช่วยเช่นemptyList()หรือemptySet()เพื่อสร้างค่าว่างเหล่านี้

  • เมื่อใช้วิธีการที่ส่งคืนค่า nullable ที่คุณมีค่าเริ่มต้นหรือทางเลือกให้ใช้ตัวดำเนินการ Elvis เพื่อระบุค่าเริ่มต้น ในกรณีของการMapใช้งานgetOrElse()ซึ่งอนุญาตให้สร้างค่าเริ่มต้นแทนMapวิธีการget()ที่ส่งกลับค่า nullable เหมือนกันสำหรับgetOrPut()

  • เมื่อแทนที่เมธอดจาก Java โดยที่ Kotlin ไม่แน่ใจเกี่ยวกับความสามารถในการลบล้างโค้ด Java คุณสามารถทิ้งความสามารถในการ?ลบล้างจากการแทนที่ของคุณหากคุณแน่ใจว่าลายเซ็นและการทำงานควรเป็นอย่างไร ดังนั้นวิธีการแทนที่ของคุณจะnullปลอดภัยกว่า เช่นเดียวกับการใช้งานอินเทอร์เฟซ Java ใน Kotlin ให้เปลี่ยนค่าความสามารถเป็นสิ่งที่คุณรู้ว่าถูกต้อง

  • ดูฟังก์ชั่นที่สามารถช่วยได้อยู่แล้วเช่น for String?.isNullOrEmpty()และString?.isNullOrBlank()ซึ่งสามารถทำงานกับค่า nullable อย่างปลอดภัยและทำสิ่งที่คุณคาดหวัง ในความเป็นจริงคุณสามารถเพิ่มส่วนขยายของคุณเองเพื่อเติมช่องว่างใด ๆ ในไลบรารีมาตรฐาน

  • ฟังก์ชั่นการยืนยันเช่นcheckNotNull()และrequireNotNull()ในห้องสมุดมาตรฐาน

  • ฟังก์ชั่นตัวช่วยเช่นการfilterNotNull()ลบค่า null จากคอลเลกชันหรือlistOfNotNull()ส่งคืนรายการศูนย์หรือรายการเดียวจากnullค่าที่เป็นไปได้

  • มีโอเปอเรเตอร์การโยนที่ปลอดภัย (เป็นโมฆะ)เช่นกันที่อนุญาตให้ส่งคืนคาสต์ในประเภทที่ไม่เป็นโมฆะหากไม่สามารถทำได้ แต่ฉันไม่มีกรณีการใช้งานที่ถูกต้องสำหรับสิ่งนี้ที่ไม่ได้รับการแก้ไขโดยวิธีการอื่น ๆ ที่กล่าวถึงข้างต้น


1

คำตอบก่อนหน้านี้เป็นการกระทำที่ยากที่จะติดตาม แต่นี่เป็นวิธีหนึ่งที่ง่ายและรวดเร็ว:

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

หากไม่เป็นโมฆะจริง ๆ ข้อยกเว้นจะไม่เกิดขึ้น แต่ถ้าเป็นเช่นนั้นคุณจะเห็นสิ่งที่ผิดพลาด


12
วาลบางอย่าง: Xyz = createPossiblyNullXyz () !! จะส่ง NPE เมื่อ createPossiblyNullXyz () ส่งคืนค่าว่าง มันง่ายและปฏิบัติตามอนุสัญญาเพื่อจัดการกับค่าที่คุณรู้ว่าไม่ใช่โมฆะ
สตีเวนวอเตอร์แมน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.