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

constructor เป็น private แล้ว แต่​ยัง​มี​ประตู​อีก​บาน​ที่ compiler สร้าง​ให้​เอง

คุณ​ทำ data class ที่​มี invariant · คุณ​ตั้ง constructor เป็น private แล้ว​เปิด factory ให้​เรียก​ทาง​เดียว

data class Token private constructor(val value: String) {
companion object {
fun of(raw: String): Token {
require(raw.startsWith("sk-"))
return Token(raw)
}
}
}

Token("อะไรก็ได้") คอม​ไพล์​ไม่​ผ่าน​จริง · แต่ token.copy(value = "อะไรก็ได้") ผ่าน

ตัว​วัด​ใส่​ตัว​นับ​ไว้​สอง​ที่: ใน init และ​ใน factory · แล้ว​เดิน​ห้า​เส้น​ทาง

เส้น​ทางinit ทำงานfactory ทำงานผล
factory11สร้าง​สำเร็จ
factory-bad01IllegalArgumentException
copy10สร้าง​สำเร็จ
public-copy10สร้าง​สำเร็จ
copy-empty10IllegalArgumentException

แถว​ที่​สาม​คือ​ใจความ · copy() ผ่าน init 1 ครั้ง แต่​ผ่าน factory 0 ครั้ง

และ​มัน​สร้าง​อ็​อบ​เจ​กต์​ที่ factory ปฏิเสธ​ได้​สำเร็จ · แถว​ที่​สอง​แสดง​ว่า factory ยัง​กัน​ค่า​เดียวกัน​นั้น​อยู่

คำ​อธิบาย​ที่​เจอ​บ่อย​คือ “copy() ข้าม init block”

แถว​สุดท้าย​ของ​ตาราง​ปฏิเสธ​ข้อ​นั้น · copy() ด้วย​ค่าที่ init ห้าม ได้ IllegalArgumentException โดย init ทำงาน 1 ครั้ง

init ทำงาน​ทุก​ครั้ง เพราะ​มัน​เป็น​ของ constructor และ copy() ก็​เรียก constructor

สิ่ง​ที่ copy() ข้าม คือ factory ไม่ใช่ init

ความ​ต่าง​นี้​ไม่ใช่​ราย​ละเอียด​ปลีก​ย่อย มัน​เปลี่ยน​ว่า​คุณ​ควร​วาง invariant ไว้​ที่ไหน

invariant ที่​อยู่​ใน init คุ้มครอง​ทุก​เส้น​ทาง เพราะ​ทุก​เส้น​ทาง​ต้อง​สร้าง​อ็​อบ​เจ​กต์ · invariant ที่​อยู่​ใน factory คุ้มครอง​เฉพาะ​คน​ที่​เดิน​เข้า​ประตู​ที่​คุณ​ตั้งใจ

คน​ที่​เชื่อ​คำ​อธิบาย​ผิด​จะ​ย้าย invariant ออก​จาก init เพราะ​คิด​ว่า​มัน​ไม่​ทำงาน ซึ่ง​เป็นการ​ถอด​ยาม​ตัว​เดียว​ที่​ยัง​ยืน​อยู่

ตัว​วัด​มี class ฝาแฝด​ที่​เหมือน​กัน​ทุก​อย่าง ยกเว้น constructor เป็น public

classinitfactoryผล
ctor เป็น private10สร้าง​สำเร็จ
ctor เป็น public10สร้าง​สำเร็จ

เท่า​กัน​ทุก​ช่อง · และ​นั่น​คือ​ประเด็น ไม่ใช่​ข้อ​บกพร่อง​ของ​การ​ทดลอง

private ไม่​ได้​เปลี่ยน​พฤติกรรม​ตอน​รัน​เลย​สัก​หน่วย · สิ่ง​ที่​มัน​เปลี่ยน​คือ ความ​คาด​หวัง​ของ​คน​อ่าน code ว่า​มี​ประตู​อยู่​บาน​เดียว

สมาชิก​ตาม​ที่ javap เห็น
public static final Token$Companion Companion;
private final java.lang.String value;
private Token(java.lang.String);
public final java.lang.String getValue();
public final java.lang.String component1();
public final Token copy(java.lang.String);
public static Token copy$default(Token, java.lang.String, int, java.lang.Object);
public java.lang.String toString();
public int hashCode();
public boolean equals(java.lang.Object);
public Token(java.lang.String, kotlin.jvm.internal.DefaultConstructorMarker);
static {};

constructor ที่​คุณ​ประกาศ​เป็น private ก็​เป็น private จริง · public final Token copy(java.lang.String); เป็น public ตาม​ที่​ตาราง​บอก

และ​มี​ตัว​ที่​สาม​ที่​คุณ​ไม่​ได้​เขียน — constructor สังเคราะห์​ที่​รับ DefaultConstructorMarker ซึ่ง​เป็น public ที่​ชั้น JVM · code ที่​เรียก​ผ่าน reflection ใช้​มัน​ได้​โดย​ไม่​ต้อง​เรียก setAccessible

โหมด​ปริยาย​ของ info: kotlinc-jvm 2.4.10 (JRE 26.0.2.1) เตือน​เรื่อง​นี้​อยู่​แล้ว · มัน​บอก​ตรง ๆ ว่า copy() จะ​เปลี่ยน​ระดับ​การ​มอง​เห็น​ใน​รุ่น​ถัด​ไป

ตัว​วัด​ลอง​เปิด​โหมด​ภาษา 2.5 ดู · kotlinc คืน​รหัส 1 พร้อม error 3 รายการ

แปล​ว่า​นี่​ไม่ใช่​พฤติกรรม​ที่​ทีม​ภาษา​ตั้งใจ​ให้​อยู่​ถาวร · มัน​คือ​หนี้​ที่​มี​กำหนด​ชำระ​แล้ว

ปิด​ล่วง​หน้า​ได้​ตอน​นี้​ด้วย @ConsistentCopyVisibility บน class นั้น

ถ้า invariant ของ​คุณ​ตรวจ​ได้​จาก​ค่าที่​รับ​มา​อย่าง​เดียว ให้​ไว้​ใน init · มัน​คุ้มครอง​ทั้ง factory ทั้ง copy() และ​ทั้ง​เส้น​ทาง​ที่​ยัง​ไม่มี​ใคร​คิดถึง

ถ้า factory ทำ​อย่าง​อื่น​ด้วย เช่น​ทำให้​ค่า​อยู่​ใน​รูป​มาตรฐาน เรียก​ของ​ข้าง​นอก หรือ​ดู​ของ​ที่​ไม่​ได้​อยู่​ใน parameter · งาน​ส่วน​นั้น​อยู่​ใน init ไม่​ได้ และ copy() จะ​ข้าม​มัน​ไป · ติด @ConsistentCopyVisibility แล้ว​ปิด​ประตู​นั้น​ให้​จบ

และ​ถ้า​คุณ​แค่​อยาก​ได้ equals กับ hashCode โดย​ไม่​อยาก​ได้ copy() · อย่า​ใช้ data class เขียน​สอง method นั้น​เอง​สั้น​กว่า​ที่​คิด

  • ไม่​ได้​วัด​เวลา หน่วย​ของ​หน้า​นี้​คือ​จำนวน​ครั้ง​ที่​บล็อก​ถูก​เรียก, ชื่อ class ของ exception, access flag และ​รหัส​ออก​ของ​คอม​ไพ​เลอ​ร์
  • ไม่​ได้​วัด @ConsistentCopyVisibility หน้า​นี้​บอก​ว่า​มัน​มี​อยู่ แต่​ไม่​ได้​วัดผล​ของ​มัน
  • ไม่​ได้​วัด Kotlin/JS หรือ Kotlin/Native ตัวเลข​ทั้งหมด​มา​จาก Kotlin/JVM
  • ผูก​กับ version ของ​คอม​ไพ​เลอ​ร์ วัด​บน info: kotlinc-jvm 2.4.10 (JRE 26.0.2.1) · วัน​ที่​โหมด​ปริยาย​พลิก​เป็น error หน้า​นี้​จะ​เล่า​เรื่อง​อดีต

ตัว​วัด​อยู่​ที่ scripts/kotlin-private-copy/ รัน​ด้วย npm run verify:kotlin-copy · มัน​อยู่​ใน​เกต​ของ CI จึง​มี​คน​รัน​ทุก​ครั้ง​ที่​มี​คน push

ด่าน​นี้​ไม่​ดู​รหัส​ออก​ของ kotlinc เดี่ยว ๆ และ​นั่น​ตั้งใจ

การ​ตรวจ “โหมด 2.5 ทำให้​เป็น error” ด้วย​รหัส​ออก​อย่าง​เดียว ผ่าน​ได้​ฟรี​ใน​กรณี​ที่ สวิตช์​ไม่​เคย​ถึง​คอม​ไพ​เลอ​ร์

ถ้า argument ถูก​ส่ง​ไป​เป็น​ก้อน​เดียว kotlinc ตอบ invalid argument แล้ว​ออก​ด้วย​รหัส 1 ซึ่ง​เป็น​รหัส​เดียว​กับ​ที่​ด่าน​รอ · ด่าน​จะ​เขียว​โดย​ไม่​เคย​เปิด​โหมด​นั้น​เลย · รี​โป​นี้​เคย​โดน​กับ​ดัก​ตระกูล​เดียวกัน​มา​แล้ว​ใน​บทความ C++ ที่​ได้ warning เป็น​ศูนย์

ด่าน​จึง​บังคับ​สาม​อย่าง​พร้อม​กัน: ต้อง​เจอ error ที่​บรรทัด​ของ copy() · ต้อง​เจอ​คำ​เตือน​ว่า​โหมด​นี้​ยัง​ทดลอง · และ​ต้อง ไม่มี บรรทัด invalid argument