ข้าม​ไป​ยัง​เนื้อหา

lateinit แลก​ข้อความ error ที่​ดี ด้วย field ที่​กลาย​เป็น public

lateinit ถูก​เสนอ​เป็น​ทางออก​ของ​กรณี​ที่​ค่า​จริง​มา​ทีหลัง เช่น field ที่ framework ฉีด​ให้ · ทาง​เลือก​อีก​ทาง​คือ​ประกาศ​เป็น nullable แล้ว​จ่าย​ด้วย ?. หรือ !! ทุก​จุด​ที่​ใช้

เรื่อง​ที่​คน​เทียบ​กัน​คือ​ความ​สะดวก · หน้า​นี้​เทียบ​สิ่ง​ที่ JVM เห็น​แทน

ข้อความ​ตอน​แตะ​ก่อน​กำหนด​ค่า บอก​ชื่อ property ออก​มา​ตรงๆ

class WithLateinit { lateinit var name: String }
class WithNullable { var name: String? = null }
ทรงชนิด​ของ exceptionข้อความ
lateinit แล้ว​อ่าน​ก่อน​กำหนด​ค่าkotlin.UninitializedPropertyAccessExceptionlateinit property name has not been initialized
nullable แล้ว​ใช้ !!java.lang.NullPointerExceptionnull

ฝั่ง​แรก​บอก​ว่า property ชื่อ​อะไร​และ​ปัญหา​คือ​อะไร · ฝั่ง​หลัง​ตอบ​ว่า null ซึ่ง​เป็น​ข้อความ​ที่​ทุก !! ใน​โปรแกรม​พูด​เหมือน​กัน​หมด

ส่วน ?. ไม่​โยน​อะไร​เลย มัน​คืน null แล้ว​เดิน​ต่อ · นั่น​คือ​คนละ​การ​ตัดสิน​ใจ ไม่ใช่​ทาง​แก้​ที่​ดี​กว่า

เปิด javap แล้ว modifier ของ field ไม่​เหมือน​กัน

$ javap -p WithLateinit WithNullable
ประกาศ​ใน Kotlinfield ที่ JVM เห็น
lateinit var name: Stringpublic java.lang.String name
var name: String?private java.lang.String name

property แบบ​ปกติ​ได้ field เป็น private แล้ว​มี getter กับ setter ครอบ​ไว้ · ส่วน lateinit ได้ field เป็น public

ด่าน​ตรวจ​ของ lateinit อยู่​ที่ getter · ไม่​ได้​อยู่​ที่ field

ข้อความ lateinit property name has not been initialized ถูก​โยน​จาก getter ที่ compiler สร้าง

field ที่​เป็น public แปล​ว่า code ที่​ไม่​ผ่าน getter มอง​เห็น​ค่า​ดิบ​ได้​ตรงๆ ทั้ง Java ที่​เรียก​ข้าม​ภาษา และ library ที่​เดิน field ด้วย reflection

สิ่ง​ที่​ได้​จาก​ตรง​นั้น​คือ null เปล่าๆ ไม่ใช่ exception ที่​บอก​ชื่อ property · ด่าน​ที่​หลบ​ได้ ไม่ใช่​ด่าน และ lateinit ไม่​เคย​สัญญา​ว่า​จะ​กัน​ตรง​นั้น

มัน​ไม่ใช่ property ธรรมดา และ​เรียก​จาก​นอก class ไม่​ได้

class WithLateinit {
lateinit var name: String
fun ready(): Boolean = ::name.isInitialized
}
จังหวะready()
ก่อน​กำหนด​ค่าfalse
หลัง​กำหนด​ค่าtrue

ย้าย​บรรทัด​เดียวกัน​ออก​ไป​นอก class แล้ว compiler ปฏิเสธ

error: backing field of 'var name: String' is not accessible at this point.

isInitialized อ่าน backing field โดยตรง มัน​จึง​ติด​กฎ​การ​มอง​เห็น​ของ field · code ที่​อยาก​รู้​ว่า “พร้อม​หรือ​ยัง” จาก​ข้าง​นอก ต้อง​ให้ class เปิด method ให้​เอง

คำถาม​ที่​ตัดสิน​ไม่ใช่​ความ​สะดวก แต่​เป็น​ว่า “ไม่มี​ค่า” เป็น​สถานะ​ที่​ถูก​ต้อง​หรือ​ไม่

  • ถ้า null เป็น​สถานะ​ที่​ถูก​ต้อง​ของ​โดเมน ให้​ประกาศ nullable · ระบบ​ชนิด​จะ​บังคับ​ให้​ทุก​จุด​ที่​ใช้​ตอบ​ว่า​จะ​ทำ​อย่างไร​กับ​กรณี​นั้น
  • ถ้า​ไม่มี​ค่า​คือ​บั๊ก และ​ค่า​จะ​ถูก​ใส่​ก่อน​ใช้​เสมอ lateinit ให้​ข้อความ​ที่​ดี​กว่า ตอน​สมมติฐาน​นั้น​ผิด
  • ถ้า​ค่า​คำนวณ​ได้​จาก​ตัว​เอง ทั้ง​สอง​ทาง​ผิด · ใช้ by lazy แล้ว​ไม่มี​สถานะ “ยัง​ไม่​พร้อม” ให้​ใคร​แตะ​เลย

ข้อ​จำกัด​พวก​นี้​ตัด​ตัว​เลือก​ทิ้ง​ตั้งแต่​ยัง​ไม่​ได้​ชั่ง​ข้อดี​ข้อ​เสีย

เขียน​ว่าcompiler ตอบ
lateinit var n: Int'lateinit' modifier is not allowed on properties of primitive types.
lateinit var s: String ที่​มี get() เอง'lateinit' modifier is not allowed on properties with a custom getter or setter.
lateinit val s: String'lateinit' modifier is allowed only on mutable properties.

ข้อ​แรก​คือ​เหตุผล​ที่ lateinit ใช้​กับ Int, Long, Boolean ไม่​ได้​เลย · ด่าน​ของ​มัน​อาศัย​ค่า null ใน​ช่อง ซึ่ง​ชนิด​พื้น​ฐาน​บน JVM ไม่มี​ที่​ให้​เก็บ

🔗 วิธี​วัด​ซ้ำ

ทุก​ค่า​มา​จาก kotlinc 2.4.10 กับ javap ตัว​จริง · แปล​ด้วย -Werror -Wextra -jvm-target 25 และ​ไม่มี​หน่วย​เวลา​สัก​ตัว

python3 scripts/kotlin-bytecode-facts/probe.py
npm run verify:kotlin-bytecode

probe มี​ช่อง​ควบคุม​สอง​ช่อง​ของ​หัวข้อ​นี้ คือ isInitialized ต้อง​เป็น​เท็จ​ก่อน​กำหนด​ค่า และ​จริง​หลัง​กำหนด · และ​ทรง​ที่​เรียก​จาก​นอก class ต้อง​แปล​ไม่​ผ่าน​จริง