Является ли создание нового Списка для изменения коллекции в каждом цикле недостатком дизайна?


13

Недавно я столкнулся с этой распространенной недопустимой операцией Collection was modifiedв C #, и, хотя я полностью ее понимаю, кажется, что это такая распространенная проблема (Google, около 300 тыс. Результатов!). Но это также кажется логичным и простым, чтобы изменить список, пока вы просматриваете его.

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 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, который не поддерживает необязательные операции ) и выходные итераторы (т.е. только для записи, что более полезно, чем кажется).
Жюль

Ответы:


9

Является ли создание нового Списка для изменения коллекции в каждом цикле недостатком дизайна?

Краткий ответ: нет

Проще говоря, вы создаете неопределенное поведение, когда вы перебираете коллекцию и изменяете ее одновременно. Подумайте об удалении в 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 вы правы: D я думал о модификации как недостаток ...
Томас Джанк

6

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

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

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

С другой стороны, копирование списка для его изменения позволяет использовать LINQ для указания модификации, что обычно приводит к более четкому коду, чем прямая итерация по списку, IMO.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.