Работа с вложенными выражениями "using" в С#
Я заметил, что уровень вложенных операторов using
в последнее время увеличился в моем коде. Причина, вероятно, связана с тем, что я использую все больше шаблонов async/await
, которые часто добавляют по меньшей мере еще один using
для CancellationTokenSource
или CancellationTokenRegistration
.
Итак, как уменьшить вложенность using
, поэтому код не похож на елку? Схожие вопросы были заданы на SO раньше, и я хотел бы обобщить то, что я узнал из ответов.
Используйте смежный using
без отступов. Поддельный пример:
using (var a = new FileStream())
using (var b = new MemoryStream())
using (var c = new CancellationTokenSource())
{
// ...
}
Это может работать, но часто существует код между using
(например, может быть слишком рано создавать другой объект):
// ...
using (var a = new FileStream())
{
// ...
using (var b = new MemoryStream())
{
// ...
using (var c = new CancellationTokenSource())
{
// ...
}
}
}
Объединить объекты одного типа (или отличить от IDisposable
) в один using
, например:
// ...
FileStream a = null;
MemoryStream b = null;
CancellationTokenSource c = null;
// ...
using (IDisposable a1 = (a = new FileStream()),
b1 = (b = new MemoryStream()),
c1 = (c = new CancellationTokenSource()))
{
// ...
}
У этого те же ограничения, что и выше, плюс более многословный и менее читаемый, IMO.
Рефакторинг метода в несколько методов.
Это, по-моему, предпочтительный способ. Тем не менее, мне любопытно, почему было бы считаться плохой практикой?
public class DisposableList : List<IDisposable>, IDisposable
{
public void Dispose()
{
base.ForEach((a) => a.Dispose());
base.Clear();
}
}
// ...
using (var disposables = new DisposableList())
{
var a = new FileStream();
disposables.Add(a);
// ...
var b = new MemoryStream();
disposables.Add(b);
// ...
var c = new CancellationTokenSource();
disposables.Add(c);
// ...
}
[UPDATE] В комментариях имеется немало действительных точек, которые вставляют инструкции using
, чтобы Dispose
вызывался на каждый объект, даже если некоторые внутренние вызовы Dispose
, Тем не менее, существует несколько неясная проблема: все вложенные исключения, которые могут быть сброшены путем размещения вложенных "используемых" кадров, будут потеряны, кроме самого внешнего. Подробнее об этом здесь.
Ответы
Ответ 1
В одном методе первым вариантом будет мой выбор. Однако в некоторых случаях полезно использовать DisposableList
. В частности, если у вас много одноразовых полей, которые необходимо удалить (в этом случае вы не можете использовать using
). Реализованная реализация - хорошее начало, но у нее есть несколько проблем (указано в комментариях Алексея):
- Требуется не забудьте добавить элемент в список. (Хотя вы также можете сказать, что вам нужно запомнить
using
.)
- Прерывает процесс удаления, если один из методов удаления выбрасывается, оставляя оставшиеся предметы неактивными.
Пусть исправить эти проблемы:
public class DisposableList : List<IDisposable>, IDisposable
{
public void Dispose()
{
if (this.Count > 0)
{
List<Exception> exceptions = new List<Exception>();
foreach(var disposable in this)
{
try
{
disposable.Dispose();
}
catch (Exception e)
{
exceptions.Add(e);
}
}
base.Clear();
if (exceptions.Count > 0)
throw new AggregateException(exceptions);
}
}
public T Add<T>(Func<T> factory) where T : IDisposable
{
var item = factory();
base.Add(item);
return item;
}
}
Теперь мы извлекаем любые исключения из вызовов Dispose
и будем бросать новый AggregateException
после прохождения всех элементов. Я добавил вспомогательный метод Add
, который позволяет более простое использование:
using (var disposables = new DisposableList())
{
var file = disposables.Add(() => File.Create("test"));
// ...
var memory = disposables.Add(() => new MemoryStream());
// ...
var cts = disposables.Add(() => new CancellationTokenSource());
// ...
}
Ответ 2
Вы всегда должны ссылаться на ваш поддельный пример. Если это невозможно, как вы упомянули, то очень вероятно, что вы можете реорганизовать внутренний контент в отдельный метод. Если это также не имеет смысла, вы должны просто придерживаться своего второго примера. Все остальное просто кажется менее читаемым, менее очевидным и менее распространенным кодом.
Ответ 3
Я бы придерживался используемых блоков. Почему?
- Это ясно показывает ваши намерения с этими объектами.
- Вам не нужно возиться с блоками try-finally. Он подвержен ошибкам, и ваш код становится менее читаемым.
- Вы можете повторно использовать встроенные репозитории с помощью утверждений (извлеките их в методы)
- Вы не путаете своих коллег-программистов, включая новый слой абстракций, создавая свою собственную логику.
Ответ 4
Ваше последнее предложение скрывает тот факт, что a
, b
и c
должны быть удалены явно. Вот почему это уродливо.
Как упоминалось в моем комментарии, если вы будете использовать принципы чистого кода, вы не столкнетесь с этими проблемами (обычно).
Ответ 5
Другой вариант - просто использовать блок try-finally
. Это может показаться немного многословным, но оно сокращает ненужное вложенность.
FileStream a = null;
MemoryStream b = null;
CancellationTokenSource c = null;
try
{
a = new FileStream();
// ...
b = new MemoryStream();
// ...
c = new CancellationTokenSource();
}
finally
{
if (a != null) a.Dispose();
if (b != null) b.Dispose();
if (c != null) c.Dispose();
}