如何将一个IEnumerable<T>添加到现有的ICollection<T>中。

13

如何在现有的ICollection<T>实例(例如dest)中,以最高效且易读的方式添加来自IEnumerable<T>的项目?

在我的用例中,我有某种实用方法Collect(IEnumerable items),它返回一个包含来自items的元素的新ICollection,因此我按照以下方式执行:

public static ICollection<T> Collect<T>(IEnumerable<T> items) where T:ICollection<T>
{
    ...
    ICollection<T> dest = Activator.CreateInstance<T>();
    items.Aggregate(dest, (acc, item) => { acc.Add(item); return acc; });
    ...
    return dest;
}

问题:有没有更“好”的方法(更高效或更易读)来做这件事?
更新:我认为使用Aggregate()非常流畅,不像调用ToList()。ForEach()那么低效。但它看起来并不是很易读。由于没有人同意使用Aggregate(),所以我想听听您不使用Aggregate()的原因。

4
我认为这是对Aggregate扩展方法的滥用。 - Codor
@Codor,我理解你对可变共享状态的感受。但是我并没有找到一个“更便宜”的替代方案。 - Miguel Gamboa
1
为什么不用一个简单的for循环将项目添加到“dest”中呢? - ckruczek
3
LINQ 中的副作用非常糟糕。 - CodesInChaos
1
“Add”会产生副作用,即改变你正在添加的集合。 - TheInnerLight
显示剩余7条评论
5个回答

16

只需使用Enumerable.Concat

IEnumerable<YourType> result = dest.Concat(items);

如果您想要一个 List<T> 作为结果,请使用 ToList:
List<YourType> result = dest.Concat(items).ToList();
// perhaps:
dest = result;

如果dest已经是一个列表并且您想要修改它,请使用AddRange

dest.AddRange(items);

更新:

如果你需要向ICollection<T>方法参数添加项目,可以使用以下扩展:

public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> seq)
{
    List<T> list = collection as List<T>;
    if (list != null)
        list.AddRange(seq);
    else
    {
        foreach (T item in seq)
            collection.Add(item);
    }
}

// ...

public static void Foo<T>(ICollection<T> dest)
{
    IEnumerable<T> items = ... 
    dest.AddRange(items);
}

10

个人而言,我会选择@ckruczek的建议,使用foreach循环:

foreach (var item in items)
    dest.Add(item);

简单、干净,几乎每个人都能立即理解它的作用。

如果您坚持要隐藏循环的某些方法调用,那么有些人会为 IEnumerable<T> 定义一个自定义的 ForEach 扩展方法,类似于为 List<T> 定义的方法。实现非常简单:

public static void ForEach<T>(this IEnumerable<T> source, Action<T> action) {
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (action == null) throw new ArgumentNullException(nameof(action));
    foreach (item in source)
        action(item);
}

鉴于此,您将能够编写

items.ForEach(dest.Add);

我个人认为它没有太多好处,但也没有任何缺点。


4
为什么不直接为 ICollection<T> 添加 AddRange 扩展方法,而是要使用 ForEach 方法?这样可以使 API 更加清晰,也能减少操作开销。 - Leri
1
@Leri 这不是一个坏主意,但它更加复杂。List<T> 可用的 AddRange 比一系列 Add 更高效(如果可能的话)。你为 ICollection<T> 添加的自定义 AddRange 应该具有相同的效果(再次强调,如果可能的话)。我认为,一个无条件转换为重复调用 ICollection<T>.Add 的实现会产生误导。 - user743382
6
Eric Lippert的必备博客文章:“foreach” vs ForEach”: https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/本文介绍了C#语言中两个不同大小写形式的"foreach"关键字的区别。其中,小写的"foreach"是一个C#关键字,而大写的"ForEach"则是一个扩展方法。这篇文章还涵盖了如何使用它们以及它们之间的一些微妙差异。 - Dennis
1
@Chips_100 我基本上同意Eric Lippert的观点,但我不确定我被他那个观点所说服。例如,F#提供了Seq.iter,它与IEnumerable<T>ForEach方法完全等效。结合惰性求值和副作用可能非常危险,但是不提供该方法甚至无法最小化与此相关的风险。 - TheInnerLight

4

我们实际上为此编写了一个扩展方法(以及一堆其他 ICollection 扩展方法):

public static class CollectionExt
{
    public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> source)
    {
        Contract.Requires(collection != null);
        Contract.Requires(source != null);

        foreach (T item in source)
        {
            collection.Add(item);
        }
    }
}

因此,我们可以在ICollection()上使用AddRange()

ICollection<int> test = new List<int>();
test.AddRange(new [] {1, 2, 3});

注意:如果您想在基础集合为List<T>类型时使用List<T>.AddRange(),您可以这样实现扩展方法:
public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> source)
{
    var asList = collection as List<T>;

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

2

最有效的:

foreach(T item in itens) dest.Add(item)

最易读的(但效率较低,因为它创建了一个一次性的列表):

items.ToList().ForEach(dest.Add);

不太易读,但并不低效:

items.Aggregate(dest, (acc, item) => { acc.Add(item); return acc; });

4
“ToList.ForEach” 是最不恰当的方法之一。你需要创建一个临时列表才能使用 “List.ForEach”。这样会枚举整个集合来填充另一个列表,然后再次枚举该列表以将项目添加到原始集合中。每当我看到这种写法时,总是感到不适。这也是为什么 LINQ 在某些公司中声誉不佳的原因之一。 - Tim Schmelter
1
我完全同意你的看法,@TimSchmelter。这就是为什么我在我的答案中包含了最有效的方法。但我会遵循你的提醒,在我的答案中加入那个注释。 - Gaspium

1
items.ToList().ForEach(dest.Add);

如果您不想创建新的集合实例,则可以创建一个扩展方法。
public static class Extension
{
    public static void AddRange<T>(this ICollection<T> source, IEnumerable<T> items)
    {
        if (items == null)
        {
            return;
        }

        foreach (T item in items)
        {
            source.Add(item);
        }
    }
}

然后您可以像这样编辑您的代码:
        ICollection<T> dest = ...;
        IEnumerable<T> items = ...;
        dest.AddRange(items);

也许是 AddRange?顺便提一下,你把返回值丢掉了。 - Uwe Keim
@SebastianSchulz ToList() 会创建一个新的实例(在这种情况下是临时的),稍后将被 GC 丢弃。我认为这并不更有效率。 - Miguel Gamboa
1
@UweKeim AddRange 只能在 List 上使用。 - Miguel Gamboa
  1. ICollection 没有 AddRange 方法。
  2. 在其他任何短代码中,我们都会创建一个新的集合实例。=> .Concat 创建一个新的列表等等...
- Sebastian Siemens
1
@MiguelGamboa:如果您不想要一个新实例,那么除了使用for each list之外别无选择。但是您可以创建一个扩展方法来为您完成此操作。然后,您可以直接调用items.ForEach(dest.Add); - Sebastian Siemens

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