วิธีการอยู่ร่วมกับ Null ให้มีความสุขบน Kotlin

Akexorcist
Black Lens
Published in
3 min readMar 8, 2020

--

ภาษา Kotlin มี Null Safety ก็จริง แต่โค้ดที่ดีก็ต้องมี Null เป็นองค์ประกอบอยู่ดี

ขนาด Pokemon Sword/Shield ยังมี Null เลย

Null Safety เป็นหนึ่งในคุณสมบัติของ Kotlin ที่ช่วยให้นักพัฒนารับมือกับ Null ได้ดีขึ้น ลดการเจอ NullPointerException ได้น้อยลง เพราะ Kotlin นั้นซีเรียสเรื่อง Null มาก ถึงขนาดที่ว่าถ้าไม่เขียนดัก Null ให้ถูกต้อง ก็จะไม่ยอม Compile เลยทีเดียว

ด้วยความสามารถ Null Safety ของ Kotlin ทำให้เราสามารถกำหนดได้ง่าย ๆ เพียงแค่ใส่เครื่องหมาย ? เพื่อบอกให้รู้ว่าตัวแปรนั้นเป็น Nullable

// Nullable
val name1: String? = "Black Lens"
// Not Null
val name2: String = "Black Lens"

จากตัวอย่างข้างบนจะเห็นว่า name1 นั้นเป็น Nullable ส่วน name2 ไม่สามารถมีค่าเป็น Null ได้ (Not-null)

นั่นหมายความว่า ถ้าเราเผลอกำหนดค่า Null ให้กับ name2 ก็จะโดน Lint ด่าทันที เพราะเราเป็นคนกำหนดเองว่ามันเป็น Null ไม่ได้

// OK
name1 = null
// Error
name2 = null

ซึ่งการใช้ Null Safety จะทำให้นักพัฒนาต้องสนใจเรื่อง Nullable มากขึ้น เพราะว่าการนำไปใช้งานก็ต้องดูว่ามันสามารถเป็น Null ได้จริง ๆ หรือไม่

ยกตัวอย่างเช่น

fun updateNewName(name: String) {
// Update new name to web server
}
val name: String? = "Black Lens"
...
// Error
updateNewName(name)

ปัญหานี้เกิดขึ้นได้บ่อยมาก เพราะว่าฟังก์ชันบางตัวของเรารับค่าเป็นแบบ Not-null แต่ค่าที่เราจะโยนเข้าไปในฟังก์ชันนี้ดันเป็น Nullable

Nullable != Not-null เสมอ ถึงแม้จะเป็นคลาสเดียวกันก็ตาม เช่น String != String?

จงเขียนโค้ดเพื่อจัดการกับ Null ให้เหมาะสม

จากโค้ดตัวอย่างที่ผมยกขึ้นมานี้ จริง ๆ แล้วใน Kotlin มีวิธีรับมือได้หลายรูปแบบมาก

ใช้ Not-null Assertion Operator

ในเมื่อไม่ยอมให้ใช้ก็บังคับด้วยการใส่เครื่องหมาย !! ซะเลย

val name: String? = "Black Lens"
...
updateNewName(name!!)

โค้ดในลักษณะนี้พบเจอได้บ่อยมาก โดยเฉพาะนักพัฒนาที่เพิ่งเริ่มเขียน Kotlin ใหม่ ๆ หรือให้ IDE ช่วยแปลงโค้ดจาก Java เป็น Kotlin เพราะเชื่อว่ามันเป็นวิธีแก้ปัญหาน่าหงุดหงิดจากการที่โดน Lint เตือนว่าให้จัดการเรื่อง Null ด้วย

แต่หารู้ไม่ว่า Not-null Assertion Operator นั้นคือการยอมให้เกิด NullPointerException ได้ทันทีที่พบว่าค่าเป็น Null

// Work properly
val name: String? = "Black Lens"
...
updateNewName(name!!)
// Crash with NullPointerException
val name: String? = null
...
updateNewName(name!!)

ซึ่งโปรแกรมจะสามารถทำงานได้ปกติ ตราบใดที่ค่าของตัวแปรไม่ใช่ Null แต่เมื่อใดก็ตามที่ค่ากลายเป็น Null แล้วเรียกฟังก์ชัน updateNewName(...) จะทำให้โปรแกรมนั้นพังทันที

หรือแม้กระทั่งการเรียกคำสั่งใดๆก็ตามจากตัวแปรที่เป็น Null

val name: String? = null
...
// Crash with NullPointerException
name.toUpperCase()

และกลายเป็นว่าถ้าอยากจะใช้ Not-null Assertion Operator ก็ต้องมานั่งเขียน Try-catch ครอบคำสั่งนั้นๆไว้ แล้วเขียนโค้ดเผื่อกรณีที่เข้า Catch ให้เรียบร้อยซะ

val name: String? = "Black Lens"
...
try {
updateNewName(name!!)
} catch (e: NullPointerException) {
// Handle code when error
}

ซึ่งการเอา Try-catch มาครอบเพื่อดัก NullPointerException ไม่ใช่วิธีที่ถูกต้องซักเท่าไร เพราะการทำให้เกิด Exception จะมี Overhead อยู่ด้วย ดังนั้นการใช้ If-else เพื่อเช็คแบบง่าย ๆ จึงดีกว่าวิธีนี้เสียอีก

ใช้ Elvis Operator

ความเท่อย่างหนึ่งของ Kotlin ก็คือ Elvis Operator นี่แหละ ที่ใช้ ?: เพื่อบอกให้รู้ว่าถ้าคำสั่งที่อยู่ข้างหน้าเครื่องหมายนี้เกิดมีค่าเป็น Null ขึ้นมา ก็ให้ใช้ค่าที่เรากำหนดไว้ (Default Value) ข้างหลังเครื่องหมายนี้แทน

ดังนั้นบ่อยครั้งเราจึงใช้ Elvis Operator ในการแก้ปัญหาแบบนี้

val name: String? = "Black Lens"
...
updateNewName(name ?: "")

ในเมื่อ updateNewName(...) รับค่าเป็น Null ไม่ได้ ถ้าตัวแปร name เกิดเป็น Null ขึ้นมาก็จะกลายเป็น Empty String แทน แต่วิธีนี้จะเหมาะกับคำสั่งที่สามารถรับค่า Default Value ได้ทุกครั้งที่เกิด Null

นั่นหมายความว่าเราก็ต้องดูด้วยว่าคำสั่งที่อยู่ใน updateNewName(...) นั้นทำงานยังไง ระหว่าง “โยน Empty String เข้าไป” กับ “ไม่เรียกคำสั่งนั้นตั้งแต่แรก” อันไหนเหมาะสมกว่ากัน

ถ้าคำสั่งข้างในจำเป็นต้องทำงาน การโยน Empty String เข้าไปก็เป็นทางเลือกที่เหมาะสมกว่า

ใช้ If-else แบบโง่ ๆ ไปเลย

ก็ในเมื่อคำสั่ง updateNewName(...) รับค่าเป็น Null ไม่ได้ ก็เช็ค Null ด้วย If-else ไปเลยดีกว่า

val name: String? = "Black Lens"
...
if (name != null) {
updateNewName(name)
}

หรือจะใช้คู่กับ else ก็ได้ ถ้าจะให้ทำคำสั่งบางอย่างในกรณีที่ name มีค่าเป็น Null

val name: String? = "Black Lens"
...
if (name != null) {
updateNewName(name)
} else {
// Handle code when name is null
}

แต่พอดูโค้ดนี้ ก็อาจจะสงสัยว่าตัวแปร name ที่อยู่ข้างใน if กลายเป็น Not-null ได้ยังไง เพราะว่าจริง ๆ แล้วตัวแปรเป็น Nullable ต่างหาก

นั่นก็เพราะว่า Kotlin มีความสามารถที่เรียกว่า Smart Cast อยู่นั่นเอง สามารถรู้ได้ว่า name ที่อยู่ข้างใน if จะไม่มีทางเป็น Null ได้ เพราะว่าเราเขียนเช็คไว้แล้วนั่นเอง

สังเกตได้จากตัวแปรที่เปลี่ยนเป็นสีเหลือง

จึงทำให้ Smart Cast ทำการแปลงตัวแปร name จาก Nullable ให้กลายเป็น Not-null ซะ โดยที่ยังคงชื่อตัวแปรเหมือนเดิม ซึ่งผลของ Smart Cast จะเกิดขึ้นในปีกกาของ if เท่านั้น ถ้าเรียกใช้งานข้างนอกก็จะมองว่าเป็น Nullable เหมือนปกติ

แต่วิธีแบบนี้จะไม่สามารถใช้กับตัวแปรแบบ Mutable Properties ได้ ซึ่งเป็นข้อจำกัดของ Smart Cast เมื่อใช้ใน if

ใช้ Scope Function ที่ชื่อว่า let

การใช้ If-else ก็เกือบจะตอบโจทย์แล้วล่ะ แต่ติดปัญหาเรื่อง Mutable Properties จึงแนะนำ let ที่เป็น Scope Function ซึ่งเป็นหนึ่งในความสามารถสำคัญของ Kotlin ดีกว่า

val name: String? = "Black Lens"
...
name?.let {
updateNewName(name)
}

หรือถ้าต้องการใส่คำสั่งในกรณีที่ name มีค่าเป็น Null (แบบ else) ก็ให้ใช้ Elvis Operator และ run เข้ามาช่วยแทน

val name: String? = "Black Lens"
...
name?.let {
updateNewName(name)
} ?: run {
// Handle code when name is null
}

อาจจะดูเหมือนว่าวิธีนี้จะเหมาะกับการรับมือกับ Null โดยที่ไม่ต้องการให้คำสั่งถูกเรียกใช้งานเลย แต่ถ้าตัวแปรมีมากกว่า 1 ตัว โค้ดก็จะออกมาดูรก ๆ กว่า If-else เสียอีก และไม่เหมาะกับคำสั่งที่มีหลาย Condition อีกด้วย

อย่ามองข้ามความสำคัญของ Null

บางอย่างก็มีเหตุผลที่จะเป็น Nullable และบางอย่างก็สามารถเป็น Not-null ได้ เพราะสุดท้ายมันขึ้นอยู่กับ Requirement ของโปรแกรมนั้น ๆ ต่างหาก

ผมเคยเจอโค้ดที่พยายามเลี่ยง Null โดยใช้ Not-null หรือแม้กระทั่ง Elvis Operator ทั้งหมด เพื่อทำให้ตัวแปรไม่มีค่าเป็น Null เพราะเชื่อว่าถ้าไม่มี Null แล้วจะลดโอกาสที่โปรแกรมพังได้ และกลายเป็นโปรแกรมที่ไม่สามารถใช้งานได้จริง

ถึงแม้ Kotlin จะมี Null Safety ให้ แต่ก็มีไว้เพื่อช่วยให้นักพัฒนาสามารถรับมือกับตัวแปรที่มีค่าเป็น Null ได้อย่างถูกต้องเท่านั้น สุดท้าย Null ก็ยังเป็นสิ่งจำเป็นสำหรับโปรแกรมอยู่ดี

นั่นก็เพราะว่า

โค้ด Kotlin ที่ขาด Null ไป ก็คงอยู่ไม่ได้และไม่สมบูรณ์ เหมือนกับที่เราขาดเธอไปยังไงล่ะ

😉😉😉😉😉

--

--

Akexorcist
Black Lens

Lovely android developer who enjoys learning in android technology, habitual article writer about Android development for Android community in Thailand.