Cùng với các collection, thư viện chuẩn Kotlin chứa một loại vùng chứa khác – chuỗi ( Sequence<T>). Các chuỗi cung cấp các chức năng tương tự như Iterable interface nhưng thực hiện một cách tiếp cận khác để xử lý collection nhiều bước.

Khi quá trình xử lý một Iterable interface bao gồm nhiều bước, chúng được thực hiện một cách hiệu quả: mỗi bước xử lý hoàn thành và trả về kết quả của nó – một collection trung gian. Bước sau sẽ thực hiện trên collection này. Đổi lại, quá trình xử lý nhiều bước của chuỗi cũng có thể được thực hiện một cách chậm chạp: tính toán thực tế chỉ có thể được thực hiện khi kết quả của toàn bộ chuỗi xử lý được yêu cầu.

Thứ tự thực hiện các hoạt động cũng khác nhau: chuỗi thực hiện tất cả các bước xử lý cho lần lượt mỗi phần tử. Trong khi đó, Iterable hoàn thành từng bước cho toàn bộ collection và sau đó chuyển sang bước tiếp theo.

Vì vậy, các chuỗi giúp bạn tránh xây dựng kết quả của các bước trung gian, do đó cải thiện hiệu suất của toàn bộ chuỗi xử lý collection. Tuy nhiên, tính ỳ của các chuỗi sẽ khiến cho tổng thời gian có thể tăng đáng kể khi xử lý các collection nhỏ hơn hoặc thực hiện các phép tính đơn giản hơn. Do đó, bạn nên xem xét cả chuỗi và Iterable để quyết định cái nào là tốt hơn cho mỗi trường hợp cụ thể mà bạn gặp phải.

1. Tạo chuỗi 

1.1. Từ các phần tử

Để tạo một chuỗi, hãy gọi hàm sequenceOf() liệt kê các phần tử làm đối số của nó.

val numbersSequence = sequenceOf("four", "three", "two", "one")

1.2. Từ Iterable

Nếu bạn đã có một đối tượng Iterable (chẳng hạn như List hoặc Set), bạn có thể tạo một chuỗi từ nó bằng cách gọi hàm asSequence().

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()

1.3. Từ hàm

Một cách nữa để tạo một chuỗi là xây dựng nó bằng một hàm tính toán các phần tử của nó. Để xây dựng một chuỗi dựa trên một hàm, hãy gọi hàm generateSequence() này làm đối số. Bạn có thể tùy ý chỉ định phần tử đầu tiên dưới dạng giá trị rõ ràng hoặc dưới dạng kết quả của một lệnh gọi hàm. Quá trình tạo chuỗi dừng lại khi hàm được cung cấp trả về null. Vì vậy, chuỗi trong ví dụ dưới đây là vô hạn.

val oddNumbers = generateSequence(1) { it + 2 } // `it` is the previous element
println(oddNumbers.take(5).toList())
//println(oddNumbers.count())     // error: the sequence is infinite

Để tạo một chuỗi hữu hạn với createSequence(), hãy cung cấp một hàm trả về null sau phần tử cuối cùng bạn cần.

val oddNumbersLessThan10 = generateSequence(1) { if (it + 2 < 10) it + 2 else null }
println(oddNumbersLessThan10.count())

1.4. Từ khối

Cuối cùng, có một hàm cho phép bạn tạo ra từng phần tử chuỗi một hoặc từng phần có kích thước tùy ý – đó là hàm sequence(). Hàm này nhận một biểu thức lambda chứa các lệnh gọi hàm yield()yieldAll(). Chúng trả lại một phần tử cho người tiêu dùng chuỗi và tạm dừng việc thực hiện hàm sequence() cho đến khi phần tử tiếp theo được người tiêu dùng yêu cầu. Hàm yield()lấy một phần tử duy nhất làm đối số; hàm yieldAll()có thể lấy một đối tượng Iterable, một trình lặp hoặc một đối tượng khác chuỗi. Một đối số chuỗi của hàm yieldAll()có thể là vô hạn. Tuy nhiên, những lệnh gọi hàm như vậy phải là lệnh gọi cuối cùng: tất cả các lệnh gọi hàm tiếp theo sẽ không bao giờ được thực hiện.

val oddNumbers = sequence {
    yield(1)
    yieldAll(listOf(3, 5))
    yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())

2. Thao tác với chuỗi

Các thao tác với chuỗi có thể được phân loại thành các nhóm sau theo yêu cầu trạng thái của chúng:

  • Các thao tác không trạng thái không yêu cầu trạng thái và xử lý từng phần tử một cách độc lập, ví dụ, hàm map()hoặc filter(). Các thao tác không trạng thái cũng có thể yêu cầu một lượng nhỏ trạng thái không đổi để xử lý một phần tử, chẳng hạn như hàm take()hoặcdrop() .
  • Các thao tác trạng thái yêu cầu một lượng trạng thái đáng kể, thường là tỷ lệ với số phần tử trong một chuỗi.

Nếu một thao tác với chuỗi trả về một chuỗi khác, được tạo ra một cách thụ động, thì thao tác đó được gọi là trung gian . Nếu không, thao tác đó là đầu cuối. Ví dụ về thao tác đầu cuối là hàm toList()hoặc sum(). Các phần tử trong chuỗi có thể được truy xuất chỉ với các hoạt động đầu cuối.

Các chuỗi có thể được lặp lại nhiều lần; tuy nhiên một số triển khai chuỗi có thể hạn chế chúng chỉ được lặp lại một lần. Điều đó được đề cập cụ thể trong tài liệu của chúng.

3. Ví dụ về xử lý chuỗi

Hãy xem sự khác biệt giữa Iterable và chuỗi qua một ví dụ.

3.1. Iterable

Giả sử rằng bạn có một danh sách các từ. Đoạn code dưới đây lọc ra các từ dài hơn ba ký tự và in độ dài của bốn từ đầu tiên đáp ứng tiêu đó (dài hơn 3 kí tự).

val words = "The quick brown fox jumps over the lazy dog".split(" ")
val lengthsList = words.filter { println("filter: $it"); it.length > 3 }
    .map { println("length: ${it.length}"); it.length }
    .take(4)

println("Lengths of first 4 words longer than 3 chars:")
println(lengthsList)

Khi bạn chạy code này, bạn sẽ thấy rằng các hàm filter()và map()được thực thi theo thứ tự giống như chúng xuất hiện trong mã. Đầu tiên, bạn hãy xét filter:ap dụng cho tất cả các phần tử, sau đó length: áp dụng cho các phần tử còn lại sau khi lọc, và sau đó là kết quả của hai dòng cuối cùng. Dưới đây là cách xử lý danh sách từ nêu trên:

3.2. Chuỗi

Bây giờ chúng ta hãy viết code tương tự cho các chuỗi:

val words = "The quick brown fox jumps over the lazy dog".split(" ")
//convert the List to a Sequence
val wordsSequence = words.asSequence()

val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
    .map { println("length: ${it.length}"); it.length }
    .take(4)

println("Lengths of first 4 words longer than 3 chars")
// terminal operation: obtaining the result as a List
println(lengthsSequence.toList())

Kết quả của đoạn code này cho thấy rằng các hàm filter()và map()chỉ được gọi khi xây dựng danh sách kết quả. Vì vậy, trước tiên bạn nhìn thấy dòng văn bản “Lengths of..” và sau đó quá trình xử lý chuỗi mới bắt đầu. Lưu ý rằng đối với các phần tử còn lại sau khi lọc, bản đồ thực thi trước khi lọc phần tử tiếp theo. Khi kích thước kết quả đạt đến 4, quá trình xử lý sẽ dừng lại vì đó là kích thước lớn nhất take(4)thể có thể trả về.

Quá trình xử lý chuỗi diễn ra như sau:

Trong ví dụ này, quá trình xử lý chuỗi cần 18 bước thay vì 23 bước để thực hiện tương tự với cùng 1 danh sách đã cho.

Tài liệu từ cafedev:

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!