如何向IEnumerable<T>集合中添加一个项目?

502
我的问题就是如上标题所述。例如:
IEnumerable<T> items = new T[]{new T("msg")};
items.ToList().Add(new T("msg2"));

但毕竟它只有一个项目。我们是否可以像List<T>那样拥有一个items.Add(item)的方法?与此类似。


6
IEnumerable<T> 仅用于查询集合,是 LINQ 框架的核心。它始终是其他集合类型(如 Collection<T>List<T>Array)的抽象表示。该接口只提供了一个 GetEnumerator 方法,返回一个 IEnumerator<T> 实例,使得能够一次迭代扩展集合中的一个项。由于该接口的限制,LINQ 扩展方法也受到了限制。IEnumerable<T> 被设计成只读的,因为它可能代表多个集合部分的聚合。 - Jordan
3
举个例子,只需声明IList<T>而不是IEnumerable<T>IList<T> items = new T[]{new T("msg")}; items.Add(new T("msg2")); - NightOwl888
17个回答

570

你不能这样做,因为 IEnumerable<T> 并不一定表示可以添加项目的集合。实际上,它甚至不一定表示一个集合!例如:

IEnumerable<string> ReadLines()
{
     string s;
     do
     {
          s = Console.ReadLine();
          yield return s;
     } while (!string.IsNullOrEmpty(s));
}

IEnumerable<string> lines = ReadLines();
lines.Add("foo") // so what is this supposed to do??

不过,你可以创建一个新的IEnumerable对象(未指定类型),当枚举时,将提供旧对象的所有项以及一些你自己添加的项。你可以使用 Enumerable.Concat实现:

 items = items.Concat(new[] { "foo" });

这个操作不会改变数组对象(无论如何你都不能向数组中插入项)。但它将创建一个新的对象,其中列出了数组中的所有项,然后是 "Foo"。此外,该新对象将跟踪数组中的更改(即,每当枚举它时,您将看到项当前的值)。


3
创建一个数组来添加单个值的开销有点高。更加可重用的做法是定义一个扩展方法,用于将单个值连接到另一个集合中。 - JaredPar
4
这要看情况——如果Concat被重复循环调用,那么这么做可能是值得的,但我认为这是过早优化的表现。而且如果确实需要这样做,那么你可能会遇到更大的问题,因为每次调用Concat都会增加一个枚举器层级,因此最终单个MoveNext调用将导致与迭代次数相同长度的调用链。 - Pavel Minaev
2
@Pavel, 嘿。通常我不介意创建数组所需的内存开销,而是觉得额外的输入很烦人 :)。 - JaredPar
7
我有一个针对C#中IEnumerable<T>的语法糖的Connect特性请求,其中包括将operator+重载为Concat(顺便说一下,在VB中,您可以像使用数组一样索引IEnumerable,它在幕后使用了ElementAt): https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=375929。如果我们还可以得到两个重载版本的`Concat`,分别在左侧和右侧都接受单个项目,那么这将把输入缩减到`xs +=x` - 因此请去找Mads(Torgersen),让他优先考虑在C# 5.0中实现这个功能 :) - Pavel Minaev
2
值得一提的是,Concat本身就是一个扩展方法。除非您包含System.Linq,否则它将不可用。 - duggulous
显示剩余2条评论

125

类型 IEnumerable<T> 不支持这些操作。 IEnumerable<T> 接口的目的是允许使用者查看集合的内容。 而不是修改内容。

当您执行像 .ToList().Add() 这样的操作时,您正在创建一个新的 List<T> 并向该列表添加值。 它与原始列表没有任何连接。

您可以使用Add扩展方法来创建一个新的 IEnumerable<T>,其中包含添加的值。

items = items.Add("msg2");

即使在这种情况下,它也不会修改原始的 IEnumerable<T> 对象。可以通过保留对其的引用来验证这一点。例如:

var items = new string[]{"foo"};
var temp = items;
items = items.Add("bar");

在这组操作之后,变量temp仍将只引用值集合中的一个元素“foo”的可枚举对象,而items将引用另一个具有值“foo”和“bar”的可枚举对象。 编辑 我经常忘记Add不是IEnumerable<T>上的典型扩展方法,因为它是我定义的第一个扩展方法之一。以下是它的定义。
public static IEnumerable<T> Add<T>(this IEnumerable<T> e, T value) {
  foreach ( var cur in e) {
    yield return cur;
  }
  yield return value;
}

3
@Pavel,感谢你指出这一点。即使是使用Concat也不行,因为它需要另一个IEnumerable<T>类型的参数。我粘贴了我在项目中定义的典型扩展方法。 - JaredPar
18
我认为这并不应该叫做“Add”,因为在几乎任何其他 .NET 类型(而不仅仅是集合)上,Add 都会对集合进行原地修改。或许可以用 With?或者甚至可以作为 Concat 的另一个重载。 - Pavel Minaev
4
我同意使用 Concat 重载可能是更好的选择,因为 Add 通常暗示着可变性。 - Dan Abramov
16
修改代码为return e.Concat(Enumerable.Repeat(value,1));如何? - Roy Tinker
2
将其命名为“Add”的问题在于List.Add不返回List,而是改变原始列表,而您的扩展方法IEnumerable.Add返回IEnumerable,但不会改变原始列表。这种行为差异足以使我认为“Add”是一个不可取的名称。 - ErikE
显示剩余3条评论

78
你是否考虑过使用ICollection<T>IList<T>接口呢?它们的存在就是为了让你在IEnumerable<T>上拥有Add方法。 IEnumerable<T>用于标记类型为可枚举类型或仅仅是项的序列,而不一定保证底层对象是否支持添加/删除项。同时要记住,这些接口实现了IEnumerable<T>,因此您还可以获得所有与IEnumerable<T>相关的扩展方法。

74

.Net Core 中有一个名为Enumerable.Append的方法,它正是你所需要的。

该方法的源代码可以在GitHub 上获得...... 该实现(比其他答案中的建议更为复杂)值得一看 :)。


6
不仅是在核心部分,还包括框架4.7.1及更高版本:https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.append?view=netframework-4.8 - sschoof
很好,正是我在寻找的。很多答案都忽略了这一点,但你可以简单地返回一个新的可枚举对象,首先消耗现有的项,一旦没有更多的项,就产生额外的项。我认为这就是 Append 的作用。 - Yarek T
5
仅供参考-此方法不会修改集合中的元素。相反,它会创建一个包含新元素的集合副本。-https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.append?view=net-6.0 - spacebread
@sschoof Append/Prepend 在 .NET Standard 1.6+ 中也可用。 - Rich Armstrong

37

对于我来说,IEnumerableIEnumerable<T>上的几个简短而甜美的扩展方法可以解决问题:

public static IEnumerable Append(this IEnumerable first, params object[] second)
{
    return first.OfType<object>().Concat(second);
}
public static IEnumerable<T> Append<T>(this IEnumerable<T> first, params T[] second)
{
    return first.Concat(second);
}   
public static IEnumerable Prepend(this IEnumerable first, params object[] second)
{
    return second.Concat(first.OfType<object>());
}
public static IEnumerable<T> Prepend<T>(this IEnumerable<T> first, params T[] second)
{
    return second.Concat(first);
}

优雅(好吧,除了非通用版本)。真是太糟糕了,这些方法不在BCL中。

第一个Prepend方法给我报错了。 "'object []'不包含“Concat”的定义,最佳扩展方法重载'System.Linq.Enumerable.Concat<TSource>(System.Collections.Generic.IEnumerable<TSource>, System.Collections.Generic.IEnumerable<TSource>)'有一些无效的参数"。 - Tim Newton
@TimNewton,你做错了。谁知道呢,因为从那个错误代码片段中无法得知。专业提示:始终捕获异常并对其进行ToString()操作,因为这样可以捕获更多的错误详细信息。我猜你没有在文件顶部包含System.Linq;,但是谁知道呢?如果你无法解决问题,请尝试创建一个最小的原型来展示相同的错误,然后在提问中寻求帮助。 - user1228
我只是复制并粘贴了上面的代码。它们都可以正常工作,除了 Prepend 函数会出现我提到的错误。这不是运行时异常,而是编译时异常。 - Tim Newton
@TimNewton:我不知道你在说什么,它完美地运行,你显然是错了。现在忽略编辑中的更改,我必须走了,先生,祝你好运。 - user1228
也许是我们的环境有些不同。我正在使用VS2012和.NET 4.5。不要再浪费时间在这上面了,我只是想指出上面的代码存在问题,虽然很小。无论如何,我已经点赞了这个答案,因为它很有用。谢谢。 - Tim Newton
@ErikE 因为并非所有东西都是通用的,而且实现起来很简单。 - user1228

30

不,IEnumerable 不支持向其中添加项。替代方案是

var myList = new List(items);
myList.Add(otherItem);

4
你的建议并不是唯一的选择。例如,ICollection 和 IList 在这里都是合理的选项。 - jason
7
但是您也不能实例化任何一个。 - Paul van Brenk

23
要添加第二个消息,您需要 -
IEnumerable<T> items = new T[]{new T("msg")};
items = items.Concat(new[] {new T("msg2")})

1
但是您还需要将Concat方法的结果分配给items对象。 - Tomasz Przychodzki
1
是的,Tomasz,你说得对。我已经修改了最后一个表达式,将连接的值分配给了项。 - Aamol

15

除了Enumerable.Concat扩展方法之外,.NET Core 1.1.1 中似乎还有另一种名为Enumerable.Append的方法。后者允许您将单个项目连接到现有序列中。因此,Aamol的答案也可以写成:

IEnumerable<T> items = new T[]{new T("msg")};
items = items.Append(new T("msg2"));

请注意,此函数不会改变输入序列,它只返回一个包装器,将给定的序列和追加的项放在一起。


10

不能像你所说的那样添加项目,而且如果你向List<T>(或几乎任何其他非只读集合)添加项目,并且你有一个现有的枚举器,那么枚举器将无效(从那时起会抛出 InvalidOperationException 异常)。

如果您正在聚合某种类型的数据查询结果,则可以使用Concat扩展方法:

编辑:我最初在示例中使用了Union扩展,这实际上是不正确的。我的应用程序广泛使用它来确保重叠查询不重复结果。

IEnumerable<T> itemsA = ...;
IEnumerable<T> itemsB = ...;
IEnumerable<T> itemsC = ...;
return itemsA.Concat(itemsB).Concat(itemsC);

5

其他人已经对为什么不能(也不应该!)向 IEnumerable 添加项目给出了很好的解释。我只想补充一点,如果您想继续编写代表集合的接口并想要添加方法,则应编写 ICollectionIList。作为额外的奖励,这些接口实现了 IEnumerable


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