Kotlin Bài 7: Lớp, thuộc tính và phương thức

Lớp (class) là khái niệm cơ bản nhất trong lập trình hướng đối tượng. Nó đặc trưng cho 1 loại đối tượng xác định, với những thuộc tính và hành động xác định. Lớp bao gồm các thuộc tính và các phương thức. Hôm nay, hãy cùng mình tìm hiểu về khái niệm cơ bản nhưng cũng là quan trọng nhất của lập trình hướng đối tượng này nhé.

Định nghĩa lớp

Ta định nghĩa lớp qua từ khóa class, điều này khá giống với Java:

class Human

Dòng code trên vừa khai báo 1 lớp rỗng, lớp đơn giản nhất trong lập trình hướng đối tượng. Nó không có thuộc tính cũng chẳng có thân hàm, tuy nhiên ta vẫn có thể khởi tạo 1 đối tượng của lớp Human trên như sau:

val human = Human()

Như vậy là không giống với Java, ta không cần từ khóa new để khởi tạo 1 đối tượng mới.

Thuộc tính và hàm khởi tạo của lớp

Lớp ta vừa mới khởi tạo ở phần trước còn khá đơn giản, giờ ta sẽ thêm vài thuộc tính cho nó, đồng thời sẽ thêm cách khởi tạo mới cho class này:

class Human(var name: String, var age: Int) {

}

Lớp Human đã được bổ sung 2 thuộc tính là name age. Và nó có hàm khởi tạo gồm 2 tham số đầu vào kiểu Stringint(var name: String, var age: Int) được gọi là primary constructor (hàm khởi tạo chính).

Vậy là với đúng 1 dòng, ta đã vừa khai báo được các thuộc tính của class Human, vừa định nghĩa được hàm khởi tạo ứng với 2 thuộc tính đó. Rất ngắn gọn và dễ hiểu đúng không. Giờ ta sẽ thử khởi tạo 1 đối tượng Human bằng hàm khởi tạo ta vừa định nghĩa:

var me: Human = Human("Dương Vũ", 25)

Đơn giản là khai báo biến có kiểu dữ liệu là Human và khởi tạo cho nó! Theo đúng định nghĩa lớp viết ra ở trên, mình dùng hàm khởi tạo có các tham số đầu vào là StringInt, tương ứng với tên và tuổi. Giờ mình sẽ lấy ra giá trị các thuộc tính của thằng me mà mình vừa khởi tạo:

print("name = ${me.name}, age = ${me.age}") // In ra: name = Dương Vũ, age = 25

Cách truy xuất đến thuộc tính của đối tượng rất đơn giản dễ dàng đúng ko. Ta sử dụng cú pháp:

<tên_đối_tượng>.<tên_thuộc_tính>

để truy xuất đến thuộc tính của đối tượng me.

Định nghĩa phương thức cho lớp

Từ đầu đến giờ ta đã định nghĩa class Human, có các thuộc tính của riêng nó, nhưng chưa định nghĩa các hành động cho nó thực hiện. Ta sẽ viết định nghĩa 1 phương thức nằm trong class Human như thế này:

fun introduce() {
    print("Hello, I am $name, I am $age years old")
}

Như vậy việc định nghĩa phương thức trong class đơn giản chỉ là định nghĩa hàm để xử lý các thuộc tính trong class đó. Như ở trên ta đã xây dựng 1 phương thức cho phép Human giới thiệu về bản thân mình. Phương thức này sẽ log ra tên, tuổi của đối tượng gọi phương thức:

var me: Human = Human("Dương Vũ", 25)
me.introduce() // in ra: Hello, I am Dương Vũ, I am 25 years old

Như vậy là phương thức introduce đã hoạt động. Ta thấy, khi ở bên trong thân class, thì truy xuất đến các thuộc tính đơn giản là gọi thẳng tên thuộc tính đó ra: $name, $age. Còn ở bên ngoài class ta cần sử dụng cú pháp <tên_đối_tượng>.<tên thuộc tính>. Mình sẽ có 1 bài viết riêng về các quy tắc truy xuất đến thuộc tính và phương thức của 1 lớp để các bạn hiểu rõ hơn. Còn giờ thì ta cứ biết cơ bản là vậy đã, ok 😉

Ok, giờ ta sẽ tạo 1 phương thức khác cho quen tay. Thêm hàm sau vào lớp Human:

fun greeting(){
   print("From $name with love")
}

Ta sẽ gọi hàm greeting này:

var harry = Human("Harry", 15) // Khởi tạo đối tượng harry
harry.greeting() //In ra: From Harry with love

Block init

Trong Kotlin class, ta có thể định nghĩa ra 1 block init, bao gồm các câu lệnh được thực hiện ngay sau khi đối tượng của lớp đó được khởi tạo. Ví dụ nhé, ta thêm khối lệnh sau vào lớp Human:

init {
   println("$name has been born")
}

Block init này thực hiện việc log ra dòng thông báo ngay sau khi mỗi đối tượng của class Human được khởi tạo. Thử khởi tạo 1 đối tượng mới:

var peter = Human("Peter", 15) // In ra: Peter has been born
peter.introduce() // in ra: Hello, I am Peter, I am 15 years old

Như ta thấy các câu lệnh trong block init đã được gọi ngay sau khi đối tượng peter được tạo ra. Và tất nhiên là trước khi phương thức introduce được thực hiện.

Từ khóa this

Trong 1 lớp , từ khoá this trỏ tới đối tượng hiện tại của lớp đó. Nghe hơi trúc trắc nhỉ, mình sẽ demo ngay để các bạn dễ hình dung. Ta viết 1 phương thức như sau:

fun increaseAge() {
   this.age += 1
}

Phương thức này tăng giá trị của thuộc tính age lên 1 đơn vị. Ở đây, từ khoá this trỏ đến đối tượng hiện tại, tức là đối tượng đang gọi phương thức increaseAge này, và thay đổi thuộc tính age của đối tượng đó. Ta thử gọi phương thức:

var human = Human("Nam", 20)
human.increaseAge() 
human.introduce() //In ra: Hello, I am Nam, I am 21 years old

Secondary constructor

Một lớp có thể có nhiều hàm khởi tạo khác nhau. Secondary constructor là các hàm khởi tạo khác với hàm khởi tạo chính (primary constructor). Mình sẽ lấy thêm 1 ví dụ khác cho các bạn hiểu rõ hơn nữa nhé. Ta sẽ tạo 1 lớp Animal. Đầu tiên, khai báo lớp và hàm khởi tạo:

class Animal(var kind:String, var name: String){

}

Animal là lớp đặc trưng cho động vật, có các thuộc tính là kind (loài) name (tên)Giờ mình sẽ thêm 1 thuộc tính nữa:

var age:Int?=null

Thuộc tính age có kiểu Int. Thuộc tính này có thể có giá trị hoặc null. Nếu bạn còn chưa rõ các khái niệm về nullable type, hãy đọc lại bài hướng dẫn về Nullable type – kiểu dữ liệu có thể null của mình nhé.

Thêm 1 phương thức:

fun getInfo() {
   print("Hello, i am a $kind, my name is $name")
   age?.let {
       print(", i am $age years old")
   }
}

Đơn giản là log ra câu chào ứng với các giá trị của kind name. Nếu age được gán giá trị (khác null) thì log thêm cả giá trị của age. Toàn bộ lớp Animal của ta sẽ như thế này:

class Animal(var kind: String, var name: String) {
    
    var age: Int? = null
    
    fun getInfo() {
        print("Hello, i am a $kind, my name is $name")
        age?.let {
            print(", i am $age years old")
        }
    }
}

Thử khởi tạo 1 đối tượng, và gọi phương thức getInfo:

var cat = Animal("cat", "Miu Miu")
cat.age = 1
cat.getInfo() //In ra: Hello, i am a cat, my name is Miu Miu, i am 1 years old

Tạo 1 đối tượng Animal khác:

var dog = Animal("dog", "Milu")
dog.getInfo() // In ra: Hello, i am a dog, my name is Milu

Lần này, dòng log ko có thông báo age vì age đang có giá trị nullCác bạn có thể thấy rằng mỗi khi khởi tạo 1 đối tượng Animal, ngoài kind name là các thuộc tính bắt buộc phải có trong hàm khởi tạo. Thì thuộc tính age là không bắt buộc. Nếu ta quên không set giá trị cho age thì đương nhiên nó sẽ null. Giờ ta muốn đưa age vào 1 hàm khởi tạo khác mà vẫn muốn giữ hàm khởi tạo ta định nghĩa lúc đầu, phải làm thế nào. Ta sẽ định nghĩa thêm 1 hàm khởi tạo nữa, tất cả (những) hàm khởi tạo được định nghĩa thêm được gọi là Secondary constructor:

constructor ( kind: String, name: String,age: Int) : this(kind, name) {
   this.age = age
}

Ở đây có 2 điều cần nói:

  • Secondary constructor phải gọi đến primary constructor: this(kind, name) hoặc gọi đến Secondary constructor nào có gọi primary constructor. Tóm lại bằng mọi giá phải tham chiếu đến primary constructor.
  • Secondary constructor không cho phép định nghĩa các thuộc tính, tức là ta không thể sử dụng các từ khóa var, val khi định nghĩa Secondary constructor.

Ta thử khởi tạo 1 phương thức dùng hàm Secondary constructor mới được viết xem sao:

var bird  = Animal("bird", "Kiki", 3)
bird.getInfo() // In ra: Hello, i am a bird, my name is Kiki, i am 3 years old

Ta sẽ thêm 1 thuộc tính nữa:

var favorite: String? = null

Và thêm 1 hàm khởi tạo nữa:

constructor(kind: String, name: String, age: Int, favorite: String) : this(kind, name, age) {
   this.favorite = favorite
}

Hàm khởi tạo này gọi đến hàm khởi tạo thứ 2 ta định nghĩa ở trên, do hàm khởi tạo thứ 2 đã tham chiếu đến primary constructor, nên hàm khởi tạo thứ 3 này hoàn toàn hợp lệ. Ok, hi vọng các bạn đã hiểu được về secondary constructor.

Getter và setter

Khác với java, mỗi thuộc tính trong các Kotlin class đều được khởi tạo 1 hàm getter và 1 hàm setter mặc định. Hàm getter của 1 thuộc tính được gọi đến khi ta truy xuất đến giá trị của thuộc tính đó. Hàm setter thì được gọi khi ta thay đổi giá trị của thuộc tính. Để ví dụ mình sẽ định nghĩa thêm 1 thuộc tính trong lớp Animal trên và viết getter setter cho nó. Các bạn hãy nhớ kỹ rằng, nếu ta ko viết getter setter cho 1 thuộc tính thì thuộc tính đó sẽ có các hàm getter setter mặc định, các hàm đó đơn giản chỉ lấy và gán giá trị cho thuộc tính của chúng ta.

var isSafe: Boolean
   get() {
       println("Getter of property isSafe called")
       if (kind.equals("tiger", true) || kind.equals("puma", true)) {
           return false
       } else {
           return true
       }
   }
   set(value) {
       println("Setter of property isSafe called")
   }

Giờ thử tạo 1 đối tượng và lấy giá trị của thuộc tính isSafe:

var dog = Animal("dog", "Lili")
println("${dog.isSafe}") 

Kết quả:

Getter of property isSafe called
true

Như vậy khi lấy giá trị của isSafe thì hàm getter ứng với thuộc tính isSafe đã được gọi. Ta sẽ thử thay đổi giá trị của isSafe xem sao:

dog.isSafe = false // In ra: Setter of property isSafe called

Vậy khi thay đổi giá trị của isSafe thì hàm setter đã được gọi.

Lưu ý: Hàm getter và setter thường được viết lại cho các thuộc tính mà giá trị của nó phụ thuộc vào gía trị của các thuộc tính khác. Như ví dụ trên, giá trị của thuộc tính isSafe phụ thuộc vào gía trị của kind. Nếu trong hàm getter hay setter mà các bạn truy xuất đến thuộc tính tương ứng của nó thì sẽ xảy ra lỗi. Mình ví dụ như sau, ta sẽ thay đổi lại nội dung hàm getter ở trên:

get() {
   return isSafe
}

Hàm getter của isSafe trả về đúng giá trị của isSafe nghe hợp lý đúng ko. Thử xem:

var dog = Animal("dog", "Lili")
println("${dog.isSafe}")

Kết quả là dòng log đỏ chói : Exception in thread “main” java.lang.StackOverflowError 😀 . Lý do vì bản thân hàm getter ở trên đã return về giá trị của isSafe, mà mỗi khi truy xuất đến isSafe thì hàm getter lại được gọi, điều đó tạo ra 1 vòng lặp không có điểm dừng, đây chính là nguyên nhân exception java.lang.StackOverflowError bị văng ra. Vì vậy, hãy luôn nhớ, chỉ viết getter setter cho các thuộc tính mà giá trị của nó phụ thuộc vào các thuộc tính khác. Còn nếu không, setter getter đã được mặc định viết cho chúng ta rồi, chúng ta ko nên chọc vào chúng nữa.

Kotlin Bài 6: Sử dụng hàm trong Kotlin (nâng cao)

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.

[toc]

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 functionKotlin 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:

Hàm upperFirstLetter hiển thị trong danh sách các hàm gợi ý của String

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é.

Kotlin Bài 4: Các cấu trúc vòng lặp và điều kiện trong Kotlin

Xin chào, Mình là Dương Vũ, ở bài viết trước mình đã nói về cách truy xuất biến một cách an toàn. Bài này mình sẽ trình bày về các cấu trúc vòng lặp và điều kiện trong Kotlin. Vòng lặp và điều kiện là những tính năng không thể thiếu đối với 1 ngôn ngữ lập trình hiện đại. Và Kotlin còn làm cho chúng trở nên vô cùng thú vị. Cùng tìm hiểu nhé.

[toc]

Các cấu trúc điều kiện

Cấu trúc if và if else

Cấu trúc if kiểm tra tính thoả mãn của 1 điều kiện và thực hiện một (chuỗi) hành động nếu điều kiện đó thoả mãn. Nếu bạn đã từng làm việc với java, bạn sẽ thấy cấu trúc ifif else của Kotlin sử dụng giống hệt như Java. Đây là 1 ví dụ với việc sử dụng if:

val age = 20
if(age>18){
   print("You are adult")
}

bởi điều kiện của cấu trúc if (age>18)  thoả mãn, Màn hình sẽ hiển thị dòng log You are adult. 1 ví dụ với if – else:

val a = 10
val b = 20
if(a>b){
   print("$a > $b")
}else if(a == b){
   print("$a = $b")
}else{
   print("$a < $b")
}

Màn hình sẽ hiển thị ra dòng log 10<20. 

Tuy nhiên ngoài cách sử dụng truyền thống ra, ta có thể dùng cấu trúc if như 1 biểu thức, tức là biểu thức if cũng trả về giá trị, sau đây là ví dụ:

val firstNumber = 10
val secondNumber = 25
var max = if(firstNumber>secondNumber){
   println("$firstNumber is max")
   firstNumber
}else{
   println("$secondNumber is max")
   secondNumber
}
println("Max = $max")

Ta thấy trên đây là bài toán tìm max của 2 số rất đơn giản. Cái hay là trong Kotlin, cấu trúc if có thể return về giá trị và giá trị đó có thể được gán cho 1 biến. Như ở ví dụ trên nếu firstNumber > secondNumber thì cấu trúc if sẽ return về giá trị firstNumber, ngược lại sẽ return về giá trị secondNumber. Chạy đoạn code trên ta sẽ thu được

25 is max
Max = 25

 

Lưu ý: nếu bạn sử dụng if dưới dạng 1 biểu thức, thì cấu trúc if bắt buộc phải có else.

Cấu trúc when

Cấu trúc when tương tự như switch – case của Java hay C. Đây cũng là 1 cấu trúc điều kiện mình rất hay sử dụng. Nó có dạng như thế này

val age = 19
when (age){
   in 1..17 -> print("Child")
   in 18..40 -> print("Adult")
   else -> print("Old")
}

Cấu trúc when kiểm tra tham số đầu vào (ở trường hợp trên là number) với các điều kiện trong thân cấu trúc. Khi bắt gặp điều kiện đúng, các câu lệnh tương ứng với điều kiện đó sẽ được thực hiện và các điều kiện khác sẽ không được xét đến nữa.

Nếu như trong java, các nhánh điều kiện trong cấu trúc switch – case chỉ có thể là hằng số, thì cấu trúc when trong Kotlin tỏ ra mềm dẻo hơn với việc cho phép các điều kiện có thể là các biểu thức, hoặc hàm. Ví dụ:

var numberOne = 19
var numberTwo = 21

when(numberOne){
   numberTwo/2 -> println("$numberOne = $numberTwo /2")
   else -> print("$numberOne != $numberTwo /2")
}

Ví dụ trên kiểm tra xem numberOne có phải là 1 nửa của numberTwo hay không. Ví dụ có vẻ ngu ngốc nhưng mô tả rõ ràng việc sử dụng biến trong các nhánh điều kiện của when :)) Khi chạy đoạn code trên ta thu đc dòng log: 19 != 21 /2.

Nếu ta thay giá trị của  numberTwo = 38 thì ta sẽ thấy log 19 = 38 /2.

Vậy ta có cấu trúc tổng quát của when là thế này

when(tên_biến){
   điều_kiện_1 -> { chuỗi lệnh xử lý 1  }
   điều_kiện_2 -> { chuỗi lệnh xử lý 2  }
   ……
   else -> { chuỗi lệnh xử lý ngoại lệ }
}

Lưu ý: nếu chỉ có 1 dòng lệnh thì ta có thể không dùng cặp { }

Vòng lặp

Vòng lặp được sinh ra để thực hiện lặp đi lặp lại 1 công việc nào đó. Kotlin cung cấp cho ta 3 dạng vòng lặp là while, do … while và for. Mình sẽ tìm hiểu từng loại 1 nhé.

Vòng lặp while

Đây là 1 dạng vòng lặp có điều kiện. Vòng lặp sẽ chạy chừng nào điều kiện đó vẫn còn đang đúng. Nếu ai đã code Java thì sẽ thấy vòng lặp while trong Kotlin không khác gì Java cả. Ví dụ:

var age = 1
while (age<100){
   println("Now i am $age years old")
   age++
}
print("i die T_T")

Trên đây là ví dụ về vòng đời của 1 người. Vòng lặp sẽ chạy chừng nào điều kiện age<100 còn thỏa mãn. Và các bạn thấy khi run đoạn lệnh trên console sẽ hiển thị:

Now i am 2 years old
Now i am 3 years old
…
Now i am 100 years old
i die T_T

Vòng lặp do … while

var age = 1
do {
   println("Now i am $age years old")
   age++
} while (age <= 100)
print("i die T_T")

Đây là cách viết khác của ví dụ vòng đời trên sử dụng vòng lặp do … while. Vẫn là sống từ 1 đến 100 tuổi rồi chết. Vòng lặp do … while thực tế cũng gần giống như vòng lặp while. Cũng là dạng vòng lặp có điều kiện. Chỉ khác 1 điều là, ở vòng chạy đầu tiên thì điều kiện sẽ không được xem xét. Có nghĩa là với vòng lặp do … while, sẽ có ít nhất 1 vòng được thực thi. Để thấy rõ, bạn hãy thử đặt giá trị khởi tạo của biến age là 101. Khi đó dù age không thoả mãn điều kiện (age <= 100) nhưng vẫn sẽ có 1 câu lệnh được in ra: Now i am 101 years old.

Vòng lặp for

Vòng lặp for duyệt qua tất cả các phần tử trong 1 mảng (array) hoặc danh sách (list) và xử lý giá trị ứng với mỗi phần tử. Ví dụ:

for (i in 1..5) {
    print("$i ") // in ra  1 2 3 4 5
}

Ok, giờ ta muốn mỗi vòng lặp nhảy 2 đơn vị thì sao? Ta sẽ làm thế này:

for (i in 1..5 step 2) {
    print("$i ") // in ra  1 3 5
}

Đơn giản là thêm từ khoá step và cuối điều kiện lặp.

Thế nếu ta muốn duyệt từ 5 trở về 1 thì sao? Ta có từ khoá downto giúp ta làm việc đó:

for (i in 5 downTo 1) {
   print("$i ") //in ra 5 4 3 2 1
}

Và duyệt từ 5 trở về 1, mỗi vòng lặp nhảy 2 đơn vị:

for (i in 5 downTo 1 step 2) {
   print("$i ") //in ra 5 3 1
}

Sử dụng vòng for để duyệt qua các phần tử của 1 mảng (Array):

val listOfNumber = listOf<Int>(1, 2, 3, 4, 5, 6)
for (number in listOfNumber){
   if(number%2==0){
       println("$number is even number")
   }else{
       println("$number is odd number")
   }
}

Ví dụ trên khởi tạo 1 list các số nguyên và duyệt qua list đó, in ra console tính chẵn lẻ của từng phần tử. Đơn giản đến mức ko thể giải thích rõ hơn, đúng ko? Thêm 1 ví dụ nữa nhé:

val listName = arrayOf("Neymar", "Vidal", "Rooney", "Ronaldo", "Messi", "Bale")
for (name in listName) {
   if (name.startsWith("R")) {
       println(name)
   }
}

Ví dụ trên khởi tạo 1 mảng các tên cầu thủ, sau đó sử dụng vòng lặp để duyệt qua tất cả các phần tử trong mảng đó, phần tử nào bắt đầu bằng ký tự R thì sẽ log ra màn hình. Kết quả:

Rooney
Ronaldo

Ngoài cách trên ra ta cũng có thể duyệt mảng bằng cách khác, đó là duyệt qua chỉ số:

val listNumber = listOf<Int>(1, 3, 2, -1, 4, 6, 7)
var sum = 0
for (index in 0..listNumber.size - 1) {
   sum += listNumber.get(index)
}
print("Summary of list: $sum")

Đây là bài toán đơn giản tính tổng 1 mảng. Nhưng ở ví dụ này ta duyệt mảng thông qua chỉ số (thứ tự) chứ ko duyệt trực tiếp qua các phần tử của listNumber. Bản chất của việc này là ta tạo ra 1 list chỉ số: 0..listNumber.size – 1 gồm các giá trị từ 0 đến listNumber.size – 1. Sau đó ta duyệt list này để lấy các chỉ số (index) của  listNumber và truy xuất các phần tử của listNumber thông qua index, phục vụ cho việc tính tổng.

Break và continue

Lưu ý: breakcontinue trong Kotlin giống hệt như trong java, anh em nào biết rồi thì có thể bỏ qua không đọc :))

breakcontinue là 2 từ khóa để tạo ra các bước nhảy trong vòng lặp. Cụ thể:

break dùng để thoát ra khỏi vòng lặp.

continue dùng để lập tức chuyển tới vòng lặp tiếp theo, bỏ qua các câu lệnh còn lại của vòng lặp hiện tại.

breakcontinue có thể dùng được trong mọi thể loại vòng lặp, dù có là for hay while hay là do – while vẫn ok tất. Ví dụ về break

for(age in 1..100){
   println("Now i am $age years old")
   if(age == 25){
       println("i see girl when ride motorbike, then i hug a big tree in the road")
       break
   }
}
print("i die T_T")

Vẫn là ví dụ về vòng đời, nhưng ở đây có chút khác biệt. Khi age có giá trị là 25, thanh niên console đi xe máy ra ngoài đường và ngắm gái, kết quả là đâm vào cây và hẹo. Thế nên ta dùng lệnh break để thoát khỏi vòng lặp for(age in 1..100)  dù age mới có giá trị là 25. Kết quả hiện ra trên màn hình

Now i am 1 years old
Now i am 2 years old
…..
Now i am 25 years old
i see girl when ride motorbike, then i hug a big tree in the road
i die T_T

Ví dụ về continue:

var level = 1
while (level <= 10) {
   if (level == 3) {
       level += 6
       println("I have a special gift code, my level up!!!")
       continue
   }
   println("My level is $level")
   level++

}

Đây là 1 ví dụ về 1 anh cày game, ở mỗi level của anh ấy đều có 1 dòng log println(“My level is $level”), tuy nhiên khi đang ở level 3 thì anh ấy nhận được gift code khủng nên nhảy 1 phát lên level 9: level += 6 và bỏ qua dòng log println(“My level is $level”) ở vòng lặp khi level = 3. Điều này có được nhờ câu lệnh continue, nó sẽ bỏ qua tất cả những câu lệnh ở phía sau nó (trong vòng lặp) và lập tức chuyển sang vòng lặp tiếp theo.

Ví dụ khác:

for (number in 1..10) {
   if (number % 2 == 0) {
       continue
   }
   print("Number = $number")
}

Tổng kết

Như vậy, cơ bản mình đã trình bày xong về Các cấu trúc vòng lặp và điều kiện trong Kotlin.

Đến đây ta đã có những trang bị kiến thức:

  • Các kiểu dữ liệu cơ bản
  • Các kiểu dữ liệu mảng, danh sách
  • Kiểu dữ liệu nullable, các cách sử dụng biến an toàn (tránh NullPoiterException)
  • Cấu trúc điều kiện if, if-else, when
  • Các dạng vòng lặp for, while
  • Các cách ngắt vòng lặp sử dụng break, continue

Đây là các thành phần cơ bản nhất mà ngôn ngữ nào cũng phải cung cấp. Đối với ai đã từng làm với Java chắc ko có khó khăn gì. Bài tiếp theo sẽ bắt đầu trình bày về các kiến thức liên quan đến cách sử dụng hàm trong Kotlin. Và những sự ưu việt của Kotlin so với Java (tất nhiên là về cú pháp). Rất mong các bạn sẽ ủng hộ mình.