Взаимоисключающие ссылки / «Алиасинг XOR изменяемость»
Мы можем использовать взаимное исключение ссылок &T и &mut T, чтобы не допустить использования данных до того, как они будут готовы.
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
pub struct QueryResult;
pub struct DatabaseConnection {/* поля опущены */}
impl DatabaseConnection {
pub fn new() -> Self {
Self {}
}
pub fn results(&self) -> &[QueryResult] {
&[] // фиктивные результаты
}
}
pub struct Transaction<'a> {
connection: &'a mut DatabaseConnection,
}
impl<'a> Transaction<'a> {
pub fn new(connection: &'a mut DatabaseConnection) -> Self {
Self { connection }
}
pub fn query(&mut self, _query: &str) {
// Отправляем запрос, но не ждём результатов.
}
pub fn commit(self) {
// Завершаем выполнение транзакции и получаем результаты.
}
}
fn main() {
let mut db = DatabaseConnection::new();
// Транзакция `tx` изменяемо заимствует `db`.
let mut tx = Transaction::new(&mut db);
tx.query("SELECT * FROM users");
// Это не скомпилируется, потому что `db` уже изменяемо заимствован транзакцией `tx`.
// let results = db.results(); // ❌🔨
// Заимствование `db` заканчивается, когда `tx` потребляется методом `commit()`.
tx.commit();
// Теперь можно снова заимствовать `db`.
let results = db.results();
}
-
Мотивация: в этом API базы данных запросы запускаются на асинхронное выполнение, а результаты становятся доступны только после завершения всей транзакции.
Пользователь может подумать, что запросы выполняются сразу, и попытаться прочитать результаты до того, как они станут доступны. Такое неправильное использование API может привести к тому, что приложение прочитает неполные или некорректные данные.
Хотя это очевидное недоразумение, подобные ситуации случаются на практике.
Спросите: кто-нибудь неправильно понимал API, не читая документацию по правильному использованию?
Ожидаемый ответ: примеры ошибок и недоразумений на начальном этапе карьеры или в университете.
По мере роста API и числа пользователей всё меньший процент пользователей обладает глубоким знанием системы, которую представляет API.
-
Этот пример показывает, как можно использовать Aliasing XOR Mutability, чтобы предотвратить такое неправильное использование.
-
Код может прочитать результаты до их готовности, если программист предполагает, что запросы выполняются сразу, а не запускаются на асинхронное выполнение.
-
Конструктор типа
Transactionпринимает изменяемую ссылку на подключение к базе данных и сохраняет её в возвращаемом значенииTransaction.Явное время жизни здесь не должно пугать: в данном случае оно означает лишь, что
DatabaseConnection, переданное в конструктор, живёт дольшеTransaction.Ссылка изменяемая, чтобы полностью заблокировать
DatabaseConnectionдля любого другого использования, например для запуска новых транзакций или чтения результатов. -
Пока существует
Transaction, мы не можем обращаться к переменнойDatabaseConnection, из которой он был создан.Продемонстрируйте: раскомментируйте строку
db.results(). Это приведёт к ошибке компиляции, так какdbуже изменяемо заимствован. -
Примечание: то, что результаты запросов не являются публичными и доступны только через функцию-геттер, позволяет обеспечить инвариант «пользователи могут смотреть результаты запросов, только если нет активных транзакций».
Если бы результаты запросов находились в публичном поле структуры, этот инвариант можно было бы нарушить.