Khi làm việc với Kotlin đôi khi có những thành phần mà chúng ta đã sử dụng rất nhiều nhưng lại không biết chính xác tên gọi của nó. Bài viết này sẽ chia sẻ một số thành phần của Kotlin để chúng ta có thể biết được tên gọi cũng như cách sử dụng của chúng.
1. Lambda Expression
Lambda Expression hiểu đơn giản là một hàm mà khi khai báo ta không cần đặt tên cho nó (anonymous function).
fun main() { val a = { println("Lambda expression") } a() a.invoke() }
Lambda expression
Lambda expression
Ví dụ trên ta khai báo 1 lambda đơn giản, nếu muốn truyền thêm biến đầu vào cho hàm ta làm như ví dụ sau:
fun main() { val present = { a: String -> println(a) } val sum = { a: Int, b: Int -> a + b} val a = 5 val b = 4 present("Sum of ($a, $b): ${sum(a, b)}") }
Sum of (5, 4): 9
Lambda expression ngắn gọn và rất dễ sử dụng.
2. Higher-order functions trong Kotlin
Chúng ta đã quen thuộc với việc khai báo một hàm mà có nhận các tham số đầu vào và trả ra các kết quả là các kiểu dữ liệu như Int, String, Boolean,.. Nhưng nếu đầu vào của hàm mà chúng ta khai báo và kết quả trả về lại là một hàm khác thì sẽ như thế nào. Higher-order functions chính là câu trả lời.
Tham số dùng cho Higher-order functions là lambda hoặc anonymous function. Thường thì lambda sẽ được sử dụng nhiều hơn.
fun showSum(a: Int, b: Int, operation: (a: Int, b: Int) -> Int) { println("Sum of ($a, $b): ${operation(a, b)}") } fun main() { showSum(5, 6) { a, b -> a + b} }
Sum of (5, 6): 11
Ví dụ trên hàm showSum nhận vào 1 lambda tính tổng hai số. Còn ví dụ dưới đây 1 Higher-order function sẽ trả về 1 hàm khác.
fun operation(action: String): ((Int, Int) -> Int) { return when (action) { "add" -> {a, b -> a + b} "sub" -> {a, b -> a - b} else -> {_, _ -> 0} } } fun main() { val a = 5 val b = 6 val sum = operation("add") val sub = operation("sub") println("Result sum: ${sum(a, b)} and sub: ${sub(a, b)}") }
Result sum: 11 and sub: -1
3. Extension functions trong Kotlin
Extension functions trong Kotlin cho phép ta thêm các hàm mới cho một class đã có trước đó. Điều đặc biệt ở đây là chúng ta không cần phải can thiệp sửa trực tiếp hay kế thừa lại class đó.
fun Int.addTen(): Int { return this + 10 } fun Int.printNum() { println("Number: ${this}") } fun main() { val i: Int = 10 val b: Int = i.addTen() b.printNum() }
Ví dụ trên class Int đã được viết thêm 2 hàm là addTen và printNum, việc gọi và sử dụng các hàm mở rộng này cũng giống như việc sử dụng các hàm được khai báo nội bộ bên trong class. Kết quả thu được của ví dụ trên:
Number: 20
4. Sealed class trong Kotlin
Sealed class cho phép chúng ta giới hạn các class con được phép kế thừa lại, toàn bộ các class con sẽ được khai báo bên trong của sealed class cha.
Sealed class thường được dùng trong trường hợp khi ta muốn tất cả các class con kế thừa phải được triển khai và sử lý cho mọi trường hợp. Đơn giản hơn thì ta sẽ xem ví dụ dưới đây:
sealed class Result { data class Success(val data: String) : Result() data class Error(val message: String) : Result() object Loading : Result() } fun handleRequest(result: Result) { when (result) { is Result.Success -> { println("Success: ${result.data}") } is Result.Error -> { println("Error: ${result.message}") } Result.Loading -> { println("Loading...") } } } fun main() { handleRequest(Result.Loading) handleRequest(Result.Success("This is a response")) }
Loading...
Success: This is a response
5. Object expressions trong Kotlin
Object expressions cho phép chúng ta khởi tạo các đối tượng mà không cần phải đặt tên khi khai báo (anonymous object). Nếu đã biết về callback thì chúng ta sẽ không còn xa lạ gì với Object expressions.
interface ActionListener { fun doAction() } fun setActionListener(listener: ActionListener) { listener.doAction() } fun main() { setActionListener(object : ActionListener { override fun doAction() { println("Start Action...") } }) }
Start Action...
6. Reified modifier trong Kotlin
Reified kết hợp với inline sẽ trở thành một công cụ đắc lực khi làm việc với các tham số generic. Thông qua việc cho phép truy cập thông tin của các class tại thời điểm runtime. Khi reified và inline được sử dụng cho 1 hàm, thì trình biên dịch sẽ sao chép bytecode của hàm này tới các vị trí mà hàm được gọi (tính năng của inline function). Bằng cách này, generic type T sẽ được gán đúng với kiểu mà biến được gán cho hàm đang sử dụng.
inline fun <reified T> showInfo(info: T) { println("Info is $info") println("Type of info: ${T::class.java}") } fun main() { showInfo<String>("Info String") showInfo<Int>(1) showInfo<Boolean>(true) }
Info is Info String
Type of info: class java.lang.String
Info is 1
Type of info: class java.lang.Integer
Info is true
Type of info: class java.lang.Boolean
7. Delegates trong Kotlin
Delegates trong kotlin cung cấp một cơ chế cho phép uỷ quyền triển khai các thuộc tính (Properties) hoặc hàm (functions) cho một đối tượng. Việc sử dụng delegates cho phép tái sử dụng lại các các phương thức (functions) cũng như thêm các phương thức mới cho đối tượng mà không cần phải dùng tới kế thừa.
Có hai loại delegate trong Kotlin là Property delegate và function delegates. Chúng ta cùng tham khảo một vài ví dụ để hiểu về 2 loại delegates này.
Delegates property
Lazy property cho phép việc khởi tạo giá trị cho thuộc tính diễn ra khi lần đầu tiên thuộc tính đó được gọi.
val testLazy : String by lazy { "Data test" } fun main() { println(testLazy) }
Data test
Observable property lắng nghe sự thay đổi giá trị của thuộc tính.
import kotlin.properties.Delegates var test by Delegates.observable("Z") { property, oldValue, newValue -> println("Old value $oldValue") println("New value $newValue") } fun main() { test = "A" test = "B" test = "C" }
Old value Z
New value A
Old value A
New value B
Old value B
New value C
Vetoable delegate cũng giống như Observable delegates khi có thể lắng nghe các thay đổi giá trị của thuộc tính, tuy nhiên vetoable có thể cho phép giá trị mới có được gán cho thuộc tính hay không.
import kotlin.properties.Delegates var dataTest: Int by Delegates.vetoable(0) { property, oldValue, newValue -> newValue > 10 } fun main() { dataTest = 9 println(dataTest) dataTest = 11 println(dataTest) }
0
11
Từ ví dụ trên ta thấy thuộc tính dataTest được khởi tạo với giá trị bằng 0 và được Vetoable lọc điều kiện với giá trị gán cho nếu nó hơn 10 sẽ từ chối việc gán giá trị.
Delegates function
Giả sử ta có một bài toán về xử lý gọi api được viết theo Delegate pattern được code như sau:
interface PostMethod { fun doPost() } interface GetMethod { fun doGet() } class RequestHandler( val postMethod: PostMethod, val getMethod: GetMethod ): PostMethod, GetMethod { override fun doPost() { postMethod.doPost() } override fun doGet() { getMethod.doGet() } } class PostImage(val imageFile: String): PostMethod { override fun doPost() { println("Post $imageFile") } } class GetImage(val imageFile: String): GetMethod { override fun doGet() { println("Get $imageFile") } } fun main() { val imageFile = "happy.png" val requestHandler = RequestHandler( PostImage(imageFile), GetImage(imageFile) ) requestHandler.doPost() requestHandler.doGet() }
Post happy.png
Get happy.png
Ta sử dụng Delegates functions với đoạn code trên sẽ như sau:
interface PostMethod { fun doPost() } interface GetMethod { fun doGet() } class RequestHandler( val postMethod: PostMethod, val getMethod: GetMethod ): PostMethod by postMethod, GetMethod by getMethod class PostImage(val imageFile: String): PostMethod { override fun doPost() { println("Post $imageFile") } } class GetImage(val imageFile: String): GetMethod { override fun doGet() { println("Get $imageFile") } } fun main() { val imageFile = "happy.png" val requestHandler = RequestHandler( PostImage(imageFile), GetImage(imageFile) ) requestHandler.doPost() requestHandler.doGet() }
8. Operator overloading trong Kotlin
Trong Kotlin ta có thể nối 2 String lại với nhau bằng toán tử +, cũng như thêm 1 phần tử mới vào một list cũng bằng toán tử +. Việc có thể sử dụng các toán tử (+,-,*,/,…) giữa các đối tượng là operator overloading. Cho phép chúng ta tạo ra các phương thức riêng mà có thể được sử dụng như những toán tử.
data class A(val x: Int, val y: Int) { operator fun plus(anotherA: A): A { return A ( x + anotherA.x, y + anotherA.y ) } } fun main() { val l = mutableListOf<String>() l.add("A") val newList = l + "B" println(newList) val a = A(1, 3) val b = A(2, 3) val c = a + b println(c) }
[A, B]
A(x=3, y=6)
9. Tail recursion trong Kotlin
Thông thường khi gọi 1 hàm đệ quy (recursion), hàm đệ quy sẽ được gọi trước cho đến khi lần đệ quy cuối cùng thì kết quả đó sẽ là input cho hàm trước đó và cứ thế cho đến khi có kết quả cuối cùng. Dễ tưởng tượng thì ta có thể tham khảo hàm tính giai thừa kinh điển sau:
fun Fact(num: Int):Long{ return if(num==1) { 1 } else { num * Fact(num-1) } } fun main() { println(Fact(3)) }
120
Hàm hàm Fact trên sẽ được thực hiện như sau:
Fact(3)
3 * Fact(2)
3 * (2 * Fact(1))
3 * (2 * 1)
3 * 2
6
Như ta thấy mỗi lần gọi đệ quy thì trạng thái hàm hiện tại phải được lưu lại vào stack để chờ tính toán tiếp nên trong nhiều trường hợp chương trình sẽ gặp lỗi stack overflow. Tail recursion đã ra đời để giải quyết vấn đề này.
tailrec fun Fact(num: Long, x: Long):Long{ return if(num==1L) { x } else { Fact(num-1, x * num) } } fun main() { println(Fact(3, 1)) }
6
Việc viết lại hàm tính giai thừa sử dụng tailrec, đệ quy sẽ được gọi như sau:
Fact(3, 1)
Fact(2, 3)
Fact(1, 6)
6
Việc sử sử dụng tailrec Kotlin compiler sẽ thực hiện khử để quy thông qua sử dụng vòng lặp thay thế. Điều này sẽ đảm bảo việc stack overflow sẽ không xảy ra.
10. Inline function trong Kotlin
Inline function sẽ nói với trình biên dịch chuyển toàn bộ nội dung của thân hàm inline đó đến nơi mà hàm inline đó được gọi.
inline fun addNumberDouble(a: Int, b: Int): Int { val x = a * 2 val y = b * 2 return x + y } fun doCalculate() { val a = 10 val b = 15 val c = addNumberDouble(a, b) println(c) }
Ta có hàm inline function addNumberDouble như trên, giờ ta sẽ sử dụng công cụ Decompile của Kotlin bytecode và kết quả thu được sẽ làm rõ thêm khái niệm về inline function.
public final int addNumberDouble(int a, int b) { int $i$f$addNumberDouble = 0; int x = a * 2; int y = b * 2; return x + y; } public final void doCalculate() { int a = 10; int b = 15; int $i$f$addNumberDouble = false; int x$iv = a * 2; int y$iv = b * 2; int c = x$iv + y$iv; System.out.println(c); }
Leave a Reply