Чи створює новий Список для зміни колекції в для кожного циклу недолік дизайну?


14

Нещодавно я наткнувся на цю загальну недійсну операцію Collection was modifiedв C #, і хоча я її повністю розумію, здається, це така поширена проблема (google, близько 300 000 результатів!). Але також здається логічною і зрозумілою річ змінити список, поки ви проходите його.

List<Book> myBooks = new List<Book>();

public void RemoveAllBooks(){
    foreach(Book book in myBooks){
         RemoveBook(book);
    }
}

RemoveBook(Book book){
    if(myBooks.Contains(book)){
         myBooks.Remove(book);
         if(OnBookEvent != null)
            OnBookEvent(this, new EventArgs("Removed"));
    }
}

Деякі люди створять інший список для повторення, але це лише ухиляється від проблеми. Яке справжнє рішення, або яке саме актуальне питання дизайну тут? Ми всі, здається, хочемо це зробити, але чи це свідчить про ваду дизайну?


Можна скористатися ітератором або видалити з кінця списку.
ElDuderino

@ElDuderino msdn.microsoft.com/en-us/library/dscyy5s0.aspx . Не пропустіть You consume an iterator from client code by using a For Each…Next (Visual Basic) or foreach (C#) statementлінію
SJuan76,

У Java є Iterator(працює так, як ви описали) і ListIterator(працює як хочете). Це вказує на те, що для забезпечення функціональності ітератора в широкому діапазоні типів колекцій та реалізацій він Iteratorє надзвичайно обмежувальним (що трапиться, якщо ви видалите вузол у врівноваженому дереві? Чи next()має сенс більше), ListIteratorоскільки він є більш потужним через меншу кількість випадки використання.
SJuan76

@ SJuan76 в інших мовах є ще більше типів ітераторів, наприклад, C ++ має ітератори вперед (еквівалентні Ітератору Java), ітератори двонаправленого характеру (як ListIterator), ітератори довільного доступу, ітератори введення (наприклад, ітератор Java, який не підтримує додаткові операції) ) та виводити ітератори (тобто лише для запису, що корисніше, ніж це звучить).
Жуль

Відповіді:


10

Чи створює новий Список для зміни колекції в для кожного циклу недолік дизайну?

Коротка відповідь: ні

Простіше кажучи, ви створюєте невизначену поведінку, коли ви повторюєте колекцію та одночасно змінюєте її. Подумайте про видаленні в nextелемент послідовності. Що було б, якщо MoveNext()його зателефонують?

Перелік залишається дійсним до тих пір, поки колекція не зміниться. Якщо в колекцію вносяться зміни, такі як додавання, модифікація або видалення елементів, перелік невірно визнаний недійсним, а його поведінка не визначена. Перелік не має ексклюзивного доступу до колекції; отже, перерахування через збірку не є безпечною для потоків процедурою.

Джерело: MSDN

До речі, ви могли скоротити своє RemoveAllBooksпростоreturn new List<Book>()

А щоб видалити книгу, рекомендую повернути відфільтровану колекцію:

return books.Where(x => x.Author != "Bob").ToList();

Можлива реалізація на полиці виглядає так:

public class Shelf
{
    List<Book> books=new List<Book> {
        new Book ("Paul"),
        new Book ("Peter")
    };

    public IEnumerable<Book> getAllBooks(){
        foreach(Book b in books){
            yield return b;
        }
    }

    public void RemovePetersBooks(){
        books= books.Where(x=>x.Author!="Peter").ToList();
    }

    public void EmptyShelf(){
        books = new List<Book> ();
    }

    public Shelf ()
    {
    }
}

public static void Main (string[] args)
{
    Shelf s = new Shelf ();
    foreach (Book b in s.getAllBooks()) {
        Console.WriteLine (b.Author);
    }
    s.RemovePetersBooks ();

    foreach (Book b in s.getAllBooks()) {
        Console.WriteLine (b.Author);
    }
    s.EmptyShelf ();
    foreach (Book b in s.getAllBooks()) {
        Console.WriteLine (b.Author);
    }
}

Ви суперечите собі, коли відповідаєте "так" і надаєте приклад, який також створює новий список. Крім цього я згоден.
Есбен Сков Педерсен

1
@EsbenSkovPedersen ви праві: DI думав про модифікацію як про ваду ...
Thomas Junk

6

Створення нового списку для його зміни є хорошим рішенням проблеми, що існуючі ітератори списку не можуть надійно продовжуватись після зміни, якщо вони не знають, як змінився список.

Ще одне рішення - зробити модифікацію за допомогою ітератора, а не через інтерфейс списку. Це працює лише в тому випадку, якщо може бути лише один ітератор - він не вирішує проблему для кількох потоків, повторюваних над списком одночасно, що (доки прийняття доступу до застарілих даних) створює новий список.

Альтернативним рішенням, яке могли прийняти реалізатори фреймворку, є те, щоб список відстежував усі його ітератори та повідомляв їх про зміни. Однак, накладні витрати цього підходу є високими - щоб він працював в цілому з декількома потоками, всі операції ітератора повинні були заблокувати список (щоб переконатися, що він не змінюється під час виконання операції), що зробить весь список операції повільні. Також не було б накладних витрат на нетривіальну пам'ять, плюс для цього потрібен час виконання для підтримки м'яких посилань, а IIRC перша версія CLR не стала.

З іншого боку, копіювання списку для його модифікації дозволяє використовувати LINQ для визначення модифікації, яка, як правило, призводить до більш чіткого коду, ніж безпосередньо ітерація над списком IMO.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.