向集合中添加范围

144
今天我的同事问我如何将范围添加到集合中。他有一个继承自Collection<T>的类。该类型已经包含了一些项目的只读属性。他想要将另一个集合中的项添加到该属性集合中。在C#3中,他应该如何做呢?(请注意get-only属性的限制,这会阻止像Union和重新分配之类的解决方案。) 当然,使用Property.Add的foreach可以实现目标。但是,像List<T>一样的AddRange会更加优雅。
编写一个扩展方法非常容易:
public static class CollectionHelpers
{
    public static void AddRange<T>(this ICollection<T> destination,
                                   IEnumerable<T> source)
    {
        foreach (T item in source)
        {
            destination.Add(item);
        }
    }
}

但我感觉自己在重复造轮子。在System.Linqmorelinq中没有找到类似的东西。

设计不好?只需要调用Add吗?错过了显而易见的东西?


6
请记住,LINQ中的“Q”代表“查询”,主要用于数据检索、投影、转换等操作。修改现有集合实际上不属于LINQ旨在解决的问题范畴,这就是为什么LINQ没有为此提供任何开箱即用的功能的原因。但是扩展方法(特别是您的示例)非常适合这种情况。 - Levi
一个问题是,ICollection<T> 似乎没有 Add 方法。但是 Collection<T> 有一个。http://msdn.microsoft.com/en-us/library/system.collections.icollection_methods(v=vs.100).aspx - Tim Goodman
@TimGoodman - 这是非泛型接口。请参阅http://msdn.microsoft.com/en-us/library/92t2ye13.aspx - TrueWill
修改现有的集合确实不属于LINQ旨在解决的问题范畴。@Levi那么为什么一开始还要有Add(T item)呢?似乎这是一个半成品的方法,提供了添加单个项的能力,然后期望所有调用者按顺序迭代以一次添加多个项。你的说法对于IEnumerable<T>肯定是正确的,但我发现自己在处理ICollections时有过不少挫败感。我不反对你的观点,只是在抱怨。 - akousmata
9个回答

75
不,这似乎很合理。有一个List<T>.AddRange()方法可以完成这个任务,但需要您的集合是一个具体的List<T>

1
谢谢,非常正确,但大多数公共属性都遵循MS指南,并不是列表。 - TrueWill
7
是的-我更多是用这个作为理由来解释为什么我认为这样做没有问题。只要意识到这种方法比List<T>版本低效(因为List<T>可以预先分配)。 - Reed Copsey
请注意,在.NET Core 2.2中,如果不正确使用AddRange方法,可能会出现奇怪的行为,如此问题所示:https://github.com/dotnet/core/issues/2667 - Bruno
1
我对这个的唯一问题是扩展方法适用于数组,而数组是不能添加元素的。例如,下面的代码现在会编译通过,但这似乎是不可取的:new [] { "" }.AddRange(new [] { "Alice" }); - dbc
1
我对这个的唯一问题是扩展方法适用于数组,而数组是不能添加元素的。例如,下面的代码现在会编译通过,这似乎是不可取的:new [] { "" }.AddRange(new [] { "Alice" }); - undefined

45

自从.NET4.5以来,如果你想要简短的语句,你 可以使用 System.Collections.Generic 的 ForEach 方法。

source.ForEach(o => destination.Add(o));

甚至可以更短,如下所示:

source.ForEach(destination.Add);

就性能而言,它与每个循环相同(语法糖)。

另外不要尝试像这样赋值

var x = source.ForEach(destination.Add) 

因为ForEach是空的。

编辑:从评论中复制,Lippert关于ForEach的观点


11
就个人而言,我支持Lippert的观点:http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx。 - TrueWill
1
应该是 source.ForEach(destination.Add) 吗? - Frank
7
ForEach 似乎只在 List<T> 上有定义,而不是 Collection 上? - Protector one
2
Lippert现在可以在以下网址找到:https://web.archive.org/web/20190316010649/https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/ - user7610
3
更新了Eric Lippert的博客文章链接:编码中的奇妙冒险 | “foreach” vs “ForEach” - Alexander

45
在运行循环之前,在扩展方法中尝试将其转换为List。这样,您就可以利用List.AddRange的性能优势。
public static void AddRange<T>(this ICollection<T> destination,
                               IEnumerable<T> source)
{
    List<T> list = destination as List<T>;

    if (list != null)
    {
        list.AddRange(source);
    }
    else
    {
        foreach (T item in source)
        {
            destination.Add(item);
        }
    }
}

5
啊!把条件分支交换一下,求你了! - nicodemus13
3
@nicodemus13,(假设你至少有点认真)你交换它们的原因是什么?难道不是更自然地首先考虑我们进行检查的唯一原因吗? - rymdsmurf
19
我很认真,主要原因是这会增加额外的认知负荷,通常非常困难。你不断地尝试评估负面条件,这通常相对较难,而且无论如何你都必须处理两个分支,所以(在我看来)更容易说“如果为空,则执行此操作,否则”执行此操作,而不是相反。此外,这也涉及默认值,应该尽可能采用积极的概念,例如 `if (!thing.IsDisabled) {} else {}' 需要你停下来思考“啊,not is disabled 意味着 enabled,对吧,注意到了,因此其他分支就是当它被禁用时。”难以理解。 - nicodemus13
16
“something != null”和“something == null”的解释并不比较难。然而,否定运算符是完全不同的东西,在您最后的示例中,重写if-else语句将消除该运算符。这是一种客观上的改进,但与原始问题无关。在这种特殊情况下,两种形式是个人偏好的问题,根据上述推理,我更喜欢“!=”运算符。 - rymdsmurf
32
模式匹配会让每个人都开心... ;-) 如果 (目标是 List<T> 类型的 list) - Jacob Foshee
显示剩余2条评论

21

请记住每次Add操作都会检查集合的容量,必要时重新调整大小(较慢)。而对于AddRange方法,集合将被设置为相应容量,然后添加项目(更快)。尽管这个扩展方法速度极慢,但其可以工作。


4
除了这个之外,每次添加也会有一条集合变更通知,而不是使用AddRange一次性发送一个批量通知。 - Nick Udell

5

这是一个稍微高级一些的/生产就绪的版本:

    public static class CollectionExtensions
    {
        public static TCol AddRange<TCol, TItem>(this TCol destination, IEnumerable<TItem> source)
            where TCol : ICollection<TItem>
        {
            if(destination == null) throw new ArgumentNullException(nameof(destination));
            if(source == null) throw new ArgumentNullException(nameof(source));

            // don't cast to IList to prevent recursion
            if (destination is List<TItem> list)
            {
                list.AddRange(source);
                return destination;
            }

            foreach (var item in source)
            {
                destination.Add(item);
            }

            return destination;
        }
    }

rymdsmurf的回答可能看起来很幼稚,过于简单,但它能处理异构列表。有可能使这段代码支持这种用例吗? - richardsonwtr
“destination” 是一个抽象类 “Shape”的列表。而“source”是一个继承类“Circle”的列表。 - richardsonwtr

1

C5通用集合库的类都支持AddRange方法。C5具有更加强健的接口,实际上公开了其基础实现的所有功能,并且与System.Collections.GenericICollectionIList接口兼容,这意味着C5的集合可以轻松地替换为基础实现。


0

同意上面一些人和Lipert的观点。 在我的情况下,经常这样做:

ICollection<int> A;
var B = new List<int> {1,2,3,4,5};
B.ForEach(A.Add);

在我看来,为这样的操作编写扩展方法有点多余。

你的回答并没有为众多早期答案所提出的内容增添任何东西。 - EricSchaefer
当我想要这个时,我应该做什么: aList = If(True,Nothing,aList.AddRange(...)) 在expressionB中我想要添加项目。但是If表达式不允许此操作。表达式没有返回值… - Jürgen Scheffler
也许我没有完全理解你的问题,但如果是关于按条件添加项目,可以像这样完成:ICollection<int> A = new List<int>(); var B = new List<int> { 1, 2, 3, 4, 5 }; B.ForEach(s =>{ if (s > 3) A.Add(s); }); - Egor Sindeev

0

您可以将IEnumerable范围添加到列表中,然后将ICollection设置为该列表。

        IEnumerable<T> source;

        List<item> list = new List<item>();
        list.AddRange(source);

        ICollection<item> destination = list;

4
虽然这个功能可行,但它违反了微软的指南,即使集合属性应该是只读的。(http://msdn.microsoft.com/en-us/library/ms182327.aspx) - Nick Udell

0

或者你可以像这样创建一个 ICollection 扩展:

 public static ICollection<T> AddRange<T>(this ICollection<T> @this, IEnumerable<T> items)
    {
        foreach(var item in items)
        {
            @this.Add(item);
        }

        return @this;
    }

使用它就像在列表上使用它一样:

collectionA.AddRange(IEnumerable<object> items);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接