12/08/2018, 16:35

Sự khác nhau tinh tế giữa with(), apply(), let(), also() và run() trong Kotlin

Kotlin định nghĩa một số hàm mở rộng như with() và apply() trong tệp Standard.kt của nó. Bạn có thể đã nhìn thấy một số trong số chúng trong các hướng dẫn khác nhau hoặc thậm chí đã từng sử dụng chúng. Đôi khi bạn cũng có thể tự hỏi chọn cái nào để sử dụng. Trong bài đăng này, tôi sẽ đi qua những ...

Kotlin định nghĩa một số hàm mở rộng như with() và apply() trong tệp Standard.kt của nó. Bạn có thể đã nhìn thấy một số trong số chúng trong các hướng dẫn khác nhau hoặc thậm chí đã từng sử dụng chúng. Đôi khi bạn cũng có thể tự hỏi chọn cái nào để sử dụng. Trong bài đăng này, tôi sẽ đi qua những chức năng khó hiểu và xem liệu chúng ta có thể hiểu được những khác biệt. Có 4 (cộng thêm một trong phiên bản Kotlin 1.1) các chức năng trông giống nhau, và chúng được định nghĩa như sau:

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
public inline fun <T, R> T.run(block: T.() -> R): R = block()

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()

/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

Nếu bạn cố đọc các nhận xét, rất khó để hiểu được sự khác biệt ngay lập tức. Thay vào đó, chúng ta hãy đọc qua các method signatures "theo nghĩa đen" và tìm kiếm những thứ giống nhau và những thứ khác nhau.

Điểm giống nhau:

  1. T là kiểu cá thể mà bạn muốn vận hành, và trong thuật ngữ của Kotlin nó được gọi là receiver type. Mặc dù with() không phải là một phương thức mở rộng, trường hợp của T vẫn còn được gọi là receiver trong định nghĩa hàm.
  2. Đối số cuối cùng là một phương thức mà bạn có thể cung cấp để vận hành đối tượng nhận. Vì nó là đối số cuối cùng, vì vậy Kotlin cho phép bạn di chuyển khai báo hàm ra khỏi ngoặc đơn.

Điểm khác nhau:

  1. apply() và also() trả về T, nhưng những phương thức khác trả về R.
  2. block trong also() và let() là một hàm lấy T làm đối số, nhưng ở các hàm khác nó là một hàm mở rộng kiểu T.

Một vài ví dụ:

Không có gì là tốt hơn đoạn code thực, đúng không? Hãy bắt đầu với phiên bản Java, vì vậy chúng ta có thể hiểu rõ hơn về những chức năng của Kotlin có thể giúp chúng ta viết nhiều mã súc tích và dễ đọc hơn vào cùng một thời điểm.

StringBuilder builder = new StringBuilder();
builder.append("content: ");
builder.append(builder.getClass().getCanonicalName());

out.println(builder.toString()); // content: java.lang.StringBuilder

Xem triển khai tương đương và so sánh các chức năng

also() vs apply()

Hãy so sánh hai chức năng này trước:

  1. Giá trị trả lại luôn luôn là chính nó, với trường hợp đối tượng nhận kiểu T.
  2. block cùng trả về kiểu là Unit

Việc thực hiện tương tự:

StringBuilder().also {
    it.append("content: ")
    it.append(it.javaClass.canonicalName)
}.print() // content: java.lang.StringBuilder

StringBuilder().apply {
    append("content:")
    append(javaClass.canonicalName)
}.print() // content: java.lang.StringBuilder

phương thức print()được định nghĩa ngắn gọn:

fun Any.print() = println(this)

Cả hai chức năng đều hoạt động chính xác theo cùng một cách, nhưng với một sự khác biệt tinh tế trong also(), chúng ta phải sử dụng một biến tiềm ẩn it để nối thêm nội dung. Với apply() chúng ta có thể gọi trực tiếp append() như thể block là một phần của instance. Tại sao? Bởi vì block được định nghĩa là (T) -> Unit, nhưng nó lại được định nghĩa là T.() -> Unit trong apply(). Vì vậy, bạn chỉ có thể xem apply() như là một phiên bản "đơn giản" hơn của also() rằng nó có một implicit this được định nghĩa để được sử dụng trong thân hàm. also() yêu cầu bạn sử dụng it, nhưng nó có thể dễ đọc nhiều hơn trong một số trường hợp, hoặc bạn thậm chí có thể đặt tên cho instance để phù hợp với bối cảnh của bạn tốt hơn.

let() vs run()

So sánh:

  1. block được định nghĩa là (T) -> R trong let(), lại được định nghĩa là T.() -> R trong run()
  2. kiểu trả về của chúng đều là R từ khối block function

Thực hiện:

StringBuilder().let {
    it.append("content: ")
    it.append(it.javaClass.canonicalName)
}.print()

StringBuilder().run {
    append("content: ")
    append(javaClass.canonicalName)
}.print()

Tương tự như so sánh trước, , let() yêu cầu một explicit it và run() có một implicit this trong thân khối block của chúng. Tuy nhiên, cặp chức năng này có sự khác biệt lớn hơn also() và apply(). Chúng trả về giá trị trả về bởi bản thân block. Nói cách khác, cả hai let() và run() trả về bất cứ thứ gì block trả về. Hãy xem ví dụ sau đây:

StringBuilder().run {
    append("run (append):")
    append(" ")
    append(javaClass.canonicalName)
}.print() // run (append): java.lang.StringBuilder

StringBuilder().run {
    append("run (length):")
    append(" ")
    append(javaClass.canonicalName)
    length
}.print() // 37

Cái đầu tiên trả về enclosing string builder khi append() trả về chính chuỗi string builder, nhưng cái thứ 2 trả về 37 khi length() trả về Integer. Vì vậy, cả hai let() và run() có thể được sử dụng khi bạn muốn sử dụng một thể hiện của T nhưng bạn cũng muốn có một giá trị trả về khác nhau R.

with()

Thực hiện:

with(StringBuilder()) {
    append("content: ")
    append(javaClass.canonicalName)
}.print()

with() không phải là một phương thức mở rộng trên kiểu T, nhưng phải cần một instance của T như là đối số đầu tiên. Kotlin cũng cho phép chúng ta di chuyển các đối số chức năng cuối cùng trong ngoặc đơn. block được định nghĩa là T.() -> R vì vậy bạn không phải sử dụng nó, và bạn có thể thay đổi giá trị trả lại trong thân block.

Bảng so sánh

Tôi đã thực hiện một sơ đồ so sánh đơn giản để bao gồm các đặc tính và hy vọng chúng ta có thể thấy sự khác biệt dễ dàng hơn:

Name Is Extension Function? Return Type Argument in block block definition
also() yes T (this) explicit it (T) -> Unit
apply() yes T (this) implicit this T.() -> Unit
let() yes R (from block body) explicit it (T) -> R
run() yes R (from block body) implicit this T.() -> R
with() no R (from block body) implicit this T.() -> R

Kết luận

Các chức năng này trông rất giống nhau và có thể thay đổi trong một số trường hợp sử dụng. Để có được tính nhất quán và dễ đọc hơn cho người mới bắt đầu Kotlin, tôi sẽ đề nghị rằng có thể bắt đầu với also() và let() vì chúng yêu cầu developers đặt tên cho đối số hoặc sử dụng ngầm định nó trong thân block. Nó có thể hơi dài dòng hơn các phương thức khác, nhưng luôn có một điểm chung giữa các thành viên trong nhóm của bạn khi bắt đầu sử dụng Kotlin.

0