Работа с вложенными выражениями "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();
}