Iterable і Sequence Котліна виглядають абсолютно однаково. Чому потрібні два типи?


86

Обидва ці інтерфейси визначають лише один метод

public operator fun iterator(): Iterator<T>

Документація стверджує, що Sequenceвона призначена для ледачого. Але чи не Iterableлінь теж (якщо не підкріплений а Collection)?

Відповіді:


136

Ключова відмінність полягає у семантиці та реалізації функцій розширення stdlib для Iterable<T>та Sequence<T>.

  • Адже Sequence<T>функції розширення виконують ліниво, де це можливо, подібно до проміжних операцій Java Streams . Наприклад, Sequence<T>.map { ... }повертає інший Sequence<R>і фактично не обробляє елементи, доки не буде викликана операція терміналу, подібна toListабо foldвикликана.

    Розглянемо цей код:

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

    Друкується:

    before sum 1 2
    

    Sequence<T>призначений для ледачого використання та ефективного конвеєрингу, коли ви хочете максимально скоротити роботу, виконану в термінальних операціях, так само, як і для Java Streams. Однак лінощі вносять деякі накладні витрати, що небажано для звичайних простих перетворень менших колекцій і робить їх менш ефективними.

    Взагалі, немає хорошого способу визначити, коли це потрібно, тому в Kotlin stdlib лінощі чітко виражаються та витягуються в Sequence<T>інтерфейс, щоб уникнути його використання на всіх Iterables за замовчуванням.

  • Адже Iterable<T>навпаки, функції розширення з проміжною операційною семантикою працюють з працею, обробляють елементи відразу і повертають інші Iterable. Наприклад, Iterable<T>.map { ... }повертає a List<R>із результатами відображення в ньому.

    Еквівалентний код для Iterable:

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

    Це роздруковує:

    1 2 before sum
    

    Як було сказано вище, Iterable<T>за замовчуванням не ледачий, і це рішення добре себе демонструє: у більшості випадків воно має хорошу локалізацію посилань, таким чином використовуючи переваги кеша процесора, прогнозування, попереднього вибору тощо, так що навіть багаторазове копіювання колекції все ще працює добре достатньо і краще працює в простих випадках з невеликими колекціями.

    Якщо вам потрібен більше контролю над конвеєром оцінки, існує явне перетворення в ледачу послідовність з Iterable<T>.asSequence()функцією.


3
Можливо, великий сюрприз для Java(в основному Guava) вболівальників
Венката Раджу

@VenkataRaju для функціональних людей вони можуть бути здивовані альтернативою ледачих за замовчуванням.
Джейсон Мінард,

9
Ледачий за замовчуванням, як правило, є менш продуктивним для менших і частіше використовуваних колекцій. Копія може бути швидшою, ніж ледачий евал, якщо скористатися кешем процесора тощо. Тож для звичайних випадків використання краще не полінуватися. І , на жаль , загальні контракти на такі функції , як map, filterі інші не несуть достатньо інформації , щоб вирішити , крім від типу колекції джерела, і так як більшість колекцій також Iterable, що не є хорошим маркером «лінуватися» , тому що це зазвичай СКРІЗ. лінивий повинен бути явним, щоб бути в безпеці.
Jayson Minard

1
@naki Один із прикладів нещодавнього анонсу Apache Spark, вони, очевидно, турбуються про це, див. розділ "Обчислення з кешем" на databricks.com/blog/2015/04/28/… ... але їх турбують мільярди речі повторюються, тому їм потрібно піти на повну межу.
Джейсон Мінар

3
Окрім того, загальною ловушкою ледачої оцінки є захоплення контексту та зберігання отриманих ледачих обчислень у полі разом із усіма захопленими місцевими жителями та тим, що у них є. Отже, важко налагодити витоки пам'яті.
Ілля Риженков

49

Заповнення відповіді гарячої клавіші:

Важливо зауважити, як послідовність та ітерація повторюються у ваших елементах:

Приклад послідовності:

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

Результат журналу:

фільтр - Карта - Кожен; фільтр - Карта - Кожен

Ітеративний приклад:

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

filter - filter - Карта - Карта - Кожен - Кожен


5
Це чудовий приклад різниці між ними.
Олексій Сошин

Це чудовий приклад.
frye3k

2

Iterableвідображається на java.lang.Iterableінтерфейсі на JVMі реалізується загальновживаними колекціями, такими як List або Set. Функції розширення колекції на них оцінюються з бажанням, що означає, що всі вони негайно обробляють всі елементи, що вводяться, і повертають нову колекцію, що містить результат.

Ось простий приклад використання функцій збору для отримання імен перших п’яти людей у ​​списку, вік яких становить принаймні 21 рік:

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

Цільова платформа: JVMRunning на kotlin v. 1.3.61 По-перше, перевірка віку виконується для кожної окремої Особи у списку, а результат вноситься в абсолютно новий список. Потім відображення їхніх імен виконується для кожної Особи, яка залишилася після оператора фільтра, і потрапляє до чергового нового списку (це тепер a List<String>). Нарешті, є останній новий список, створений, щоб містити перші п’ять елементів попереднього списку.

На відміну від цього, Sequence - це нова концепція в Котліні, яка представляє ліньо оцінену колекцію цінностей. Для Sequenceінтерфейсу доступні ті самі розширення колекції , але вони негайно повертають екземпляри Sequence, які представляють оброблений стан дати, але фактично не обробляючи жодних елементів. Щоб розпочати обробку, термін Sequenceповинен бути завершений за допомогою оператора терміналу, в основному це запит до Послідовності матеріалізувати дані, які вона представляє, у певній формі. Приклади включають в себе toList, toSetі sum, згадати лише деякі з них. Коли вони викликані, буде оброблено лише мінімально необхідну кількість елементів для отримання необхідного результату.

Перетворення існуючої колекції в послідовність досить просто, вам просто потрібно використовувати asSequenceрозширення. Як вже згадувалося вище, вам також потрібно додати оператора терміналу, інакше Послідовність ніколи не буде виконувати жодну обробку (знову ж таки, ліниво!).

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

Цільова платформа: JVMRunning на kotlin v. 1.3.61. У цьому випадку кожен екземпляр Person у послідовності перевіряється на свій вік, якщо вони проходять, їх ім’я витягується, а потім додається до списку результатів. Це повторюється для кожної людини у вихідному списку, поки не знайдеться п’ять людей. На цьому етапі функція toList повертає список, а решта людей у ​​файлі Sequenceне обробляються.

Також є щось додаткове, на що здатна Послідовність: вона може містити нескінченну кількість предметів. З огляду на це, має сенс, що оператори працюють так, як вони працюють - оператор на нескінченній послідовності ніколи не зможе повернутися, якщо він виконує свою роботу з бажанням.

Як приклад, ось послідовність, яка буде генерувати стільки потужностей 2, скільки вимагає оператор терміналу (ігноруючи той факт, що це швидко переповнюється):

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

Ви можете знайти більше тут .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.