(предупреждение новичка: я только что это узнал, поэтому не доверяйте всему, что вы здесь читаете).
Предисловие: Итераторы в Python
Итераторы Python - пример крайне неуклюжей реализации элегантности. В них все выглядит нормально, но под капотом ... Хуже только декораторы.
Итак, что такое итератор в Python? По сути, итератор - это .next функция для данных, которая либо возвращает «следующий» элемент данных, либо вызывает исключение StopIteration.
(обычно вам следует прекратить чтение, как только вы увидите такой тройной тулуп в языках программирования).
Но это еще не все. Он спрятан где-то посередине кода Python, но он есть. Когда вы повторяете, где вы сохраняете «текущий» элемент или информацию о том, что будет «следующим»? Все итераторы сохраняют состояние (за исключением бесконечных итераторов, которые представляют собой интересный способ создания бесконечных циклов), потому что каждый следующий вызов возвращает вам что-то другое, и эта «разница» зависит от предыдущих вызовов.
Итак, где они хранят состояние итераторов? Что ж, если вы пишете итератор, вы можете делать все, что захотите. Вы можете добавить поле со счетчиком в объекты (даже если этот объект имеет неизвестный вам тип, это Python), или вы можете использовать для этого глобальную переменную, или вы можете использовать побочный эффект (например, текущая позиция в операционной системе FD), или, что наиболее разумное решение, можно создать замыкание. В Python вы используете ключевое слово yield, которое просто сохраняет ваше состояние как закрытие и позволяет вам получить к нему доступ при следующем вызове. Когда вы это делаете, есть состояние, но оно настолько автоматическое, что вы не думаете об этом, за исключением нескольких неловких моментов, когда ваше состояние больше не действует для объекта, который вы повторяете (например, добавление в список, который вы повторяете или переместить указатель позиции файла, или дважды вызвать итератор с глобальной переменной…).
У меня сильный опыт работы с Python, поэтому для меня это «автоматическое состояние где-то там» было ожидаемым, а небольшие ошибки из-за параллельной модификации объекта - нормальная часть «будьте осторожны». Но не в случае с Rust.
Использование итераторов
Я начинаю с этой «позорной самомодификации». Python не может защитить вас от этого. Ржавчина может.
Когда Rust начинает перебирать объект, он может «потреблять» его (например, использовать и уничтожать), он может заимствовать его для мутации или может заимствовать его для чтения.
Вопрос к читателю: какой итератор позволяет модифицировать объект, по которому выполняется итерация?
Ответ: Нет.
Потребляющий итератор не оставляет объекта для изменения, модифицирующий итератор взаимно заимствует объект, поэтому никто не может его прочитать или записать, а неизменяемый итератор позволяет другим заглядывать в объект, но не позволяет модификации. Rust просто не будет компилироваться с «самомодифицирующимся итератором». Магия? Больше никаких «осторожностей». Кто-то поставил охрану и забор вокруг минного поля? Ура!
Где государство?
Следующий вопрос - тот, который надолго съел мне мозги. Как только я задал себе этот вопрос, мне все стало ясно, потому что ответ очевиден. Трудно было найти правильный вопрос (напомним, в Python состояние появляется «где-то там» само по себе, поэтому думать о состоянии итератора было необычно).
Когда мы создаем итератор, есть два варианта сохранения состояния:
- Объект (то, что мы повторяем) модифицируется, чтобы приспособиться к новому состоянию (итерация началась)
- Он где-то хранится, а объект и состояние итератора хранятся вместе.
(есть и третий с побочными эффектами, но пока оставим его).
Первый вариант нельзя сделать для объектов только для чтения. Если вы хотите перебрать объект и сохранить в нем состояние итератора, вам необходимо его изменить. Итак, Rust никогда не позволяет использовать итератор вместо объекта, доступного только для чтения.
Но вы можете создать новый объект (изменяемый объект), который может сохранять объект только для чтения, который вы повторяете, и изменяемое состояние для итератора.
Итак, Rust различил их:
Iterator может быть потребляющим или изменяемым.
IntoIterator может потреблять, изменять или неизменяемо заимствовать объект и возвращать изменяемое состояние итератора для этого объекта, которое будет использоваться в итераторе.
"for x in…" Руста ожидает чего-то, что имеет черту IntoIterator или Iterator. Если мы можем создать итератор для самого объекта, мы можем использовать Iterator. Если мы не хотим добавлять что-то к объекту или изменять его, мы можем использовать IntoIterator.
Итак, вот мое простое упражнение для IntoIterator: для объекта типа «Данные» с тремя именованными полями вернуть каждое из них:
struct Data{
a: i32,
b: i32,
c: i32
}
Наш тестовый код:
fn main() {
let d = Data::new(1, 2, 3);
for a in d{
println!("{}", a);
}
}
Здесь нельзя использовать Iterator, потому что состояние сохранить негде.
Я использую дополнительную структуру данных для хранения исходного объекта и позиции итератора (состояния):
struct DataIter {
data: Data,
pos: i32
}
Теперь мы можем реализовать Iterator для DataIter:
impl Iterator for DataIter {
type Item=i32;
fn next(&mut self) -> Option<i32>{
match self.pos {
0 => { self.pos+=1; Some(self.data.a)}
1 => { self.pos+=1; Some(self.data.b)}
2 => { self.pos+=1; Some(self.data.c)}
_ => None
}
}
}
Да, и я забыл сказать, насколько хорош Rust для использования Option в качестве итератора. это лучше, чем raise StopIteration в Python.
Что делает этот код? Начнем с функции «следующая».
Он взаимно заимствует себя и возвращает «Option» со следующим значением или None.
Сложная строка - это «Item», она нужна здесь для объявления признака. Я не буду много говорить об этом, так как это ведет к «доброте» и «типам высшего порядка». Я читал об этом, но повторить не могу (это доказательство, что я с трудом понимаю). Поэтому я принимаю их как «параметр черты характера». type Item=i32 - параметр трейта, указывающий, какие данные возвращает наш итератор.
Мы также можем использовать это «Item» в нашем коде. Этот вариант функции «next» абсолютно такой же, как и предыдущий:
impl Iterator for DataIter {
type Item=i32;
fn next(&mut self) -> Option<Self::Item>{
match self.pos {
0 => { self.pos+=1; Some(self.data.a)}
1 => { self.pos+=1; Some(self.data.b)}
2 => { self.pos+=1; Some(self.data.c)}
_ => None
}
}
}
Поскольку мы получили структуру «DataIter» с реализованным Iterator. Чтобы использовать его, нам нужно создать его явно:
fn main(){
let data=Data::new(1,2,3);
let mut data_iter: DataIter{data:data, pos:0};
for x in data_iter:
println!("{}", x);
}
Здесь оператор for берет наш data_iter и использует его метод «next» из-за реализованного признака «Iterator».
IntoInterator
Теперь у нас есть итератор и структура данных для итератора. Давайте создадим функцию для нашей исходной структуры Data, чтобы вернуть ее:
impl IntoIterator for Data{
type Item=i32;
type IntoIter = DataIter;
fn into_iter(self) -> DataIter{
DataIter{data: self, pos: 0}
}
}
и мы можем использовать его напрямую:
fn main() {
let d = Data::new(1, 2, 3);
for a in d{
println!("{}", a);
}
}
Что делает этот into_iter? Как видите, одно: возвращает DataIter, который потребляет наши данные и устанавливает позицию на 0.
Над этой функцией вы можете увидеть еще один набор «параметров для характеристики»:
Item говорит, что возвращается значение для итератора, а IntoIter говорит, какой тип возвращается от into_iter.
Благодаря этим параметрам мы можем написать такую функцию:
impl IntoIterator for Data{
type Item=i32;
type IntoIter = DataIter;
fn into_iter(self) -> Self::IntoIter {
DataIter{data: self, pos: 0}
}
}
Это точно такой же код, но с большим количеством «имплиативных переменных».
Примечание. Я задержался здесь на некоторое время, потому что пытался написать: fn into_iter(self) -> <Self::IntoIter> {
Это синтаксически неверно, более того, здесь нет обобщений, поэтому <> здесь бесполезен.
Это была легкая часть. А теперь давайте попробуем сделать итераторы, которые не потребляют много ресурсов. Это должно быть легко, правда? Кроме того, когда вы что-то одалживаете, возникают эти ужасные жизни, так что держитесь, держитесь!
Непотребляющий итератор
Начнем с изменения структуры DataIter. Теперь он должен ссылаться на данные, а не на сами данные.
struct DataIter {
data: &Data,
pos: i32
}
К сожалению, мы не можем это скомпилировать. Есть ссылка, и Rust понятия не имеет, как связаны время жизни DataIter и Data.
Опишем это отношение:
struct DataIter<'a> {
data: &'a Data,
pos: i32
}
И мы получаем ошибку компиляции: неявное пропущенное время жизни недопустимо в next функции. Хорошо, что в Rust есть хороший совет, как это исправить: добавить время жизни thowaway (‘_):
impl Iterator for DataIter<'_> {
type Item=i32;
fn next(&mut self) -> Option<Self::Item>{
match self.pos {
0 => { self.pos+=1; Some(self.data.a)}
1 => { self.pos+=1; Some(self.data.b)}
2 => { self.pos+=1; Some(self.data.c)}
_ => None
}
}
}
Поскольку это время жизни нигде не используется в функции, мы просто просим компилятор игнорировать его.
IntoIterator становятся следующей проблемой:
impl<'a> IntoIterator for Data{
type Item=i32;
type IntoIter = DataIter<'a>;
fn into_iter(&'a self) -> Self::IntoIter {
DataIter{data:&self, pos: 0}
}
}
Он не скомпилируется: существует неограниченное время жизни.
Чтобы исправить это, нам нужно изменить то, что мы делаем: нам больше не нужны «Данные», нам нужна ссылка на Data.
impl<'a> IntoIterator for &'a Data{
type Item=i32;
type IntoIter = DataIter<'a>;
fn into_iter(self) -> Self::IntoIter {
DataIter{data:&self, pos: 0}
}
}
Важно, что мы больше не можем работать с Data, только с &Data.
Старый "основной" код больше не работает при повторении d:
41 | for a in d{
| ^ `Data` is not an iterator
Давайте исправим это:
fn main() {
let d = Data::new(1, 2, 3);
for a in &d{
println!("{}", a);
}
for a in &d{
println!("{}", a);
}
}
и он работает как положено. Непотребляющий итератор, созданный функцией into_iter. Я сделал здесь две петли, чтобы убедиться, что это не лишний раз.
Этот последний кусок (куда поместить время жизни было действительно сложно для меня, но я счастлив, что не сдался здесь и не решил его, потому что это не проблема только времени жизни, но эта черта, на самом деле, была необходима для быть построенным для другого типа).