创建新的List来修改每个循环中的集合是设计缺陷吗?


14

我最近Collection was modified在C#中遇到了这个常见的无效操作,尽管我完全理解它,但这似乎是一个常见的问题(谷歌,大约30万个结果!)。但这在您浏览列表时修改列表似乎是合乎逻辑且直接的事情。

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/zh-CN/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迭代器) )和输出迭代器(即只写,比听起来更有用)。
2015年

Answers:


10

创建新的List来修改每个循环中的集合是设计缺陷吗?

简短的回答:

简而言之,当您遍历集合并同时对其进行修改时,会产生不确定的行为。考虑删除next序列中的元素。如果MoveNext()被称为会发生什么?

只要集合保持不变,枚举数将保持有效。如果对集合进行了更改(例如添加,修改或删除元素),则枚举数将无法恢复,并且其行为是不确定的。枚举器没有对该集合的独占访问权;因此,通过集合进行枚举本质上不是线程安全的过程。

资料来源:MSDN

顺便说一句,您可以将其缩短RemoveAllBooksreturn 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);
    }
}

当您回答“是”时您会自相矛盾,然后提供一个示例,该示例还会创建一个新列表。除此之外,我同意。
Esben Skov Pedersen

1
@EsbenSkovPedersen您是对的:DI曾考虑将其修改为缺陷……
Thomas Junk

6

创建新列表进行修改是解决现有列表迭代器在更改后无法可靠继续的问题的好方法,除非它们知道列表的更改方式。

另一个解决方案是使用迭代器而不是通过列表接口本身进行修改。这仅在只有一个迭代器的情况下有效-它不能解决多个线程同时在列表上进行迭代的问题,只要创建新列表即可(只要访问陈旧数据是可以接受的)。

框架实现者可以采用的另一种解决方案是让列表跟踪其所有迭代器,并在发生更改时通知它们。但是,这种方法的开销很高-为了使其能够与多个线程一起正常工作,所有迭代器操作都需要锁定列表(以确保在操作进行期间它不会更改),这会使所有列表操作缓慢。也将有不小的内存开销,并且它需要运行时来支持软引用,而IIRC的CLR第一版则不需要。

从不同的角度来看,复制列表以进行修改可以使您使用LINQ来指定修改,与直接遍历列表IMO相比,这样做通常可以得到更清晰的代码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.