LINUX.ORG.RU

Массивы в Rust

 


0

4

Продолжаю изучать Rust и вот решил попробовать в нём массивы.

Массивы в Rust неитерабельные, тоесть нельзя написать for item in array {...}. Вместо этого придётся написать либо for item in &array {...} либо for item in array.iter() {...} либо for item in array.to_vec() {...} либо что-то другое похожее, что создаёт из массива объект итерабельного типа. Ну или использовать цикл, который бежит по индексам for i in 0..array.len() { let n = array[i]; ...}. По моему это недостаток Rust. Для примера в Java for-each неявно разворачивается в одну из двух версий: с использованием индекса или с использованием итератора - в зависимости от того массив ли это или Iterable. Rust умеет делать лишь второй вариант, хотя это такой же синтаксический сахар, как и for-each в Java.

Так же, на сколько я понял, массив в Rust является не объектом, как в Java, а просто куском памяти с обвесами из макросов, как в C/C++. Таким образом функция не может получить, в качестве параметра, массив, длина которого неизвестна во время компиляции. Следующие три варианта функции не скомпилируются с тремя разными сообщениями об ошибке:

fn print_array1(array: [i32]) {
    //
}

fn print_array2(array: [i32; n], n: i32) {
    //
}

fn print_array3(n: i32, array: [i32; n]) {
    //
}

Варианты вроде fn print_array(array: [i32; 27]) {...} работать будут, но практической пользы не имеют. Значит придётся передавать не массив, а созданный из него итерабельный объект. Но это приводит к накладным расходам и с точки зрения системного программирования выглядит не очень хорошо. Может быть это ограничение снято в unsafe?

Ну хорошо, пусть речь идёт не о системном, а о прикладном программировании. Предположим, что я хочу пройтись по всем элементам двумерного массива, например, чтобы распечатать его в виде таблицы. В этом случае удобнее сразу пользоваться не массивом, а скажем слайсом (срезом), более похожим на массив в Java. В итоге у меня получился следующий код:
fn main() {
    let desk: &[&[i32]] = &[&[1, 2, 3, 4], &[5, 6, 7, 8], &[9, 10, 11, 12], &[13, 14, 15, 16]];

    print_desk(desk)
}

fn print_desk(desk: &[&[i32]]) {
    for line in desk {
        for cell in *line {
            print!("{:02} ", cell)
        }
        println!()
    }
}

Здесь у меня возникла проблема во внутреннем цикле. Я полагал, что типом переменной line является &[i32], но оказалост, что он &&[i32]. Поэтому во внутреннем цикле пришлось бежать не по line, а по *line. Другими вариантами были line.iter() или line.to_vec(). Почему здесь такой неконсистентный синтаксис? Так же, почему в сообщении об ошибке предлагается использовать более накладный line.iter() а не *line?

★★★★★
Ответ на: комментарий от RazrFalcon

&array - это сахар для array.iter()

В конечном итоге да. Но цепочка там сложнее. &array с типом &[i32; N] неявно преобразуется в &[i32], для которого вызывается into_iter(), который возвращает то же самое, что и array.iter().

red75prim ()

Опять накатил, и встряну к умным людям до первого модератора.

Нет никаких массивов. Это высокоуровневые абстракции, которых не существует. Искать преимущества и недостатки языка в реализации библиотек работы с массивами - это как выбирать автомобильчик по цвету капота.

Берём поток данных, и отсчитываем каждые 24 бита - вот тебе и массив 24-битных целых ( так это было в СУБД HyTech https://hytechdb.ru/ ).

Берём поток данных, и договариваемся, что FE это конец атрибута, FD это конец значения атрибута - вот тебе трехмерный массив ( как это было в PostRelational Pick System https://en.wikipedia.org/wiki/Pick_operating_system ).

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

Toxo1 ()
Ответ на: комментарий от Virtuos86

И не успел отредактировать. &[i32; N] неявно преобразуется в &[i32] только при явном вызове iter() или into_iter(). С неявным вызовом into_iter() в for _ in &array это не работает. Надо пожалуй issue написать, если ещё нет.

red75prim ()
Последнее исправление: red75prim (всего исправлений: 1)
Ответ на: комментарий от bbk123

Выходит .iter() даже лучше, поскольку inline, а .into_iter() не inline и просто вызывает .iter()

А LLVM то вчера написали наверное и она не заинлайнит функцию, в которой один лишь вызов, лол.

mersinvald ★★★ ()
Ответ на: комментарий от bbk123

Но почему в случае массива в Rust [i32; N] длину, для итерации такого массива, нельзя брать из константы N, известной на этапе компиляции?

потому что интерфейс функции должен быть универсальным. поставишь [#inline] - в машкодах соптимизирует как надо. если надо.

из константы, известной на этапе компиляции, это как раз так:

const N: usize = 3;

fn f(a: [i32; N]) {
    
}

но по-человечески лучше в typedef такое завернуть, или в struct.

MyTrooName ★★★★★ ()
Ответ на: комментарий от Toxo1

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

То есть сишку должен победить еще более убогий язык?

Virtuos86 ★★★★★ ()
Ответ на: комментарий от bbk123

Правда если &cell заменить на просто cell, тоже будет работать

во втором случае cell будет иметь тип &i32 вместо i32, как я понимаю. отдать её в println! все равно можно, конечно.

UPD наоборот

UPDUPD не, все верно было

MyTrooName ★★★★★ ()
Последнее исправление: MyTrooName (всего исправлений: 3)

тред не читал, но первые сообщения напомнили бывшего коллегу, доказывающего, что .EACH ЭТА БЕЗ ИТЕРАЦИИ БЕЗ FOR ПАТАМУ БЫТСРЕЕ И ЛУЧШЕ, извините, чудес не бывает

trashymichael ★★★ ()
Ответ на: комментарий от MyTrooName

Если ты передаешь куда-то cell, а не &cell, ты передаешь владение. Если тип cell поддерживает копирование, как в случае i32, то передастся копия, если же нет, код не скомпилируется.

Virtuos86 ★★★★★ ()
Ответ на: комментарий от MyTrooName

потому что интерфейс функции должен быть универсальным.

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

bbk123 ★★★★★ ()
Ответ на: комментарий от mersinvald

Расскажи лучше, зачем в стандартной библиотеке вообще понадобился .into_iter() если там уже есть .iter() который вызывается из .into_iter() ? Чтобы оптимизатору LLVM совсем скучно не было?

bbk123 ★★★★★ ()
Последнее исправление: bbk123 (всего исправлений: 1)
Ответ на: комментарий от RazrFalcon

Какой смысл передавать массив в функцию? Это же дорого.

Почему дорого? Например в C/C++ переменная массив - это просто поинтер, а сам массив на уровне компилятора вообще не существует, поскольку обрабатывается до него препроцессором. Именно поэтому a[5] = 7; тождественно 5[a] = 7; и оба варианта прекрасно компилируются как дереференс суммы a и 7 - в том или ином порядке, от которого ничего не зависит. Но в Rust ввели защиту от дурака и просто так передавать поинтер на массив запретили. Поэтому приходится передавать слайс (аналог Java массива), внутри которого есть его длина. Но с тем же успехом можно было бы передавать эту же длину просто вторым параметром (смотри исходное сообщение) и не создавать никаких новых объектов (слайсов).

bbk123 ★★★★★ ()
Ответ на: комментарий от bbk123

сам массив на уровне компилятора вообще не существует, поскольку обрабатывается до него препроцессором

Бл*ь.

Но с тем же успехом можно было бы передавать эту же длину просто вторым параметром

Не с тем же.

tailgunner ★★★★★ ()
Ответ на: комментарий от bbk123

не создавать никаких новых объектов (слайсов).

Это не Java. Структуры по умолчанию кладутся на стек (или в регистры после оптимизации), а не на кучу. Так что на джавовский объект это совсем не похоже.

red75prim ()
Ответ на: комментарий от bbk123

Именно поэтому a[5] = 7; тождественно 5[a] = 7;

Не уверен, что приколы синтаксиса С имеют какое-либо отношение к железу.

не создавать никаких новых объектов

Ещё раз: создание слайса - (фактически) бесплатное.

RazrFalcon ★★★ ()
Ответ на: комментарий от red75prim

Это не Java. Структуры по умолчанию кладутся на стек (или в регистры после оптимизации), а не на кучу. Так что на джавовский объект это совсем не похоже.

Не похоже лишь тем, где этот слайс хранится. Я говорил не об этом, а о том, что там внутри.

bbk123 ★★★★★ ()
Ответ на: комментарий от bbk123

сам массив на уровне компилятора вообще не существует, поскольку обрабатывается до него препроцессором

Бл*ь.

Открой K&R

Процитируй.

Но с тем же успехом можно было бы передавать эту же длину просто вторым параметром

Не с тем же.

В чём разница?

В том, что со слайсом второй параметр гарантированно правильный.

tailgunner ★★★★★ ()
Ответ на: комментарий от bbk123

Например в C/C++ переменная массив - это просто поинтер

Используй Vec в расте и будете такой же поинтер. И разберись уже с выделением стека (декремнт регистра) и выделением кучи (сискол) у них очень разная стоимость.

anonymous ()
Ответ на: комментарий от bbk123

Itanium ABI - de facto стандарт на 64-битные ABI.

https://itanium-cxx-abi.github.io/cxx-abi/abi.html#value-parameter

In general, C++ value parameters are handled just like C parameters. This includes class type parameters passed wholly or partially in registers.

Так что хоть указатель на начало и длину передавай в разных параметрах, хоть передавай структуру в которой лежат указатель на начало и длина, результат будет один и тот же - всё будет лежать в регистрах.

И то и то передаётся в регистрах.

Как не передавай, будет лежать в регистрах.

Не похоже лишь тем, где этот слайс хранится.

Слайс будет лежать в регистрах. Итератор по слайсу будет лежать в регистрах. Ссылка на начало массива и длина будут лежать в регистрах. Нет никаких объектов.

red75prim ()
Ответ на: комментарий от anonymous

разберись уже с выделением стека (декремнт регистра) и выделением кучи (сискол) у них очень разная стоимость.

И выделение стека может закончится входом в ядро, и выделение из кучи не обязательно закончится сисколлом.

Более того, где-то я читал, что большие объекты на стеке - причина некоторых тормозов в Rust.

tailgunner ★★★★★ ()
Ответ на: комментарий от tailgunner

Процитируй.

http://cs.indstate.edu/~cbasavaraj/cs559/the_c_programming_language_2.pdf

In evaluating a[ i ], C converts it to *(a+i) immediately;


В том, что со слайсом второй параметр гарантированно правильный.

Он и в этом случае может быть гарантированно правильным, если научить компилятор соответствующей проверке. Компилятор уже сейчас умеет ругаться при попытке передать один и тот же объект через два параметра как &obj и &mut obj одновременно. В чём проблема проверить, что константа n в записе fn fu(array[i32; n], n: i32) правильная? Кстати, как в Rust объявить n константой в сигнатуре функции?

bbk123 ★★★★★ ()
Ответ на: комментарий от bbk123

сам массив на уровне компилятора вообще не существует, поскольку обрабатывается до него препроцессором

Бл*ь.

Открой K&R

Процитируй.

In evaluating a[ i ], C converts it to *(a+i) immediately;

И из этого фрагмента ты заключил, что «массив обрабатывается препроцессором»? Мде.

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

И тогда какой смысл делать 2 параметра?

tailgunner ★★★★★ ()
Ответ на: комментарий от red75prim

Количество регистров CPU не резиновое, соответственно рано или поздно придётся переходить на стек. Например Microsoft начинает пользоваться стеком уже после четвёртого параметра:

https://msdn.microsoft.com/en-us/library/zthk2dkh.aspx

bbk123 ★★★★★ ()
Ответ на: комментарий от tailgunner

И из этого фрагмента ты заключил, что «массив обрабатывается препроцессором»?

Ну а кто, по твоему, конвертирует a[ i ] в *(a+i) если не препроцессор? Задача компилятора - не конвертировать исходники в другие исходники на том же языке программирования. Это задача препроцессора.

И тогда какой смысл делать 2 параметра?

Не создавать слайс - ещё один объект.

bbk123 ★★★★★ ()
Ответ на: комментарий от bbk123

И из этого фрагмента ты заключил, что «массив обрабатывается препроцессором»?

Ну а кто, по твоему, конвертирует a[ i ] в *(a+i) если не препроцессор?

Ты еще и упорствуешь? O_o Компилятор конвертирует.

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

Эпично. Решить, что фраза «in evaluating a[ i ], C converts it to *(a+i) immediately» означает source-to-source преобразование, может только дикарь (в компиляторном смысле); но даже дикарь мог бы провериться - задать себе вопрос «способен ли препроцессор Си на такое?», ответить «нет, конечно» и дальше попробовать найти правильный ответ.

И тогда какой смысл делать 2 параметра?

И какая разница между объектом из двух слов и двумя отдельными словами?

tailgunner ★★★★★ ()
Последнее исправление: tailgunner (всего исправлений: 1)
Ответ на: комментарий от MyTrooName
fn iter(&self) -> Iter<T>
fn into_iter(self) -> Self::IntoIter;


&self vs self объяснять?


У меня fn into_iter(self) -> Iter<'a, T> Попал туда по Ctrl+Click в IntelliJ написав desk.into_iter().

&self не перемещает, а self перемещает. Но into_iter(self) вызывает всё тот же iter(&self), поэтому какая разница? Вот на Reddit был вопрос на тему, почему в стандартной библиотеке Rust не искользуют только &self

https://www.reddit.com/r/rust/comments/2rb211/methods_self_vs_self/

bbk123 ★★★★★ ()
Ответ на: комментарий от tailgunner

Ты еще и упорствуешь? O_o Компилятор конвертирует.

Для кого конвертирует? Если он такой умный, он может сразу и откомпилировать. На самом деле речь идёт о K&R. Современные компиляторы, если и компилируют обращения к массивам сразу, обязаны это делать по той же логике, что и K&R.

И какая разница между объектом из двух слов и двумя отдельными словами?

Как слайс передаётся в функцию, по ссылке или по значению? Полагаю, что по ссылке. Тогда у тебя три слова, а не два (два в слайсе и третье - ссылка на слайс).

bbk123 ★★★★★ ()
Ответ на: комментарий от bbk123

Современные компиляторы, если и компилируют обращения к массивам сразу, обязаны это делать по той же логике, что и K&R.

Компиляторы обязаны соблюдать стандарт.

Полагаю, что по ссылке.

Неверно.

anonymous ()
Ответ на: комментарий от bbk123

&self не перемещает, а self перемещает

да

Но into_iter(self) вызывает всё тот же iter(&self), поэтому какая разница?

разница в том, что &self не перемещает, а self перемещает

семантика перемещения (into_iter) как раз позволяет лучше оптимизировать код. если объект еще понадобится в вызывающей функции, приходится отдавать по ссылке

MyTrooName ★★★★★ ()
Последнее исправление: MyTrooName (всего исправлений: 1)
Ответ на: комментарий от anonymous

Компиляторы обязаны соблюдать стандарт.

Они их соблюдают. Вот, что написано в стандарте C99:

The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))).

Неверно.

Создаёт копию на стеке перед передачей в каждый новый вызов функции? Но ведь это ещё дороже.

bbk123 ★★★★★ ()
Ответ на: комментарий от bbk123

Создаёт копию на стеке перед передачей в каждый новый вызов функции?

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

anonymous ()
Ответ на: комментарий от anonymous

Как уже выяснилось выше, после определённого количества параметров функции регистры процессора заканчиваются и приходится использовать стек, тоесть копировать.

bbk123 ★★★★★ ()
Ответ на: комментарий от bbk123

Что значит «копировать»? Писать два слова в стек? А если делать объект, то нужно будет писать одно слово в стек (указатель на объект). И где здесь выиграш? Известно же что дополнительная косвенность почти всегда замедляет.

anonymous ()