2011-01-28 8 views
8

¿El método de extensión .Last() tiene en cuenta si se invoca en un IList? Me pregunto si hay una diferencia significativa entre el rendimiento siguientes:¿Llamar .Last() en un IList itera toda la lista?

IList<int> numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 

int lastNumber1 = numbers.Last(); 
int lastNumber2 = numbers[numbers.Count-1]; 

La intuición me dice que la primera alternativa es O (n), pero el segundo es O (1). ¿Es .Last() "inteligente" lo suficiente como para tratar de convertirlo a un IList?

Respuesta

19
Probablemente no

, ya que puede hacer list[list.count-1]

verificado por el reflector:

public static TSource Last<TSource>(this IEnumerable<TSource> source) 
{ 
    if (source == null) 
    { 
     throw Error.ArgumentNull("source"); 
    } 
    IList<TSource> list = source as IList<TSource>; 
    if (list != null) 
    { 
     int count = list.Count; 
     if (count > 0) 
     { 
      return list[count - 1]; 
     } 
    } 
    ... 
} 
+2

Gracias. Lo que realmente aprendí es que estoy loco por no haber instalado .NET Reflector, después de todo este tiempo. :) –

3

reflector:

public static TSource Last<TSource>(this IEnumerable<TSource> source) 
{ 
    ... 
    if (list != null) 
    { 
     int count = list.Count; 
     if (count > 0) 
     { 
      return list[count - 1]; 
     } 
    } 
    else 
    { 
     using (IEnumerator<TSource> enumerator = source.GetEnumerator()) 
     { 
      ... 
     } 
    } 
    throw Error.NoElements(); 
} 
1

Respuesta: Sí.

Aquí está una manera ordenada para averiguar:

class MyList<T> : IList<T> { 
    private readonly List<T> list = new List<T>(); 
    public T this[int index] { 
     get { 
      Console.WriteLine("Inside indexer!"); 
      return list[index]; 
     } 
     set { 
      list[index] = value; 
     } 
    } 

    public void Add(T item) { 
     this.list.Add(item); 
    } 

    public int Count { 
     get { 
      Console.WriteLine("Inside Count!"); 
      return this.list.Count; 
     } 
    } 

    // all other IList<T> interface members throw NotImplementedException 
} 

continuación:

MyList<int> list = new MyList<int>(); 
list.Add(1); 
list.Add(2); 
Console.WriteLine(list.Last()); 

Salida:

Inside Count! 
Inside indexer! 
2 

Si intenta esto:

Console.WriteLine(list.Last(n => n % 2 == 0)); 

luego obtiene una excepción en GetEnumerator que muestra que está tratando de recorrer la lista. Si ponemos en práctica a través de GetEnumerator

public IEnumerator<T> GetEnumerator() { 
    Console.WriteLine("Inside GetEnumerator"); 
    return this.list.GetEnumerator(); 
} 

y tratar de nuevo vemos

Inside GetEnumerator! 
2 

en la consola que demuestra que nunca fue utilizado el indexador.

6

Se trata de una optimización indocumentada, pero la sobrecarga sin predicados de Enumerable.Last se omite directamente hasta el final.

Tenga en cuenta que la sobrecarga con un predicado no solo va desde el final, trabajando hacia atrás como cabría esperar, va hacia adelante desde el principio. Creo que esto es para evitar inconsistencias cuando el predicado puede lanzar una excepción (o causar otros efectos secundarios).

Consulte mi blog post about implementing First/Last/Single etc para obtener más información, y una incoherencia que es presente entre las sobrecargas de Single/SingleOrDefault.

+0

Ese es un buen punto extra. También esperaría que lo hagan porque el predicado podría causar efectos secundarios (aparte de arrojar excepciones). –

+0

@Scott: cierto. He editado para incluir eso. Por otro lado, los indexadores/iteradores * podrían * tener efectos secundarios también ... es todo un desastre :( –

+0

wow, no pensé en eso. Si lo piensas, incluso si es un predicado hace una evaluación perezosa, no * quiere * que evalúe todos ellos ... ese es el punto de evaluación perezosa. Tiene razón, es un verdadero desastre. –

0

El cartel original está hablando de una interfaz, no de la implementación.

Por lo tanto, depende de la implementación subyacente detrás del IList/Ilist<T> en cuestión. No se sabe cómo se implementa su indexador.Creo que el marco List<T> tiene una implementación concreta que utiliza una matriz, por lo que es posible realizar una búsqueda directa, pero si todo lo que tiene es una referencia a IList<T>, eso no es un hecho por ningún medio.

+0

Técnicamente cierto, pero dado que la interfaz IList contiene un indexador, sería lógico pensar que en la mayoría de los casos la lista [i] sería o (1) (acceso directo) –

+1

Entonces ... algo que ** se ve ** como una matriz ** es ** una matriz? Creo que no. No confundas la interfaz con la implementación. Es seguro decir que _algunas_ de las clases marco estándar que implementan 'IList ' son O (1), pero eso está muy lejos de hacer una afirmación general. Sin saber nada sobre implementati en los detalles, no se puede decir nada sobre la pregunta tal como se plantea, con la advertencia de que, para su ejemplo, donde la implementación de respaldo es una matriz, es una apuesta segura Last() le dará O (1). –

+0

dice la verdad aquí. Hay al menos tantas implementaciones de 'IList ' que no son 'O (1)' ya que hay implementaciones que son 'O (1)'. Para cualquier implementación que sea 'O (1)' puedo simplemente envolverlo en una clase que implemente 'IList ' y duerma durante un período de tiempo proporcional a 'n' (aquí' n' es 'Count'). Hay muchas implementaciones realmente malas de 'IList '. Puede argumentar todo lo que quiera que estos no son realistas, pero el punto es que existen, y niegan cualquier afirmación de que 'IList ' es 'O (1)' acceso aleatorio. – jason

Cuestiones relacionadas