+3

Kotlin standard functions: run, with, let, also and apply

Một vài standard functions trong Kotlin khá giống nhau khiến chúng ta không chắc chắn nên sử dụng function nào cho hợp lý.

Bài viết này sẽ giới thiệu tới các bạn cách đơn giản để phân biệt rõ ràng sự khác biệt của chúng và cách chọn cái nào để sử dụng.

Scoping functions

Chúng ta có thể hiểu, scoping function là các hàm phạm vi, chúng chỉ tác động tới 1 phạm vi nhất định.

Các scoping functions mà chúng ta sẽ tìm hiểu bao gồm run, with, T.run, T.let, T.alsoT.apply.

Dưới đây là minh hoạ một scoping function:

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

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

    println(mood)  // I am sad
}

Với ví dụ trên, trong hàn test, bạn có thể có một scope (phạm vi) riêng biệt nơi mà mood được định nghĩa lại trước khi được in ra, và nó hoàn toàn nằm trong phạm vi của chức năng run.

Có thể nói, đặc điểm hữu ích nhất của scoping function là nó trả về đối tượng cuối cùng nằm trong scope.

Ví dụ dưới đây được viết theo 2 cách, 1 cách sử dụng run, cách còn lại không dùng. Chúng ta có thể thấy sử dụng run sẽ gọn gàng hơn.

// Cách 1
if (firstTimeView) {
    introView.show()
} else {
    normalView.show()
}

// Cách 2
run {
    if (firstTimeView) introView else normalView
}.show()

3 attributes of scoping functions

1. Normal vs. extension function

Chúng ta cùng thử 1 ví dụ với withT.run, 2 hàm này tương tự nhau, điểm khác biệt duy nhất là 1 thằng là normal function (with - hàm thường), thằng kia là extension function (hàm mở rộng).

Dưới đây là ví dụ khi chúng thực thi cùng 1 công việc:

with(webview.settings) {
    this.javaScriptEnabled = true
    this.databaseEnabled = true
}
// similarly
webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

Về điểm khác nhau, giả sử webview ở ví dụ trên có thể null, vậy chúng ta cần phải kiểm tra null trước khi thực hiện gán giá trị cho các thuộc tính của webview:

with(webview.settings) {
    this?.javaScriptEnabled = true
    this?.databaseEnabled = true
}

// similarly.
webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

Chúng ta có thể thấy, trong trường hợp này hàm mở rộng T.run có thể xử lý đẹp hơn with(), chúng ta chỉ cần kiểm tra null 1 lần duy nhất trước khi gán giá trị cho các thuộc tính, còn với with, chúng ta cần phải thực hiện kiểm tra null trước khi gán giá trị mới cho bất kì thuộc tính nào.

2. This vs. it argument

Tương tự phần trên, ở phần này chúng ta dùng T.runT.let làm ví dụ.

Về cơ bản, T.runT.let rất giống nhau, điểm khác biệt duy nhất là tham số (argument) của chúng.

stringVariable?.run {
    println("The length of this String is $length")
}
// Similarly.
stringVariable?.let {
    println("The length of this String is ${it.length}")
}

Nếu bạn soi kĩ vào chi tiết của hàm T.run, bạn sẽ thấy T.run được tạo ra như một hàm mở rộng được gọi là block: T.(). Do đó tất cả trong phạm vi, T có thể được gọi như this. Trong lập trình, this có thể được bỏ qua. Do đó trong ví dụ trên, chúng ta có thể sử dụng $length trong câu lệnh println(), thay vì ${this.length}.

Tuy nhiên đối với hàm T.let, bạn sẽ nhận thấy rằng T.let gửi chính nó vào block: (T). Nó có thể được gọi trong hàm phạm vi như it.

Từ đặc điểm chi tiết của T.runT.let, có vẻ như T.run vượt trội hơnT.let, nhưng hàm T.let lại có một số ưu điểm nhỏ như sau:

  • T.let cung cấp sự phân biệt rõ ràng hơn bằng cách sử dụng biến đã cho function/member so với lớp bên ngoài function/member.
  • T.let cho phép đặt tên tốt hơn về biến được sử dụng đã chuyển đổi tức là bạn có thể chuyển đổi nó thành một tên khác.

Bạn có thể đặt tên mới cho biến thông qua tên của parameter được truyền vào như ví dụ này:

stringVariable?.let {
    nonNullString -> println("The non null string is $nonNullString")
}

3. Return this vs. other type

Tiếp theo, chúng ta cùng so sánh T.letT.also, cả hai đều giống nhau, nếu chúng ta nhìn vào phạm vi hàm bên trong của nó.

stringVariable?.let {
    println("The length of this String is ${it.length}")
}

// Exactly the same as below
stringVariable?.also {
    println("The length of this String is ${it.length}")
}

Tuy nhiên, sự khác biệt của chúng là những gì chúng trả lại (return). T.let cho phép trả về một kiểu giá trị khác, trong khiT.also trả về bản thân T, this.

Cả hai đều hữu ích cho các hàm liên tục (chaining function). Dưới đây là minh họa đơn giản cho việc sử dụng chúng:

val original = "abc"
// Thay đổi giá trị trả về và truyền nó cho hàm tiếp theo
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // thay đổi `it` như một parameter và truyền nó cho `let` tiếp theo
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // Có thể thay đổi kiểu dữ liệu
}.let {
    println("The length of the String is $it") // 3
}

// Wrong
// Tương tự với đoạn code trên, chúng ta thay `let` bằng `also` thì nó sẽ không in ra kết quả mà ta mong muốn
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // useless
}.also {
    println("The length of the String is ${it}") // "abc"
}

// Corrected for also
// Same value is sent in the chain
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3
}

T.also có vẻ như không có nhiều tác dụng ở trên, vì chúng ta có thể dễ dàng kết hợp chúng thành một khối chức năng duy nhất. Tuy nhiên, suy nghĩ kĩ một chút, chúng ta có thể thấy nó có một số điểm mạnh như:

  • Nó có thể cung cấp một quá trình tách rất rõ ràng trên cùng một đối tượng, tức là làm cho phần chức năng nhỏ hơn.
  • Nó có thể rất mạnh mẽ để tự thao tác trước khi được sử dụng, tạo ra một hoạt động xây dựng chuỗi.

Và khi cả hai, tức là một hàm sẽ thay đổi chính nó, một hàm sẽ duy trì chính nó, chúng sẽ đem lại những đoạn code ngắn gọn và dễ hiểu hơn, ví dụ:

// Normal approach
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}

// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

Looking at all attributes

Bằng cách xem xét 3 thuộc tính, chúng ta có thể biết khá nhiều về hành vi của hàm. Ở trên, chúng đã tìm hiểu qua 4/5 hàm được đề cập ở đầu của bài viết: run, with, T.run, T.letT.also.

Vậy còn T.apply thì sao? T.apply có 3 thuộc tính sau:

  • Nó là một hàm mở rộng
  • Nó gửi this như là tham số của nó.
  • Nó trả về this (trả về chính nó)

Dưới đây là ví dụ thể hiện 3 thuộc tính của T.apply:

// Normal approach
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}

// Improved approach
fun createInstance(args: Bundle) = MyFragment().apply { arguments = args }

//Or we could also making unchained object creation chain-able.

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}

// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

Function selections

Dựa vào các thuộc tính và tính năng của các hàm phạm vi mà chúng ta quyết định sẽ dùng hàm nào cho hợp lý.

Hy vọng rằng cây quyết định ở trên làm rõ các chức năng và cũng đơn giản hóa việc ra quyết định của bạn.

Nguồn tham khảo: Mastering Kotlin standard functions: run, with, let, also and apply


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí