Как стать автором
Обновить

RustyTips 0x01: Очень кратко о циклах в Rust

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров664

RustyTips - цикл микро-статей о Rust от меня.

Также есть следующие посты от меня:

Циклы позволяют управлять ходом выполнения программы.

Цикл For

Цикл for используется для итераций.

for item in container {
	// ...
}

Эта базовая форма делает каждый последующий элемент в контейнере container доступным в качестве элемента item.

До edition 2021 если у тебя коллекция (контейнер) был мутабельным, то конструкция for item in collection рассахаривалась в for item collection.into_iter_mut() который "пожирал" твою коллекция, а при добавлении & (амперсенда) он делал item.iter_mut(). Сейчас это всегда item.iter_mut().

Так что знак амперсанда - это итератор.

for item in &container {
	// ...
}

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

for item in mut collection {
	// ...
}

Для того, чтобы пропустить текущую итерацию, можно использовать ключевое слово continue. После вызова этой команды, цикл перейдет к следующей итерации.

Вдаваясь в подробности реализации Rust-конструкции цикла for, следует отметить что она расширяется компилятором в вызов метода.

Краткая форма

Ее эквивалент

Доступ

for item in collection

for item in IntoIterator::into_iter(collection)

По факту владения

for item in &collection

for item in collection.iter()

Только по чтению

for item in &mut collection

for item in collection.iter_mut()

По чтению и записи

iter_mut() - один из вариантов IterMut.

Безымянные циклы

Если в блоке не используется локальная переменная, то по соглашению применяется знак подчеркивания. Использование этой схемы в сочетании с синтаксисом исключающего диапазона (n..m) и синтаксисом включающего диапазона (n..=m) показывает, что целью является выполнение цикла фиксированное количество раз. Например:

for _ in 0..10 {
	// ...
}

Отказ от управления индексной переменной

Во многих языках программирования привычное дело - использование последовательных переборов путем использования временной переменной, увеличивающейся в конце каждой итерации. По соглашению эта переменная называется i.

let collection = [1, 2, 3, 4, 5];

for i in 0..collection.len() {
	let item = collection[i];
	println!("Item: {}", item);
}

Данная схема важна и в тех случаях, когда последовательный перебор collection напрямую, с применением кода for item in collection, невозможен. Но делать это обычно не рекомендуется. При неавтоматизированном подходе возникают две проблемы:

  1. Производительность - индексирование значений с использованием синтаксиса collection[index] не обходится без издержек времени выполнения из-за проверки границ. При непосредственном проходе элементов коллекции такая проверка не нужна. Чтобы удостовериться в невозможности запрещенного доступа, компилятор может включить анализ в ход компиляции.

  2. Безопасность - периодическое от случая и к случаю обращение к коллекции collection чревато тем, что в нее могут быть внесены изменения . Непосредственное использование в отношении collection цикла for позволяет Rust гарантировать, что collection останется в неприкосновенности со стороны других частей программы.

While

Цикл while продолжается до тех пор, пока работает условие. Условие, официально известное как "предикат" может быть любым выражением, которое возвращает логическое значение true или false. Пока условие истинно, цикл выполняется. Когда условие перестаёт быть истинным, программа вызывает break, останавливая цикл.

fn main() {
	let floor: i32 = 9;

	while floor != 0 {
		floor -= 1;
		println!("Снижаемся на {} этаж", floor);
	}

	println!("Лифт спустился");
}

Для перебора элементов коллекции, например, массива, можно использовать конструкцию while.

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

Этот код выполняет перебор элементов массива. Он начинается с индекса 0, а затем циклически выполняется, пока не достигнет последнего индекса в массиве (то есть, когда index < 5 уже не является истиной). Выполнение этого кода напечатает каждый элемент массива:

the value is: 10 the value is: 20 the value is: 30 the value is: 40 the value is: 50

Все пять значений массива появляются в терминале, как и ожидалось. Поскольку index в какой-то момент достигнет значения 5, цикл прекратит выполнение перед попыткой извлечь шестое значение из массива.

Однако такой подход чреват ошибками; мы можем вызвать панику в программе, если значение индекса или условие проверки неверны. Например, если изменить определение массива a на четыре элемента, но забыть обновить условие на while index < 4, код вызовет панику. Также это медленно, поскольку компилятор добавляет код времени выполнения для обеспечения проверки нахождения индекса в границах массива на каждой итерации цикла.

В качестве более краткой альтернативы можно использовать цикл for и выполнять некоторый код для каждого элемента коллекции. Цикл for может выглядеть так:

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

При выполнении этого кода мы увидим тот же результат. Что важнее, теперь мы повысили безопасность кода и устранили вероятность ошибок, которые могут возникнуть в результате выхода за пределы массива или недостаточно далёкого перехода и пропуска некоторых элементов.

При использовании цикла for не нужно помнить о внесении изменений в другой код, в случае изменения количества значений в массиве, как это было бы с методом, использованным в коде с while.

Безопасность и компактность циклов for делают их наиболее часто используемой конструкцией цикла в Rust.

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

fn main() {
    for number in (1..9).rev() {
        println!("Спускаемся на {number} этаж");
    }
    println!("Лифт спустился.");
}

Loop

Loop - это основа для циклических конструкций в Rust. Ключевое слово loop говорит Rust выполнять блок кода снова и снова до бесконечности или пока не будет явно приказано остановиться.

fn main() {
    loop {
        println!("again!");
    }
}

Данная программа выше будет работать бесконечно, пока не будет прервана пользователем.

К счастью, Rust также предоставляет способ выйти из цикла с помощью кода. Ключевое слово break нужно поместить в цикл, чтобы указать программе, когда следует прекратить выполнение цикла.

Также существует ключевое слово continue которое пропускает данную итерацию и переходит к следующей.

Одно из применений loop - это повторение операции, которая может закончиться неудачей, например, проверка успешности выполнения потоком своего задания. Также может понадобиться передать из цикла результат этой операции в остальную часть кода. Для этого можно добавить возвращаемое значение после выражения break, которое используется для остановки цикла. Это значение будет возвращено из цикла, и его можно будет использовать, как показано здесь:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Перед циклом мы объявляем переменную с именем counter и инициализируем её значением 0. Затем мы объявляем переменную с именем result для хранения значения, возвращаемого из цикла. На каждой итерации цикла мы добавляем 1 к переменной counter, а затем проверяем, равняется ли 10 переменная counter. Когда это происходит, мы используем ключевое слово break со значением counter * 2. После цикла мы ставим точку с запятой для завершения инструкции, присваивающей значение result. Наконец, мы выводим значение в result, равное в данном случае 20.

Метки циклов для устранения неоднозначности между несколькими циклами

Если у вас есть циклы внутри циклов, break и continue применяются к самому внутреннему циклу в этой цепочке. При желании вы можете создать метку цикла, которую вы затем сможете использовать с break или continue для указания, что эти ключевые слова применяются к помеченному циклу, а не к самому внутреннему циклу. Метки цикла должны начинаться с одинарной кавычки. Вот пример с двумя вложенными циклами:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

Внешний цикл имеет метку 'counting_up, и он будет считать от 0 до 2. Внутренний цикл без метки ведёт обратный отсчёт от 10 до 9. Первый break, который не содержит метку, выйдет только из внутреннего цикла. Инструкция break 'counting_up; завершит внешний цикл. Этот код напечатает:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

Циклы - довольно интересная вещь в Rust. Они местами отличаются от других языков программирования. Советую попрактиковаться в них.

Теги:
Хабы:
0
Комментарии1

Публикации

Истории

Работа

Rust разработчик
9 вакансий

Ближайшие события

4 – 5 апреля
Геймтон «DatsCity»
Онлайн
8 апреля
Конференция TEAMLY WORK MANAGEMENT 2025
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область