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!”
Nội dung chính
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ư:
- Một
this
chưa khởi tạo có sẵn trong một hàm khởi tạo được truyền và sử dụng ở đâu đó (gọi là “rò rỉthis
“).- Một cuộc gọi constructor của superclass đến một thành viên mở cửa mà hiện thực trong lớp dẫn xuất sử dụng một trạng thái chưa khởi tạo.
- Một
- 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ộtMutableList
của Kotlin, do đó yêu cầu mộtMutableList<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.
- 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
- Cố gắng truy cập một thành viên của một tham chiếu
- 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ểuString
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
.
Vì throw
và return
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 b
là null
:
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!