Lặp lại và Trình tự của Kotlin trông giống hệt nhau. Tại sao phải có hai loại?


86

Cả hai giao diện này chỉ xác định một phương thức

public operator fun iterator(): Iterator<T>

Tài liệu nói Sequencecó nghĩa là lười biếng. Nhưng không phải là Iterablelười biếng (trừ khi được hỗ trợ bởi a Collection)?

Câu trả lời:


136

Sự khác biệt chính nằm ở ngữ nghĩa và việc triển khai các hàm mở rộng stdlib cho Iterable<T>Sequence<T>.

  • Đối với Sequence<T>, các chức năng mở rộng thực hiện một cách lười biếng nếu có thể, tương tự như các hoạt động trung gian của Java Streams . Ví dụ, Sequence<T>.map { ... }trả về một giá trị khác Sequence<R>và không thực sự xử lý các mục cho đến khi một hoạt động đầu cuối như toListhoặc foldđược gọi.

    Hãy xem xét mã này:

    val seq = sequenceOf(1, 2)
    val seqMapped: Sequence<Int> = seq.map { print("$it "); it * it } // intermediate
    print("before sum ")
    val sum = seqMapped.sum() // terminal
    

    Nó in:

    before sum 1 2
    

    Sequence<T>nhằm mục đích sử dụng lười biếng và tổng hợp hiệu quả khi bạn muốn giảm công việc thực hiện trong các hoạt động đầu cuối nhiều nhất có thể, giống như Java Streams. Tuy nhiên, sự lười biếng tạo ra một số chi phí, điều không mong muốn đối với các phép biến đổi đơn giản phổ biến của các bộ sưu tập nhỏ hơn và làm cho chúng kém hiệu quả hơn.

    Nói chung, không có cách nào tốt để xác định thời điểm cần thiết, vì vậy trong Kotlin, stdlib lười biếng được thực hiện rõ ràng và trích xuất vào Sequence<T>giao diện để tránh sử dụng nó trên tất cả các Iterables theo mặc định.

  • Đối với Iterable<T>, ngược lại, các chức năng mở rộng với trung ngữ nghĩa hoạt động làm việc hăng hái, chế biến các mặt hàng ngay lập tức và trở lại khác Iterable. Ví dụ, Iterable<T>.map { ... }trả về a List<R>với kết quả ánh xạ trong đó.

    Mã tương đương cho có thể lặp lại:

    val lst = listOf(1, 2)
    val lstMapped: List<Int> = lst.map { print("$it "); it * it }
    print("before sum ")
    val sum = lstMapped.sum()
    

    Điều này in ra:

    1 2 before sum
    

    Như đã nói ở trên, Iterable<T>theo mặc định là không lười biếng và giải pháp này cho thấy bản thân nó tốt: trong hầu hết các trường hợp, nó có vị trí tham chiếu tốt, do đó tận dụng bộ nhớ cache của CPU, dự đoán, tìm nạp trước, v.v. để thậm chí sao chép nhiều bộ sưu tập vẫn hoạt động tốt đủ và hoạt động tốt hơn trong các trường hợp đơn giản với các bộ sưu tập nhỏ.

    Nếu bạn cần kiểm soát nhiều hơn đối với quy trình đánh giá, có một chuyển đổi rõ ràng thành chuỗi lười biếng có Iterable<T>.asSequence()chức năng.


3
Có lẽ là một bất ngờ lớn đối với Java(hầu hết Guava) người hâm mộ
Venkata Raju

@VenkataRaju đối với những người có chức năng, họ có thể ngạc nhiên về sự thay thế của lười biếng theo mặc định.
Jayson Minard

9
Theo mặc định, Lazy thường ít hoạt động hơn đối với các bộ sưu tập nhỏ hơn và được sử dụng phổ biến hơn. Một bản sao có thể nhanh hơn một bản đánh giá lười biếng nếu tận dụng bộ nhớ cache của CPU, v.v. Vì vậy, đối với các trường hợp sử dụng thông thường tốt hơn là đừng lười biếng. Và thật không may hợp đồng chung cho các chức năng thích map, filtervà những người khác không mang đủ thông tin để quyết định khác hơn là từ các loại bộ sưu tập nguồn, và vì hầu hết các bộ sưu tập cũng là Iterable, đó không phải là một dấu hiệu tốt cho "lười biếng" vì nó là thường là MỌI NƠI. lười biếng phải được rõ ràng để được an toàn.
Jayson Minard

1
@naki Một ví dụ từ thông báo Apache Spark gần đây, họ đang lo lắng về điều này rõ ràng, hãy xem phần "Tính toán nhận biết bộ nhớ cache" tại databricks.com/blog/2015/04/28/… ... nhưng họ lo lắng về hàng tỷ mọi thứ lặp đi lặp lại vì vậy chúng cần phải đi đến cực điểm đầy đủ.
Jayson Minard

3
Ngoài ra, một cạm bẫy phổ biến với đánh giá lười biếng là nắm bắt bối cảnh và lưu trữ kết quả tính toán lười biếng trong một trường cùng với tất cả những người dân địa phương bị bắt và bất cứ thứ gì họ nắm giữ. Do đó, rất khó để gỡ lỗi bộ nhớ bị rò rỉ.
Ilya Ryzhenkov

49

Hoàn thành câu trả lời của phím nóng:

Điều quan trọng cần lưu ý là cách Sequence và Iterable lặp lại trong các phần tử của bạn:

Ví dụ về trình tự:

list.asSequence().filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

Kết quả ghi:

bộ lọc - Bản đồ - Mỗi; bộ lọc - Bản đồ - Mỗi

Ví dụ có thể lặp lại:

list.filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

bộ lọc - bộ lọc - Bản đồ - Bản đồ - Mỗi - Mỗi


5
Đó là một ví dụ tuyệt vời về sự khác biệt giữa hai điều này.
Alexey Soshin

Đây là một ví dụ tuyệt vời.
Friede3k

2

Iterableđược ánh xạ tới java.lang.Iterablegiao diện trên JVMvà được triển khai bởi các bộ sưu tập thường dùng, như Danh sách hoặc Tập hợp. Các hàm mở rộng bộ sưu tập trên các hàm này được đánh giá một cách háo hức, có nghĩa là tất cả chúng đều xử lý ngay lập tức tất cả các phần tử trong đầu vào của chúng và trả về một tập hợp mới chứa kết quả.

Dưới đây là một ví dụ đơn giản về việc sử dụng các hàm tập hợp để lấy tên của năm người đầu tiên trong danh sách có tuổi ít nhất là 21:

val people: List<Person> = getPeople()
val allowedEntrance = people
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)

Nền tảng mục tiêu: JVMRunning trên kotlin v. 1.3.61 Đầu tiên, việc kiểm tra độ tuổi được thực hiện cho từng Người trong danh sách, với kết quả được đưa vào một danh sách hoàn toàn mới. Sau đó, ánh xạ đến tên của họ được thực hiện cho mọi Người vẫn ở sau toán tử bộ lọc, kết thúc trong một danh sách mới khác (đây là một List<String>). Cuối cùng, có một danh sách mới cuối cùng được tạo để chứa năm phần tử đầu tiên của danh sách trước đó.

Ngược lại, Trình tự là một khái niệm mới trong Kotlin để đại diện cho một tập hợp các giá trị được đánh giá một cách lười biếng. Các phần mở rộng bộ sưu tập tương tự có sẵn cho Sequencegiao diện, nhưng các phần mở rộng này ngay lập tức trả về các phiên bản Trình tự đại diện cho trạng thái đã xử lý của ngày, nhưng không thực sự xử lý bất kỳ phần tử nào. Để bắt đầu xử lý, Sequencenó phải được kết thúc bằng một toán tử đầu cuối, về cơ bản đây là một yêu cầu đối với Trình tự để hiện thực hóa dữ liệu mà nó đại diện ở một số dạng cụ thể. Ví dụ bao gồm toList, toSetsum, chỉ đề cập đến một số. Khi chúng được gọi, chỉ số phần tử bắt buộc tối thiểu sẽ được xử lý để tạo ra kết quả được yêu cầu.

Việc chuyển đổi một bộ sưu tập hiện có thành một Chuỗi khá đơn giản, bạn chỉ cần sử dụng asSequencetiện ích mở rộng. Như đã đề cập ở trên, bạn cũng cần phải thêm toán tử đầu cuối, nếu không Sequence sẽ không bao giờ thực hiện bất kỳ xử lý nào (một lần nữa, lười biếng!).

val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)
    .toList()

Nền tảng mục tiêu: JVMRunning trên kotlin v. 1.3.61 Trong trường hợp này, từng cá thể Người trong Chuỗi được kiểm tra tuổi của họ, nếu họ vượt qua, họ sẽ được trích xuất tên và sau đó được thêm vào danh sách kết quả. Điều này được lặp lại cho từng người trong danh sách ban đầu cho đến khi có năm người được tìm thấy. Tại thời điểm này, hàm toList trả về một danh sách và những người còn lại trong danh sách Sequencekhông được xử lý.

Ngoài ra còn có một thứ bổ sung mà Sequence có khả năng: nó có thể chứa vô số mục. Với quan điểm này, có nghĩa là các toán tử làm việc theo cách họ làm - một toán tử trên một dãy vô hạn có thể không bao giờ quay lại nếu nó thực hiện công việc của mình một cách háo hức.

Ví dụ: đây là một chuỗi sẽ tạo ra bao nhiêu lũy thừa của 2 theo yêu cầu của toán tử đầu cuối của nó (bỏ qua thực tế là điều này sẽ nhanh chóng tràn):

generateSequence(1) { n -> n * 2 }
    .take(20)
    .forEach(::println)

Bạn có thể tìm thêm ở đây .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.