เข้าใจ Null Safety ใน Dart 2 (EP1)

Pimorn Senakat
AMPOS Developers
Published in
3 min readOct 25, 2021
Photo by Anastase Maragos on Unsplash

ทำไมต้อง 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

Non-nullable and nullable type structure in Dart null safety
image from https://dart.dev/null-safety/understanding-null-safety#nullability-in-the-type-system

แต่ก็ไม่ใช่ว่า 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 นั้นๆ

image from https://dart.dev/null-safety/understanding-null-safety#nullability-in-the-type-system

แต่ในทางกลับกัน ถ้าส่งค่า 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

image from https://dart.dev/null-safety/understanding-null-safety#nullability-in-the-type-system

reference:

--

--