Мои любимые ошибки с IDisposable. 3/3
Начало
Продолжение
4. Чрезмерная очистка
Во многих случаях мы передаём экземпляр
IDisposable между методами или в конструкторы. Возникает вопрос: я должен отвечать за вызов Dispose или это сделает кто-то другой? Когда мы вызываем Dispose из неправильного места, мы получаем незаметные, трудно поддающиеся диагностике ошибки.Совет на этот случай: если вы вызываете
new, вы должны вызывать и Dispose. Если вы не вызывали new, вы не обязаны вызывать Dispose.Рассмотрим эту функцию:
void Save(IDbConnection conn) {
using(conn) {
//…
}
}
Разработчик пытается поступить правильно и очистить ресурсы после использования. Но теперь вызов Save имеет побочный эффект закрытия соединения. Это может привести к ошибке в другом месте:using(var cn = new SqlConnection("…")) {
Save(cn);
Update(cn); // ошибка
}
Мы получим исключение в Update, потому что функция Save закрыла наше соединение. Эта ошибка может быть довольно коварной: исключение будет выброшено из Update, поэтому поиск ошибки начнётся не в том месте. В зависимости от размера кодовой базы может потребоваться много времени, чтобы обнаружить, что обновление завершается ошибкой только при вызове после сохранения. Эта ошибка может тихо жить в кодовой базе в течение многих лет и проявиться, когда кто-то добавит вызов Update.Эта ошибка также может быть выражена объектно-ориентированным образом, и отладка становится ещё более мутной, когда мы получаем наш
IDisposable через внедрение зависимости (DI):public class Saver : IDisposable {
readonly IDbConnection _conn;
public Saver(IDbConnection conn) {
_conn = conn;
}
public void Save() {…}
public void Dispose() {
_conn.Dispose();
}
}
Опять же, разработчик пытается поступить правильно и очистить ресурсы после использования, но мы вводим неожиданные побочные эффекты в IDbConnection. Можно написать такой правильный код:using(var cn = new SqlConnection("…")) {
using(var sv = new Saver(cn)) {
sv.Save();
}
using(var upd = new Updater(cn)) {
upd.Update(); // ошибка
}
}
И опять трассировка стека приведет нас в Update. А если здесь ещё использовать контейнер DI для внедрения IDbConnection, то найти связь между методами сохранения и обновления может быть почти невозможно.Помимо вводящих в заблуждение исключений, действительно делает эту ошибку замечательной то, что она возникает, когда мы прилагаем дополнительные усилия, пытаясь поступить правильно. Моей внутренней реакцией на это будет раздражение и пассивно-агрессивное отношение к программе:
«Мне очень жаль, что я пытался очистить неуправляемые ресурсы, и это привело к ошибке. Я больше никогда так не буду делать.»
Итого
На первый взгляд
IDisposable выглядит просто, но, как и большинство других вещей в жизни, дьявол в деталях. Мы можем защитить себя и свои программы:- почаще читая документацию,
- подключая анализаторы кода для отслеживания вещей, которые компилятор не может уловить,
- следуя политике очистки, в которой тот, кто вызвал
new, должен вызвать и Dispose.Источник: https://www.lazy-electron.com/2021/03/06/favorite-idisposable-bugs.html
Автор оригинала - Ryan Davis