ชนิดที่จำกัดค่า null และชนิดที่อนุญาต null
(bugs.openjdk.org)- Java กำลังเตรียม ฟีเจอร์ภาษาแบบพรีวิว ที่เพิ่ม มาร์กเกอร์ nullness ให้กับชนิด โดย
Foo!ปฏิเสธnullและFoo?อนุญาตโดยเจตนา Fooที่ไม่มีมาร์กเกอร์จะยังมี nullness เป็น ไม่ได้ระบุ เพื่อคงความเข้ากันได้กับโค้ดเดิม และการแปลงระหว่างชนิดที่มี nullness ต่างกันอาจมาพร้อมคำเตือนหรือการตรวจสอบตอนรันไทม์- ฟิลด์และอาร์เรย์ที่ปฏิเสธ
nullไม่สามารถใช้ค่าเริ่มต้นnullได้ ดังนั้นฟิลด์ของอินสแตนซ์ต้องถูกกำหนดค่าอย่างแน่นอนก่อนเรียกsuper(...)และอาร์เรย์ต้องมี ค่าเริ่มต้น ของคอมโพเนนต์ - เมื่อ
nullถูกแคบลงเป็นFoo!จะเกิดNullPointerExceptionและในเส้นทางการเก็บลงอาร์เรย์/ฟิลด์ จะป้องกันการปนเปื้อนด้วย ArrayStoreException และ FieldStoreException - นี่เป็นรากฐานที่ทำให้การปรับแต่งประสิทธิภาพอย่าง การทำให้ value class แบนราบ ของ Valhalla สามารถเชื่อถือการตัด
nullออกได้ โดยการนำไปใช้กับไลบรารีมาตรฐานหรือการตีความโค้ดเดิมใหม่อัตโนมัติยังไม่ใช่เป้าหมายในตอนนี้
ฟีเจอร์พรีวิวสำหรับระบุ nullness ในชนิดของ Java
- เพิ่ม มาร์กเกอร์ nullness ให้ชนิดของ Java เพื่อแสดงว่าชุดค่าของชนิดนั้นมี
nullรวมอยู่หรือไม่ Foo!คือชนิด null-restricted ซึ่งตัดnullออกจากชุดค่าFoo?คือชนิด nullable ซึ่งตั้งใจรวมnullไว้ในชุดค่าFooที่ไม่มีมาร์กเกอร์มี nullness เป็น unspecified โดยอาจเกิดnullได้ แต่ไม่อาจรู้ได้ว่าการมีอยู่ของมันเป็นความตั้งใจหรือไม่- ฟีเจอร์นี้เป็น ฟีเจอร์พรีวิว ที่เปิดใช้ด้วยแฟล็กคอมไพล์และรันไทม์
--enable-preview
เป้าหมายและสิ่งที่ไม่ใช่เป้าหมาย
- แสดงว่าชนิดอ้างอิงของ Java คาดหวัง
nullหรือไม่ และให้คำเตือนกับการตรวจสอบสำหรับการแปลงระหว่างชนิดที่มี nullness แตกต่างกัน - เข้ากันได้ กับโค้ด Java เดิม และต้องสามารถนำมาใช้แบบค่อยเป็นค่อยไปโดยไม่สร้างความไม่เข้ากันทั้งในระดับซอร์สหรือไบนารี
- ตัวแปรของชนิดที่ปฏิเสธ
nullต้องถูกเริ่มต้นก่อนถูกอ่านครั้งแรก และการปฏิเสธnullจะถูกบังคับตอนรันไทม์แม้ในคลาสที่คอมไพล์แยกกัน - มอบเมตาดาต้าและการรับประกันความถูกต้องครบถ้วน เพื่อให้การปรับแต่งรันไทม์อย่างการทำให้ value class ของ Valhalla แบนราบสามารถเชื่อถือชนิดที่จำกัด null ได้
- รายการที่ยังไม่ใช่เป้าหมายในตอนนี้มีดังนี้
- ไม่ตีความโค้ดเดิมใหม่โดยอัตโนมัติ
- ไม่ทำให้
nullที่เป็นไปได้ทั้งหมดกลายเป็นข้อผิดพลาดตอนคอมไพล์ - ไม่เพิ่มรูปแบบ nullable ให้ชนิดพื้นฐานอย่าง
int - ไม่ใช้ส่วนขยายภาษาเหล่านี้กับไลบรารีมาตรฐานในขั้นตอนปัจจุบัน
ทำไมจึงจำเป็น
- ตัวแปร
Stringของ Java สามารถเก็บการอ้างอิงออบเจ็กต์Stringหรือnullได้ แต่ในระดับภาษาไม่มีวิธีแสดงว่าตั้งใจให้เป็นแบบใด - โปรแกรมจำนวนมากสมมติว่าไม่มี
nullแต่ต้องมีงานเพิ่มเติมเพื่อบังคับใช้เรื่องนี้ให้สอดคล้องกันทั้งในสเปก Javadoc และโค้ดอิมพลีเมนต์ - เมื่อความคาดหวังนี้พัง ค่า
nullอาจไหลอยู่ในโค้ดอิมพลีเมนต์แล้วไปทำให้เกิดข้อยกเว้น ณ จุดที่อยู่ไกลจากบั๊กต้นเหตุ - หากนักพัฒนาแสดงเจตนาว่าปฏิเสธหรืออนุญาต
nullเป็นส่วนหนึ่งของชนิด ฟีดแบ็กตอนคอมไพล์และการตรวจสอบตอนรันไทม์จะช่วยค้นหาnullที่ไม่คาดคิดได้เร็วขึ้น - ใน Valhalla ตัวแปรชนิด value class อาจถูกปรับแต่งเป็น การแทนค่าแบบแบนราบ ของค่าได้ แต่ถ้าต้องมีบิตเพิ่มเติมเพื่อเข้ารหัส
nullก็อาจเพิ่มการใช้หน่วยความจำหรือทำให้ปรับแต่งพื้นที่จัดเก็บไม่ได้ - ใน Amber ค่า nullness ของผู้สมัครสำหรับ pattern matching อาจมีผลต่อการตัดสิน exhaustiveness ของ
switchและค่า nullness ของ type pattern อาจมีผลต่อว่าจะจับคู่กับnullหรือไม่
ไวยากรณ์มาร์กเกอร์ nullness และโครงสร้างชนิด
- nullness ถือเป็นส่วนหนึ่งโดยเนื้อแท้ของชนิด และ
Foo?กับFooเป็น คนละชนิด กันเพราะมี nullness ต่างกัน - ทั้งชนิดอาร์เรย์และชนิดคอมโพเนนต์ของอาร์เรย์สามารถมีมาร์กเกอร์ nullness ได้
Foo?[]!คือชนิดที่ตัวอาร์เรย์เองเป็น null-restricted และคอมโพเนนต์เป็นFooแบบ nullable- ในอาร์เรย์หลายมิติ สามารถใส่มาร์กเกอร์หลังวงเล็บเหลี่ยมแต่ละคู่ได้ และตามธรรมเนียมจะตีความจากซ้ายไปขวา จากด้านนอกไปด้านใน
- ชนิดแบบ parameterized และอาร์กิวเมนต์ชนิดก็สามารถมีมาร์กเกอร์ nullness ได้
Predicate!<Foo?>คือPredicateแบบ null-restricted และอาร์กิวเมนต์ชนิดเป็นFooแบบ nullable
- หากต้องการแสดงชนิดที่จำกัด null หรืออนุญาต null ซอร์สต้องมี
!หรือ?ปรากฏอย่าง ชัดเจน - ในอนาคตอาจพิจารณาวิธีที่ตีความชนิดทั้งหมดในคลาสหรือหน่วยคอมไพล์ให้เป็น null-restricted โดยปริยาย และใช้
?เป็นข้อยกเว้นเท่านั้น แต่รายละเอียดเป็นงานแยกต่างหาก
กฎการเริ่มต้นฟิลด์และอาร์เรย์
- ใน Java เดิม ค่าเริ่มต้นของฟิลด์ชนิดอ้างอิงและคอมโพเนนต์อาร์เรย์คือ
nullแต่ไม่เหมาะสำหรับค่าเริ่มต้นของฟิลด์หรือคอมโพเนนต์อาร์เรย์แบบ null-restricted - ฟิลด์และอาร์เรย์แบบ null-restricted ต้องถูกโปรแกรมเริ่มต้นเสมอก่อนถูกอ่าน
- หากฟิลด์อินสแตนซ์แบบ null-restricted ไม่มี initializer จะต้องถูก กำหนดค่าอย่างแน่นอน ก่อนการเรียก
super(...)แบบชัดแจ้งหรือโดยนัยในแต่ละคอนสตรักเตอร์- Flexible Constructor Bodies JEP ทำให้เขียนโค้ดเริ่มต้นที่จำเป็นไว้ตอนต้นของคอนสตรักเตอร์ได้
- ใน early construction context นี้ จะไม่อนุญาตการกระทำที่มีความเสี่ยง เช่น การอ้างถึง
thisหรือการอ่านฟิลด์ที่ยังไม่ได้เริ่มต้น
- หากฟิลด์อินสแตนซ์แบบ null-restricted มี initializer จะถูกเรียกใช้ตอนเริ่มของแต่ละคอนสตรักเตอร์ ก่อนการเรียก
super(...)- คอนสตรักเตอร์ที่เรียก
this(...)เป็นกรณีพิเศษที่ไม่เรียกใช้ initializer เช่นเดียวกับกฎเดิม
- คอนสตรักเตอร์ที่เรียก
- ฟิลด์สแตติกแบบ null-restricted ต้องถูกกำหนดค่าอย่างแน่นอนก่อนที่ static initializer และบล็อกเริ่มต้นทั้งหมดของคลาสจะจบลง
- หากคลาสอื่นพยายามอ่านฟิลด์นั้นระหว่างการเริ่มต้นคลาส การตรวจสอบตอนรันไทม์จะตรวจพบการอ่านก่อนเวลาและโยนข้อยกเว้น
- อาร์เรย์ที่มีชนิดคอมโพเนนต์แบบ null-restricted ต้องระบุ ค่าเริ่มต้น ของแต่ละคอมโพเนนต์ในนิพจน์สร้างอาร์เรย์
- สามารถลิสต์ค่าทั้งหมดด้วย array initializer ได้
- ไวยากรณ์ย่อแบบใหม่ก็เป็นไปได้เช่นกัน แต่ไวยากรณ์ยัง TBD
nullness ของนิพจน์และการแปลง nullness
- คอมไพเลอร์ Java จะกำหนด nullness ของนิพจน์ทั้งหมดในกระบวนการตรวจสอบชนิด
- nullness ของการอ้างอิงตัวแปรมาจากประกาศตัวแปร และ nullness ของการเรียกเมธอดมาจากชนิดคืนค่าของเมธอดที่อ้างถึง
- ลิเทอรัล
nullเป็น nullable - นิพจน์ชนิดอ้างอิงอื่น ๆ ส่วนใหญ่เป็น null-restricted
- รวมถึงลิเทอรัล การต่อสตริง
thisการสร้างอินสแตนซ์คลาส การสร้างอาร์เรย์ การอ้างอิงเมธอด และนิพจน์แลมบ์ดา
- รวมถึงลิเทอรัล การต่อสตริง
- อนุญาตการแปลง nullness ในบริบทการกำหนดค่า การเรียก และการแคสต์
- การแปลง nullness แบบขยาย รวมถึง
Foo!→Foo?Foo!→Fooแบบไม่ได้ระบุFoo?→Fooแบบไม่ได้ระบุFooแบบไม่ได้ระบุ →Foo?
- การแปลง nullness แบบแคบลง รวมถึง
Foo?→Foo!Fooแบบไม่ได้ระบุ →Foo!
- การแปลง nullness แบบแคบลงจะถูกคอมไพเลอร์ทำโดยอัตโนมัติคล้ายการแปลง unboxing แต่ตอนรันไทม์จะทำการตรวจสอบแบบไดนามิกซึ่งอาจทำให้เกิด
NullPointerException - ความพยายามแปลงลิเทอรัล
nullไปเป็นชนิด null-restricted โดยตรงเป็นข้อผิดพลาดตอนคอมไพล์
การตรวจสอบตอนรันไทม์และข้อยกเว้น
- หากค่า
nullถูกแปลง nullness แบบแคบลงเป็นชนิด null-restricted ตอนรันไทม์ จะเกิด NullPointerException - การแปลง nullness แบบแคบลงที่ไม่ปรากฏอย่างชัดเจนในซอร์สโค้ดก็อาจเกิดขึ้นระหว่างการรันได้
- อาร์เรย์ที่ถูกกำหนดให้มีชนิดคอมโพเนนต์แบบ null-restricted จะปฏิเสธค่า
nullในการตรวจสอบการเก็บลงอาร์เรย์ตามปกติ แม้จะถูก扱ายในซอร์สโค้ดด้วยชนิดที่เจาะจงน้อยกว่า- ความล้มเหลวในการแปลงนี้ทำให้เกิด ArrayStoreException
- หากฟิลด์ที่ไม่ใช่ null-restricted ตอนคอมไพล์ภายหลังกลายเป็น null-restricted จากการคอมไพล์แยกกัน field store check แบบใหม่จะปฏิเสธการเก็บ
null- ความล้มเหลวในการแปลงนี้ทำให้เกิด FieldStoreException
- ในการเรียกเมธอดที่มีความสัมพันธ์แบบ overriding อาจมีการแปลงไปเป็นชนิดการเรียกของพารามิเตอร์เมธอดระดับบนก่อน แล้วจึงตามด้วยการแปลงไปเป็นชนิดพารามิเตอร์ของเมธอดที่ override
- ค่าคืนกลับของเมธอดก็อาจถูกแปลงเป็นชนิดคืนค่าของเมธอดที่ประกาศไว้ก่อน แล้วจึงแปลงเป็นชนิดคืนค่าที่คาดหวัง ณ จุดเรียก
เจเนอริก อาร์กิวเมนต์ชนิด และ overriding
- การใช้ type variable ก็สามารถใส่มาร์กเกอร์ nullness ได้ โดย
T!เป็นชนิด null-restricted และT?เป็นชนิด nullable - ชนิด type variable แบบ null-restricted และ nullable เป็นการยืนยัน nullness เฉพาะภายในโค้ดเจเนอริก
- ชนิดที่ใช้เป็นอาร์กิวเมนต์ชนิดก็สามารถแสดง nullness ได้ และมาร์กเกอร์ nullness ที่ติดกับชนิด type variable จะทับ nullness ที่ยืนยันไว้ในอาร์กิวเมนต์ชนิด
- ภายในอิมพลีเมนต์แบบ erased ของ API เจเนอริก ไม่สามารถบังคับใช้การจำกัด null ได้
- อย่างไรก็ตาม implicit cast ตามปกติที่เกิดขึ้นตรงขอบเขต API เจเนอริกจะบังคับใช้อาร์กิวเมนต์ชนิดแบบ null-restricted ตอนรันไทม์
- เพื่อการทำงานร่วมกัน nullness ภายในอาร์กิวเมนต์ชนิดจะไม่ถูกบังคับใช้อย่างเข้มงวด
Predicate<String!>สามารถแปลงเป็นPredicate<String>หรือPredicate<String?>ได้- unchecked nullness conversion แบบนี้อาจก่อให้เกิดคำเตือน
- การเปลี่ยน nullness ของชนิดคอมโพเนนต์อาร์เรย์ก็อนุญาตในฐานะ unchecked nullness conversion และเงื่อนไขที่จะมีการตรวจสอบตอนรันไทม์ยัง TBD
- ในการพิจารณาความเหมือนของเมธอดซิกเนเจอร์ จะเพิกเฉยต่อ nullness
- เมธอดหนึ่งสามารถ override อีกเมธอดได้แม้ nullness ของพารามิเตอร์และชนิดคืนค่าไม่ตรงกัน
- หาก API ต่าง ๆ นำมาร์กเกอร์ nullness มาใช้แยกกัน ความไม่ตรงกันลักษณะนี้อาจพบได้บ่อย
- nullness ไม่มีผลต่อ applicability ของเมธอด และไม่สามารถทำให้การอนุมานอาร์กิวเมนต์ชนิดล้มเหลวได้ แต่อาจมีผลต่อ nullness ที่อนุมานสำหรับชนิดคืนค่าของเมธอดเจเนอริก
- รายละเอียดของอัลกอริทึมอนุมานยัง TBD
คำเตือนและข้อผิดพลาดจากคอมไพเลอร์
- การทำให้ชนิดเป็น null-restricted อาจทำให้เกิดข้อผิดพลาดใหม่ตอนคอมไพล์
- เมื่อฟิลด์หรืออาร์เรย์ของชนิดนั้นไม่ได้ถูกเริ่มต้น
- เมื่อพยายามแปลงลิเทอรัล
nullเป็นชนิดนั้น - การเปรียบเทียบลิเทอรัล
nullกับนิพจน์ชนิด null-restricted ก็อาจกลายเป็นข้อผิดพลาดตอนคอมไพล์ได้
- ในสถานการณ์อื่น ๆ การวิเคราะห์ nullness เป็นเพียงตัวช่วยและไม่สร้างข้อผิดพลาดตอนคอมไพล์
javacจะให้คำเตือนเพื่อหลีกเลี่ยงข้อผิดพลาดตอนรันไทม์ และแนะนำให้ IDE กับเครื่องมือวิเคราะห์อื่น ๆ เดินไปในทิศทางเดียวกัน- สาเหตุที่เป็นไปได้ของคำเตือนมีดังนี้
- การแปลง nullness แบบแคบลง รวมถึงกรณีที่มาจากชนิด
? - การใช้นิพจน์ชนิด
?กับการเข้าถึงสมาชิกหรือการดำเนินการอื่นที่ไม่เป็นมิตรต่อ null - เมื่อ nullness ของอาร์กิวเมนต์ชนิดไม่สอดคล้องกับ bound
- เมื่อ nullness ของพารามิเตอร์เมธอดหรือค่าคืนกลับไม่ตรงกับเมธอดที่ถูก override
- unchecked conversion ที่เปลี่ยน nullness ของชนิด
- การแปลง nullness แบบแคบลง รวมถึงกรณีที่มาจากชนิด
ไฟล์คลาส รีเฟลกชัน และการเปลี่ยนแปลงประกอบ
- การใช้มาร์กเกอร์ null ส่วนใหญ่จะถูกลบทิ้งในไฟล์
classและการแปลงรันไทม์ที่เกี่ยวข้องจะแสดงโดยตรงใน bytecode - ไวยากรณ์ของแอตทริบิวต์
Signatureจะถูกอัปเดตให้อนุญาต!และ?ภายในชนิด - nullness จะไม่ถูกเข้ารหัสใน descriptor ของเมธอดและฟิลด์
- เพื่อป้องกันการปนเปื้อนของฟิลด์ จะมีแอตทริบิวต์ใหม่
NullRestrictedเพื่อระบุว่าฟิลด์ไม่อนุญาตค่าnull- ฟิลด์นี้ต้องถูกทำเครื่องหมาย
ACC_STRICTด้วย และต้องถูกเริ่มต้นอย่างเข้มงวด - verifier จะตรวจสอบว่าฟิลด์อินสแตนซ์แบบ strict initialization ทั้งหมดถูกกำหนดค่าแล้วหรือไม่ ณ เวลาที่คอนสตรักเตอร์เรียก
super(...) - ความพยายามเขียนลงฟิลด์ทั้งหมดจะตรวจสอบค่า
nullและหากพบจะโยนFieldStoreException
- ฟิลด์นี้ต้องถูกทำเครื่องหมาย
- การสร้างอาร์เรย์แบบ null-restricted ไม่รองรับด้วยคำสั่ง
anewarrayและต้องทำผ่านการเรียก reflection API - ไม่มีลิเทอรัล
Foo!.classหรือFoo?.classและไม่มีอินสแตนซ์java.lang.Classที่สอดคล้องกัน - RuntimeType API ใหม่จะอธิบายชุดชนิดที่ถูกบังคับใช้ตอนรันไทม์ในการตรวจสอบการเก็บลงอาร์เรย์และฟิลด์ รวมถึงรูปแบบ null-restricted ของชนิดคลาสและอินเทอร์เฟซทั้งหมด
FieldAPI รองรับการดูRuntimeTypeของฟิลด์ และค่านี้อาจแตกต่างจากผลของgetTypeArrayAPI รองรับรูปแบบnewInstanceที่แสดงชนิดคอมโพเนนต์ด้วยRuntimeType- รูปแบบนี้อนุญาตให้ระบุค่าเริ่มต้นของคอมโพเนนต์อาร์เรย์ด้วย
- ปฏิเสธความพยายามสร้างอาร์เรย์แบบ null-restricted ที่ไม่มีค่าเริ่มต้น
- deserialization แบบดั้งเดิมไม่เข้ากันกับฟิลด์และอาร์เรย์แบบ null-restricted และ JEP แยกต่างหากจะจัดเตรียมกลไก serialization ที่ไม่เปิดเผยฟิลด์และอาร์เรย์แบบ null-restricted ที่ยังไม่ได้เริ่มต้น
- เอกสารที่
javadocสร้างจะรวมมาร์กเกอร์ nullness ด้วย - API
java.lang.reflect.Typeและjavax.lang.modelจะเข้ารหัส nullness ในการแทนชนิด
ทางเลือกและความสัมพันธ์พึ่งพา
- เครื่องมือนักพัฒนาหลายตัวในระบบนิเวศ Java เคยติดตาม null ของตนเองตอนคอมไพล์ แต่เพราะไม่ได้เปลี่ยนภาษา Java ไวยากรณ์จึงมักจำกัดอยู่ที่ annotation และพฤติกรรมที่ส่งผลได้ก็จำกัดอยู่ที่การตรวจสอบตอนคอมไพล์
- ภาษาอื่นติดตาม nullness ในระบบชนิด และหลายภาษามีค่าเริ่มต้นเป็น null-restricted พร้อมมองว่าการกำหนดค่าให้ชนิด null-restricted โดยไม่มีการตรวจสอบ null อย่างชัดเจนเป็นข้อผิดพลาด
- ใน Java ฟีเจอร์ต้องเป็นแบบเลือกใช้ได้ และต้องใช้ได้อย่างค่อยเป็นค่อยไปโดยไม่ต้องทำ migration ขนาดใหญ่ครั้งเดียว
- การบังคับใช้ nullness ตอนรันไทม์สามารถทำได้ด้วยการตรวจสอบแบบชัดเจนหรือการเรียก
Objects.requireNonNull- แต่การใช้อย่างสม่ำเสมอนั้นยุ่งยาก ต้องมีเอกสารเพิ่มเติม และทำให้อ่านโปรแกรมยากขึ้น
- ไม่มีวิธีใช้โดยตรงกับที่เก็บตัวแปรอย่างฟิลด์และอาร์เรย์
- เงื่อนไขก่อนหน้าคือ Flexible Constructor Bodies (Second Preview)
- คอนสตรักเตอร์สามารถรัน statement และกำหนดค่าให้ฟิลด์อินสแตนซ์ก่อนเรียก
super(...)ได้ จึงทำให้ข้อกำหนดการเริ่มต้นฟิลด์แบบ null-restricted เป็นไปได้
- คอนสตรักเตอร์สามารถรัน statement และกำหนดค่าให้ฟิลด์อินสแตนซ์ก่อนเรียก
- งานในอนาคตรวมถึงสิ่งต่อไปนี้
- Null-Restricted Value Class Types (Preview): การปรับแต่งการเข้ารหัสของฟิลด์และอาร์เรย์ที่มีชนิดเป็น value class แบบ null-restricted
- JEP 402: Enhanced Primitive Boxing (Preview): การติดตาม nullness เมื่อภาษาใช้การแปลง boxing โดยนัยได้กว้างขึ้น
- การทำ specialization ของคลาสและเมธอดใน JVM (JEP 218 และฉบับปรับปรุง): ทำให้ nullness ของอาร์กิวเมนต์ชนิดบางส่วนเป็นรูปธรรมและบังคับใช้
- การปรับปรุงเพิ่มเติมที่อาจมีในอนาคต ได้แก่ การใส่มาร์กเกอร์ nullness ให้ API มาตรฐานบางส่วน การแสดง null check ใน bytecode ให้กระชับ การบังคับใช้ระดับต่ำที่เข้มงวดยิ่งขึ้นสำหรับพารามิเตอร์เมธอดแบบ null-restricted และกลไกภาษาที่ประกาศโดยนัยให้ชนิดทั้งหมดในบางบริบทเป็น null-restricted
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
น่าสนใจที่แนวทางนี้ต่างจากวิธีที่ C# นำมาใช้เมื่อไม่กี่ปีก่อน ใน C# ถ้าเปิดใช้งาน nullability ในโปรเจกต์ ตัวแปรทั้งหมดจะถูกประกาศเป็น non-null เว้นแต่จะระบุอย่างชัดเจนว่าเป็น nullable ส่วนข้อเสนอนี้ทำให้ตัวแปรเดิม ๆ กลายเป็นหนึ่งในสามแบบโดยพฤตินัย: nullable, nullable แบบชัดเจน, หรือ non-nullable แบบชัดเจน
Kotlin ก็เป็นภาษา JVM เช่นกัน แต่เหมือน C# ตรงที่ถ้าไม่ระบุจะถือว่าเป็น non-null เพียงแต่ Kotlin ไม่มีภาระเรื่องความเข้ากันได้ย้อนหลัง นอกจากนี้ยังมีทางเลี่ยงอย่าง
lateinit varที่ทำให้ปล่อยค่าในประเภท non-nullable ว่างไว้ได้จนกว่าจะถูกกำหนดค่าในเมธอดอื่น และถ้าเข้าถึงก่อนกำหนดค่าก็จะโยนข้อยกเว้นเฉพาะให้สงสัยว่าทำไมถึงมี ตัวเลือกสามแบบ กันแน่ รู้สึกว่าน่าจะคงตัวแปรที่ไม่มี annotation ให้เป็น nullable แล้วให้เฉพาะตัวแปรที่มี annotation เป็น non-null แบบชัดเจนก็ได้ ถ้าไม่ใส่อะไรเลยก็เป็น nullable โดยอัตโนมัติอยู่แล้ว ก็คิดไม่ออกว่าทำไมถึงอยากประกาศ nullable อีก
ผมชอบแนวทางของ C# มากกว่า แต่แนวทางนี้มีข้อดีตรงที่ใช้กับโค้ดเบส legacy ได้โดยไม่ต้องแก้ปัญหา nullability ทั้งหมดให้เสร็จ ในทางกลับกัน C# จะเผยปัญหา nullability ออกมาทันที แต่ข้อเสนอนี้ยังซ่อนมันไว้ได้เหมือนเดิม
อีกอย่าง ส่วนที่ว่า “เมธอดหนึ่งสามารถ override เมธอดอื่นได้แม้ nullness ของพารามิเตอร์และค่าที่คืนกลับจะไม่ตรงกัน” ก็ดูแปลก น่าจะกลายเป็น ปืนยิงเท้าตัวเอง เวลา override/implement callback ที่เมธอดเดิมระบุว่าคืนค่า non-null แต่ตัวที่เขียนกลับคืนค่า null
ด้วยข้อจำกัดด้านเวลา ตอนนี้เราเลือกเปิด annotation เป็นหลัก ยกเว้นบางจุดสำคัญไม่กี่แห่ง แต่ยังคงใช้
NotNullAttributeและCanBeNullAttributeของ JetBrains เป็นเครื่องหมายอยู่ เพื่อให้เห็นทันทีว่าส่วนไหนตัดสินใจอย่างตั้งใจแล้ว ตัวหลังสามารถลบได้เพราะ nullable มีเครื่องหมายชัดเจนอยู่แล้ว แต่ตัวแรกมีปัญหาชื่อชนกับฟีเจอร์ของ C# เองในแง่นั้น ตัวเลือกสามแบบจึงค่อนข้างน่าพึงประสงค์ เพราะถ้าโค้ดมีเป็นแสน ๆ บรรทัด การย้ายให้เร็วและง่ายไม่ใช่เรื่องง่าย
ในโปรเจกต์ภายในอื่น ๆ เรากำลังค่อย ๆ ขยายขอบเขต nullability โดยโปรย
#nullable enableรอบ ๆ โค้ดที่แตะอยู่ และยังกำหนดด้วยว่าโค้ดใหม่ต้องอยู่ใน nullable context วิธีนี้ก็พอใช้ได้ในการระบุส่วนที่ใส่ annotation แล้ว แต่เป็นแนวทางที่ทำได้กับโค้ดเบสและทีมที่เล็กกว่ามากString!คำว่า “platform type” กับสัญลักษณ์อัศเจรีย์อาจทำให้สับสน แต่โดยรวมแล้วแนวทางของ Kotlin ทำงานได้ค่อนข้างดี เพราะ Kotlin มี nullability ตั้งแต่แรก โปรแกรมเมอร์จึงกำหนด platform type โดยตรงไม่ได้ แต่ก็ยังจำเป็นเพื่อให้เข้ากันได้กับแพลตฟอร์มพื้นฐานที่ nullability ไม่ชัดเจน เช่น JVM หรือ JavaScript
ในแนวทางนี้ ค่าเริ่มต้นยังสมเหตุสมผลอยู่ โดยพื้นฐานแล้วควรเป็น non-nullable เสมอ และใครที่คิดต่างก็เท่ากับไม่ได้เรียนรู้อะไรจาก Tony Hoare เลย ในขณะเดียวกันก็ยังรักษาความเข้ากันได้ย้อนหลังไว้ได้ Kotlin ถือว่าง่ายกว่า ส่วน Java กับ C# ต้องรักษาความเข้ากันได้กับซอร์สโค้ดเดิมด้วย
ทั้งแนวทางของ Java และของ C# ต่างก็ไม่ใช่อุดมคติ แนวทางของ C# เปลี่ยนพฤติกรรมของโค้ดไปมากตาม compiler flag ส่วนแนวทางของ Java ทำให้ค่าเริ่มต้นเป็นตัวเลือกที่แย่ที่สุด
ถึงอย่างนั้นก็เอนเอียงไปทาง C# มากกว่า เพราะถ้าทำให้ “อาจเป็น null ได้” เป็นตัวเลือกที่ง่ายที่สุด โปรแกรมเมอร์ส่วนใหญ่ก็จะเลือกสิ่งนั้นเป็นค่าเริ่มต้น โดยเฉพาะในภาษาแนวองค์กรอย่าง Java น่าจะยิ่งเป็นแบบนั้น Linter และ warning ของคอมไพเลอร์คงช่วยได้ในระยะยาว แต่กว่าจะถึงวันที่โค้ด Java ส่วนใหญ่ใส่ annotation เรื่อง nullability อย่างถูกต้องน่าจะใช้เวลาหลายปี ผู้ใช้ C# อาจเจ็บปวดกว่าในระยะสั้น แต่มีโอกาสไปถึงเป้าหมายเรื่อง nullability ที่ชัดเจนได้เร็วกว่ามาก
https://kotlinlang.org/docs/java-interop.html#null-safety-an...
https://www.infoq.com/presentations/Null-References-The-Bill...
T?หมายถึง nullable,T!หมายถึง non-nullable และค่าเริ่มต้นของTธรรมดาในซอร์สที่คอมไพล์ใหม่ควรถูกกำหนดได้ด้วยประกาศคล้าย pragma แบบรายไฟล์ซอร์ส, รายไฟล์package-info, หรือแบบ global ผ่าน compiler switchจากนั้นใน Java รุ่น LTS ถัด ๆ ไปค่อยเปลี่ยนค่าเริ่มต้นแบบ global ก็ได้ วิธีนี้จะทำให้การย้ายโปรเจกต์ง่ายขึ้น สามารถแทรก/อัปเดต pragma อัตโนมัติได้ และถ้าจำเป็นก็ยังคงค่าเริ่มต้นแบบเดิมไว้ได้
นอกจากนี้ ถ้านำ annotation
@Nonnullของ JSR-305 ที่มีอยู่แล้วมาใช้เป็นวิธีระบุการปรากฏของประเภท non-nullable ในไฟล์คลาส ก็น่าจะให้ความเข้ากันได้แบบสองทางกับ JDK รุ่นเก่าได้ด้วย!อย่างชัดเจน”แค่มี compiler flag หรือ tag ระดับโมดูลก็น่าจะโอเคแล้ว
ถึงอย่างนั้นก็คาดว่าน่าจะมี compiler flag ที่ทำให้ถ้าไม่ระบุเป็นอย่างอื่น จะถือว่าเป็น non-nullable โดยอัตโนมัติ
ดูดีนะ ในที่สุดก็มีวิธีในระดับภาษาเพื่อกำจัด exception ที่ไม่จำเป็นเป็นพัน ๆ ตัวและการตรวจสอบ null ได้แล้ว แต่การแปลงอัตโนมัติที่ทำให้ขอบเขตของความเป็น null แคบลงยังรู้สึกไม่ค่อยถูกต้อง
ในตัวอย่างของข้อเสนอ
String? id(String! arg) { return arg; },String s = null;,Object! o1 = s; // NPE,Object o2 = id(s); // NPE,Object o3 = (String!) s; // NPEอย่างน้อยสองกรณีแรกควรเป็น ข้อผิดพลาดตอนคอมไพล์ ไม่ใช่หรือกรณีสุดท้ายเป็นการระบุชัดเจนเลยค่อนข้างก้ำกึ่ง แต่ผมว่าให้คอมไพเลอร์รู้ชนิดที่มีผลเป็น
String!ภายในif (s != null)แล้วอนุญาตString! ss = s;แบบนั้นจะดีกว่า แบบนั้นจะไม่มีความเป็นไปได้ที่จะเกิดข้อผิดพลาดจริง ๆ แล้วเมื่อเทียบกับการ cast ในกรณีสุดท้าย ผมชอบใช้
Objects.requireNonNull(s)มากกว่า เพราะชัดเจนกว่า แต่ถ้ามีอะไรอย่างObjects.unsafeForceNonNull(s)ที่ข้ามการตรวจสอบแบบชัดเจนได้ ยกเว้นเฉพาะกรณีที่ถูกกันไว้เพราะ optimization ก็คงดี ถ้ามีเมธอด unsafe ก็จะสามารถ implementrequireNonNullเองได้โดยไม่ต้องเพิ่มการวิเคราะห์สแตติกที่ซับซ้อนน่าเสียดายที่เรื่องนี้จะตรวจพบได้เฉพาะที่ รันไทม์ เท่านั้น
เช่น standard library ระบุไว้อย่างชัดเจนว่าอย่างน้อยในตอนนี้ จะไม่ย้ายไปใช้ชนิด null
ดูเหมือนจำเป็นมากที่จะต้องมีวิธีทำเครื่องหมายตัวแปรทั้งหมดเป็นค่าเริ่มต้นแบบ non-null ในระดับแพ็กเกจ หรืออย่างน้อยระดับไฟล์ ไม่อย่างนั้นข้อเรียกร้องให้ใช้ไวยากรณ์
T!กับตัวแปรแทบทุกตัวเพื่อความปลอดภัยคงจะแรงขึ้น และนั่นจะสร้าง noise จำนวนมากเท่านั้น!อย่างชัดเจน”@Nullปัญหาจะเกิดเฉพาะตรงขอบเขตของโค้ดที่โต้ตอบกับไลบรารี ไวยากรณ์ใหม่นี้ก็น่าจะคล้ายกัน
ส่วนที่บอกว่า “ในตอนนี้ เป้าหมายไม่ใช่การนำการปรับปรุงของภาษาไปใช้กับ standard library” น่าเสียดาย
จากประสบการณ์ที่จำเป็นต้องใช้ PHP เวลาต้องถอดคุณสมบัติที่รับประกันไว้ล่วงหน้าของข้อมูลออก หรือประกอบกลับเข้าไปใหม่ทุกครั้งที่โต้ตอบกับ standard library ขนาดใหญ่นั้นยุ่งยาก
Java ก็ควรใส่พลังในการแสดงออกแบบนี้เข้าไปใน standard library ให้มากขึ้นและทำให้มันเป็น first-class citizen
Optionalจะกลายเป็นภาระต่อข้อเสนอ nullable/non-nullable แบบชัดเจน ผมเคยคิดว่า record type ก็น่าจะเป็น non-nullable โดยค่าเริ่มต้นได้อีกอย่าง การแบ่งเป็นสองขั้นจะทำให้ปล่อยฟีเจอร์นี้เป็น preview feature ได้ง่ายขึ้น รับฟีดแบ็ก แล้วค่อยล็อกการออกแบบให้แน่นอน ถ้าพยายามทำทั้งหมดในครั้งเดียว ก็แทบจะไม่มีพื้นที่ให้ iterate ปรับปรุงฟีเจอร์จากการใช้งานจริง
ถ้าฟีเจอร์นี้เข้ามาใน Java ได้ก็คงดี ความสามารถในการเลือกอย่างชัดเจนในระดับภาษาอย่าง
T?ช่วยยกระดับคุณภาพชีวิตของนักพัฒนาใน Kotlin และ TypeScript อย่างมาก Java มีเครื่องมืออย่าง NullAway อยู่ แต่ยุ่งยากผมมองว่าการรองรับในระดับภาษาดีกว่า
Optional/Maybeมาก เพราะทำให้โฟกัสกับ logic จริง แทนที่จะต้องเอาโค้ดไปวางบนรางmap/flatMaphttps://github.com/uber/NullAway
แค่ใช้ภาษา JVM อื่นก็ได้นี่นา
ตอนนี้ไซต์ล่มอยู่ เลยวางลิงก์ archive ไว้: https://web.archive.org/web/20240802081039/https://bugs.open...
“การบังคับให้โปรแกรมต้องจัดการค่า null ทั้งหมดที่อาจเกิดขึ้นอย่างชัดเจนไม่ใช่เป้าหมาย และค่า null ที่ไม่ได้จัดการอาจเป็นคำเตือนตอนคอมไพล์ได้ แต่ไม่ใช่ข้อผิดพลาด” เป็นการตัดสินใจที่แย่
Java ส่วนใหญ่เป็น ภาษาแบบ static typing แล้วทำไมถึงใส่พฤติกรรมแบบ dynamic เพิ่มเข้าไปอีกก็ไม่รู้ หวังว่าจะมีวิธีง่าย ๆ ในการยกระดับคำเตือนแบบนี้ให้เป็นข้อผิดพลาด
ภาษาที่ type system บังคับให้จัดการ null/nil ในทุกกรณีนั้นดีกว่ามากจริง ๆ แต่ Java ในปัจจุบันไม่ใช่ภาษาแบบนั้น ถึงอย่างนั้น นี่ก็น่าจะเป็นการปรับปรุงครั้งใหญ่
น่าเสียดายที่บทเรียนแบบนี้เพิ่งมาเรียนรู้กันช้าเกินไป โดยพื้นฐานควรเป็น non-nullable, เป็น immutable โดยปริยาย และมี scope ที่แคบที่สุดโดยปริยาย
ในการออกแบบใหม่ ๆ มักเลือกความสะดวกเฉพาะหน้าแทนที่จะ “ทำให้ไหลไปสู่เส้นทางที่ปลอดภัย” บ่อยเกินไป ค่าเริ่มต้นที่ปลอดภัยต้องการการออกแบบและประสบการณ์ผู้ใช้ที่พิถีพิถันกว่ามาก แต่ผลลัพธ์คือแทบทุกภาษา·แพลตฟอร์ม·เทคโนโลยีเต็มไปด้วยปืนที่เล็งใส่เท้าตัวเอง วิศวกรรมโยธาและไฟฟ้ามีกฎระเบียบ แต่ซอฟต์แวร์กลับเรียนบทเรียนเดิมซ้ำในภาษาและเทคโนโลยีใหม่ ๆ ทุกประมาณ 30 ปี
Java เหมือนหมู่บ้านในโลกที่สามที่เพิ่งเรียนรู้ว่าถ้าจะฆ่าเชื้อโรคก็ให้ต้มน้ำ
งานส่วนใหญ่ที่ทำที่ Facebook คือการใช้ Hack ความเป็น null ได้หรือไม่เป็นองค์ประกอบหลักของ type system ใน Hack และช่วยแก้ข้อผิดพลาดไร้สาระได้มากมายจริง ๆ
แน่นอนว่าไม่ได้หมายความว่าจะไม่มีทางได้รับ null ที่ไม่คาดคิดเลย เพราะฟีเจอร์นี้ก็ถูกเพิ่มเข้ามาในภาษาภายหลัง จึงยังมีชนิด
mixedแบบ legacy ที่สะท้อนรากจาก PHP อยู่มาก ซึ่งโดยพื้นฐานแล้วหมายความว่าเป็นอะไรก็ได้ก่อนอื่นสงสัยว่า nullable array จะเป็นอย่างไร ตัวอย่าง
String![]แสดงกรณีที่ object อาจเป็น null ได้ แต่ตัว array เองล่ะ? ใน Java นั้นString labels[] = null;ถูกต้องตามกฎหมายเต็มที่ งั้นต้องประกาศแบบString![]! labels;หรือเปล่า?ใน Hack นั้น
vec $fooหมายถึงทั้ง foo และ element ไม่ใช่ null ส่วน?vec $fooหมายถึง element เป็น non-null แต่ foo เป็น null ได้ ในทางปฏิบัติแทบไม่มีเหตุผลให้ใช้ array ที่เป็น null ดังนั้นค่าเริ่มต้นควรเป็นห้าม null อย่างไรก็ตาม Java มีปัญหาว่าโค้ด legacy ทั้งหมดตั้งสมมติฐานว่ามีความเป็นไปได้ที่จะเป็น nullจากตัวอย่าง
Object! o1 = s,Object o2 = id(s),Object o3 = (String!) sในข้อเสนอ รู้สึกว่าข้อ 2 และ 3 ควรเป็นข้อผิดพลาดตอนคอมไพล์ไม่ใช่หรือสุดท้ายนี้ ผมชอบตัวดำเนินการบังคับ
asของ Hack มากกว่าการ cast ของ Java เช่นfoo($b)เป็นข้อผิดพลาดตอนคอมไพล์,$b as Aถ้าเป็น null จะเกิดข้อผิดพลาดตอน runtime,$a as ?Bถ้าเป็น B ก็ cast ถ้าไม่ใช่ก็คืนค่า null เป็นต้นสุดท้ายคำถามคือจะสามารถครอบสิ่งนี้ทับ Java SDK ได้หรือไม่ และในโค้ด legacy จะมีหน้าตาเป็นอย่างไร
https://docs.hhvm.com/hack/types/nullable-types
String![]? labels = null;มั้งของดี ๆ จาก Kotlin ดูเหมือนกำลังทยอยเข้ามาใน Java หมดแล้ว
ถึงอย่างนั้นก็ยังอยากทำงานต่อใน Kotlin ที่ไม่ต้องรับมือกับอะไรอย่าง Lombok อยู่ดี Java record ก็ดีอยู่หรอก