Cafedev, nơi mà cộng đồng lập trình viên tìm kiếm sự chia sẻ và tiến bộ, là nguồn động viên không ngừng cho những người đam mê công nghệ. Hôm nay, chúng ta sẽ đào sâu vào một chủ đề đặc biệt: “Kotlin biểu thức Select”. Đây không chỉ là một thử nghiệm với ngôn ngữ lập trình mới mẻ, mà còn là cơ hội để cùng nhau khám phá những khả năng mới và mở rộng kiến thức về Kotlin. Hãy cùng nhau điểm qua và tận hưởng hành trình này trên Cafedev!

Biểu thức Select cho phép đợi đồng thời nhiều hàm treo cùng một lúc và chọn hàm đầu tiên trở nên khả dụng.

Biểu thức Select là một tính năng thử nghiệm của kotlinx.coroutines. API của chúng dự kiến sẽ phát triển trong các bản cập nhật sắp tới của thư viện kotlinx.coroutines có thể có các thay đổi đột ngột.

1. Lựa chọn từ các channel

Giả sử chúng ta có hai người sản xuất chuỗi: fizzbuzz. fizz tạo ra chuỗi “Fizz” mỗi 500 ms:

fun CoroutineScope.fizz() = produce<String> {
    while (true) { // sends "Fizz" every 500 ms
        delay(500)
        send("Fizz")
    }
}


buzz tạo ra chuỗi “Buzz!” mỗi 1000 ms:

fun CoroutineScope.buzz() = produce<String> {
    while (true) { // sends "Buzz!" every 1000 ms
        delay(1000)
        send("Buzz!")
    }
}


Sử dụng hàm treo [receive], chúng ta có thể nhận từ một channel hoặc channel khác. Nhưng biểu thức select cho phép chúng ta nhận từ cả hai đồng thời bằng cách sử dụng các mệnh đề[onReceive][ReceiveChannel.onReceive]:

suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
    select<Unit> { // <Unit> means that this select expression does not produce any result
        fizz.onReceive { value ->  // this is the first select clause
            println("fizz -> '$value'")
        }
        buzz.onReceive { value ->  // this is the second select clause
            println("buzz -> '$value'")
        }
    }
}

Hãy chạy nó bảy lần:

val fizz = fizz()
val buzz = buzz()
repeat(7) {
    selectFizzBuzz(fizz, buzz)
}
coroutineContext.cancelChildren() // cancel fizz & buzz coroutines

Bạn có thể lấy toàn bộ mã nguồn ở đây.


Kết quả của đoạn mã này là:

fizz -> 'Fizz'
buzz -> 'Buzz!'
fizz -> 'Fizz'
fizz -> 'Fizz'
buzz -> 'Buzz!'
fizz -> 'Fizz'
fizz -> 'Fizz'

2. Lựa chọn khi đóng

Mệnh đề [onReceive] trong select thất bại khi channel đã đóng, gây nên ngoại lệ tương ứng cho select đó. Chúng ta có thể sử dụng mệnh đề [onReceiveCatching] để thực hiện một hành động cụ thể khi channel đã đóng. Ví dụ dưới đây cũng cho thấy rằng select là một biểu thức trả về kết quả của mệnh đề đã chọn:

suspend fun selectAorB(a: ReceiveChannel<String>, b: ReceiveChannel<String>): String =
    select<String> {
        a.onReceiveCatching { it ->
            val value = it.getOrNull()
            if (value != null) {
                "a -> '$value'"
            } else {
                "Channel 'a' is closed"
            }
        }
        b.onReceiveCatching { it ->
            val value = it.getOrNull()
            if (value != null) {
                "b -> '$value'"
            } else {
                "Channel 'b' is closed"
            }
        }
    }


Hãy sử dụng nó với channel a tạo ra chuỗi “Hello” bốn lần và channel b tạo ra chuỗi “World” bốn lần:

val a = produce<String> {
    repeat(4) { send("Hello $it") }
}
val b = produce<String> {
    repeat(4) { send("World $it") }
}
repeat(8) { // print first eight results
    println(selectAorB(a, b))
}
coroutineContext.cancelChildren()  

Bạn có thể lấy toàn bộ mã nguồn ở đây.


Kết quả của đoạn mã này khá thú vị, nên chúng ta sẽ phân tích nó chi tiết hơn.

a -> 'Hello 0'
a -> 'Hello 1'
b -> 'World 0'
a -> 'Hello 2'
a -> 'Hello 3'
b -> 'World 1'
Channel 'a' is closed
Channel 'a' is closed

Có một số quan sát cần lưu ý từ nó.


Trước hết, select được chủ quan đối với mệnh đề đầu tiên. Khi nhiều mệnh đề có thể chọn cùng một lúc, mệnh đề đầu tiên trong số chúng sẽ được chọn. Ở đây, cả hai channel đều liên tục tạo ra chuỗi, vì vậy kênh a, là mệnh đề đầu tiên trong select, chiến thắng. Tuy nhiên, do chúng ta sử dụng kênh không đệm, nên a bị treo từ thời gian này sang thời gian khác khi gọi hàm [send][SendChannel.send], và tạo cơ hội cho b gửi đi, nếu có.


Quan sát thứ hai là [onReceiveCatching]ngay lập tức được chọn khi channel đã đóng.

3. Chọn để gửi

Biểu thức Select có mệnh đề [onSend] có thể được sử dụng để một mục đích tốt khi kết hợp với tính chủ quan của lựa chọn.
Hãy viết một ví dụ về người sản xuất số nguyên gửi giá trị của mình đến một kênh side khi người tiêu thụ trên kênh chính không thể theo kịp:

fun CoroutineScope.produceNumbers(side: SendChannel<Int>) = produce<Int> {
    for (num in 1..10) { // produce 10 numbers from 1 to 10
        delay(100) // every 100 ms
        select<Unit> {
            onSend(num) {} // Send to the primary channel
            side.onSend(num) {} // or to the side channel
        }
    }
}


Người tiêu thụ sẽ khá chậm, mất 250 ms để xử lý mỗi số:

val side = Channel<Int>() // allocate side channel
launch { // this is a very fast consumer for the side channel
    side.consumeEach { println("Side channel has $it") }
}
produceNumbers(side).consumeEach { 
    println("Consuming $it")
    delay(250) // let us digest the consumed number properly, do not hurry
}
println("Done consuming")
coroutineContext.cancelChildren()  

Bạn có thể lấy toàn bộ mã nguồn ở đây.


Hãy xem điều gì sẽ xảy ra:

Consuming 1
Side channel has 2
Side channel has 3
Consuming 4
Side channel has 5
Side channel has 6
Consuming 7
Side channel has 8
Side channel has 9
Consuming 10
Done consuming

4. Chọn giá trị trì hoãn

Giá trị trì hoãn có thể được chọn bằng cách sử dụng mệnh đề onAwait. Hãy bắt đầu với một hàm async trả về một giá trị chuỗi trì hoãn ngẫu nhiên:

fun CoroutineScope.asyncString(time: Int) = async {
    delay(time.toLong())
    "Waited for $time ms"
}


Hãy bắt đầu mười hàm async này với một độ trễ ngẫu nhiên.

fun CoroutineScope.asyncStringsList(): List<Deferred<String>> {
    val random = Random(3)
    return List(12) { asyncString(random.nextInt(1000)) }
}


Bây giờ, hàm chính đợi cho đến khi cái nào đó hoàn thành và đếm số giá trị trì hoãn vẫn còn hoạt động. Lưu ý rằng chúng ta đã sử dụng ở đây việc biểu thức select là một DSL của Kotlin, vì vậy chúng ta có thể cung cấp các mệnh đề cho nó bằng cách sử dụng mã nguồn tùy ý. Trong trường hợp này, chúng ta lặp qua một danh sách các giá trị trì hoãn để cung cấp mệnh đề onAwait cho mỗi giá trị trì hoãn.

val list = asyncStringsList()
val result = select<String> {
    list.withIndex().forEach { (index, deferred) ->
        deferred.onAwait { answer ->
            "Deferred $index produced answer '$answer'"
        }
    }
}
println(result)
val countActive = list.count { it.isActive }
println("$countActive coroutines are still active")

Bạn có thể lấy toàn bộ mã nguồn ở đây.


Kết quả là:

Deferred 4 produced answer 'Waited for 128 ms'
11 coroutines are still active

5. Chuyển đổi qua một kênh giá trị trì hoãn

Hãy viết một hàm sản xuất kênh tiêu thụ một kênh giá trị chuỗi trì hoãn, đợi cho mỗi giá trị trì hoãn nhận được, nhưng chỉ cho đến khi giá trị trì hoãn tiếp theo đến hoặc kênh đóng. Ví dụ này kết hợp [onReceiveCatching] và onAwait trong cùng một select:

fun CoroutineScope.switchMapDeferreds(input: ReceiveChannel<Deferred<String>>) = produce<String> {
    var current = input.receive() // start with first received deferred value
    while (isActive) { // loop while not cancelled/closed
        val next = select<Deferred<String>?> { // return next deferred value from this select or null
            input.onReceiveCatching { update ->
                update.getOrNull()
            }
            current.onAwait { value ->
                send(value) // send value that current deferred has produced
                input.receiveCatching().getOrNull() // and use the next deferred from the input channel
            }
        }
        if (next == null) {
            println("Channel was closed")
            break // out of loop
        } else {
            current = next
        }
    }
}


Để kiểm tra nó, chúng ta sẽ sử dụng một hàm async đơn giản trả về một chuỗi cụ thể sau một khoảng thời gian nhất định:

fun CoroutineScope.asyncString(str: String, time: Long) = async {
    delay(time)
    str
}


Hàm chính chỉ khởi chạy một coroutine để in kết quả của switchMapDeferreds và gửi một số dữ liệu thử nghiệm đến nó:

val chan = Channel<Deferred<String>>() // the channel for test
launch { // launch printing coroutine
    for (s in switchMapDeferreds(chan)) 
    println(s) // print each received string
}
chan.send(asyncString("BEGIN", 100))
delay(200) // enough time for "BEGIN" to be produced
chan.send(asyncString("Slow", 500))
delay(100) // not enough time to produce slow
chan.send(asyncString("Replace", 100))
delay(500) // give it time before the last one
chan.send(asyncString("END", 500))
delay(1000) // give it time to process
chan.close() // close the channel ... 
delay(500) // and wait some time to let it finish

Bạn có thể lấy toàn bộ mã nguồn ở đây.


Kết quả của đoạn mã này:

BEGIN
Replace
END
Channel was closed

Trên Cafedev,chúng tôi hy vọng rằng bạn đã tìm thấy thông tin về “Kotlin with Select expression (thử nghiệm)” hữu ích và thú vị. Cùng chia sẻ kiến thức và trải nghiệm của bạn với cộng đồng, tạo ra sự tương tác và sự đa dạng trong cách tiếp cận vấn đề. Hãy đồng hành cùng Cafedev,nơi mà sự sáng tạo và học hỏi không ngừng, để chúng ta cùng phát triển và khám phá thêm về thế giới mã nguồn mở và công nghệ. Cảm ơn bạn đã là một phần của Cafedev,nơi tình yêu với lập trình trở nên sống động.”

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của Cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!