
Size: a a a
match str_value.as_bytes() {
[b'p', b'r', b'e', b'f, b'i', b'x', rest @ ..] => {}
_ => {}
}
, и тут даже будет помогать компилятор — он подскажет нам, если мы будем дважды проверять один и тот же префикс. Но тут есть и недостатки: остаток строки (rets во второй строчке) — не &str
, а &[u8]
, ну и, конечно, это довольно неудобно писать. Первый недостаток отчасти перекрывается str::get_unchecked
/std::str::from_utf8_unchecked
— отчасти, поскольку в паттерн байта можно написать и часть многобайтового символа, а вот второй недостаток обойти сложнее. В идеале мы бы хотели написать матч в виде сопоставления части строки, чтобы потом он скомпилировался в примерно такой же код, как наверху — чтобы к нему могли быть применены те же оптимизации, что и к обычному матчу, и чтобы получить выгоду от проверки полноты покрытия — но это довольно существенное вмешательство в синтаксис, требующее написания процедурного макроса, написание которого отводится читателю в качестве самостоятельного упражнения.macro_rules! prefixes {
(match $value:ident {
$($prefix:literal.. => $arm:expr,)*
_ => $catch_all:expr $(,)?
}) => {
match $value {
$(x if x.starts_with($prefix) => $arm,)*
_ => $catch_all,
}
}
}
Ну и давайте сделаем какую-нибудь функцию, которая использует этот макрос:fn use_prefixes(s: &str) -> String {
prefixes!(match s {
"foo".. => s.to_string(),
"bar".. => [s, s].concat(),
_ => String::new(),
})
}
fn main() {
let inputs = [
"foobar",
"barfoo",
"overall",
];
for input in &inputs[..] {
println!("{:?}", use_prefixes(input));
}
}
Но, погодите-ка, так потеряли одно из преимуществ компилятора: проверку полноты покрытия! Как мы можем её восстановить? Пойдём ленивым путём: сделаем свою функцию, в которой будем матчить по переданным строкам и позволим компилятору сделать работу за нас. Однако возникает вопрос, где эту функцию хранить? Простейший способ добиться этого — обернуть весь итоговый match
в один блок и сделать внутри этого блока функцию. Так как функция не будет использована, она будет помечена #[allow(dead_code)]
, а на внутренний match
повесим #[warn(unreachable_patterns)]
, чтобы предупреждения компилятора были даже в том случае, если они по каким-то причинам выключены на верхнем уровне:macro_rules! prefixes {
(match $value:ident {
$($prefix:literal.. => $arm:expr,)*
_ => $catch_all:expr $(,)?
}) => {{
#[allow(dead_code)]
fn non_repeating() {
#[warn(unreachable_patterns)]
match "" {
$($prefix => (),)*
_ => (),
}
}
match $value {
$(x if x.starts_with($prefix) => $arm,)*
_ => $catch_all,
}
}}
}
use_prefixes
одинаковые префиксы:fn use_prefixes(s: &str) -> String {
prefixes!(match s {
"foo".. => s.to_string(),
"foo".. => [s, s].concat(), // <--
_ => String::new(),
})
}
"java"
, то пытаться отщипнуть префикс "javascript"
уже не имеет смысла, потому что он заведомо отсутствует. Однако мой макрос проверяет строки на (не)равенство и не учитывает их возможных структурных отношений. Сейчас мы это исправим, и пойдём по уж проторенной дорожке: вынесем нужную проверку в const fn
и будем генерировать код, который не тайпчекается в случае, если проверка завершилась неудачей.const fn is_prefix(s: &str, maybe_prefix: &str) -> bool {Теперь немного подумаем о том, как детектировать недостижимые паттерны. Для этого нам нужно перебрать все пары различных строк из
if maybe_prefix.len() > s.len() {
return false;
}
let s = s.as_bytes();
let mp = maybe_prefix.as_bytes();
let mut i = 0;
while i < mp.len() {
if mp[i] != s[i] {
return false;
}
i += 1;
}
true
}
match
и проверить, что для каждой такой пары строка из более поздней ветки не является префиксом строки из более ранней ветки. Так? Так, да не совсем: у ветки может быть охранное выражение, и в этом случае выполнение ветки может не произойти по совершенно произвольным причинам. Поэтому в генерируемом коде мы будем хранить не только строки, но и признак того, что охранное выражение есть, и при в соответствующей функции будем эти строки пропускать. Сказано — сделано:const fn has_unreachable_patterns(ss: &[(&str, bool)]) -> bool {Осталось только сгенерировать код для проверки, заменив в макросах функцию
if ss.is_empty() {
return false;
}
let mut i = 0;
let mut j;
while i < ss.len() - 1 {
// строки, для которых есть охранное выражение, пропускаем
if ss[i].1 {
i += 1;
continue
}
j = i + 1;
while j < ss.len() {
// А вот вторую строку в паре нужно проверять всегда,
// вне зависимости от того, есть у него охранное выражение или нет:
// если охранного выражения нет у первой строки, то исполнение
// до ветки со второй строкой точно не дойдёт.
if is_prefix(ss[j].0, ss[i].0) {
return true;
}
j += 1;
}
i += 1;
}
false
}
non_repeating
(это изменение, кстати, одинаковое для всех двух (трёх) макросов) — и это, кажется, наиболее зубодробительная часть:const _HAS_NO_UNREACHABLE_PATTERNS: [(); 0] = [Это довольно много, так что разберём по частям:
();
has_unreachable_patterns(
&[
$((
$prefix,
false $(|| {stringify!($condition); true})?
),)*
]
) as _
];
const _HAS_NO_UNREACHABLE_PATTERNS: [(); 0] = [Это объявление константы. Если
();
has_unreachable_patterns(...) as _
];
has_unreachable_patterns(...)
вычисляется в true
, то оно при касте в usize
становится 1
, вызывая ошибку несоответствия типов.&[Здесь мы формируем ссылку на литерал массива, элементами которого являются пары, первыми элементами которых являются строки-префиксы.
$((
$prefix,
...
),)*
]
false $(|| {stringify!($condition); true})?Здесь мы формируем значение
true
, если охранное выражение имеется. Сделать повтор синтаксического фрагмента без соответствующей синтаксической метапеременной нельзя, но и само выражение нам не требуется, поэтому воспользуемся стандартным трюком: переведём выражение в безвредную строку и отбросим её. На итоговое выражение это не повлияет, а наличие нужной метапеременной обеспечено. И да, это не замыкание, как может показаться, а выражение с оператором "или".