Kotlin cơ bản – Cùng dev

Xin chào, mình là Vũ. Ở bài viết trước mình đã giới thiệu cơ bản về cách khai báo và sử dụng hàm trong Kotlin. Trong bài này mình sẽ trình bày các kiến thức nâng cao hơn (và thú vị hơn) về hàm trong Kotlin. Chúng là top-level function, lambda function, là extension function, và còn nhiều nhiều nữa. Hãy cùng theo dõi nhé.

Trước khi bắt đầu, hãy tạo cho mình 1 project mới và tạo 1 file Main.kt với hàm main. Main.kt sẽ là nơi để ta viết các đoạn code demo. Nếu bạn chưa biết làm những điều này, vui lòng tham khảo lại bài viết hướng dẫn sử dụng IntelleJ IDEA của mình.

Top-level function

Top-level function là các hàm không nằm trong bất kỳ 1 lớp (class) nào. Chúng được định nghĩa trong các package và được sử dụng bằng cách gọi trực tiếp qua tên đầy đủ (trong trường hợp không import package) hoặc tên hàm (trong trường hợp đã import package). Nếu bạn từng làm việc với Java chắc đều đã quen thuộc với các hàm static nằm trong lớp Utils. Top-level function trong Kotlin hoàn toàn tương tự như static function trong Java.

Hàm getPi ở bài trước mình định nghĩa chính là 1 top-level function. Giờ để hiểu rõ thêm sẽ làm thêm 1 ví dụ khác nữa. Ta tạo 1 file đặt tên là Utility.kt, định nghĩa package com.duongvu.utils và khai báo 1 hàm getCurrentDate trong package đó:

package com.duongvu.utils

fun getCurrentDate(): String {
    val date = Date()
    val dateFormat = "dd/MM/yyyy"
    val sdf = SimpleDateFormat(dateFormat)
    return sdf.format(date)
}

Lưu ý: tên packge không nhất thiết phải giống tên file chứa nó. Như ở trên mình đã định nghĩa package com.duongvu.utils trong file Utility.kt, điều này hoàn toàn OK.

Lưu ý: Bạn chưa cần phải hiểu rõ từng dòng trong hàm getCurrentDate ở trên, chỉ cần hiểu nó trả về ngày hiện tại dưới dạng 1 String. Và hàm này sử dụng các lớp java.util.Datejava.text.SimpleDateFormat, nên ta cần import 2 lớp trên:

import java.text.SimpleDateFormat
import java.util.Date

Giờ ta đã có thể sử dụng hàm getCurrentDate trong file Main.kt như sau:

import com.duongvu.utils.getCurrentDate

//Sử dụng bằng cách import hàm getCurrentDate trong package com.duongvu.utils
fun main(args: Array<String>) {
    print(getCurrentDate()) //in ra ngày hiện tại
}

hoặc:

import com.duongvu.utils.*

//Sử dụng bằng cách import tất cả mọi thứ trong package com.duongvu.utils
fun main(args: Array<String>) {
    print(getCurrentDate())
}

hoặc:

//Sử dụng bằng cách gọi tên đầy đủ của hàm getCurrentDate
fun main(args: Array<String>) {
    print(com.duongvu.utils.getCurrentDate())
}

Đó là tất cả các cách sử dụng 1 top-level function.

Lambda function

Lambda function là gì? Cấu trúc của lambda function?

Lambda function là các hàm không có tên. Chúng thường được sử dụng như các tham số để truyền vào 1 hàm khác (mình sẽ trình bày ở phần dưới). Lambda function còn có thể được biểu diễn dưới dạng các biến. Để rõ hơn hãy nhìn vào ví dụ của mình, trong file Main.kt mình sẽ khai báo 1 lambda function như thế này:

var message = { 
    print("Kotlin is awesome :D")
}

Ở trên mình đã định nghĩa 1 lambda function bởi 1 cặp {} và gán lambda function này vào biến message. Như các bạn thấy hàm này không cần định nghĩa bởi từ khóa fun, nó không hề có tên, cũng không hề có kiểu trả về. Trong hàm main, ta sẽ sử dụng nó như sau:

message() // in ra : Kotlin is awesome :D

Giờ ta sẽ nâng cấp lambda function trên 1 chút, ta sẽ truyền thêm tham số cho nó:

val message = {
    str:String->
    println(str)
    println("End lambda function")
}

Gọi hàm:

message("Kotlin is awesome :D") //in ra : Kotlin is awesome
                                //        End lambda function

Như vậy, nếu lambda function có tham số đầu vào thì các tham số sẽ được khai báo như đối với hàm bình thường, sau đó là ký tự -> và kế đến là thân hàm.

Higher-order functions – Sử dụng lambda function như 1 tham số đầu vào

Một điều tuyệt vời mà Kotlin cho phép ta làm đó là có thể sử dụng hàm như 1 tham số. Đây là điều không thể làm được trong Java. Ta làm điều này như thế nào? Hãy cũng xem ví dụ:

fun printSummary(number1: Int, number2: Int, summaryFunction: (Int, Int) -> Int) {
    val sum = summaryFunction(number1, number2)
    print("Sum of $number1 and $number2 is $sum")
}

Ở đây ta định nghĩa 1 hàm in ra tổng của 2 số nguyên. Hãy nhìn vào các tham số đầu vào của hàm printSummary này. 2 tham số đầu tiên là 2 số nguyên đầu vào để tính tổng, cái này quá dễ. Còn tham số cuối cùng:summaryFunctionHàm thực hiện việc tính tổng của 2 tham số đầu tiên. Hàm printSummary cần 1 tham số đầu vào là 1 hàm có đặc điểm:

  • Nhận 2 tham số Int là tham số đầu vào
  • Trả về kiểu dữ liệu Int

printSummary chỉ đơn thuần sử dụng giá trị trả về của summaryFunction. Giờ ta thử sử dụng hàm printSummary:

printSummary(number1 = 10, number2 = 10, summaryFunction = { a:Int, b:Int ->
        a + b
})
//In ra: Sum of 10 and 10 is 20

Ở đây khi gọi hàm printSummary mình đã truyền giá trị các tham số kèm theo tên để các bạn dễ hiểu. Hãy để ý vào tham số thứ 3, mình đã sử dụng 1 lambda function nhận 2 giá trị đầu vào là ab, và trả về giá trị a+b. Hàm này thỏa mãn điều kiện là nhận 2 tham số Int và trả về kiểu Int.

Lưu ý 1: Khi truyền 1 lambda function vào 1 hàm khác dưới dạng 1 tham số, ta có thể bỏ qua phần định nghĩa kiểu dữ liệu cho các tham số của lambda function. Kotlin tự động gán kiểu cho các tham số của lambda function sao cho khớp với lúc khai báo hàm. Ví dụ, ta hoàn toàn có thể gọi hàm printSummary ở trên như sau:

printSummary(number1 = 10, number2 = 10, summaryFunction = { a, b ->
        a + b
})
//In ra: Sum of 10 and 10 is 20

Ta không cần định nghĩa kiểu dữ liệu cho a và b. 2 tham số này sẽ tự động được gán kiểu là Int, bởi Kotlin thông minh, đơn giản là vậy.

Lưu ý 2: Ta cũng không cần phải chỉ ra kiểu dữ liệu trả về cho lambda function, bởi Kotlin cũng sẽ tự động định nghĩa kiểu dữ liệu trả về cho chúng ta (như ở trường hợp trên, kiểu dữ liệu trả về là Int).

Lưu ý 3: Đối với lambda function, khi muốn trả về 1 giá trị, ta không thể dùng từ khóa return. Ta đơn giản chỉ cần nêu ra giá trị đó. Như ví dụ trên là a+b. Tất nhiên giá trị ta nêu ra phải có cùng kiểu dữ liệu trả về của lambda function (như ví dụ trên là Int), nếu không sẽ nhận được thông báo lỗi Type mismatch.

Lưu ý 4: Vì không thể sử dụng từ khóa return, nên ta có thể nêu ra bao nhiêu giá trị tùy thích. Nhưng lambda function sẽ lấy giá trị cuối cùng làm giá trị trả về của hàm:

printSummary(number1 = 10, number2 = 10, summaryFunction = { a, b ->
        a+b
        a-b
        a*b
})
// a*b sẽ là giá trị trả về của hàm summaryFunction
//In ra: Sum of 10 and 10 is 100

Lưu ý 5: Nếu lambda function là tham số cuối cùng của 1 hàm, ta có thể viết nội dung của lambda function bên ngoài cặp (). Ví dụ:

printSummary(number1 = 10, number2 = 10) { a, b ->
        a + b
}
//in ra: Sum of 10 and 10 is 20

Nhìn như này trông code sẽ đẹp hơn và dễ hiểu hơn :).

Tham số it

Khi truyền lambda function vào 1 hàm khác dưới dạng 1 tham số, nếu lambda function đó chỉ có duy nhất 1 tham số, ta có thể bỏ qua việc khai báo tham số đó và sử dụng luôn tham số it. Đây là tham số được tự động generate để sử dụng đối với các lambda function chỉ nhận 1 tham số đầu vào. Ví dụ:

fun printDouble(number: Int, doubleFunction: (Int) -> Int) {
    print(doubleFunction(number))
}

Ta định nghĩa hàm printDouble nhận 1 tham số Int và 1 tham số là lambda function. Lambda function này có duy nhất 1 tham số đầu vào kiểu Int. Bình thường, ta sẽ gọi hàm printDouble như sau:

printDouble(number = 3) { x->
    x* 2
}
//In ra: 6

Tuy nhiên, ta có thể bỏ qua phần khai báo tham số x và sử dụng luôn tham số it (được tự động sinh ra, tương ứng với x):

printDouble(3) {
    it * 2
}
//in ra: 6

Ví dụ khác:

val listPlayer = arrayOf<String>("Ronaldo", "Messi", "Neymar", "Suarez", "Benzema", "Ramos")
val listR = listPlayer.filter {
    it.startsWith("R")
}
print(listR)
//In ra: [Ronaldo, Ramos]

Đoạn code trên in ra các phần tử bắt đầu bởi ký tự R.

Do hàm filter nhận 1 lambda function có đặc điểm: có duy nhất 1 tham số đầu vào kiểu String. Nên ta có thể sử dụng luôn tham số it (được tự động generate, có kiểu String). Thay vì phải viết thế này:

val listPlayer = arrayOf<String>("Ronaldo", "Messi", "Neymar", "Suarez", "Benzema", "Ramos")
val listR = listPlayer.filter {
    playerName->
    playerName.startsWith("R")
}
print(listR)
//In ra: [Ronaldo, Ramos]

Ta không cần tự định nghĩa tham số playerName.

Return trong lambda function

Hãy cùng xem 1 ví dụ. Ở ví dụ sau đây, ta sẽ truyền 1 lambda function vào hàm forEach của 1 intArray. Lambda function này duyệt qua tất cả các phần tử nếu gặp phần tử nào chia hết cho 3, thì hàm lambda này sẽ dừng lại.

fun testReturnFunction() {
    val intList = intArrayOf(1, 3, 5, 7, 9)
    intList.forEach {
        if (it % 3 == 0) {
            return
        }
    }
    println("End of testReturnFunction()")
}

Và gọi thử:

testReturnFunction() //Không có gì xảy ra

Tại sao lại không có gì xảy ra? Bởi câu lệnh return không chỉ kết thúc lambda function, nó còn kết thúc luôn hàm chứa nó là testReturnFunction. Nên câu lệnh println(“End of testReturnFunction()”) không bao giờ được gọi. Để cho Kotlin hiểu rằng, chỉ kết thúc lambda function, ta sửa lại như sau:

fun testReturnFunction() {
    val intList = intArrayOf(1, 3, 5, 7, 9)
    intList.forEach labelForEach@ { // Định nghĩa nhãn labelForEach cho hàm forEach, khi muốn return sẽ dùng đến nhãn này
        if (it % 3 == 0) {
            return@labelForEach // Câu lệnh này chỉ kết thúc hàm có nhãn labelForEach
        }
    }
    println("End of testReturnFunction()")
}

Lúc này, khi gọi hàm testReturnFunction() ta sẽ nhận được dòng log như mong đợi:

End of testRuturnFunction()

Trên đây mình đã trình bày những vấn đề cơ bản mà chúng ta cần phải biết về lambda function. Đây cũng sẽ là loại function mà mình rất hay sử dụng trong phát triển ứng dụng Android. Vì nó rất đơn giản, ngắn gọn, tường minh. Nó thay thế được cho các interface cồng kềnh trong Java (mình sẽ nói trong các bài tiếp theo). Hy vọng các bạn nắm rõ được nó. Sau này ở loạt bài Android, mình sẽ có dịp quay trở lại chủ đề này. Còn giờ thì next qua phần khác thôi 😀

Extension function

Chúng ta đều biết rằng, kiểu dữ liệu String cung cấp hàm cho ta biến 1 chuỗi chữ thường thành chữ hoa:

val normalString = "abcdef"
val upperCaseString = normalString.toUpperCase()
print(upperCaseString) // in ra: ABCDEF

Nhưng giờ nếu ta muốn String có thêm hàm chỉ biến ký tự đầu tiên thành chữ hoa thôi, còn các ký tự khác biến thành chữ thường, thì phải làm thế nào? Có thể bạn sẽ nghĩ đến việc kế thừa lớp String và viết hàm bổ sung theo ý muốn. Nhưng String là lớp final, tức là nó không cho phép kế thừa. Giờ mình muốn mỗi biến String đều được cung cấp hàm như thế này:

Kotlin lại cung cấp cho ta 1 tính năng tuyệt vời, đó là extension function. Đây là tính năng cho phép ta mở rộng 1 lớp (trong trường hợp này là String) với các hàm bổ sung mà không cần phải kế thừa lớp đó.

Định nghĩa extension function

Giờ mình sẽ tạo file StringUtils.kt chứa 1 package mới com.duongvu.stringutils, và định nghĩa 1 hàm như sau:

package com.duongvu.stringutils

fun String.upperFirstLetter(): String {
    val firstLetter = this.substring(0, 1).toUpperCase() //Lấy ký tự đầu, viết hoa lên
    return firstLetter.plus(this.substring(1)) // Nối ký tự đầu (đã viết hoa) với phần còn lại của chuỗi.
}

Đây chính là 1 extension function (hàm mở rộng) của kiểu dữ liệu String. Như các bạn thấy, để định nghĩa hàm mở rộng, ta cần phải chỉ ra kiểu dữ liệu (String) trước tên hàm mở rộng (upperFirstLetter). Từ khóa this được sử dụng trong thân hàm biểu diễn cho đối tượng gọi đến hàm upperFirstLetter.

Sử dụng extension function

Sau khi đã định nghĩa xong, để sử dụng extension function thì việc đầu tiên ta phải import package chứa nó. Sau đó ta gọi tới extension function như những hàm bình thường khác của kiểu dữ liệu ta vừa mở rộng (trường hợp này là String):

import com.duongvu.stringutils.upperFirstLetter

fun main(args: Array<String>) {
    val myName = "duong vu"
    print(myName.upperFirstLetter()) // in ra: Duong vu
}

Các bạn có thể thấy rằng, hàm upperFirstLetter đã xuất hiện trong danh sách gợi ý của IDE (như hình mình gửi bên trên).

Tổng kết

Như vậy ở bài viết này mình đã trình bày về:

  • Top level function
  • Lambda function
  • Extension function

Hy vọng qua 2 bài viết về hàm trong Kotlin vừa qua các bạn đã có được kiến thức nền tảng và kỹ năng cơ bản để sử dụng chúng. Tất nhiên vẫn còn 1 số kiến thức mà mình chưa trình bày ra ở đây. Bởi mình thấy sẽ phù hợp, dễ hiểu và bổ ích hơn khi đan xen chúng vào các kiến thức khác, hơn là việc chỉ nêu ra khái niệm. Các kiến thức đó chắc chắn mình sẽ nêu ra trong các bài viết sắp tới. Và bài tiếp theo mình sẽ bắt đầu trình bày về những khái niệm cơ bản nhất của lập trình hướng đối tượng. Đó là class và object. Đây là những khái niệm cực kỳ quan trọng, và sẽ đi theo ta trong mọi dự án. Hãy theo dõi nhé.