Kotlin standard functions: run, with, let, also and apply
Bài đăng này đã không được cập nhật trong 6 năm
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.also
và T.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 with
và T.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.run
và T.let
làm ví dụ.
Về cơ bản, T.run
và T.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.run
và T.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 đã chofunction/member
so với lớp bên ngoàifunction/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.let
và T.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.let
và T.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