В обычном 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
, вы можете сделать шаг назад и подумать, что можно применить, прежде чем я даже начну повторять.