2012-02-08 10 views
13

Hay momentos en los que es útil comprobar no repetibleIEnumerable para ver si está vacío o no. El Any de LINQ no funciona bien para esto, ya que consume el primer elemento de la secuencia, p.Comprobación segura de IEnumerables no repetibles para vacío

if(input.Any()) 
{ 
    foreach(int i in input) 
    { 
     // Will miss the first element for non-repeatable sequences! 
    } 
} 

(Nota: Estoy consciente de que no hay necesidad de hacer el registro de entrada en este caso - es sólo un ejemplo El ejemplo del mundo real está realizando una Zip contra un derecho IEnumerable que pueden ser potencialmente! . vacía si está vacío, quiero que el resultado sea de la izquierda IEnumerable tal cual)

yo he llegado con una solución potencial que tiene este aspecto:.

private static IEnumerable<T> NullifyIfEmptyHelper<T>(IEnumerator<T> e) 
{ 
    using(e) 
    { 
     do 
     { 
      yield return e.Current; 
     } while (e.MoveNext()); 
    } 
} 

public static IEnumerable<T> NullifyIfEmpty<T>(this IEnumerable<T> source) 
{ 
    IEnumerator<T> e = source.GetEnumerator(); 
    if(e.MoveNext()) 
    { 
     return NullifyIfEmptyHelper(e); 
    } 
    else 
    { 
     e.Dispose(); 
     return null; 
    } 
} 

esto puede entonces ser utilizado de la siguiente manera:

input = input.NullifyIfEmpty(); 
if(input != null) 
{ 
    foreach(int i in input) 
    { 
     // Will include the first element. 
    } 
} 

Tengo dos preguntas sobre este:

1) ¿Es esta una cosa razonable a hacer? ¿Es probable que sea problemático desde el punto de vista del rendimiento? (Supongo que no, pero vale la pena preguntar.)

2) ¿Hay una mejor manera de lograr el mismo objetivo final?


editar # 1:

Aquí está un ejemplo de un no repetible IEnumerable, para aclarar:

private static IEnumerable<int> ReadNumbers() 
{ 
    for(;;) 
    { 
     int i; 
     if (int.TryParse(Console.ReadLine(), out i) && i != -1) 
     { 
      yield return i; 
     } 
     else 
     { 
      yield break; 
     } 
    } 
} 

Básicamente, las cosas que provienen de entrada de usuario o una corriente, etc.

EDIT # 2:

Tengo que aclarar que estoy buscando una solución que conserva la perezosa naturaleza del IEnumerable - convirtiéndola en una lista o una matriz puede ser una respuesta en ciertas circunstancias, pero no es lo que estoy buscando aquí. (La razón del mundo real es que el número de elementos en el IEnumerable puede ser enorme en mi caso, y es importante no almacenarlos todos en la memoria a la vez.)

+1

Eso es un enfoque inteligente. Me interesaría escuchar los comentarios de los demás, pero a mí me parece una forma bastante elegante de resolver su problema. –

+0

No estoy seguro de lo que quiere decir con "no repetible ienumerable". ¿Tienes un ejemplo? El foreach en su primer ejemplo de código no debe perderse el primer elemento, simplemente léalo por segunda vez. No entiendo en qué situación eso no sería posible. –

+0

@ Meta-Knight: He agregado un ejemplo, espero que ayude. –

Respuesta

2

No es necesario complicar la misma. Un bucle normal de foreach con una sola variable extra bool hará el truco.

Si tiene

if(input.Any()) 
{ 
    A 
    foreach(int i in input) 
    { 
     B 
    } 
    C 
} 

y no desea leer input dos veces, usted puede cambiar esto a

bool seenItem = false; 
foreach(int i in input) 
{ 
    if (!seenItem) 
    { 
     seenItem = true; 
     A 
    } 
    B 
} 
if (seenItem) 
{ 
    C 
} 

Dependiendo de lo B hace, es posible que pueda evitar la seenItem variable por completo.

En su caso, Enumerable.Zip es una función bastante básica que se puede volver a implementar fácilmente, y su función de reemplazo puede usar algo similar a lo anterior.

Edición: Usted podría considerar

public static class MyEnumerableExtensions 
{ 
    public static IEnumerable<TFirst> NotReallyZip<TFirst, TSecond>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, TFirst> resultSelector) 
    { 
     using (var firstEnumerator = first.GetEnumerator()) 
     using (var secondEnumerator = second.GetEnumerator()) 
     { 
      if (secondEnumerator.MoveNext()) 
      { 
       if (firstEnumerator.MoveNext()) 
       { 
        do yield return resultSelector(firstEnumerator.Current, secondEnumerator.Current); 
        while (firstEnumerator.MoveNext() && secondEnumerator.MoveNext()); 
       } 
      } 
      else 
      { 
       while (firstEnumerator.MoveNext()) 
        yield return firstEnumerator.Current; 
      } 
     } 
    } 
} 
+0

+1 esto es bastante útil. De hecho, escribí una versión de 'Zip' que hizo algo como esto cuando estaba probando ideas, pero terminé pensando que sería mejor encontrar una solución más generalmente reutilizable. –

+1

@StuartGolodetz En ese caso, no veo cómo se puede llegar a una solución general mejor que la que ya tiene. :) – hvd

3

También podría leer el primer elemento y si no es nula, concatenar este primer elemento con el resto de la entrada:

var input = ReadNumbers(); 
var first = input.FirstOrDefault(); 
if (first != default(int)) //Assumes input doesn't contain zeroes 
{ 
    var firstAsArray = new[] {first}; 
    foreach (int i in firstAsArray.Concat(input)) 
    { 
     // Will include the first element. 
     Console.WriteLine(i); 
    } 
} 

para una, el primer elemento enumerables normal sería repetida dos veces, pero para un enumerable no repetible funcionaría, a menos que la iteración de dos veces es No permitido. Además, si usted tenía un enumerador como:

private readonly static List<int?> Source = new List<int?>(){1,2,3,4,5,6}; 

private static IEnumerable<int?> ReadNumbers() 
{ 
    while (Source.Count > 0) { 
     yield return Source.ElementAt(0); 
     Source.RemoveAt(0); 
    } 
} 

entonces sería imprimir: 1, 1, 2, 3, 4, 5, 6. La razón es que el primer elemento se consume después de que haya sido devuelto. Entonces, el primer enumerador, deteniéndose en el primer elemento, nunca tiene la posibilidad de consumir ese primer elemento. Pero sería un caso de un enumerador mal escrito, aquí. Si se consume el elemento, y luego regresó ...

while (Source.Count > 0) { 
    var returnElement = Source.ElementAt(0); 
    Source.RemoveAt(0); 
    yield return returnElement; 
} 

... se obtiene la salida esperada de: 1, 2, 3, 4, 5, 6.

+4

Y si el primer elemento de la entrada == 0? – Oded

+1

Sí, esto solo funcionaría realmente para los tipos de referencia. Sin embargo, sí sugiere una versión más concisa de la solución del OP ... –

+0

@Oded: si se trata de un tipo de valor, y el valor predeterminado puede aparecer en la secuencia, puede usar un tipo anulable para el enumerable. –

1

Esto no es una solución eficaz si la enumeración es larga, sin embargo, es una solución fácil:

var list = input.ToList(); 
if (list.Count != 0) { 
    foreach (var item in list) { 
     ... 
    } 
} 
+0

Desafortunadamente no es una solución perezosa: si la usa con 'ReadNumbers', esperará hasta que el usuario haya dejado de ingresar números antes de hacer cualquier cosa. +1 aunque de todos modos porque no lo dejé claro en la pregunta. –

Cuestiones relacionadas