Chào mừng các bạn đến với Cafedev! Hôm nay, chúng ta sẽ khám phá về tính năng quan trọng trong Kotlin – “Null Safety” hay tính an toàn với giá trị `null`. Đây là một điểm độc đáo giúp Kotlin tránh được những lỗi phổ biến như `NullPointerException`. Hãy cùng Cafedev tìm hiểu cách Kotlin giải quyết vấn đề này và làm cho lập trình trở nên an toàn hơn và hiệu quả hơn!”

1. Kiểu có thể là null và kiểu không thể là null

Hệ thống kiểu của Kotlin nhằm loại bỏ nguy cơ tham chiếu null, còn được biết đến với tên gọi Lỗi tỷ đô.
Một trong những lỗi phổ biến nhất trong nhiều ngôn ngữ lập trình, bao gồm cả Java, là việc truy cập thành viên của một tham chiếu null sẽ dẫn đến một ngoại lệ tham chiếu null. Trong Java, điều này tương đương với một NullPointerException, hay gọi tắt là NPE.
Các nguyên nhân duy nhất gây ra NPE trong Kotlin bao gồm:

  • Một cuộc gọi tường minh đến throw NullPointerException().
  • Sử dụng toán tử !! mà chúng ta sẽ mô tả dưới đây.
  • Mất nhất quán dữ liệu đối với việc khởi tạo, chẳng hạn như:
  • Tương tác với Java:
    • Cố gắng truy cập một thành viên của một tham chiếu null của một kiểu nền;
      • Vấn đề về khả năng làm null với kiểu generic được sử dụng để tương tác với Java. Ví dụ, một đoạn mã Java có thể thêm null vào một MutableList của Kotlin, do đó yêu cầu một MutableList<string?></string?> để làm việc với nó.
        • Những vấn đề khác do mã Java bên ngoài gây ra.
  • Trong Kotlin, hệ thống kiểu phân biệt giữa tham chiếu có thể giữ null (tham chiếu có thể là null) và những tham chiếu không thể giữ null (tham chiếu không thể là null). Ví dụ, một biến thông thường kiểu String không thể giữ null:
var a: String = "abc" // Regular initialization means non-nullable by default
a = null // compilation error

Để cho phép giá trị null, bạn có thể khai báo biến như một chuỗi có thể là null bằng cách viết String?:

var b: String? = "abc" // can be set to null
b = null // ok
print(b)

Bây giờ, nếu bạn gọi một phương thức hoặc truy cập một thuộc tính trên a, đảm bảo nó sẽ không gây ra NPE, vì vậy bạn có thể an toàn nói:

val l = a.length

Nhưng nếu bạn muốn truy cập thuộc tính tương tự trên b, điều đó sẽ không an toàn, và trình biên dịch sẽ báo lỗi:

val l = b.length // error: variable ‘b’ can be null

Nhưng bạn vẫn cần truy cập thuộc tính đó, phải không? Có một số cách để làm điều này.

2. Kiểm tra null trong điều kiện

Trước hết, bạn có thể kiểm tra một cách rõ ràng liệu b có là null không và xử lý hai trường hợp một cách riêng biệt:

val l = if (b != null) b.length else -1

Trình biên dịch theo dõi thông tin về kiểm tra bạn thực hiện và cho phép cuộc gọi length bên trong if. Các điều kiện phức tạp hơn cũng được hỗ trợ:

val b: String? = "Kotlin"
if (b != null && b.length > 0) {
    print("String of length ${b.length}")
} else {
    print("Empty string")
}

Lưu ý rằng điều này chỉ hoạt động khi b là bất biến (nghĩa là nó là một biến cục bộ không được sửa đổi giữa kiểm tra và sử dụng nó hoặc là một thành viên val có một trường hậu nghiệm và không thể ghi đè), vì nếu không, có thể xảy ra tình trạng b thay đổi thành null sau khi kiểm tra.

3. Gọi an toàn

Tùy chọn thứ hai để truy cập một thuộc tính trên một biến có thể là null là sử dụng toán tử cuộc gọi an toàn ?.:

val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // Unnecessary safe call

Điều này trả về b.length nếu b không phải là null, và null trong trường hợp khác. Kiểu của biểu thức này là Int?.
Cuộc gọi an toàn hữu ích trong chuỗi. Ví dụ, Bob là một nhân viên có thể được phân công vào một bộ phận (hoặc không). Bộ phận đó có thể lại có một nhân viên khác làm trưởng bộ phận. Để lấy tên của trưởng bộ phận của Bob (nếu có), bạn viết như sau:

bob?.department?.head?.name

Chuỗi này trả về null nếu bất kỳ thuộc tính nào trong nó là null.
Để thực hiện một thao tác cụ thể chỉ đối với các giá trị không phải là null, bạn có thể sử dụng toán tử cuộc gọi an toàn cùng với let:

val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
    item?.let { println(it) } // prints Kotlin and ignores null
}

Một cuộc gọi an toàn cũng có thể được đặt ở bên trái của một phép gán. Sau đó, nếu một trong những người nhận trong chuỗi cuộc gọi an toàn là null, phép gán sẽ được bỏ qua và biểu thức bên phải sẽ không được đánh giá:

// If either `person` or `person.department` is null, the function is not called:
person?.department?.head = managersPool.getManager()

4. Người nhận có thể là null

Các hàm mở rộng có thể được định nghĩa trên một người nhận có thể là null. Điều này giúp bạn có thể xác định hành vi cho các giá trị null mà không cần sử dụng logic kiểm tra null tại mỗi điểm gọi.
Ví dụ, hàm toString() được định nghĩa trên một người nhận có thể là null. Nó trả về Chuỗi “null” (khác với giá trị null). Điều này có thể hữu ích trong một số tình huống, ví dụ, ghi nhật ký:

val person: Person? = null
logger.debug(person.toString()) // Logs "null", does not throw an exception

Nếu bạn muốn cuộc gọi toString() của bạn trả về một chuỗi có thể là null, hãy sử dụng toán tử gọi an toàn <code< a=””>?.>:

var timestamp: Instant? = null
val isoTimestamp = timestamp?.toString() // Returns a String? object which is `null`
if (isoTimestamp == null) {
   // Handle the case where timestamp was `null`
}

5. Toán tử Elvis

Khi bạn có một tham chiếu có thể là null, b, bạn có thể nói “nếu b không phải là null, hãy sử dụng nó, nếu không hãy sử dụng một giá trị không phải là null nào đó”:

val l: Int = if (b != null) b.length else -1

Thay vì viết toàn bộ biểu thức if, bạn cũng có thể diễn đạt điều này với toán tử Elvis ?::

val l = b?.length ?: -1

Nếu biểu thức bên trái của ?: không phải là null, toán tử Elvis trả về nó, ngược lại nó trả về biểu thức bên phải. Lưu ý rằng biểu thức ở phía bên phải chỉ được đánh giá nếu phía bên trái là null.
throwreturn là biểu thức trong Kotlin, chúng cũng có thể được sử dụng ở phía bên phải của toán tử Elvis. Điều này có thể hữu ích, ví dụ, khi kiểm tra đối số hàm:

fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("name expected")
    // ...
}

6. Toán tử !!

Tùy chọn thứ ba là cho những người yêu thích NPE: toán tử khẳng định không phải null (!!) chuyển đổi bất kỳ giá trị nào thành một kiểu không thể là null và ném một ngoại lệ nếu giá trị là null. Bạn có thể viết b!!, và điều này sẽ trả về một giá trị không thể là null của b (ví dụ, một String trong ví dụ của chúng tôi) hoặc ném một NPE nếu bnull:

val l = b!!.length

Vì vậy, nếu bạn muốn có một NPE, bạn có thể có nó, nhưng bạn phải yêu cầu nó một cách rõ ràng và nó sẽ không xuất hiện từ đâu mà không cần.

7. Ép kiểu an toàn

Ép kiểu thông thường có thể dẫn đến một ClassCastException nếu đối tượng không phải là kiểu đích. Một lựa chọn khác là sử dụng ép kiểu an toàn trả về null nếu thử nghiệm không thành công:

val aInt: Int? = a as? Int

8. Bộ sưu tập của một kiểu có thể là null

Nếu bạn có một bộ sưu tập của các phần tử có thể là kiểu null và muốn lọc các phần tử không thể là null, bạn có thể làm điều đó bằng cách sử dụng filterNotNull:

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()

Cảm ơn các bạn đã dành thời gian cùng Cafedev để tìm hiểu về tính năng quan trọng “Null Safety” trong Kotlin. Chúng ta đã thấy cách Kotlin giúp chúng ta tránh được những lỗi phổ biến liên quan đến giá trị `null`, làm cho mã nguồn trở nên an toàn và dễ bảo trì hơn. Hy vọng rằng thông qua bài viết này, bạn sẽ có cái nhìn rõ ràng và hiểu biết sâu sắc hơn về sức mạnh của Kotlin trong việc quản lý giá trị `null`. Hãy tiếp tục đồng hành cùng Cafedev để khám phá thêm nhiều điều thú vị khác trong ngôn ngữ lập trình này!”

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của Cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!