c#

Tratar con declaraciones de "uso" anidadas en C#



(5)

He notado que el nivel de declaraciones de using anidadas últimamente ha aumentado en mi código. La razón probablemente se deba a que uso cada vez más el patrón async/await , que a menudo agrega al menos uno más para CancellationTokenSource o CancellationTokenRegistration .

Entonces, ¿cómo reducir la anidación del using , por lo que el código no se parece al árbol de Navidad? Preguntas similares se han formulado anteriormente en SO, y me gustaría resumir lo que he aprendido de las respuestas.

Uso adyacente sin sangría. Un ejemplo falso:

using (var a = new FileStream()) using (var b = new MemoryStream()) using (var c = new CancellationTokenSource()) { // ... }

Esto puede funcionar, pero a menudo hay algún código entre el using (por ejemplo, puede ser demasiado pronto para crear otro objeto):

// ... using (var a = new FileStream()) { // ... using (var b = new MemoryStream()) { // ... using (var c = new CancellationTokenSource()) { // ... } } }

Combine objetos del mismo tipo (o IDisposable a IDisposable ) en un solo using , por ejemplo:

// ... FileStream a = null; MemoryStream b = null; CancellationTokenSource c = null; // ... using (IDisposable a1 = (a = new FileStream()), b1 = (b = new MemoryStream()), c1 = (c = new CancellationTokenSource())) { // ... }

Esto tiene la misma limitación que la anterior, además es más correcto y menos legible, IMO.

Refactorizar el método en algunos métodos.

Esta es una forma preferida , por lo que yo entiendo. Sin embargo, tengo curiosidad, ¿por qué lo siguiente sería considerado una mala práctica?

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); // ... }

[ACTUALIZACIÓN] Hay bastantes puntos válidos en los comentarios que indica que anidando using declaraciones asegura que se llamará a Dispose en cada objeto, incluso si se lanzan algunas llamadas Dispose internas. Sin embargo, hay un problema un tanto oscuro: todas las excepciones anidadas posiblemente lanzadas al deshacerse de marcos anidados de "uso" se perderán, además del más externo. Más sobre esto here .


En un solo método, la primera opción sería mi elección. Sin embargo, en algunas circunstancias, la DisposableList es útil. En particular, si tiene muchos campos desechables de los cuales todos deben eliminarse (en cuyo caso no puede usar). La implementación dada es un buen comienzo, pero tiene algunos problemas (señalados en los comentarios de Alexei):

  1. Requiere que recuerdes agregar el artículo a la lista. (Aunque también podría decir que debe recordar usar).
  2. Aborta el proceso de eliminación si uno de los métodos de desecho se lanza, dejando los elementos restantes sin desechar.

Arreglemos esos problemas:

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; } }

Ahora detectamos las excepciones de las llamadas a Dispose y lanzaremos una nueva AggregateException después de revisar todos los elementos. He agregado un método de adición de ayuda que permite un uso más simple:

using (var disposables = new DisposableList()) { var file = disposables.Add(() => File.Create("test")); // ... var memory = disposables.Add(() => new MemoryStream()); // ... var cts = disposables.Add(() => new CancellationTokenSource()); // ... }


Me quedaría con los bloques de uso. ¿Por qué?

  • Muestra claramente tus intenciones con estos objetos.
  • No tienes que perder el tiempo con los bloques try-finally. Es propenso a errores y su código se vuelve menos legible.
  • Puede refactorizar incrustado usando declaraciones más tarde (extráigalas a métodos)
  • No confundas a tus compañeros programadores, incluyendo una nueva capa de abstracciones, creando tu propia lógica.

Otra opción es simplemente usar un bloque try-finally . Esto puede parecer un poco detallado, pero reduce el anidamiento innecesario.

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(); }


Siempre debe referirse a su ejemplo falso. Cuando esto no es posible, como mencionaste, es muy probable que puedas refactorizar el contenido interno en un método separado. Si esto tampoco tiene sentido, solo debes apegarte a tu segundo ejemplo. Todo lo demás parece un código menos legible, menos obvio y también menos común.


Su última sugerencia oculta el hecho de que a , c deben eliminarse explícitamente. Por eso es feo.

Como mencioné en mi comentario, si usara principios de código limpio, no se encontraría con estos problemas (generalmente).