c# - IList<T> y IReadOnlyList<T>
.net collections (4)
Si tengo un método que requiere un parámetro que,
- Tiene una propiedad de
Count
- Tiene un indexador entero (get-only)
¿Cuál debería ser el tipo de este parámetro? Elegiría IList<T>
antes de .NET 4.5 ya que no había otra interfaz de colección indexable para esto y las matrices lo implementan, lo cual es una gran ventaja.
Pero .NET 4.5 presenta la nueva IReadOnlyList<T>
y también quiero que mi método admita eso. ¿Cómo puedo escribir este método para admitir tanto IList<T>
como IReadOnlyList<T>
sin violar los principios básicos como DRY?
Edit : la respuesta de Daniel me dio algunas ideas:
public void Foo<T>(IList<T> list)
=> Foo(list, list.Count, (c, i) => c[i]);
public void Foo<T>(IReadOnlyList<T> list)
=> Foo(list, list.Count, (c, i) => c[i]);
private void Foo<TList, TItem>(
TList list, int count, Func<TList, int, TItem> indexer)
where TList : IEnumerable<TItem>
{
// Stuff
}
Edición 2: O simplemente podría aceptar una IReadOnlyList<T>
y proporcionar un ayudante como este:
public static class CollectionEx
{
public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> list)
{
if (list == null)
throw new ArgumentNullException(nameof(list));
return list as IReadOnlyList<T> ?? new ReadOnlyWrapper<T>(list);
}
private sealed class ReadOnlyWrapper<T> : IReadOnlyList<T>
{
private readonly IList<T> _list;
public ReadOnlyWrapper(IList<T> list) => _list = list;
public int Count => _list.Count;
public T this[int index] => _list[index];
public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
Entonces podría llamarlo como Foo(list.AsReadOnly())
Edición 3: Las matrices implementan tanto IList<T>
como IReadOnlyList<T>
, al igual que la clase List<T>
. Esto hace que sea bastante raro encontrar una clase que implemente IList<T>
pero no IReadOnlyList<T>
.
Dado que IList<T>
e IReadOnlyList<T>
no comparten ningún "antecesor" útil, y si no desea que su método acepte ningún otro tipo de parámetro, lo único que puede hacer es proporcionar dos sobrecargas.
Si decide que la reutilización de los códigos es una prioridad máxima, puede hacer que estas sobrecargas reenvíen la llamada a un método private
que acepte IEnumerable<T>
y use LINQ de la manera que Daniel sugiere, permitiendo que LINQ realice la normalización en tiempo de ejecución.
Sin embargo, en mi humilde opinión, probablemente sería mejor simplemente copiar / pegar el código una vez y solo mantener dos sobrecargas independientes que difieran solo en el tipo de argumento; No creo que la microarquitectura de esta escala ofrezca algo tangible, y por otro lado requiere maniobras no obvias y es más lenta.
Lo que necesita es el IReadOnlyCollection<T>
disponible en .Net 4.5 que es esencialmente un IEnumerable<T>
que tiene Count
como la propiedad, pero si también necesita indexación, también necesita el IReadOnlyList<T>
que también proporcionaría un indexador.
No sé sobre usted, pero creo que esta interfaz es una necesidad que había faltado durante mucho tiempo.
Si está más preocupado por mantener el principio de DRY sobre el rendimiento, puede usar dynamic
, como:
public void Do<T>(IList<T> collection)
{
DoInternal(collection, collection.Count, i => collection[i]);
}
public void Do<T>(IReadOnlyList<T> collection)
{
DoInternal(collection, collection.Count, i => collection[i]);
}
private void DoInternal(dynamic collection, int count, Func<int, T> indexer)
{
// Get the count.
int count = collection.Count;
}
Sin embargo, no puedo decir de buena fe que recomendaría esto, ya que las trampas son demasiado grandes:
- Cada llamada en la
collection
enDoInternal
se resolverá en tiempo de ejecución. Pierdes seguridad de tipo, controles de tiempo de compilación, etc. - La degradación del rendimiento (aunque no es grave, para el caso singular, pero puede ser cuando se agrega) ocurrirá
La sugerencia de su ayudante es la más útil, pero creo que debería darle la vuelta; IReadOnlyList<T>
que la IReadOnlyList<T>
se introdujo en .NET 4.5, muchas API no tienen soporte para ello, pero tienen soporte para la interfaz IList<T>
.
Dicho esto, debe crear un contenedor AsList
, que toma un IReadOnlyList<T>
y devuelve un contenedor en una implementación IList<T>
.
Sin embargo, si desea enfatizar en su API que está tomando una IReadOnlyList<T>
(para enfatizar el hecho de que no está mutando los datos), entonces la extensión AsReadOnlyList
que tiene ahora sería más apropiada, pero yo d hacer la siguiente optimización para AsReadOnly
:
public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> collection)
{
if (collection == null)
throw new ArgumentNullException("collection");
// Type-sniff, no need to create a wrapper when collection
// is an IReadOnlyList<T> *already*.
IReadOnlyList<T> list = collection as IReadOnlyList<T>;
// If not null, return that.
if (list != null) return list;
// Wrap.
return new ReadOnlyWrapper<T>(collection);
}
Usted está fuera de suerte aquí. IList<T>
no implementa IReadOnlyList<T>
. List<T>
implementa ambas interfaces, pero creo que eso no es lo que quieres.
Sin embargo, puedes usar LINQ:
- El método de extensión
Count()
verifica internamente si la instancia es de hecho una colección y luego usa la propiedadCount
. - El método de extensión
ElementAt()
verifica internamente si la instancia es de hecho una lista y luego utiliza el indexador.