เข้าใจ Null Safety ใน Dart 2 (EP1)
ทำไมต้อง null safety
ปัญหาที่พบได้บ่อยในการเขียนโปรแกรมคือ โปรแกรม crash จากการเรียก method ผ่านตัวแปรที่มีค่าเป็น null ซึ่งอาจมาจากการลืม initialize ค่า หรือ ส่งค่า null เข้ามาใน function
// Without null safety:
bool isEmpty(String string) => string.length == 0;
main() {
final String someString; // someString is null
isEmpty(someString);
}
เมื่อ run code ชุดนี้ จะเกิด exceptionNoSuchMethodError
เนื่องจากตัวแปรที่ถูกส่งเข้าไปใน functionisEmpty
ยังไม่ได้มีการ initialize ค่า โดยค่า default มีค่าเป็น null
เมื่อ function isEmpty
เรียก .length
จึงเป็นการเรียก length method ผ่าน Null
ซึ่ง .length
เป็น method ที่ไม่มีใน Null
ทำให้เกิด exception ขึ้น
ดั้งนั้นการที่ Dart 2.12 ได้เปลี่ยนมาเป็น null safety จะทำให้ compiler สามารถตรวจสอบความผิดพลาดที่จะเกิด null exception ได้ก่อนที่จะ run code จริง คือเปลี่ยนจากการเจอปัญหาตอน runtime มาเป็น compile time แทน ซึ่งทำให้สามารถเจอปัญหาในตอน development แทนที่จะไปเจอเมื่อใช้งานโปรแกรม
หลักการออกแบบ null safety ใน Dart
- Code should be safe by default
คือถ้าเขียน Dart code ปกติธรรมดาไม่ได้ใช้ explicit unsafe feature ใด ๆ โปรแกรมจะต้องไม่ throw exception ตอน runtime และโอกาสที่จะทำให้เกิด null exception จะต้องโดยจับได้ใน compile time - Null safe code should be easy to write
จะต้องง่ายต่อการเขียน ไม่ได้เปลี่ยนแปลงไปจากเดิม และต้องไม่ทำให้เขียนยากขึ้นเพื่อแลกกับการเป็น null safety - The resulting null safe code should be fully sound
Code จะต้องปลอดภัยอย่างสมบูรณ์ คือถ้า type ไหนที่ไม่สามารถเป็น null ได้ Dart จะต้องไม่อนุญาตให้ใส่ค่าnull
หรือทำให้เกิดการ evaluate ค่านั้นเป็นnull
ได้ โดย Dart จะการันตีผ่านทางการทำ static check และ runtime check ร่วมด้วยในบางกรณี
Null typeใน Dart type system
ใน version Dart 1.x ก่อนที่ Dart จะเป็น null safety นั้น Dart จะมอง Null
เป็น subtype ของทุก type คือสามารถให้ทุก type มีค่าเป็น null
ได้ ซึ่งก็ทำให้เกิดปัญหา runtime exception ได้
ดั้งใน Dart 2.12 จึงมีการแยก Null
type ออกจาก type ต่างๆ และทุก type จะมีค่า default เป็น non-nullable ดั้งจะไม่มี type ไหนที่จะมีค่า null
ได้ยกเว้น type Null
อย่างเช่นเมื่อประกาศ String
หมายความว่ามันจะต้องมีค่า string อยู่ ห้ามเป็น null
แต่ก็ไม่ใช่ว่า Null
จะไม่มีประโยชน์ เพราะ Null
สามารถเป็นการแสดงค่าแบบ optional ได้เป็นอย่างดี
void makeCoffee(String coffee, [String? dairy]) {
if (dairy != null) {
print('$coffee with $dairy');
} else {
print('Black $coffee');
}
}
dairy
คือค่า optional ที่สามารถจะรับค่า String
หรือ null
ได้ ซึ่งก็คือString | Null
การใส่ เครื่องหมาย ?
หลัง type นั้นๆ เป็นการบอกว่า type สามารถรับได้ทั้งค่าของ type นั้น และค่า null
เรียกว่า nullable type
ถ้าส่งค่า non-nullable type ให้ function ที่รับค่า nullable type เช่น void func(String? s)
function นี้รับค่า String?
(nullable String) หรือก็คือรับได้ทั้งค่า string และค่า null
ดังนั้นการส่ง typeString
(non-nullable String) เข้าไปก็ไม่ได้ก่อให้เกิดปัญหาใดใด
ดั้งนั้น Dart จึงออกแบบให้ nullable type เป็น supertype ของ type นั้นๆ
แต่ในทางกลับกัน ถ้าส่งค่า nullable type ให้ function ที่รับค่า non-nullable type นั้นอาจทำให้เกิดปัญหาได้ เนื่องจาก code ที่รับค่าเป็น non-nuallable ก็อาจมีการเรียกใช้ method ที่ไม่มีบน Null
และก่อให้เกิด null exception ตอน runtime อีกเช่นเดิม
// Hypothetical unsound null safety:
void requireStringNotNull(String definitelyString) {
print(definitelyString.length);
}void main() {
String? maybeString = null; // Or not!
requireStringNotNull(maybeString);
}
การ run code ชุดนี้จะทำให้เกิด null exception เนื่องจาก Dart 1.x จะสามารถทำ implicit downcasts ได้ (implecit downcasts คือการ cast จาก supertype ไปเป็น subtype) ทำให้การ cast จาก nullable type ซึ่งเป็น supertype ของ non-nullable เป็นไปได้ การมี implicit downcasts จึงสามารถทำให้เกิดปัญหาใน null safety ได้ทำให้ขัดกับหลักการออกแบบ Null safety ดั้งนั้น Dart จึงได้ตัด implicit downcasts ทิ้งไปใน Dart 2.x
ถ้านำ code ตัวอย่างไปรันบน Dart 2.12 จะมี compile error แสดงว่าThe argument type 'String?' can't be assigned to the parameter type 'String'.
ซึ่ง implicit downcasts นั้น Dart คิดว่าดีกว่าใส่ไว้ เนื่องจากใน Dart 1.x การทำ implicit downcast ก็ได้ก่อให้เกิดปัญหา runtime exception ได้อยู่แล้ว ในบางกรณี
// Without null safety:
List<int> filterEvens(List<int> ints) {
return ints.where((n) => n.isEven);
}
จาก code ข้างต้น .where()
จะ return Iterable<int>
ซึ่งไม่ใช่ List<int>
ดังนั้นเมื่อ run code ชุดนี้จะเกิด runtime exception ขึ้น แต่ใน Dart 2.12 จากการตัด implicit downcasts ทิ้งไป จะทำให้ code ชุดนี้สามารถตรวจพบปัญหาในตอน compile แทน
สรุปได้ว่าใน Dart 2.x จะแบ่งออกเป็น 2 type หลัก ๆ คือ Non-nullable types
และ Nullable types
reference: