В обычном JavaScript для выполнения фильтра по списку и последующего протоколирования каждого элемента требуется O(2n). В Rust требуется O(n). Вот почему и другие интересные особенности итераторов Rust.

Мы часто слышим, что итераторы Rust ленивы, но они никогда не по-настоящему понимали, что это означает, пока мне не пришлось фильтровать список как в Rust, так и в JavaScript (технически TypeScript, но все же).

В JavaScript фильтр списка выглядит так:

myList.filter((t) => t !== "Filter me out")

а затем, чтобы зарегистрировать каждый элемент, вы должны связать его следующим образом:

myList.filter((t) => t !== "Filter me out").forEach(console.log)

За кулисами происходит то, что функция filter выполняет итерацию по списку, возвращая каждый элемент, не соответствующий условию. в этом случае он отфильтрует все строки, в которых нет надписи «Отфильтруйте меня». Затем он будет повторять новый список и регистрировать каждый из них.

Исправить это так, чтобы стало O(n) тривиально:

myList.forEach((t) {if (t!== "Filter me out") console.log(t)})

но это выглядит не так чисто.

В Rust у вас есть O(n) из коробки, используя аналогичный первому способ.

myList
.into_iter()
.filter(|v| v != "Filter me out")
.for_each(|v| println!("{}", v))

Итак, где здесь волшебство?

Итераторы Rust ленивы, что означает, что они не выполняются, пока не получат указание. Это означает, что вы можете добавить к итератору любую серию операций, и он никогда не будет «запускаться», пока вы его не используете. Это означает, что когда мы вызываем for_each на итераторе, он потребляет итератор, но получает только элементы, прошедшие фильтр.

Это полезно, потому что теперь мы можем делать с итератором все, что угодно, функционально.

myList
.into_iter()
.filter(..) //one filter
.filter(..) // two filter
.map(..) //map 
.filter(..) //filter because why not
.map(..) //map again... who's writing this code???
.cloned(..) //for the hell of it, we clone ever element

Приведенный выше пример непрактичен в реальном мире (потому что зачем вам отображать x 2 и фильтровать x 3?), Но это действительный код Rust ... за исключением того, что он ничего не делает.

если бы вы поставили точку с запятой в конце, вы бы ничего не сделали, кроме уничтожения myList (потому что мы использовали into_iter.. Если бы мы использовали iter, мы бы вообще ничего не сделали, точка.). Нам нужно использовать итератор с чем-то, что потребляет итератор.

В этом случае, вероятно, это будет for_each, но вы можете сделать многое. Может быть, вы хотите .sum, или .collect его в другой вектор, или, может быть, .sort? Любая из них использует итератор, и как только он израсходован, он готов.

Этот дизайн очень умный, потому что он позволяет сделать наш код более читабельным. Нам не нужно помещать каждую операцию в одно и то же замыкание, как в JS, и при желании мы могли бы условно добавить фильтры, сохранив итератор перед его выполнением.

Вот список операций, которые вы могли бы использовать со своими итераторами, чтобы сделать код более функциональным и читаемым:

  • .take(n) сокращает итератор до первых n элементов
  • .skip(n) пропускает первые n элементы
  • .cloned() клонирует каждый элемент итератора
  • .enumerate превращает итератор по элементам t в итератор по элементам (t, i), где i - индекс элемента. Это полезно, и я им много пользуюсь :)
  • .cycle бесконечно зацикливает итератор.
  • .rev меняет итератор на противоположный.

И еще много, еще много здесь, в документации. Я только что выбрал 6 наиболее распространенных / полезных, но если вы когда-нибудь пишете слишком много логики в map или for_each, вы можете сделать шаг назад и подумать, что можно применить, прежде чем я даже начну повторять.