c# .net collections extension-methods

c# - Cómo eliminar condicionalmente elementos de una colección.NET



collections extension-methods (5)

Estoy intentando escribir un método de extensión en .NET que operará en una colección genérica y eliminar todos los elementos de la colección que coincidan con un criterio dado.

Este fue mi primer intento:

public static void RemoveWhere<T>(this ICollection<T> Coll, Func<T, bool> Criteria){ foreach (T obj in Coll.Where(Criteria)) Coll.Remove(obj); }

Sin embargo, esto produce una excepción InvalidOperationException, "la colección se modificó; la operación de enumeración puede no ejecutarse". Lo que sí tiene sentido, así que hice un segundo intento con una segunda variable de colección para contener los elementos que deben eliminarse e iterar a través de eso:

public static void RemoveWhere<T>(this ICollection<T> Coll, Func<T, bool> Criteria){ List<T> forRemoval = Coll.Where(Criteria).ToList(); foreach (T obj in forRemoval) Coll.Remove(obj); }

Esto arroja la misma excepción; No estoy seguro de entender realmente por qué, como ''Coll'' ya no es la colección que se está iterando, ¿por qué no puede modificarse?

Si alguien tiene alguna sugerencia sobre cómo puedo hacer que esto funcione, o una mejor manera de lograr lo mismo, sería genial.

Gracias.


Acabo de probar tu segundo ejemplo y parece funcionar bien:

Collection<int> col = new Collection<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; col.RemoveWhere(x => x % 2 != 0); foreach (var x in col) Console.WriteLine(x); Console.ReadLine();

No obtuve una excepción.


Acabo de probarlo y tu segundo método funciona bien (como debería). Algo más debe salir mal, ¿puede proporcionar un poco de código de ejemplo que muestre el problema?

List<int> ints = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; ints.RemoveWhere(i => i > 5); foreach (int i in ints) { Console.WriteLine(i); }

Obtiene:

1 2 3 4 5


Como dijo Marc, List<T>.RemoveAll() es el camino a seguir para las listas.

Sin embargo, me sorprende que su segunda versión no haya funcionado, dado que recibió la llamada a ToList() después de la llamada Where() . Sin la llamada ToList() , sin duda tendría sentido (porque se evaluaría perezosamente), pero debería estar bien como está. ¿Podrías mostrar un ejemplo corto pero completo de esta falla?

EDIT: Con respecto a su comentario en la pregunta, todavía no puedo conseguir que falle. Aquí hay un breve pero completo ejemplo que funciona:

using System; using System.Collections.Generic; using System.Linq; public class Staff { public int StaffId; } public static class Extensions { public static void RemoveWhere<T>(this ICollection<T> Coll, Func<T, bool> Criteria) { List<T> forRemoval = Coll.Where(Criteria).ToList(); foreach (T obj in forRemoval) { Coll.Remove(obj); } } } class Test { static void Main(string[] args) { List<Staff> mockStaff = new List<Staff> { new Staff { StaffId = 3 }, new Staff { StaffId = 7 } }; Staff newStaff = new Staff{StaffId = 5}; mockStaff.Add(newStaff); mockStaff.RemoveWhere(s => s.StaffId == 5); Console.WriteLine(mockStaff.Count); } }

Si pudiera proporcionar un ejemplo completo similar que falla, estoy seguro de que podemos resolver el motivo.


Otra versión de Marcs RemoveAll:

public static void RemoveAll<T>(this IList<T> list, Func<T, bool> predicate) { int count = list.Count; for (int i = count-1; i > -1; i--) { if (predicate(list[i])) { list.RemoveAt(i); } } }


Para la List<T> , esto ya existe, como RemoveAll(Predicate<T>) . Como tal, sugeriría que mantengas el nombre (permitiendo familiaridad y prioridad).

Básicamente, no se puede eliminar mientras se está iterando. Hay dos opciones comunes:

  • usar iteración basada en indexador ( for ) y eliminación
  • almacena en búfer los elementos para eliminar y elimina después de foreach (como ya lo has hecho)

Así que tal vez:

public static void RemoveAll<T>(this IList<T> list, Func<T, bool> predicate) { for (int i = 0; i < list.Count; i++) { if (predicate(list[i])) { list.RemoveAt(i--); } } }

O más generalmente para cualquier ICollection<T> :

public static void RemoveAll<T>(this ICollection<T> collection, Func<T, bool> predicate) { T element; for (int i = 0; i < collection.Count; i++) { element = collection.ElementAt(i); if (predicate(element)) { collection.Remove(element); i--; } } }

Este enfoque tiene la ventaja de evitar muchas copias adicionales de la lista.