c# - unity - enumeraciones de ''rendimiento'' que no son ''finalizadas'' por la persona que llama-lo que sucede
yield return c# stack overflow (4)
Me acabo de quedar con una conexión filtrada.
No tu no eres.
¿Cuándo se invoca finalmente?
Cuando se IEnumerator<T>
el IEnumerator<T>
, lo First
que va a hacer después de obtener el primer elemento de la secuencia (como todos deberían estar haciendo cuando usan un IEnumerator<T>
).
Ahora si alguien escribió:
//note no `using` block on `iterator`
var iterator = Foo().GetEnumerator();
iterator.MoveNext();
var first = iterator.Current;
//note no disposal of iterator
luego se filtraría el recurso, pero allí el error está en el código de la persona que llama, no en el bloque del iterador.
supongo que tengo
IEnumerable<string> Foo()
{
try
{
/// open a network connection, start reading packets
while(moredata)
{
yield return packet;
}
}
finally
{
// close connection
}
}
(O tal vez hice un ''uso'' - lo mismo). ¿Qué pasa si mi interlocutor se va
var packet = Foo().First();
Me acabo de quedar con una conexión filtrada. ¿Cuándo se invoca finalmente? O lo correcto siempre sucede por arte de magia
editar con respuestas y pensamientos
Mi muestra y otros patrones de llamadas "normales" (por ejemplo, ...) funcionarán muy bien porque eliminan el IEnumerable (en realidad, el IEnumerator devuelto por GetEnumerator). Debo, por lo tanto, tener una persona que llama en algún lugar que está haciendo algo funky (obtener explícitamente un enumerador y no eliminarlo o algo similar). Los haré disparar
el código malo
Encontré un llamador haciendo
IEnumerator<T> enumerator = foo().GetEnumerator();
cambiado a
using(IEnumerator<T> enumerator = foo().GetEnumerator())
No hay magia especial. Si comprueba el documento en IEnumerator<T>
, encontrará que hereda de IDisposable
. La construcción foreach
, como saben, es azúcar sintáctico descompuesto por el compilador en una secuencia de operaciones en un enumerador, y todo se envuelve en un bloque try
/ finally
, llamando al objeto Dispose
on enumerator.
Cuando el compilador convierte un método de iterador (es decir, un método que contiene declaraciones de yield
) en una implementación de IEnumerable<T>
/ IEnumerator<T>
, maneja la lógica try
/ finally
en el método Dispose
de la clase generada.
Puede intentar usar ILDASM para analizar el código generado en su caso. Va a ser bastante complejo, pero te dará la idea.
No terminarías con una conexión filtrada. Los objetos IDisposable
producidos por yield return
son IDisposable
, y las funciones LINQ son cuidadosas para asegurar una eliminación adecuada.
Por ejemplo, First()
se implementa de la siguiente manera:
public static TSource First<TSource>(this IEnumerable<TSource> source) {
if (source == null) throw Error.ArgumentNull("source");
IList<TSource> list = source as IList<TSource>;
if (list != null) {
if (list.Count > 0) return list[0];
}
else {
using (IEnumerator<TSource> e = source.GetEnumerator()) {
if (e.MoveNext()) return e.Current;
}
}
throw Error.NoElements();
}
Tenga en cuenta cómo se envuelve el resultado de source.GetEnumerator()
. Esto asegura la llamada a Dispose
, que a su vez asegura la llamada de su código en el bloque finally
.
Lo mismo ocurre con las iteraciones por bucle foreach
: el código garantiza la eliminación del enumerador independientemente de si la enumeración se completa o no.
El único caso en el que puede terminar con una conexión filtrada es cuando llama a GetEnumerator
usted mismo y no lo elimina adecuadamente. Sin embargo, esto es un error en el código que usa IEnumerable
, no en el IEnumerable
.
Ok esta pregunta podría usar un poco de datos empíricos.
Utilizando VS2015 y un proyecto de borrador, escribí el siguiente código:
private IEnumerable<string> Test()
{
using (TestClass t = new TestClass())
{
try
{
System.Diagnostics.Debug.Print("1");
yield return "1";
System.Diagnostics.Debug.Print("2");
yield return "2";
System.Diagnostics.Debug.Print("3");
yield return "3";
System.Diagnostics.Debug.Print("4");
yield return "4";
}
finally
{
System.Diagnostics.Debug.Print("Finally");
}
}
}
private class TestClass : IDisposable
{
public void Dispose()
{
System.Diagnostics.Debug.Print("Disposed");
}
}
Y luego lo llamó de dos maneras:
foreach (string s in Test())
{
System.Diagnostics.Debug.Print(s);
if (s == "3") break;
}
string f = Test().First();
Que produce la siguiente salida de depuración
1
1
2
2
3
3
Finally
Disposed
1
Finally
Disposed
Como podemos ver, ejecuta tanto el bloque finally
como el método Dispose
.