c# - query - Lista<IEnumerator>.All(e=> e.MoveNext()) no mueve mis enumeradores
linq where c# example (6)
Estoy tratando de localizar un error en nuestro código. Lo he reducido al fragmento de abajo. En el siguiente ejemplo, tengo una cuadrícula de ints (una lista de filas), pero quiero encontrar los índices de las columnas que tienen un 1. La implementación de esto es crear un enumerador para cada fila y recorrer cada columna Gire manteniendo los enumeradores en el paso.
class Program
{
static void Main(string[] args)
{
var ints = new List<List<int>> {
new List<int> {0, 0, 1}, // This row has a 1 at index 2
new List<int> {0, 1, 0}, // This row has a 1 at index 1
new List<int> {0, 0, 1} // This row also has a 1 at index 2
};
var result = IndexesWhereThereIsOneInTheColumn(ints);
Console.WriteLine(string.Join(", ", result)); // Expected: "1, 2"
Console.ReadKey();
}
private static IEnumerable<int> IndexesWhereThereIsOneInTheColumn(
IEnumerable<List<int>> myIntsGrid)
{
var enumerators = myIntsGrid.Select(c => c.GetEnumerator()).ToList();
short i = 0;
while (enumerators.All(e => e.MoveNext())) {
if (enumerators.Any(e => e.Current == 1))
yield return i;
i++;
if (i > 1000)
throw new Exception("You have gone too far!!!");
}
}
}
Sin embargo, me he dado cuenta de que MoveNext()
no se recuerda cada vez que se realiza el bucle while. MoveNext()
siempre devuelve true, y Current
es siempre 0. ¿Es esta una característica útil de Linq para que sea más libre de efectos secundarios?
Me di cuenta de que esto funciona:
private static IEnumerable<int> IndexesWhereThereIsOneInTheColumn(
IEnumerable<List<int>> myIntsGrid)
{
var enumerators = myIntsGrid.Select(c =>
c.ToArray().GetEnumerator()).ToList(); // added ToArray()
short i = 0;
while (enumerators.All(e => e.MoveNext())) {
if (enumerators.Any(e => (int)e.Current == 1)) // added cast to int
yield return i;
i++;
}
}
Entonces, ¿esto es solo un problema con List?
¿Por qué no puedes obtener esos índices como este?
var result = ints.Select (i => i.IndexOf(1)).Distinct().OrderBy(i => i);
Parece ser mucho más fácil ...
Alternativamente, podrías hacerlo con una extensión lambda.
var ids = Enumerable.Range(0,ints.Max (row => row.Count)).
Where(col => ints.Any(row => (row.Count>col)? row[col] == (1) : false));
o
var ids = Enumerable.Range(0,ints.Max (row=> row.Count)).
Where(col => ints.Any (row => row.ElementAtOrDefault(col) == 1));
Aquí hay una implementación simple usando bucles y yield
:
private static IEnumerable<int> IndexesWhereThereIsOneInTheColumn(
IEnumerable<List<int>> myIntsGrid)
{
for (int i=0; myIntsGrid.Max(l=>l.Count) > i;i++)
{
foreach(var row in myIntsGrid)
{
if (row.Count > i && row[i] == 1)
{
yield return i;
break;
}
}
}
}
Alternativamente, use esto dentro del bucle for
:
if (myIntsGrid.Any(row => row.Count > i && row[i] == 1)) yield return i;
Como la respuesta de Sriram Sakthivel dice que el problema se debe a la falta de boxeo y, accidentalmente, la implementación del enumerador de listas es una struct
, no un tipo de referencia . Por lo general, uno no esperaría el comportamiento de tipo de valor para un enumerador, ya que la mayoría están expuestos por las IEnumerator
/ IEnumerator<T>
o son tipos de referencia en sí mismos. Una forma rápida de solucionar esto es cambiar esta línea.
var enumerators = myIntsGrid.Select(c => c.GetEnumerator()).ToList();
a
var enumerators
= myIntsGrid.Select(c => (IEnumerator) c.GetEnumerator()).ToList();
en lugar.
El código anterior construirá una lista de enumeradores ya encuadrados , que se tratarán como instancias de tipo de referencia, debido a la conversión de la interfaz. A partir de ese momento, deben comportarse como esperas en tu código posterior.
Si necesita un enumerador genérico (para evitar las conversiones cuando este último usa el enumerator.Current
Propiedad IEnumerator<T>
), puede realizar la IEnumerator<T>
interfaz genérica apropiada de IEnumerator<T>
:
c => (IEnumerator<int>) c.GetEnumerator()
o mejor
c => c.GetEnumerator() as IEnumerator<int>
Se dice que la palabra clave as
se desempeña mucho mejor que los lanzamientos directos, y en el caso de un bucle podría aportar un beneficio de rendimiento esencial. Solo tenga cuidado de que as
devuelve un null
si la conversión falla según la solicitud de Flater en los comentarios:. En el caso del OP, está garantizado que el enumerador implementa IEnumerator<int>
, por lo que es seguro IEnumerator<int>
as
cast.
Esto se debe a que el enumerador de la List<T>
es una struct
mientras que el enumerador de Array
es una class
.
Entonces, cuando llama a Enumerable.All
con la estructura, se hace una copia del enumerador y se pasa como un parámetro a Func
ya que las estructuras se copian por valor. Así que e.MoveNext
se llama en la copia, no el original.
Prueba esto:
Console.WriteLine(new List<int>().GetEnumerator().GetType().IsValueType);
Console.WriteLine(new int[]{}.GetEnumerator().GetType().IsValueType);
Se imprime:
True
False
Solo por diversión, aquí hay una consulta LINQ que no causará efectos secundarios difíciles de rastrear en su código:
IEnumerable<int> IndexesWhereThereIsOneInTheColumn(IEnumerable<IEnumerable<int>> myIntsGrid)
{
return myIntsGrid
// Collapse the rows into a single row of the maximum value of all rows
.Aggregate((acc, x) => acc.Zip(x, Math.Max))
// Enumerate the row
.Select((Value,Index) => new { Value, Index })
.Where(x => x.Value == 1)
.Select(x => x.Index);
}