ReactiveMap<K, V>
Коллекция, чьи ключи появляются в рантайме. Каждая запись лежит по своему пути,
поэтому следить можно и за одной записью, и за всей картой сразу. Этим она и
отличается от HashMap, засунутого в одно поле.
let one: Option<u64> = widths.get("cpu");
if let Some(width) = widths.get("cpu") { println!("cpu is {width}");}
let there = widths.contains_key("mem");let how_many = widths.len();
for key in widths.keys() { println!("{key}");}
for (key, value) in widths.entries() { println!("{key}: {value}");}len читает счётчик в собственной проекции карты — за одно обращение, вместе
с записями, которые до диска ещё не дошли.
keys и entries идут по той же проекции, по одной записи за раз. keys
значений не касается вовсе: кроме ключа не десериализуется ничего.
Посмотреть, не забирая
Заголовок раздела «Посмотреть, не забирая»entries клонирует то, что отдаёт: Iterator не умеет одалживать из того, по
чему идёт. view как раз одалживает — свяжите его, потом iter(), и
склонируется только то, что вы склонируете сами.
let held = widths.view();let total: u64 = held.iter().map(|(_, width)| width).sum();
let mut widest = 0;for (_, width) in &widths.view() { widest = widest.max(*width);}for держит временное значение живым весь цикл, так что связывать нужно
только тогда, когда прочитанное переживёт цикл.
Обход владеет версией
Заголовок раздела «Обход владеет версией»view, entries и keys берут текущую версию карты и читают из неё. Пока
такой обход жив, ничего не заблокировано: он не ждёт пишущего и не заставляет
ждать его, на каких бы потоках эти двое ни сидели.
Писать в карту во время обхода — обычное дело, в том числе с того самого потока, который по ней и идёт:
let mut walked = Vec::new();
for (key, width) in widths.entries() { widths.remove(&key)?; walked.push((key, width));}
assert_eq!(walked, [("cpu".to_string(), 120), ("mem".to_string(), 80)]);assert!(widths.is_empty());Каждый ключ всё равно предложат: обход читает ту версию, с которой начался, а удаления уходят в карту. Запись, легшая посреди обхода, достанется следующему чтению, а не этому.
Знать это надо там, где обход не доводят до конца, а держат. view, связанный
на кадр, показывает то, что карта держала в момент, когда его взяли, — поэтому
два чтения из него сойдутся друг с другом, даже если карта между ними успела
сдвинуться. Платите вы за него той версией, которую он закрепил: за то, что
карта с тех пор отбросила, а не за копию карты.
widths.insert("cpu".to_string(), &120)?;
widths.update("cpu", &200)?;widths.modify("cpu", |width| *width += 10)?;
let absent = widths.update("gpu", &90);
widths.remove("cpu")?;widths.clear()?;insert добавляет ключ или заменяет его. update пишет в ключ, который уже
есть, а если его нет — падает с Absent.
Причина — в том, что достаётся подписчикам. MapChange::Update несёт
предыдущее значение, а у несуществующего ключа его взять негде: update по
отсутствующему ключу объявил бы изменение, которое сам же и не может описать.
modify — тот же update, только значение не собирают заново. Это важно,
когда значение — большая структура.
remove шлёт один MapChange::Remove с id этого хендла и одну запись в store.
clear устроен иначе: одно событие и одно удаление, сколько бы карта ни
держала.
Когда новое значение зависит от старого, а ключа может ещё не быть — счётчик,
растущий список, — оба случая берёт на себя upsert, одной функцией:
for page in ["home", "about", "home"] { hits.upsert(page, |seen| seen.map_or(1, |count| count + 1))?;}
let now = hits .durable() .upsert("home", |seen| seen.map_or(1, |count| count + 1))?;Функция получает Some со значением или None, а то, что она вернёт,
записывается и возвращается вам. Ключ берётся взаймы, так что для ключа, который
уже есть, вызывающему не нужно собирать String. Подписчики в первый раз видят
Insert, потом Update — как у insert. Чтение и запись — два шага, как в
modify: запись из другого потока, легшая между ними, будет затёрта.
Чтобы выбросить часть, обойдите и удалите — обход читает свою версию, поэтому цикл выходит самый обычный:
for (key, width) in widths.entries() { if width < 80 { widths.remove(&key)?; }}Чем может быть ключ
Заголовок раздела «Чем может быть ключ»Ключ — это имя, на котором сидит его запись. Ровно этого и просит
ReactiveMapKey — AsRef<str>, — так что строковый ключ пишется собой:
при поиске ничего не отрисовывается, при чтении с диска ничего не разбирается.
Нестроковый ключ идёт через Id, который отрисовывает значение один раз и
держит написание при себе:
let ports = store.kv().map::<Id<u16>, bool>("ports")?;
ports.insert(Id::new(8080), &true)?;assert_eq!(ports.get("8080"), Some(true));Id::new пишет значение в момент постройки ключа, а не на каждом чтении, — и
нужнее всего это Uuid: его 36 символов рядом со значением не помещаются, то
есть написание стоит кучи, и так она платится один раз за всю жизнь ключа.
get отдаёт обратно то, что было написано.
Пустое имя — тоже имя. Всякий формат, который пишет эта библиотека, разрешает члену называться ничем, поэтому такую запись карта держит, перечисляет и пишет наравне с прочими.
В каком порядке приходят записи
Заголовок раздела «В каком порядке приходят записи»По имени, которое ключ одалживает, — а это и есть порядок, в котором перечисляет store: запись лежит на один уровень ниже карты, а ключи store упорядочивает по уровням.
counts.insert("9".to_string(), &1)?;counts.insert("10".to_string(), &1)?;counts.insert("a.b".to_string(), &1)?;counts.insert("a1b".to_string(), &1)?;
let order: Vec<String> = counts.keys().collect();assert_eq!(order, ["10", "9", "a.b", "a1b"]);Число, записанное текстом, и упорядочено как текст, поэтому "10" раньше "9"
— и Id<u16> идёт так же, по своему написанию, а не по числу. Имя с
разделителем внутри упорядочено по имени: a.b встаёт перед a1b, потому что
. ниже 1. Экранирование, которым store пишет этот разделитель, — это
написание, а по написанию пути не сравнивают.
Порядок вставки нигде не записан. Если порядок важен — колонки таблицы, шаги в списке, — держите его сами, а карту берите для поиска.
Как подписаться на изменения
Заголовок раздела «Как подписаться на изменения»let _all = widths.subscribe_any(|change| { println!("{change:?}");});
let _cpu = widths.subscribe_key("cpu".to_string(), |change| { println!("cpu: {change:?}");});
widths.insert("cpu".to_string(), &120)?;widths.insert("mem".to_string(), &80)?;subscribe_any срабатывает на любое изменение любого ключа, subscribe_key
сужает до одного. Оба отдают guard, и подписка кончается, когда его дропают.
subscription_with() — то же самое, но со звеньями: .key(..) сужает,
.external() пропускает собственные записи, .stream() забирает изменения в
ваш собственный цикл вместо колбэка. См.
Подписки.
Что фильтрует external
Заголовок раздела «Что фильтрует external»Что такое external и по каким идентификаторам он различает записи — тема
Подписок. Здесь
о том, чем от них отличается карта.
На карте он фильтрует один Update. Insert, Remove и Clear доходят до
каждого подписчика, включая того, кто их и вызвал.
Граница здесь между «правят значение» и «меняется то, что карта держит». Значение, которое вы записали сами, — ваше личное дело. А ключ, который появился или исчез, меняет форму карты, и виду, перечисляющему ключи, перестраиваться придётся — сам он этот ключ добавил или кто-то другой.
Отсюда следствие, которое стоит держать в голове: insert по новому ключу —
это Insert, а по существующему — Update. Значит, вернётся ли к вам ваш
собственный вызов, зависит от того, был ключ там или нет.
Одна запись как ячейка
Заголовок раздела «Одна запись как ячейка»entry_cell(key) даёт
ReactiveCell на одну запись —
чтобы отдать одно значение туда, где про карту знать не полагается.