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 ทำงาน | ผล |
|---|---|---|---|
factory | 1 | 1 | สร้างสำเร็จ |
factory-bad | 0 | 1 | IllegalArgumentException |
copy | 1 | 0 | สร้างสำเร็จ |
public-copy | 1 | 0 | สร้างสำเร็จ |
copy-empty | 1 | 0 | IllegalArgumentException |
แถวที่สามคือใจความ · copy() ผ่าน init 1 ครั้ง
แต่ผ่าน factory 0 ครั้ง
และมันสร้างอ็อบเจกต์ที่ factory ปฏิเสธได้สำเร็จ · แถวที่สองแสดงว่า factory ยังกันค่าเดียวกันนั้นอยู่
คำอธิบายที่เล่ากันบ่อยที่สุด และมันผิด
หัวข้อที่มีชื่อว่า “คำอธิบายที่เล่ากันบ่อยที่สุด และมันผิด”คำอธิบายที่เจอบ่อยคือ “copy() ข้าม init block”
แถวสุดท้ายของตารางปฏิเสธข้อนั้น · copy() ด้วยค่าที่ init ห้าม
ได้ IllegalArgumentException โดย init ทำงาน
1 ครั้ง
init ทำงานทุกครั้ง เพราะมันเป็นของ constructor และ copy() ก็เรียก constructor
ความต่างนี้ไม่ใช่รายละเอียดปลีกย่อย มันเปลี่ยนว่าคุณควรวาง invariant ไว้ที่ไหน
invariant ที่อยู่ใน init คุ้มครองทุกเส้นทาง เพราะทุกเส้นทางต้องสร้างอ็อบเจกต์
· invariant ที่อยู่ใน factory คุ้มครองเฉพาะคนที่เดินเข้าประตูที่คุณตั้งใจ
คนที่เชื่อคำอธิบายผิดจะย้าย invariant ออกจาก init เพราะคิดว่ามันไม่ทำงาน
ซึ่งเป็นการถอดยามตัวเดียวที่ยังยืนอยู่
ฝาแฝดที่ constructor เป็น public ให้ตัวเลขเดียวกันเป๊ะ
หัวข้อที่มีชื่อว่า “ฝาแฝดที่ constructor เป็น public ให้ตัวเลขเดียวกันเป๊ะ”ตัววัดมี class ฝาแฝดที่เหมือนกันทุกอย่าง ยกเว้น constructor เป็น public
| class | init | factory | ผล |
|---|---|---|---|
ctor เป็น private | 1 | 0 | สร้างสำเร็จ |
ctor เป็น public | 1 | 0 | สร้างสำเร็จ |
เท่ากันทุกช่อง · และนั่นคือประเด็น ไม่ใช่ข้อบกพร่องของการทดลอง
private ไม่ได้เปลี่ยนพฤติกรรมตอนรันเลยสักหน่วย · สิ่งที่มันเปลี่ยนคือ
ความคาดหวังของคนอ่าน code ว่ามีประตูอยู่บานเดียว
ที่ชั้น JVM มันไม่ได้ปิดด้วยซ้ำ
หัวข้อที่มีชื่อว่า “ที่ชั้น JVM มันไม่ได้ปิดด้วยซ้ำ”สมาชิกตามที่ 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
การตรวจ “โหมด 2.5 ทำให้เป็น error” ด้วยรหัสออกอย่างเดียว ผ่านได้ฟรีในกรณีที่ สวิตช์ไม่เคยถึงคอมไพเลอร์
ถ้า argument ถูกส่งไปเป็นก้อนเดียว kotlinc ตอบ invalid argument แล้วออกด้วยรหัส 1
ซึ่งเป็นรหัสเดียวกับที่ด่านรอ · ด่านจะเขียวโดยไม่เคยเปิดโหมดนั้นเลย
· รีโปนี้เคยโดนกับดักตระกูลเดียวกันมาแล้วในบทความ C++ ที่ได้ warning เป็นศูนย์
ด่านจึงบังคับสามอย่างพร้อมกัน: ต้องเจอ error ที่บรรทัดของ copy()
· ต้องเจอคำเตือนว่าโหมดนี้ยังทดลอง · และต้อง ไม่มี บรรทัด invalid argument