web analytics

สรุปเรื่อง Null Safety ใน Dart

สวัสดีครับเพื่อนๆ ผู้อ่านทุกท่าน บล็อกนี้เป็นบล็อกที่ผมตั้งใจเขียนสรุปเรื่อง Null safety ในภาษา dart ว่ามี syntax หรือ keyword อะไรเพิ่มมาบ้าง เพื่อให้เพื่อนๆที่เคยเขียน dart แบบเดิม (Non-null safety) เข้าใจ Null safety มากขึ้น เอาล่ะ! ไปลุยกันเลย

ที่มาของ Null

สำหรับคนที่เขียนโปรแกรมมาระยะหนึ่ง คงจะคุ้นชินกับคำว่า Null เป็นอย่างดี และผมเองก็เจอกับมันมาตั้งแต่เริ่มเขียนโปรแกรมเช่นกัน เคยสงสัยบ้างไหมว่าสิ่งที่เรียกว่า Null มันมีที่มาอย่างไร และใครเป็นคนคิด

Null Reference ความหมายของมันแบบสั้นๆ คือ ไม่มี ไม่รู้ (Nothing & Unknow) ถูกคิดค้นโดย ท่าน Sir Charles Antony Richard Hoare โดยเขาคิดค้นมันในปี 1965 ใช้ในการออกแบบภาษาเชิงวัตถุ ที่ชื่อว่า ALGOL W ซึ่งนั่นก็เป็นจุดเริ่มต้นของการเขียนโปรแกรมเชิงวัตถุ และ Null เริ่มเป็นที่รู้จักและถูกนำไปใช้แพร่หลายมาก จนกระทั่งในปี 2009 เขาได้พูดในงาน Software Conf ว่าเขาขอโทษที่ประดิษฐ์ Null reference ขึ้นมา ทำให้ตลอด 50 ปีที่ผ่านมามันสร้างความเสียหายมหาศาล เพราะระบบมักจะ error จาก Null ตัวปัญหา ทำให้ต้องเสียโอกาสในการทำเงินไปหลายล้านเหรียญ เขาเรียกมันว่าเป็น my billion-dollar mistake กันเลยทีเดียว

ปัญหาของ Null

ในเมื่อ null แปลว่า ไม่มี และแน่นอนว่า เราจะเข้าถึงสิ่งที่ไม่มีไม่ได้ และนั้นก็คือปัญหา เพราะไม่มีอะไรรับประกันว่า ตอนนั้นจะมีหรือไม่มีกันแน่ ดังนั้น developer แทบทุกคนบนโลก จะต้องพบเจอ Error ที่เรียกว่า Null Exception และ error นี้ก็จะเป็นคำถามวนซ้ำไปซ้ำมาใน stack overflow หรือแม้แต่ใน Github จากมือใหม่ทุกๆวัน

ตัวอย่าง Null exception แบบง่ายๆ

// Non-null safety
var person;
String name = person.name; // Error! Null Exception.

การป้องกัน Null exception เบื้องต้น

ในเมื่อเราต้องการเข้าถึงค่าบางอย่าง และมันไม่ควรเป็น null เราก็สามารถดักทางมันด้วย if ได้

// Non-null safety
Person person;
if(person != null){
  String name = person.name;
}

หรือจะใช้ ความสามารถของ Dart ที่เรียกว่า Null conditional ช่วยให้ข้าม null ไปได้

// Non-null safety
Person person;
String name = person?.name;

แต่ปัญหาของการใช้ If condition ก็คือ จะเกิด code ที่ซ้ำซ้อนจำนวนมาก เพราะเราจะต้อง if เพื่อเช็คตลอดทุกครั้ง และการที่ต้อง check แบบนี้ ทำให้เราต้องกังวล คิดไว้เสมอว่ามันมีโอกาสเป็น null ส่วนการใช้ Null conditional ก็เป็นเพียงตัวช่วยที่ปลายเหตุเท่านั้น

สู่การแก้ปัญหาที่แท้จริง

หลายๆภาษาจึงหาทางเพิ่มจัดการกับปัญหา null นี้ โดยเพิ่มให้สามารถกำหนดตั้งแต่การประกาศตัวแปรได้เลยว่า สามารถเป็น null ได้หรือไม่ ถ้าไม่ได้ หากเรากำหนดเป็น null ตัวภาษาจะ warning ทันที ช่วยให้ปัญหา null exception มีโอกาสเกิดน้อยลงมาก เพราะจะเกิดขึ้นจากตัวแปรที่เราอนุญาตให้เป็น null ได้เท่านั้น และความสามารถนี้ เพิ่มเข้ามาใน Dart 2.12 เรียกว่า Null safety

Nullable ไม่ควรเป็น Default

จากการเขียน Dart แบบเดิม เมื่อเราประกาศตัวแปรหนึ่งขึ้นมา ค่าเริ่มต้นของมันคือ null เราไม่สามารถกำหนดได้ว่า ห้ามเป็น null ได้ใช่ไหม ใช่แล้ว มันคือจุดเริ่มต้นของปัญหา

คุณ Filip Hracek (Flutter Team) กล่าวว่า หากเราดูจาก code ของระบบต่างๆ ที่ผ่านมา หลายล้านบรรทัด จะพบว่ากว่า 80-90% เป็นตัวแปรที่ไม่มีทางเป็น null มันจะต้องมีค่านึงเสมอ นั่นหมายความว่าจริงๆแล้ว มีส่วนน้อยเท่านั้นที่เราตั้งใจให้มันเป็น null จริงๆ ดังนั้นเราควรจะมองที่ Non-nullable มากกว่า และให้ Nullable เป็นทางเลือกแทน

คุณ Filip ยังเล่าว่า

โอเค! มาเริ่มเขียน Null Safety ใน Dart กันเถอะ

เริ่มต้นกับ Null Safety

หากอยากลองเขียนไปพร้อมกับตัวอย่าง สามารถเขียนได้ใน Dartpad เลยนะ
https://nullsafety.dartpad.dev/

เริ่มจาก ตัวอย่างตัวแปรหนึ่ง คือ int age จะเก็บค่าอายุเป็นตัวเลขจำนวนเต็ม ซึ่งคนเราทุกคนมีค่าอายุนี้ จะไม่มีคนที่ไม่มีอายุ เกิดมาก็เริ่มที่ 0 หรือ 1 หรือต่อให้ตายแล้วอายุก็จะหยุดอยู่ที่ค่านั้นเป็นต้น ดังนั้น ตัวแปรนี้จึงไม่ควรที่จะเป็น null ได้

แต่ว่าหากเราเขียนแบบเดิมที่ยังไม่ใช้ Null safety เรายังคงสามารถกำหนด null ให้ ตัวแปร age ได้อยู่ ซึ่งมีโอกาสทำให้เกิด Null exception นั่นเอง

// Non-null safety
int age;
age = null;
print(age);  // null

ดังนั้น ใน Null safety จึงเปลี่ยน concept ใหม่ ให้ Non-nullable เป็น default แทน หากเราประกาศตัวแปรตามปกติ แล้วไม่กำหนดค่า แล้วมีการนำค่านั้นไปใช้ ก็จะ error ทันที โดยไม่ต้อง compile เลย

// Null safety
int age;     // ค่า age จะไม่สามารถเป็น null ได้
print(age);  // Error เพราะ age ยังไม่ได้กำหนดค่า ตอนนี้เลยเป็น null ซึ่งขัดแย้งกับบรรทัดบน

เมื่อลองกำหนดค่าให้ตัวแปร age ก็จะ compile ผ่าน

// Null safety  
int age = 27;
print(age);  // 27

ซึ่งในโลกความเป็นจริง ค่าบางอย่างก็สามารถ “ไม่มี” ได้ใช่ไหม เช่น สีที่ชื่นชอบ แฟน?
การประกาศตัวแปรที่อนุญาตให้มีค่าเป็น null ได้ (Nullable) จะใช้สัญลักษณ์ ? ตามหลัง data type

// Null safety  
Color? favorite;  // ค่า favorite สามารถเป็น null ได้
print(favorite);  // null

ซึ่งรวมถึง Argument ของ method ด้วยนะ

// Null safety  
main() {
  getArea(4,5);     // 20
  getArea(4,null);  // Error เพราะ parameter height ไม่สามารถเป็น null ได้
}

// width and height are Non-nullable.
getArea(int width,int height){
  return width * height;
}

พิสูจน์ให้ได้ ว่าจะไม่ Null

นอกจากเรื่องประกาศตัวแปร Non-nullable เป็น default แล้ว Null safety ยังช่วยตรวจสอบกรณีอื่นๆที่จะ error จาก null ได้อีกด้วย

เช่น argument ใน method เป็น nullable หรืออนุญาตให้เป็น null แล้วมีการใช้ค่านั้น ก็จะ error ทันที

// Null safety  
getArea(int? width,int? height){
  return width * height; // Error เพราะ ถ้าค่า width หรือ height เป็น null จะใช้กับ * ไม่ได้
}

สิ่งที่เราต้องทำ คือ พิสูจน์ให้ Dart เห็นว่า หากค่านั้นเป็น null จะไม่มีโอกาส error เช่น เพิ่มเงื่อนไขกรณีที่เป็น null

// Null safety  
main() {
  getArea(4,null);    // 0
}

getArea(int? width,int? height){
  // ถ้า width หรือ height เป็น null ให้คืนค่า 0
  if(width == null || height == null){ 
    return 0;
  }

  return width * height;
}

เชื่อฉันสิ Dart!

อย่างไรก็ตาม มีกรณีที่ Dart ไม่สามารถพิสูจน์ได้ว่า มันจะไม่ null เราจะต้องเป็นผู้ยืนยันว่า ตัวแปรนั้นจะมีค่าเสมอด้วยตัวเราเอง

เช่น class Dog มี field ชื่อว่า name เป็น Nullable (สังเกตสัญลักษณ์ ?)

// Null safety  
class Dog{
  String? name;
  
  Dog({this.name});
}

จากนั้นมี method นึงที่มี parameter ที่เป็น Non-nullable หรือไม่สามารถเป็น null ได้

// Method นี้รับ parameter ที่เป็น Non-nullable
doSomething(String str){
  //
}

จากนั้นสร้าง instance Dog และกำหนดค่า name ให้

// Null safety  
main() {
  var dog = Dog(); 
  dog.name = "pangkung";   // กำหนดค่า name
}

ปัญหาก็คือ เมื่อเราเรียก method ที่มี argument แบบ non-nullable แต่เราใส่ parameter เป็น nullable สิ่งที่เกิดขึ้นคือ Error เพราะ Dart คิดว่า type มันไม่ตรงกัน และพิสูจน์ไม่ได้ว่า มันจะมีค่าเสมอ แม้ว่าเราเพิ่งจะกำหนดค่าในบรรทัดก่อนหน้าก็ตาม

doSomething(dog.name);  // Error, The argument type 'String?' can't be assigned to the parameter type 'String'

วิธีการก็ง่ายมาก คือ การเพิ่มสัญลักษณ์ ! ไปด้านหลังตัวแปรนั้น เสมือนกับเราบอก Dart ว่า “เชื่อฉันสิ ว่ามันจะไม่ null มันจะมีค่าเสมอ”

doSomething(dog.name!);  

รู้จักกับ late keyword

อีกสิ่งหนึ่งที่เพิ่มเข้ามาใน null safety คือ keyword “late” ขอยกตัวอย่างจาก กรณี class Dog โดยผมจะเพิ่ม field id เข้าไป โดย field นี้จะเป็น Non-nullable และมันจะถูก generate ขึ้นอัตโนมัติใน constructor

// Null safety  
class Dog{
  int id;   
  String? name;

  // Error, Non-nullable instance field 'id' must be initialized 
  Dog({this.name}){  
    id = DateTime.now().microsecondsSinceEpoch;
  }
}

แต่สิ่งที่เกิดขึ้น คือ Error ว่า “Non-nullable instance field ‘id’ must be initialized.” หรือก็คือ ตัวแปรที่เรากำหนดว่า null ไม่ได้ จะต้องกำหนดค่าด้วย ซึ่งก็ถูกของ Dart เพราะเราไม่ได้กำหนดค่าในทันที แต่กำหนดหลังจาก constructor ทำงานแล้ว

วิธีการแก้ไขก็คือ การเพิ่ม keyword late ไปด้านหน้าของ data type ของตัวแปร ที่เราต้องการบอกว่า “มันจะไม่ null นะ มันจะมีค่าแน่ๆ แต่ว่ายังไม่ใช่ตอนนี้ เดี๋ยวฉันจะกำหนดภายหลังน่ะ”

// Null safety  
class Dog{
  late int id;   // เพิ่ม late keyword
  String? name;

  Dog({this.name}){  
    id = DateTime.now().microsecondsSinceEpoch;
  }
}

มาแล้ว required keyword

หากเพื่อนๆ ที่เขียน Flutter มาบ้างแล้ว น่าจะคุ้นเคยกับ @required notation ที่เราจะใส่ใน argument ที่บังคับให้ใส่ โดยเจ้าสิ่งนี้ เราจะต้อง import flutter/material.dart และแม้จะใส่ @required แต่ถ้าไม่ใส่ parameter ก็จะไม่ error แต่อย่างใด จะแค่ warning เท่านั้น

ใน Dart null safety เพิ่ม required ในรูปแบบ keyword มาในตัวภาษา และจะ error หากเราไม่ใส่ parameter ที่ required นับว่ามีประโยชน์มาก

// Null safety  
class Dog{
  String name;
  
  Dog({required this.name});
}

List<T> กับ Nullable

อีกสิ่งที่น่าสนใจ ใน null safety คือ การประกาศ nullable กับ data ประเภท List, Set, Map ซึ่งอย่างที่รู้ว่า List จะมี Generic type อยู่ ซึ่งทั้งตัว List และ Generic type ก็สามารถเป็น nullable ได้ทั้งคู่ ดังนั้น จะสามารถสร้าง List<T> ได้ 4 แบบ ดังนี้

List<String> = List ห้าม null และ ห้าม push null ใส่ List ด้วย
List<String?> = List ห้าม null แต่สามารถ push null ใส่ List ได้
List<String>? = List เป็น null ได้ แต่ไม่สามารถ push null ใส่ List ได้
List<String?>? = List เป็น null ได้ และสามารถ push null ใส่ List ได้

ซึ่งการใช้งานใน Set และ Map ก็จะคล้ายกัน

จบแล้ว

ขอบคุณที่อ่านจนจบนะครับ หวังว่าบทความในบล็อกนี้จะมีประโยนช์กับผู้อ่านนะครับ (:

วิดีโออธิบาย Null safety ถึงจะเก่าหน่อย แต่ละเอียดดีครับ