Trong Kotlin, cách thành ngữ để xử lý các giá trị nullable, tham chiếu hoặc chuyển đổi chúng là gì


177

Nếu tôi có một loại nullable Xyz?, tôi muốn tham chiếu nó hoặc chuyển đổi nó thành một loại không nullable Xyz. Cách thức thành ngữ trong Kotlin là gì?

Ví dụ: mã này bị lỗi:

val something: Xyz? = createPossiblyNullXyz()
something.foo() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Xyz?"

Nhưng nếu tôi kiểm tra null trước thì nó được cho phép, tại sao?

val something: Xyz? = createPossiblyNullXyz()
if (something != null) {
    something.foo() 
}

Làm thế nào để tôi thay đổi hoặc coi một giá trị là không phải nullkhông yêu cầu ifkiểm tra, giả sử tôi biết chắc chắn nó thực sự không bao giờ null? Ví dụ, ở đây tôi đang truy xuất một giá trị từ bản đồ mà tôi có thể đảm bảo tồn tại và kết quả get()là không null. Nhưng tôi có một lỗi:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")
something.toLong() // Error: "Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Int?"

Phương thức get()nghĩ rằng có thể là mục bị thiếu và trả về loại Int?. Vì vậy, cách tốt nhất để buộc loại giá trị là không thể là gì?

Lưu ý: câu hỏi này được cố ý viết và trả lời bởi tác giả ( Câu hỏi tự trả lời ), để các câu trả lời thành ngữ cho các chủ đề thường gặp của Kotlin có trong SO. Ngoài ra để làm rõ một số câu trả lời thực sự cũ được viết cho bảng chữ cái của Kotlin không chính xác cho Kotlin ngày nay.

Câu trả lời:


296

Trước tiên, bạn nên đọc tất cả về Null Safety trong Kotlin bao gồm các trường hợp kỹ lưỡng.

Trong Kotlin, bạn không thể truy cập giá trị null mà không chắc chắn là không null( Kiểm tra null trong điều kiện ) hoặc khẳng định rằng nó chắc chắn không nullsử dụng !!toán tử chắc chắn , truy cập bằng ?.Cuộc gọi an toàn hoặc cuối cùng đưa ra một cái gì đó có thể nulllà giá trị mặc định bằng cách sử dụng ?:toán tử Elvis .

Đối với trường hợp đầu tiên trong câu hỏi của bạn, bạn có các tùy chọn tùy thuộc vào mục đích của mã bạn sẽ sử dụng một trong những điều này và tất cả đều là thành ngữ nhưng có kết quả khác nhau:

val something: Xyz? = createPossiblyNullXyz()

// access it as non-null asserting that with a sure call
val result1 = something!!.foo()

// access it only if it is not null using safe operator, 
// returning null otherwise
val result2 = something?.foo()

// access it only if it is not null using safe operator, 
// otherwise a default value using the elvis operator
val result3 = something?.foo() ?: differentValue

// null check it with `if` expression and then use the value, 
// similar to result3 but for more complex cases harder to do in one expression
val result4 = if (something != null) {
                   something.foo() 
              } else { 
                   ...
                   differentValue 
              }

// null check it with `if` statement doing a different action
if (something != null) { 
    something.foo() 
} else { 
    someOtherAction() 
}

Đối với "Tại sao nó hoạt động khi kiểm tra null", hãy đọc thông tin cơ bản bên dưới trên phôi thông minh .

Đối với trường hợp thứ 2 trong câu hỏi của bạn trong câu hỏi với Map, nếu bạn là nhà phát triển chắc chắn về kết quả không bao giờ xảy ra null, !!hãy sử dụng toán tử chắc chắn làm xác nhận:

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.get("a")!!
something.toLong() // now valid

hoặc trong một trường hợp khác, khi COULD bản đồ trả về null nhưng bạn có thể cung cấp một giá trị mặc định, thì Mapchính nó có một getOrElsephương thức :

val map = mapOf("a" to 65,"b" to 66,"c" to 67)
val something = map.getOrElse("z") { 0 } // provide default value in lambda
something.toLong() // now valid

Thông tin lai lịch:

Lưu ý: trong các ví dụ dưới đây tôi đang sử dụng các loại rõ ràng để làm cho hành vi rõ ràng. Với suy luận kiểu, thông thường các loại có thể được bỏ qua cho các biến cục bộ và các thành viên riêng.

Thông tin thêm về !!toán tử chắc chắn

Các !!nhà điều hành khẳng định rằng giá trị không phải là nullhay ném một NPE. Điều này nên được sử dụng trong trường hợp nhà phát triển đảm bảo rằng giá trị sẽ không bao giờ null. Hãy nghĩ về nó như một sự khẳng định theo sau bởi một diễn viên thông minh .

val possibleXyz: Xyz? = ...
// assert it is not null, but if it is throw an exception:
val surelyXyz: Xyz = possibleXyz!! 
// same thing but access members after the assertion is made:
possibleXyz!!.foo()

đọc thêm: !! Nhà điều hành chắc chắn


Tìm hiểu thêm về nullKiểm tra và thông minh

Nếu bạn bảo vệ quyền truy cập vào một loại nullable bằng một nullkiểm tra, trình biên dịch sẽ thông minh bỏ giá trị trong phần thân của câu lệnh thành không nullable. Có một số dòng phức tạp trong đó điều này không thể xảy ra, nhưng đối với các trường hợp phổ biến hoạt động tốt.

val possibleXyz: Xyz? = ...
if (possibleXyz != null) {
   // allowed to reference members:
   possiblyXyz.foo()
   // or also assign as non-nullable type:
   val surelyXyz: Xyz = possibleXyz
}

Hoặc nếu bạn thực hiện iskiểm tra loại không thể rỗng:

if (possibleXyz is Xyz) {
   // allowed to reference members:
   possiblyXyz.foo()
}

Và tương tự cho các biểu thức 'khi' cũng diễn viên an toàn:

when (possibleXyz) {
    null -> doSomething()
    else -> possibleXyz.foo()
}

// or

when (possibleXyz) {
    is Xyz -> possibleXyz.foo()
    is Alpha -> possibleXyz.dominate()
    is Fish -> possibleXyz.swim() 
}

Một số điều không cho phép nullkiểm tra đúc thông minh cho lần sử dụng sau của biến. Ví dụ trên sử dụng một biến địa phương mà không có cách nào có thể biến đổi trong dòng chảy của các ứng dụng, cho dù valhay varbiến này đã không có cơ hội để biến đổi thành một null. Nhưng, trong các trường hợp khác mà trình biên dịch không thể đảm bảo phân tích luồng, đây sẽ là một lỗi:

var nullableInt: Int? = ...

public fun foo() {
    if (nullableInt != null) {
        // Error: "Smart cast to 'kotlin.Int' is impossible, because 'nullableInt' is a mutable property that could have been changed by this time"
        val nonNullableInt: Int = nullableInt
    }
}

Vòng đời của biến nullableIntkhông hoàn toàn hiển thị và có thể được chỉ định từ các luồng khác, nullkiểm tra không thể được đúc thông minh thành một giá trị không thể rỗng. Xem chủ đề "Cuộc gọi an toàn" bên dưới để biết cách giải quyết.

Một trường hợp khác không thể tin tưởng bởi một diễn viên thông minh để không đột biến là một thuộc valtính trên một đối tượng có một getter tùy chỉnh. Trong trường hợp này, trình biên dịch không có khả năng hiển thị những gì làm thay đổi giá trị và do đó bạn sẽ nhận được thông báo lỗi:

class MyThing {
    val possibleXyz: Xyz? 
        get() { ... }
}

// now when referencing this class...

val thing = MyThing()
if (thing.possibleXyz != null) {
   // error: "Kotlin: Smart cast to 'kotlin.Int' is impossible, because 'p.x' is a property that has open or custom getter"
   thing.possiblyXyz.foo()
}

đọc thêm: Kiểm tra null trong điều kiện


Tìm hiểu thêm về ?.toán tử cuộc gọi an toàn

Toán tử cuộc gọi an toàn trả về null nếu giá trị bên trái là null, nếu không thì tiếp tục đánh giá biểu thức ở bên phải.

val possibleXyz: Xyz? = makeMeSomethingButMaybeNullable()
// "answer" will be null if any step of the chain is null
val answer = possibleXyz?.foo()?.goo()?.boo()

Một ví dụ khác mà bạn muốn lặp lại một danh sách nhưng chỉ khi không nullvà không trống, một lần nữa toán tử cuộc gọi an toàn lại có ích:

val things: List? = makeMeAListOrDont()
things?.forEach {
    // this loops only if not null (due to safe call) nor empty (0 items loop 0 times):
}

Trong một trong những ví dụ ở trên, chúng tôi đã có một trường hợp chúng tôi đã ifkiểm tra nhưng có cơ hội một luồng khác làm thay đổi giá trị và do đó không có diễn viên thông minh . Chúng ta có thể thay đổi mẫu này để sử dụng toán tử cuộc gọi an toàn cùng với letchức năng để giải quyết điều này:

var possibleXyz: Xyz? = 1

public fun foo() {
    possibleXyz?.let { value ->
        // only called if not null, and the value is captured by the lambda
        val surelyXyz: Xyz = value
    }
}

đọc thêm: Cuộc gọi an toàn


Thông tin thêm về ?:Nhà điều hành Elvis

Toán tử Elvis cho phép bạn cung cấp một giá trị thay thế khi một biểu thức ở bên trái của toán tử là null:

val surelyXyz: Xyz = makeXyzOrNull() ?: DefaultXyz()

Nó cũng có một số cách sử dụng sáng tạo, ví dụ như ném một ngoại lệ khi một cái gì đó là null:

val currentUser = session.user ?: throw Http401Error("Unauthorized")

hoặc để trở về sớm từ một chức năng:

fun foo(key: String): Int {
   val startingCode: String = codes.findKey(key) ?: return 0
   // ...
   return endingValue
}

đọc thêm: Nhà điều hành Elvis


Toán tử Null với các chức năng liên quan

Kotlin stdlib có một loạt các chức năng hoạt động thực sự độc đáo với các toán tử được đề cập ở trên. Ví dụ:

// use ?.let() to change a not null value, and ?: to provide a default
val something = possibleNull?.let { it.transform() } ?: defaultSomething

// use ?.apply() to operate further on a value that is not null
possibleNull?.apply {
    func1()
    func2()
}

// use .takeIf or .takeUnless to turn a value null if it meets a predicate
val something = name.takeIf { it.isNotBlank() } ?: defaultName

val something = name.takeUnless { it.isBlank() } ?: defaultName

Chủ đề liên quan

Trong Kotlin, hầu hết các ứng dụng đều cố gắng tránh nullcác giá trị, nhưng không phải lúc nào cũng có thể. Và đôi khi nulllàm cho ý nghĩa hoàn hảo. Một số hướng dẫn để suy nghĩ về:

  • trong một số trường hợp, nó đảm bảo các kiểu trả về khác nhau bao gồm trạng thái của lệnh gọi phương thức và kết quả nếu thành công. Các thư viện như Kết quả cung cấp cho bạn loại kết quả thành công hoặc thất bại cũng có thể phân nhánh mã của bạn. Và thư viện Promising cho Kotlin có tên Kovenant cũng thực hiện tương tự dưới dạng lời hứa.

  • đối với các bộ sưu tập dưới dạng các kiểu trả về luôn trả về một bộ sưu tập trống thay vì a null, trừ khi bạn cần trạng thái thứ ba là "không có mặt". Kotlin có các hàm trợ giúp như emptyList()hoặcemptySet() để tạo các giá trị trống này.

  • khi sử dụng các phương thức trả về giá trị nullable mà bạn có mặc định hoặc thay thế, hãy sử dụng toán tử Elvis để cung cấp giá trị mặc định. Trong trường hợp Mapsử dụng getOrElse(), cho phép tạo ra giá trị mặc định thay vì Mapphương thức get()trả về giá trị null. Giống vớigetOrPut()

  • khi ghi đè các phương thức từ Java trong đó Kotlin không chắc chắn về tính không hợp lệ của mã Java, bạn luôn có thể loại bỏ ?tính không hợp lệ khỏi ghi đè của mình nếu bạn chắc chắn chữ ký và chức năng phải là gì. Do đó phương pháp ghi đè của bạn là nullan toàn hơn . Tương tự như vậy đối với việc triển khai các giao diện Java trong Kotlin, thay đổi tính không hợp lệ thành những gì bạn biết là hợp lệ.

  • nhìn vào các chức năng có thể giúp đỡ, chẳng hạn như String?.isNullOrEmpty()String?.isNullOrBlank()có thể hoạt động trên một giá trị nullable một cách an toàn và làm những gì bạn mong đợi. Trên thực tế, bạn có thể thêm các tiện ích mở rộng của riêng mình để điền vào bất kỳ khoảng trống nào trong thư viện chuẩn.

  • chức năng khẳng định như checkNotNull()requireNotNull()trong thư viện tiêu chuẩn.

  • các hàm trợ giúp như filterNotNull()loại bỏ null khỏi các bộ sưu tập hoặc listOfNotNull()để trả về một danh sách mục không hoặc đơn từ một nullgiá trị có thể .

  • cũng có một toán tử cast An toàn (nullable) cũng cho phép chuyển kiểu thành không thể null trở về null nếu không thể. Nhưng tôi không có trường hợp sử dụng hợp lệ cho trường hợp này không được giải quyết bằng các phương pháp khác được đề cập ở trên.


1

Câu trả lời trước đây là một hành động khó theo dõi, nhưng đây là một cách nhanh chóng và dễ dàng:

val something: Xyz = createPossiblyNullXyz() ?: throw RuntimeError("no it shouldn't be null")
something.foo() 

Nếu nó thực sự không bao giờ là null, ngoại lệ sẽ không xảy ra, nhưng nếu có thì bạn sẽ thấy điều gì đã xảy ra.


12
val gì đó: Xyz = createdPossizableNullXyz () !! sẽ ném NPE khi createdPossizableNullXyz () trả về null. Nó đơn giản hơn và tuân theo các quy ước để xử lý một giá trị mà bạn biết là không có giá trị
Steven Waterman
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.