Ví dụ về thời điểm chúng ta nên sử dụng run, let, apply, also và with trên Kotlin


100

Tôi muốn có một ví dụ tốt cho mỗi hàm chạy, cho phép, áp dụng, cũng như với

Tôi đã đọc bài viết này nhưng vẫn thiếu một ví dụ

Câu trả lời:


121

Tất cả các hàm này được sử dụng để chuyển đổi phạm vi của hàm hiện tại / biến. Chúng được sử dụng để giữ những thứ thuộc về nhau ở một nơi (chủ yếu là khởi tạo).

Dưới đây là một số ví dụ:

run - trả về bất kỳ thứ gì bạn muốn và tái phạm vi biến mà nó được sử dụng để this

val password: Password = PasswordGenerator().run {
       seed = "someString"
       hash = {s -> someHash(s)}
       hashRepetitions = 1000

       generate()
   }

Máy phát điện mật khẩu bây giờ là rescoped như thisvà do đó chúng ta có thể thiết lập seed, hashhashRepetitionskhông sử dụng một biến. generate()sẽ trả về một thể hiện của Password.

applytương tự, nhưng nó sẽ trả về this:

val generator = PasswordGenerator().apply {
       seed = "someString"
       hash = {s -> someHash(s)}
       hashRepetitions = 1000
   }
val pasword = generator.generate()

Điều đó đặc biệt hữu ích khi thay thế cho mẫu Builder và nếu bạn muốn sử dụng lại các cấu hình nhất định.

let- chủ yếu được sử dụng để tránh kiểm tra rỗng, nhưng cũng có thể được sử dụng để thay thế cho run. Sự khác biệt là, điều đó thissẽ vẫn giống như trước đây và bạn truy cập vào biến phạm vi lại bằng cách sử dụng it:

val fruitBasket = ...

apple?.let {
  println("adding a ${it.color} apple!")
  fruitBasket.add(it)
}

Đoạn mã trên sẽ chỉ thêm quả táo vào giỏ nếu nó không rỗng. Cũng lưu ý rằng itbây giờ không còn là tùy chọn nữa nên bạn sẽ không gặp phải NullPointerException ở đây (hay còn gọi là bạn không cần sử dụng ?.để truy cập các thuộc tính của nó)

also- sử dụng nó khi bạn muốn sử dụng apply, nhưng không muốn bóngthis

class FruitBasket {
    private var weight = 0

    fun addFrom(appleTree: AppleTree) {
        val apple = appleTree.pick().also { apple ->
            this.weight += apple.weight
            add(apple)
        }
        ...
    }
    ...
    fun add(fruit: Fruit) = ...
}

Sử dụng applyở đây sẽ đổ bóng this, do đó this.weightsẽ đề cập đến quả táo, không phải giỏ trái cây.


Lưu ý: Tôi không biết xấu hổ đã lấy các ví dụ từ blog của mình


2
Đối với bất kỳ ai như tôi giật mình với mã đầu tiên, dòng cuối cùng của lambda được coi như một kho trả lại trong Kotlin.
Jay Lee

62

Có một số bài báo khác như ở đây , và đây là giá trị để xem.

Tôi nghĩ rằng đó là thời điểm bạn cần ngắn hơn, súc tích hơn trong một vài dòng và để tránh phân nhánh hoặc kiểm tra câu lệnh có điều kiện (chẳng hạn như nếu không phải là null, thì hãy làm điều này).

Tôi thích biểu đồ đơn giản này, vì vậy tôi đã liên kết nó ở đây. Bạn có thể thấy nó từ điều này được viết bởi Sebastiano Gottardo.

nhập mô tả hình ảnh ở đây

Hãy cũng xem biểu đồ kèm theo giải thích của tôi bên dưới.

Ý tưởng

Tôi nghĩ đó là một cách đóng vai trò bên trong khối mã của bạn khi bạn gọi các hàm đó + cho dù bạn muốn bản thân quay lại (để gọi chuỗi hàm hay đặt thành biến kết quả, v.v.).

Trên đây là những gì tôi nghĩ.

Ví dụ về khái niệm

Hãy xem ví dụ cho tất cả chúng ở đây

1.) myComputer.apply { }nghĩa là bạn muốn đóng vai trò là một diễn viên chính (bạn muốn nghĩ rằng bạn là máy tính), và bạn muốn bản thân trở lại (máy tính) để bạn có thể làm

var crashedComputer = myComputer.apply { 
    // you're the computer, you yourself install the apps
    // note: installFancyApps is one of methods of computer
    installFancyApps() 
}.crash()

Đúng vậy, bản thân bạn chỉ cần cài đặt ứng dụng, tự gặp sự cố và tự lưu làm tài liệu tham khảo để cho phép người khác xem và làm điều gì đó với nó.

2.) myComputer.also {}nghĩa là bạn hoàn toàn chắc chắn rằng bạn không phải là máy tính, bạn là người ngoài muốn làm điều gì đó với nó, và cũng muốn nó là máy tính như một kết quả trả về.

var crashedComputer = myComputer.also { 
    // now your grandpa does something with it
    myGrandpa.installVirusOn(it) 
}.crash()

3.) with(myComputer) { }có nghĩa là bạn là diễn viên chính (máy tính) và bạn không muốn bản thân trở lại như vậy.

with(myComputer) {
    // you're the computer, you yourself install the apps
    installFancyApps()
}

4.) myComputer.run { }nghĩa là bạn là diễn viên chính (máy tính), và bạn không muốn bản thân trở lại như vậy.

myComputer.run {
    // you're the computer, you yourself install the apps
    installFancyApps()
}

nhưng nó khác với with { }một ý nghĩa rất tinh tế là bạn có thể gọi chuỗi run { }như sau

myComputer.run {
    installFancyApps()
}.run {
    // computer object isn't passed through here. So you cannot call installFancyApps() here again.
    println("woop!")
}

Đây là do run {}là chức năng mở rộng, nhưng with { }không phải. Vì vậy, bạn gọi run { }thisbên trong khối mã sẽ được phản ánh với loại đối tượng người gọi. Bạn có thể thấy điều này để có lời giải thích tuyệt vời cho sự khác biệt giữa run {}with {}.

5.) myComputer.let { }có nghĩa là bạn là người ngoài nhìn vào máy tính và muốn làm điều gì đó với nó mà không cần quan tâm đến phiên bản máy tính sẽ được trả lại cho bạn một lần nữa.

myComputer.let {
    myGrandpa.installVirusOn(it)
}

Cách nhìn nhận nó

Tôi có xu hướng nhìn vào alsoletnhư một cái gì đó bên ngoài, bên ngoài. Bất cứ khi nào bạn nói hai từ này, nó giống như bạn đang cố gắng thực hiện một điều gì đó. letcài đặt vi-rút trên máy tính này và làm alsonó bị hỏng. Vì vậy, điều này đóng vai trò quyết định bạn có phải là diễn viên hay không.

Đối với phần kết quả, nó rõ ràng ở đó. alsothể hiện rằng nó cũng là một thứ khác, vì vậy bạn vẫn giữ được tính khả dụng của chính đối tượng. Do đó, nó trả về kết quả là.

Mọi thứ khác liên kết với this. Ngoài ra, run/withrõ ràng là không quan tâm đến việc trả lại đối tượng-self trở lại. Bây giờ bạn có thể phân biệt tất cả chúng.

Tôi nghĩ rằng đôi khi chúng ta thoát khỏi lập trình 100% / dựa trên logic của các ví dụ, thì chúng ta sẽ ở vị trí tốt hơn để khái niệm hóa mọi thứ. Nhưng điều đó phụ thuộc đúng :)


1
Sơ đồ nói lên tất cả; Tốt nhất cho đến nay.
Shukant Pal

Đây phải là câu trả lời được chấp nhận và được bình chọn nhiều nhất
Segun Wahaab

8

let, also, apply, takeIf, takeUnless là các hàm mở rộng trong Kotlin.

Để hiểu các hàm này, bạn phải hiểu các hàm Mở rộng và các hàm Lambda trong Kotlin.

Chức năng mở rộng:

Bằng cách sử dụng hàm mở rộng, chúng ta có thể tạo một hàm cho một lớp mà không cần kế thừa một lớp.

Kotlin, tương tự như C # và Gosu, cung cấp khả năng mở rộng một lớp với chức năng mới mà không cần phải kế thừa từ lớp hoặc sử dụng bất kỳ loại mẫu thiết kế nào như Decorator. Điều này được thực hiện thông qua các khai báo đặc biệt được gọi là phần mở rộng. Kotlin hỗ trợ các chức năng mở rộng và thuộc tính mở rộng.

Vì vậy, để tìm nếu chỉ các số trong String, bạn có thể tạo một phương thức như bên dưới mà không cần kế thừa Stringlớp.

fun String.isNumber(): Boolean = this.matches("[0-9]+".toRegex())

bạn có thể sử dụng chức năng mở rộng ở trên như thế này,

val phoneNumber = "8899665544"
println(phoneNumber.isNumber)

đó là bản in true.

Chức năng Lambda:

Các hàm Lambda cũng giống như Giao diện trong Java. Nhưng trong Kotlin, các hàm lambda có thể được truyền như một tham số trong các hàm.

Thí dụ:

fun String.isNumber(block: () -> Unit): Boolean {
    return if (this.matches("[0-9]+".toRegex())) {
        block()
        true
    } else false
}

Bạn có thể thấy, khối là một hàm lambda và nó được truyền dưới dạng một tham số. Bạn có thể sử dụng chức năng trên như thế này,

val phoneNumber = "8899665544"
    println(phoneNumber.isNumber {
        println("Block executed")
    })

Hàm trên sẽ in như thế này,

Block executed
true

Tôi hy vọng, bây giờ bạn đã có ý tưởng về các hàm Mở rộng và các hàm Lambda. Bây giờ chúng ta có thể lần lượt vào các chức năng Mở rộng.

để cho

public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

Hai loại T và R được sử dụng trong hàm trên.

T.let

Tcó thể là bất kỳ đối tượng nào như lớp String. vì vậy bạn có thể gọi hàm này với bất kỳ đối tượng nào.

block: (T) -> R

Trong tham số let, bạn có thể thấy hàm lambda ở trên. Ngoài ra, đối tượng gọi được truyền như một tham số của hàm. Vì vậy, bạn có thể sử dụng đối tượng lớp đang gọi bên trong hàm. thì nó trả về R(một đối tượng khác).

Thí dụ:

val phoneNumber = "8899665544"
val numberAndCount: Pair<Int, Int> = phoneNumber.let { it.toInt() to it.count() }

Trong ví dụ trên, chúng ta hãy lấy String làm tham số của hàm lambda và nó trả về Pair .

Theo cách tương tự, chức năng mở rộng khác hoạt động.

cũng thế

public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }

hàm mở rộng alsonhận lớp đang gọi làm tham số hàm lambda và không trả về gì.

Thí dụ:

val phoneNumber = "8899665544"
phoneNumber.also { number ->
    println(number.contains("8"))
    println(number.length)
 }

ứng dụng

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

Tương tự như vậy nhưng cùng một đối tượng gọi được truyền dưới dạng hàm để bạn có thể sử dụng các hàm và các thuộc tính khác mà không cần gọi nó hoặc tên tham số.

Thí dụ:

val phoneNumber = "8899665544"
phoneNumber.apply { 
    println(contains("8"))
    println(length)
 }

Bạn có thể thấy trong ví dụ trên, các hàm của lớp String được gọi trực tiếp bên trong lambda funtion.

takeIf

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null

Thí dụ:

val phoneNumber = "8899665544"
val number = phoneNumber.takeIf { it.matches("[0-9]+".toRegex()) }

Trong ví dụ trên numbersẽ có một chuỗi phoneNumberchỉ nó khớp với regex. Nếu không, nó sẽ là null.

takeUnless

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? = if (!predicate(this)) this else null

Nó là mặt trái của takeIf.

Thí dụ:

val phoneNumber = "8899665544"
val number = phoneNumber.takeUnless { it.matches("[0-9]+".toRegex()) }

numbersẽ có một chuỗi phoneNumberchỉ nếu không khớp với regex. Nếu không, nó sẽ là null.

Bạn có thể xem các câu trả lời tương tự hữu ích ở đây sự khác biệt giữa kotlin also, apply, let, use, takeIf và takeUnless trong Kotlin


Bạn có một lỗi đánh máy trong ví dụ cuối cùng của mình, có thể bạn muốn nói phoneNumber. takeUnless{}thay vì phoneNumber. takeIf{}.
Ryan Amaral

1
Đã sửa. Cảm ơn @Ryan Amaral
Bhuvanesh BS

5

Có 6 chức năng xác định phạm vi khác nhau:

  1. T.run
  2. T.let
  3. T.apply
  4. T.also
  5. với
  6. chạy

Tôi đã chuẩn bị một ghi chú trực quan như bên dưới để cho thấy sự khác biệt:

data class Citizen(var name: String, var age: Int, var residence: String)

nhập mô tả hình ảnh ở đây

Quyết định tùy thuộc vào nhu cầu của bạn. Các trường hợp sử dụng của các chức năng khác nhau chồng chéo lên nhau, do đó bạn có thể chọn các chức năng dựa trên các quy ước cụ thể được sử dụng trong dự án hoặc nhóm của bạn.

Mặc dù các hàm phạm vi là một cách làm cho mã ngắn gọn hơn, nhưng hãy tránh lạm dụng chúng: nó có thể làm giảm khả năng đọc mã của bạn và dẫn đến lỗi. Tránh lồng ghép các hàm phạm vi và cẩn thận khi xâu chuỗi chúng: rất dễ nhầm lẫn về đối tượng ngữ cảnh hiện tại và giá trị của đối tượng này hoặc nó.

Đây là một sơ đồ khác để quyết định sử dụng cái nào từ https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84 nhập mô tả hình ảnh ở đây

Một số quy ước như sau:

Cũng sử dụng cho các hành động bổ sung không làm thay đổi đối tượng, chẳng hạn như ghi nhật ký hoặc in thông tin gỡ lỗi.

val numbers = mutableListOf("one", "two", "three")
 numbers
 .also { println("The list elements before adding new one: $it") }
 .add("four")

Trường hợp phổ biến để áp dụng là cấu hình đối tượng.

val adam = Person("Adam").apply {
age = 32
city = "London"        
}
println(adam)

Nếu bạn cần đổ bóng, hãy sử dụng run

fun test() {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}

Nếu bạn cần trả lại chính đối tượng người nhận, hãy sử dụng áp dụng hoặc cũng


3

Theo kinh nghiệm của tôi, vì các hàm như vậy là đường cú pháp nội tuyến mà không có sự khác biệt về hiệu suất, bạn nên chọn hàm yêu cầu viết ít mã nhất trong lamda.

Để thực hiện việc này, trước tiên hãy xác định xem bạn muốn lambda trả về kết quả của nó (select run/ let) hay chính đối tượng (select apply/ also); thì trong hầu hết các trường hợp khi lambda là một biểu thức đơn lẻ, hãy chọn những cái có cùng kiểu hàm khối với biểu thức đó, vì khi đó là biểu thức bộ thu, thiscó thể bị bỏ qua, khi đó là biểu thức tham số, itngắn hơn this:

val a: Type = ...

fun Type.receiverFunction(...): ReturnType { ... }
a.run/*apply*/ { receiverFunction(...) } // shorter because "this" can be omitted
a.let/*also*/ { it.receiverFunction(...) } // longer

fun parameterFunction(parameter: Type, ...): ReturnType { ... }
a.run/*apply*/ { parameterFunction(this, ...) } // longer
a.let/*also*/ { parameterFunction(it, ...) } // shorter because "it" is shorter than "this"

Tuy nhiên, khi lambda bao gồm sự kết hợp của chúng, thì việc chọn cái phù hợp hơn với bối cảnh hoặc bạn cảm thấy thoải mái hơn là tùy thuộc vào bạn.

Ngoài ra, hãy sử dụng những cái có chức năng khối tham số khi cần giải cấu trúc:

val pair: Pair<TypeA, TypeB> = ...

pair.run/*apply*/ {
    val (first, second) = this
    ...
} // longer
pair.let/*also*/ { (first, second) -> ... } // shorter

Dưới đây là so sánh ngắn gọn giữa tất cả các chức năng này từ khóa học Kotlin chính thức của JetBrains trên Coursera Kotlin dành cho nhà phát triển Java : Bảng khác biệt Triển khai đơn giản hóa

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.