#prog #rust #моё
Рассмотрим следующий код на Rust:
Окей, признаю, я вас немного обдурил, вся фишка кроется в коде, который я не показал. Разумеется, я его покажу, а попутно затрону две темы в Rust: deref coercion и interiour mutability.
Но сначала давайте поговорим о слайсах. Они достаточно удобны. Их можно индексировать и снова получать слайсы. Для них определено множество полезных методов: сортировка, поворот, обращения порядка, линейный и бинарный поиски... Однако, все эти методы можно также вызвать и на массивах, и на
Давайте посмотрим на метод Vec::sort. Этот метод описан в блоке "Methods from
Ладно, это было поучительно, но какое оно имеет отношение к безобразию, показанному в начале? Дело в том, что одна из (почему-то малоизвестных) ситуаций, когда методы этих трейтов могут вызваны — это... Доступ к полю. Если в коде есть
Рассмотрим следующий код на Rust:
// код опущен для краткостиОчевидно, этот код не работает. И действительно, если мы запустим эту программу, то один из ассертов запустит панику. В данном случае это будет... Будет... Погодите-ка, оно работает? Что?! Но ведь в Rust нет свойств!
fn main() {
let tricky = <Tricky as Default>::default();
assert_eq!(tricky.field, 7);
assert_eq!(tricky.field, 4);
assert_eq!(tricky.field, 13);
let mut nice_try = <NiceTry as Default>::default();
assert_eq!(nice_try.field, 0);
nice_try.field += 12;
assert_eq!(nice_try.field, 0);
let (mut a, mut b) = <(Unstable, Unstable) as Default>::default();
assert_eq!((a.field, b.field), (0, 0));
std::mem::swap(&mut a.field, &mut b.field);
assert_eq!((a.field, b.field), (10, 10));
}
Окей, признаю, я вас немного обдурил, вся фишка кроется в коде, который я не показал. Разумеется, я его покажу, а попутно затрону две темы в Rust: deref coercion и interiour mutability.
Но сначала давайте поговорим о слайсах. Они достаточно удобны. Их можно индексировать и снова получать слайсы. Для них определено множество полезных методов: сортировка, поворот, обращения порядка, линейный и бинарный поиски... Однако, все эти методы можно также вызвать и на массивах, и на
Vec
. Как это работает? Неужели реализация этих методов скопирована с методов для слайсов? Конечно, нет.Давайте посмотрим на метод Vec::sort. Этот метод описан в блоке "Methods from
Deref<Target = [T]>
". Что это означает? В стандартной библиотеке Rust (на самом деле core) есть трейт Deref, описанный следующим образом:trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
Как нетрудно видеть, он позволяет получить из ссылки на значение одного типа ссылку на значение другого типа. У этого трейта есть дополняющий его DerefMut, который выполняет преобразование между мутабельными ссылками:trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
Возникает закономерный вопрос: а чем они отличается от AsRef и AsMut, которые также выполняют преобразования между ссылками? Ну, во-первых, в силу того, что целевой тип преобразования у Deref
и DerefMut
является ассоциированным типом, а не обобщённым параметром, у любого типа может быть не более одной реализации Deref
и DerefMut
. А во-вторых — и это уже куда как более существенное отличие — это один из "магических" (известных компилятору) трейтов, методы которого часто вызываются неявно. Конкретно методы Deref{, Mut}
вызываются для кастомных реализаций операторов разыменовывания, а также ещё неявно в некоторых контекстах, в частности, при вызове методов через точку. Это, в том числе, позволяет эргономично пользоваться умными указателями. Если на минуту забыть о том, что Vec
может менять свою длину, мы можем считать его ссылкой на лежащий в куче массив с длиной, известной лишь на этапе исполнения. То есть... Указатель на слайс. И действительно, Vec
реализует Deref{, Mut}<Target = [T]>
, что позволяет вызывать на нём методы, определённые для слайса. И при этом без каких-либо дополнительных телодвижений с вызывающей стороны!Ладно, это было поучительно, но какое оно имеет отношение к безобразию, показанному в начале? Дело в том, что одна из (почему-то малоизвестных) ситуаций, когда методы этих трейтов могут вызваны — это... Доступ к полю. Если в коде есть
foo.field
и foo
имеет тип Foo
, у которого нету поля field
, но Foo
реализует Deref<Target = Bar>
, где у типа Bar
есть поле field
, то такое обращение к полю будет корректно и будет вызывать deref
(или deref_mut
). Все те странные структуры, которые я показал, так или иначе оборачивают структуру Simple
:#[derive(Default)]
struct Simple {
field: u32,
}
Wikipedia
Property (programming)
type of class member in object-oriented programming which is accessed like a field but implemented as subroutine(s)
Давайте сначала разберём
Тот же анализ заимствований, который компилятор Rust делает на этапе компиляции, можно делать во время исполнения. Именно этот паттерн и реализует RefCell. Этот тип подсчитывает количествоссылок доступов к данным во время исполнения и паникует, если в какой-то момент должны возникнуть одновременно изменяемый доступ и хоть сколько-то неизменяемых — или более одного изменяемого. На проблему гонок данных также можно взглянуть с другого конца. Гонка данных считается неопределённым поведением, поскольку для многих типов данных операция обновления не является атомарной, т. е. неделимой. Если два ядра будут менять значение одновременно, то это может привести к тому, что промежуточные операции обновления будут чередоваться друг с другом и привести к тому, что в памяти значение окажется в неожиданном состоянии — неожиданное для того, кто писал код в расчёте на атомарность изменений (на самом деле тут есть ещё более сложные противные вещи, связанные с кешами процессоров, но сегодня я не хочу в это углубляться). Однако для простых значений, умещающихся в одно машинное слово, процессор на самом деле может гарантировать атомарность изменений. Тогда, если два ядра одновременно изменяют атомарное значение, оба этих изменения будут учтены. В таком случае гонка данных просто не может причинить вред, а значит, атомарные значения вполне можно отдавать по разделяемым (
NiceTry
. Вот её определение:#[derive(Default)]У нас есть два поля одного типа. Как же работает трюк с подменой? Очень просто: в
struct NiceTry {
first: Simple,
second: Simple,
}
Deref::deref
мы возвращаем ссылку на одно поле, а в DerefMut::deref_mut
— на другое:impl Deref for NiceTry {
type Target = Simple;
fn deref(&self) -> &Self::Target {
&self.first
}
}
impl DerefMut for NiceTry {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.second
}
}
Неудивительно, что мы не могли поменять field
— мы не могли получить к нему мутабельный доступ!Unstable
устроен несколько сложнее. Если у нас есть мутабельный доступ к полю внутри метода, то мы можем его... Мутировать. То есть поменять. Собственно, именно это и происходит в реализации deref_mut
:#[derive(Default)]
struct Unstable(Simple);
impl Deref for Unstable {
type Target = Simple;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Unstable {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.field += 10;
&mut self.0
}
}
"Ладно" — может сказать кто-то из моих читателей — "я понимаю, как работает трюк с NiceTry и Unstable, но что, чёрт побери, происходит с Tricky? Мы ведь даже не используем мутабельный доступ к Tricky, так что мы не можем что-то там поменять!". Что ж, мой недоумённый читатель (а также другие), я вынужден раскрыть одну из самых грязных тайн Rust: &mut T
на самом деле не означает "изменяемая ссылка"! Что же это тогда? Фактически, &mut T
означает уникальную ссылку на значение типа T
— то есть такую, что в любой момент времени одновременно с этой ссылкой не существует никаких других. Почему это так важно? Дело, что одна из вещей, которая является в Rust неопределённым поведением — это гонка данных, ситуация, когда доступ к одному и тому же значению происходит из двух (или более) разных мест одновременно, и при этом как минимум один доступ — на запись. Простейший (концептуально, разумеется) доступ добиться отсутствия гонок данных — это убедиться, что в любой момент времени только кто-то один имеет изменяемый доступ к данным. Это именно то, что компилятор Rust проверяет, используя концепции владения и заимствования. Но это не единственный способ!Тот же анализ заимствований, который компилятор Rust делает на этапе компиляции, можно делать во время исполнения. Именно этот паттерн и реализует RefCell. Этот тип подсчитывает количество
&
) ссылкам. И действительно, если мы откроем документацию к, скажем, AtomicU32, то мы увидим, что практически все методы принимают &self
.Wikipedia
Race condition
situation in computer system that occurs when multiple processes try to access a common resource
Но что же используется внутри
Ответ — Cell! Этот тип оборачивает значение и позволяет менять их по разделяемой ссылке. Почему же это не может привести к гонкам данных? На это есть две причины. Первая — API у
Вооружившись этим знанием, нетрудно понять, как реализована
Обычно значение в Rust можно поменять в том случае, если код им владеет или имеет
Тут у читателя может возникнуть вопрос: имеет ли смысл использовать подобные трюки у себя в коде? Скорее всего, да, если вы реализуете свой умный указатель (правда, в таком случае у меня уже к вам будут вопросы). Для всего остального? Да пожалуйста. Только будьте готовы к тому, что коллеги больше не захотят с вами разговаривать.
PS: код.
Tricky
? Какое-то атомарное значение? Нет, field
— это просто u32
, а сделать атомарной саму структуру Simple
мы не можем. RefCell
? Тоже нет. Метод RefCell::borrow
возвращает значение особого типа, которое реализует Deref
в оборачиваемые данные и которое в деструкторе уменьшает счётчик неизменяемых доступов. Но, напомним, в методе deref
нам нужно вернуть именно ссылку. Единственный способ получить получить ссылку на внутреннее значение в RefCell
— это вызвать borrow{, _mut}
, который вернёт тот прокси-тип. Но Rust просто не даст нам вернуть ссылку на локальную переменную! Выходит, этот вариант тоже отпадает. Что же тогда используется в Tricky
?Ответ — Cell! Этот тип оборачивает значение и позволяет менять их по разделяемой ссылке. Почему же это не может привести к гонкам данных? На это есть две причины. Первая — API у
Cell
позволяет доставать (копию) внутреннего значения, переписывать значение, замещать его другим (в том числе и значением по умолчанию) и обмениваться значением с другим Cell
, но не даёт возможности получить разделяемую ссылку на значение внутри — это дало бы возможность передать ссылку в другой поток и организовать чтение из одного потока и запись в другом (кстати, достать &mut
ссылку можно, ибо её уникальность гарантирует компилятор). Вторая причина — и это, на мой взгляд, очень хорошо демонстрирует силу Rust — Cell
не реализует Sync, то есть &Cell<T>
нельзя пересылать из одного потока в другой. Это означает, что в любой момент времени ссылки на Cell
, если таковые есть, все принадлежат одному потоку, то есть коду, который по отношению к Cell
исполняется последовательно. Таким образом, даже не смотря на то, что доступ к Cell
может быть из нескольких мест и каждый доступ может менять значение внутри, однопоточность кода гарантирует, что эти доступы не могут быть активны одновременно.Вооружившись этим знанием, нетрудно понять, как реализована
Tricky
:struct Tricky {
choice: Cell<Choice>,
first: Simple,
second: Simple,
third: Simple,
}
impl Deref for Tricky {
type Target = Simple;
fn deref(&self) -> &Self::Target {
let choice = self.choice.get();
self.choice.set(choice.next());
match choice {
Choice::First => &self.first,
Choice::Second => &self.second,
Choice::Third => &self.third,
}
}
}
Здесь Choice
— это некоторое перечисление, которое являет собой выбор из трёх вариантов. В deref
код обновляет выбор поля и в соответствии с предыдущим значением choice
выбирает нужное поле.Обычно значение в Rust можно поменять в том случае, если код им владеет или имеет
&mut
-ссылку на него, но нельзя через &
-ссылку. Это называется inherited mutability (наследуемая изменяемость), поскольку значение "наследует" изменяемость обёртки. Однако, как мы видели, значение может быть изменяемым и через &
-ссылку. Это называется interiour mutability (внутренняя изменяемость). Это нужная на практике вещь, поскольку Rc/Arc и мьютекс с безопасным интерфейсом без неё создать невозможно.Тут у читателя может возникнуть вопрос: имеет ли смысл использовать подобные трюки у себя в коде? Скорее всего, да, если вы реализуете свой умный указатель (правда, в таком случае у меня уже к вам будут вопросы). Для всего остального? Да пожалуйста. Только будьте готовы к тому, что коллеги больше не захотят с вами разговаривать.
PS: код.
doc.rust-lang.org
Cell in std::cell - Rust
A mutable memory location.
Дальнейшее чтение:
- UnsafeCell — единственный тип, который можно легально (но, конечно, небезопасно) менять по разделяемой ссылке. Абсолютно все типы с interiour mutability в конечном счёте опираются именно на него.
- Focusing on ownership — старая (до выхода Rust 1.0) статьяНикитоса Niko Matsakis, в которой он предлагает заменить
- Common Rust Lifetime Misconceptions (мой перевод) — вскользь касается темы уникальности ссылок в пункте 9.
- Deadlock empire — офигенная образовательная игра, в которой нужно взять на себя роль планировщика потоков и, манипулируя порядком исполнения шагов, сломать программу (завести в дедлок, заставить два потока одновременно зайти в критическую секцию и т. д.). Наглядно показывает, насколько сильно в многопотоке мешается неатомарность изменений. Если честно, даже странно, что я раньше её не выкладывал.
- Rustonomicon, конкретно раздел про параллельное исполнение. nuff said.
- Readonly — хтонический макрос от Толяна, который позволяет сделать помеченные поля публичными, но при этом неизменяемыми (извне крейта). Также абьюзит Deref.
- UnsafeCell — единственный тип, который можно легально (но, конечно, небезопасно) менять по разделяемой ссылке. Абсолютно все типы с interiour mutability в конечном счёте опираются именно на него.
- Focusing on ownership — старая (до выхода Rust 1.0) статья
&mut
на &uniq
, убедительно аргументируя, что это куда точнее отражает суть. Это предложение вызвало в своё время бурные обсуждения, но в итоге, как вы видите, так и не было принято. Возможно, и зря.- Common Rust Lifetime Misconceptions (мой перевод) — вскользь касается темы уникальности ссылок в пункте 9.
- Deadlock empire — офигенная образовательная игра, в которой нужно взять на себя роль планировщика потоков и, манипулируя порядком исполнения шагов, сломать программу (завести в дедлок, заставить два потока одновременно зайти в критическую секцию и т. д.). Наглядно показывает, насколько сильно в многопотоке мешается неатомарность изменений. Если честно, даже странно, что я раньше её не выкладывал.
- Rustonomicon, конкретно раздел про параллельное исполнение. nuff said.
- Readonly — хтонический макрос от Толяна, который позволяет сделать помеченные поля публичными, но при этом неизменяемыми (извне крейта). Также абьюзит Deref.
doc.rust-lang.org
UnsafeCell in std::cell - Rust
The core primitive for interior mutability in Rust.
#prog #article #amazingopensource
Алгоритм валидации UTF-8 практически без условных переходов, использующий меньше одной процессорной инструкции на байт входных данных. Реализация доступна как часть библиотеки simdjson
Алгоритм валидации UTF-8 практически без условных переходов, использующий меньше одной процессорной инструкции на байт входных данных. Реализация доступна как часть библиотеки simdjson
GitHub
simdjson/doc/basics.md at master · simdjson/simdjson
Parsing gigabytes of JSON per second : used by Facebook/Meta Velox, the Node.js runtime, ClickHouse, WatermelonDB, Apache Doris, Milvus, StarRocks - simdjson/simdjson
#prog #rust
Хозяйке на заметку
Пара советов по строкам в Rust:
1) Если вам нужно разбить строку по одному из нескольких возможных символов — не спешите расчехлять регулярки, для это задачи вполне хватит стандартной библиотеки. Множество строковых методов навроде {, r}split{, _terminator}, trim{, _start, _end}_matches, find и прочие принимают в качестве аргумента для поиска значение, тип которого реализует пока нестабильный трейт Pattern. В настоящий момент его реализуют
Вызвать `next`?
Нет.
Также вызвать `next_back`?
Нет.
Это всё неполные ответы. Если мы получаем мутабельную ссылку на
Покажу на примере.
Вот первый способ вытащить строку из
Хозяйке на заметку
Пара советов по строкам в Rust:
1) Если вам нужно разбить строку по одному из нескольких возможных символов — не спешите расчехлять регулярки, для это задачи вполне хватит стандартной библиотеки. Множество строковых методов навроде {, r}split{, _terminator}, trim{, _start, _end}_matches, find и прочие принимают в качестве аргумента для поиска значение, тип которого реализует пока нестабильный трейт Pattern. В настоящий момент его реализуют
&str
, &&str
, &String
, impl FnMut(char) -> bool
и (почему-то малоизвестный) &[char]
. Таким образом, разбить строку по нескольким символам легко:let result = "Hello, world!".split(&['o', 'l'][..]).collect::<Vec<_>>();2) Если функция принимает на вход
assert_eq!(result, vec!["He", "", "", ", w", "r", "d!"]);
&mut std::str::Chars
, что она может с ним сделать?Вызвать `next`?
Нет.
Также вызвать `next_back`?
Нет.
Это всё неполные ответы. Если мы получаем мутабельную ссылку на
Chars
, мы можем редактировать произвольным образом, в том числе и поменять его целиком. Chars
внутри себя содержит строки, символы которой он перебирает, и при помощи метода Chars::as_str эту строку можно достать. Таким образом, имея мутабельную ссылку на Chars
, можно вытащить из него строку, вырезать из строки нужный кусок и переписать переданный итератор .chars()
от этого кусочка.Покажу на примере.
Вот первый способ вытащить строку из
Chars
(медленный, требующий аллокаций и не совсем корректный):fn extract_line2(chars: &mut Chars) -> String {Второй способ (побыстрее и не требующий аллокаций):
chars.take_while(|&ch| !matches!(ch, '\r' | '\n')).collect()
}
fn extract_line<'s>(chars: &mut Chars<'s>) -> Option<&'s str> {
let s = chars.as_str();
let line = s.lines().next()?;
*chars = s[line.len()..].chars();
Some(line)
}
doc.rust-lang.org
Pattern in std::str::pattern - Rust
A string pattern.
#prog #article
Статья об антипаттерне использования
да, это статья десятилетней давности, ну и что?
Статья об антипаттерне использования
File.exists
.да, это статья десятилетней давности, ну и что?
Paranoidcoding
The File System is Unpredictable
Блог*
Донаты! Донаты?
Я, конечно, совсем ни на что не намекаю, но вот вам номер моей карты:
4274 3200 5402 8520
4274 3200 5402 8520
Блог*
Я, конечно, совсем ни на что не намекаю, но вот вам номер моей карты: 4274 3200 5402 8520
Не переживайте, я не собираюсь возводить paywall, все посты в обозримом будущем продолжат публиковаться на канале
Forwarded from мне не нравится реальность (waffle 🧇🍓)
Хвастаюсь: мой PR добавляющий
[Tracking Issue]
as_str
методы к разным Split
-строковым итераторам недавно смерджил толян![Tracking Issue]
Как же достали люди, которые носят маски, но при этом не прикрывают нос. Они что, правда не понимают, что это всё равно что без маски ходить?