2010-09-27 8 views
22

Quiero comprobar que un IEnumerable contiene exactamente un elemento. Este fragmento de código funciona:¿Cómo me aseguro de que una secuencia tenga una cierta duración?

bool hasOneElement = seq.Count() == 1 

Sin embargo, no es muy eficiente, ya que Count() a enumerar la lista completa. Obviamente, saber que una lista está vacía o contiene más de un elemento significa que no está vacía. ¿Hay algún método de extensión que tenga este comportamiento de cortocircuito?

+0

Creo que el error de lógica que hice en mi código puede haber arrojado personas con respecto a * al menos * y * exactamente *. Creo que hemos llegado a la mejor solución ahora. – guhou

+1

Count() solo repetirá la lista completa si determina que el IEnumerable pasado no se transfiere a ICollection . Por lo tanto, si pasa una instancia de List o una matriz, no se repetirá. –

+1

Creo que esto es una optimización prematura. –

Respuesta

20

Esto debe hacerlo:

public static bool ContainsExactlyOneItem<T>(this IEnumerable<T> source) 
{ 
    using (IEnumerator<T> iterator = source.GetEnumerator()) 
    { 
     // Check we've got at least one item 
     if (!iterator.MoveNext()) 
     { 
      return false; 
     } 
     // Check we've got no more 
     return !iterator.MoveNext(); 
    } 
} 

Usted podría eludir esto más, pero no sugerimos que lo haga:

public static bool ContainsExactlyOneItem<T>(this IEnumerable<T> source) 
{ 
    using (IEnumerator<T> iterator = source.GetEnumerator()) 
    { 
     return iterator.MoveNext() && !iterator.MoveNext(); 
    } 
} 

Es el tipo de truco que es funky, pero probablemente no debería usarse en el código de producción. Simplemente no es lo suficientemente claro. El hecho de que se requiere el efecto secundario en el LHS del operador & & para el RHS para trabajar adecuadamente es sólo desagradable ... mientras que un montón de diversión;)

EDIT: acabo de ver que haya venido con exactamente lo mismo, pero para una longitud arbitraria. Sin embargo, su declaración final de devolución está equivocada, debería ser la devolución !en.MoveNext(). He aquí un método completo con un nombre más bonito (OMI), chequeo de argumentos y la optimización de ICollection/ICollection<T>:

public static bool CountEquals<T>(this IEnumerable<T> source, int count) 
{ 
    if (source == null) 
    { 
     throw new ArgumentNullException("source"); 
    } 
    if (count < 0) 
    { 
     throw new ArgumentOutOfRangeException("count", 
               "count must not be negative"); 
    } 
    // We don't rely on the optimizations in LINQ to Objects here, as 
    // they have changed between versions. 
    ICollection<T> genericCollection = source as ICollection<T>; 
    if (genericCollection != null) 
    { 
     return genericCollection.Count == count; 
    } 
    ICollection nonGenericCollection = source as ICollection; 
    if (nonGenericCollection != null) 
    { 
     return nonGenericCollection.Count == count; 
    } 
    // Okay, we're finally ready to do the actual work... 
    using (IEnumerator<T> iterator = source.GetEnumerator()) 
    { 
     for (int i = 0; i < count; i++) 
     { 
      if (!iterator.MoveNext()) 
      { 
       return false; 
      } 
     } 
     // Check we've got no more 
     return !iterator.MoveNext(); 
    } 
} 

EDIT: Y ahora para los fanáticos funcionales, una forma recursiva de CountEquals (no utilizan por favor este , es sólo aquí por diversión):

public static bool CountEquals<T>(this IEnumerable<T> source, int count) 
{ 
    if (source == null) 
    { 
     throw new ArgumentNullException("source"); 
    } 
    if (count < 0) 
    { 
     throw new ArgumentOutOfRangeException("count", 
               "count must not be negative"); 
    } 
    using (IEnumerator<T> iterator = source.GetEnumerator()) 
    { 
     return IteratorCountEquals(iterator, count); 
    } 
} 

private static bool IteratorCountEquals<T>(IEnumerator<T> iterator, int count) 
{ 
    return count == 0 ? !iterator.MoveNext() 
     : iterator.MoveNext() && IteratorCountEquals(iterator, count - 1); 
} 

EDIT: Tenga en cuenta que para algo como LINQ a SQL, debe utilizar el enfoque simple Count() - ya que va a permitir que se realiza en la base de datos en lugar de después de ir a buscar resultados actuales.

+1

Sí, es feo, y sí, es hermoso :-) Pero no, no es muy obvio cuando ves este código por primera vez. –

+3

LOL @Jon para su 'return iterator.MoveNext() &&! Iterator.MoveNext(); '. ¡La respuesta de mí es "ambos"! – mauris

+2

+1 iterator.MoveNext() &&! Iterator.MoveNext() wow! – onof

7

No, pero se puede escribir uno mismo:

public static bool HasExactly<T>(this IEnumerable<T> source, int count) 
{ 
    if(source == null) 
     throw new ArgumentNullException("source"); 

    if(count < 0) 
     return false; 

    return source.Take(count + 1).Count() == count; 
} 

EDIT: Se ha cambiado desde al menos a exactamente después de la clarificación.

Para una solución más general y eficaz (que utiliza sólo el 1 empadronador y comprueba si la secuencia implementa ICollection o ICollection<T> en cuyo caso la enumeración no es necesario), es posible que desee echar un vistazo a mi respuesta here, que le permite especifique si está buscando Exact, AtLeast, o AtMost pruebas.

+0

¿aún no terminas enumerando toda la lista en ese fragmento? (debido a 'Take' y luego 'Count') – guhou

+0

No estoy pidiendo * al menos *, estoy pidiendo * exactamente *. – guhou

+0

@ BleuM937: Editado. – Ani

0

Creo que lo que estás buscando es .Single(). Cualquier cosa que no sea exactamente una lanzará InvalidOperationException que puedes atrapar.

http://msdn.microsoft.com/nb-no/library/bb155325.aspx

+1

Supongo que lo que intento hacer se puede lograr con esto, pero no me gusta arrojar excepciones para el flujo de control. – guhou

+1

Luego use SingleOrDefault() y verifique si el valor de retorno es nulo usted mismo. – danijels

+0

SingleOrDefault seguirá generando una excepción si hay más de un elemento en la lista. No creo que este sea el camino a seguir, el código con el enumerador debería ser más eficiente. – SamStephens

4

seq.Skip(1).Any() le dirá si la lista tiene cero o un elemento.

Creo que la edición que ha realizado es sobre la forma más eficaz de comprobar la longitud es n. Pero hay una falla lógica, los elementos con una longitud inferior a la verdadera devolverán. Vea lo que he hecho a la segunda declaración de devolución.

public static bool LengthEquals<T>(this IEnumerable<T> en, int length) 
    { 
     using (var er = en.GetEnumerator()) 
     { 
      for (int i = 0; i < length; i++) 
      { 
       if (!er.MoveNext()) 
        return false; 
      } 
      return !er.MoveNext(); 
     } 
    } 
+0

Sí, eso mismo me estaba dando cuenta. Prefiero tu nombre de método, pero mis nombres de variables :) - de hecho, he decidido que prefiero CountEquals después de todo, ya que coincide con el método Count() mejor :) –

+0

Sí, CountEquals suena bien para mí, también. – SamStephens

1

¿Qué le parece esto?

public static bool CountEquals<T>(this IEnumerable<T> source, int count) { 
    return source.Take(count + 1).Count() == count; 
} 

El Take() se asegurará de que nunca llamamos MoveNext más de count+1 veces.

me gustaría tener en cuenta que para cualquier instancia de ICollection, la implementación original source.Count() == count debería ser más rápido porque está optimizado para Count() basta con ver el miembro de Count.

Cuestiones relacionadas