将单个项作为IEnumerable<T>传递

459
有没有一种常用的方法可以将类型为 T 的单个项目传递给期望一个 IEnumerable 参数的方法?语言是 C#,框架版本为 2.0。
目前我正在使用一个帮助方法(它是 .Net 2.0,所以我有一堆类似于 LINQ 的转换/投影帮助方法),但这似乎很愚蠢:
public static class IEnumerableExt
{
    // usage: IEnumerableExt.FromSingleItem(someObject);
    public static IEnumerable<T> FromSingleItem<T>(T item)
    {
        yield return item; 
    }
}

另一种方式当然是创建和填充一个List<T>Array,并传递它而不是IEnumerable<T>

[编辑] 作为扩展方法,它可以被命名为:

public static class IEnumerableExt
{
    // usage: someObject.SingleItemAsEnumerable();
    public static IEnumerable<T> SingleItemAsEnumerable<T>(this T item)
    {
        yield return item; 
    }
}

我在这里漏掉了什么吗?

[编辑2] 我们发现someObject.Yield()(如下面评论中@Peter建议的)是这个扩展方法的最佳名称,主要是为了简洁,所以这里提供它的XML注释,如果有人想要使用:

public static class IEnumerableExt
{
    /// <summary>
    /// Wraps this object instance into an IEnumerable&lt;T&gt;
    /// consisting of a single item.
    /// </summary>
    /// <typeparam name="T"> Type of the object. </typeparam>
    /// <param name="item"> The instance that will be wrapped. </param>
    /// <returns> An IEnumerable&lt;T&gt; consisting of a single item. </returns>
    public static IEnumerable<T> Yield<T>(this T item)
    {
        yield return item;
    }
}

7
我会对扩展方法的代码进行轻微修改:if (item == null) yield break;
这样你就不能传递空值并且也能利用(平凡的)null对象模式来处理IEnumerable。(foreach (var x in xs)能够很好地处理空的xs)。
顺便说一下,这个函数是列表单子类型IEnumerable<T>的单子单位,并且鉴于Microsoft对单子的热爱之情,我很惊讶像这样的东西在框架中竟然没有出现。
- Matt Enright
4
对于扩展方法,你不应该将其命名为“AsEnumerable”,因为已经有一个具有该名称的内置扩展方法存在。(当T实现IEnumerable时,例如string。) - Jon-Eric
26
给这个方法取名为“Yield”怎么样?简洁明了无可比拟。 - Philip Guin
4
这里有一些命名建议。"SingleItemAsEnumerable" 有点啰嗦。 "Yield" 描述了实现而不是接口 - 这不好。为了获得更好的名称,我建议使用 "AsSingleton",它对应了行为的确切含义。 - Earth Engine
6
我讨厌这里的 left==null 检查。它破坏了代码的美感并阻止了代码变得更加灵活——如果某天您需要生成一个可以为空的单例,该怎么办? 我的意思是,new T[] { null } 不同于 new T[] {},有一天您可能需要区分它们。 - Earth Engine
显示剩余19条评论
20个回答

201

如果这个方法期望的是一个 IEnumerable,那么你必须传递一个列表,即使它只包含一个元素。

传递

new[] { item }

我认为参数应该足够了


146

在C# 3.0中,您可以利用System.Linq.Enumerable类:

// using System.Linq

Enumerable.Repeat(item, 1);

这会创建一个新的IEnumerable,其中只包含您的项目。


35
我认为这个解决方案会使代码更难阅读。 :( 重复一个元素相当不直观,你不觉得吗? - Drahakar
8
重复阅读一次对我来说没问题。这是一个更简单的解决方案,无需担心添加另一个扩展方法,并确保在您想使用它的任何地方都包含命名空间。 - si618
17
是的,我实际上只是使用new[]{ item },我只是认为这是一个有趣的解决方案。 - luksan
8
这个解决方案很有价值。 - ProVega
3
在我看来,这应该是被接受的答案。它易于阅读,简洁明了,并且在BCL上可以用一行代码实现,不需要自定义扩展方法。 - Ryan

111

在我看来,您的辅助方法是最清晰的方法。如果传入列表或数组,则不怀好意的代码可能会进行类型转换并更改内容,从而导致某些情况下出现奇怪的行为。您可以使用只读集合,但这很可能需要更多的包装。我认为您的解决方案已经非常简洁了。


如果列表/数组是即席构建的,则其范围在方法调用后结束,因此不应该引起问题。 - Mario F
如果以数组的形式发送,该如何更改?我猜它可以强制转换为一个数组,然后将引用更改为其他内容,但那有什么好处呢?(虽然我可能错过了什么……) - Svish
2
假设您决定创建一个可枚举对象,以传递给两个不同的方法...然后第一个方法将其强制转换为数组并更改其内容。然后,您将其作为参数传递给另一个方法,却不知道已经发生了更改。我并不是说这很可能发生,只是说当不需要可变性时,拥有可变对象比自然不可变性要不那么整洁。 - Jon Skeet
3
这是一个被接受的答案,很可能会被读取,所以我在这里补充我的担忧。我尝试了这种方法,但它破坏了我之前编译时调用 myDict.Keys.AsEnumerable() 的代码,其中 myDictDictionary <CheckBox,Tuple<int,int>> 类型。在我将扩展方法重命名为 SingleItemAsEnumerable 后,一切都开始正常工作了。无法将 IEnumerable<Dictionary<CheckBox,System.Tuple<int,int>>.KeyCollection>' 转换为 IEnumerable<CheckBox>。显式转换存在(是否缺少强制类型转换?)。我可以暂时接受一个hack,但其他高级用户能否找到更好的方法呢? - Hamish Grubijan
3
听起来你只是想从dictionary.Values开始。基本上不太清楚正在发生什么,但我建议你提一个新问题来询问。 - Jon Skeet
显示剩余2条评论

44
在C# 3中(我知道你说的是2),你可以编写一个通用扩展方法,这可能会使语法更易于接受:
static class IEnumerableExtensions
{
    public static IEnumerable<T> ToEnumerable<T>(this T item)
    {
        yield return item;
    }
}

客户端代码是item.ToEnumerable()


谢谢,我知道这一点(如果我使用C# 3.0,它可以在.Net 2.0中工作),我只是想知道是否有内置机制来实现这一点。 - vgru
7
顺便提一下,对于“IObservable<T>”已经存在一个名为“ToEnumerable”的扩展方法,因此这个方法名也会干扰现有的惯例。老实说,我建议根本不要使用扩展方法,因为它过于通用了。 - cwharris

23

这个帮助方法适用于单个或多个项目。

public static IEnumerable<T> ToEnumerable<T>(params T[] items)
{
    return items;
}    

1
有用的变体,因为它支持多个项目。我喜欢它依赖于 params 来构建数组,所以生成的代码看起来很干净。我还没有决定是否比 new T[]{ item1, item2, item3 }; 更好用于多个项目。 - ToolmakerSteve
1
聪明!喜欢它 - Mitsuru Furuta
考虑到它不需要一个 this 参数,因此不会应用于对象,我认为 AsEnumerableToEnumerable 更好的名称。 - NetMage
2
这实际上在内存中创建了一个数组:https://sharplab.io/#gist:cf122bcb10a521c314b865dc5c7a6581。如果您不想进行内存分配,请勿使用它。 - Teejay

13

我有些惊讶,没有人建议增加一个类型为T的参数重载方法,以简化客户端API。

public void DoSomething<T>(IEnumerable<T> list)
{
    // Do Something
}

public void DoSomething<T>(T item)
{
    DoSomething(new T[] { item });
}

现在您的客户端代码只需执行以下操作:
MyItem item = new MyItem();
Obj.DoSomething(item);

或者使用列表方式:
List<MyItem> itemList = new List<MyItem>();
Obj.DoSomething(itemList);

21
更好的做法是,你可以编写 DoSomething<T>(params T[] items) 方法,这意味着编译器会处理将单个项转换为数组的过程。(这样还允许您传入多个单独的项,编译器仍会帮助您将它们转换为数组。) - LukeH
我认为我更喜欢这个,它可以使用new[] { item }而不需要泛型参数T,并且如果多次使用,可以保持客户端使用语法更清晰。 - Kioshiki

11
这比在foreach中使用yield或Enumerable.Repeat快30%,这是由于C#编译器优化所致,而在其他情况下性能相同。
public struct SingleSequence<T> : IEnumerable<T> {
    public struct SingleEnumerator : IEnumerator<T> {
        private readonly SingleSequence<T> _parent;
        private bool _couldMove;
        public SingleEnumerator(ref SingleSequence<T> parent) {
            _parent = parent;
            _couldMove = true;
        }
        public T Current => _parent._value;
        object IEnumerator.Current => Current;
        public void Dispose() { }

        public bool MoveNext() {
            if (!_couldMove) return false;
            _couldMove = false;
            return true;
        }
        public void Reset() {
            _couldMove = true;
        }
    }
    private readonly T _value;
    public SingleSequence(T value) {
        _value = value;
    }
    public IEnumerator<T> GetEnumerator() {
        return new SingleEnumerator(ref this);
    }
    IEnumerator IEnumerable.GetEnumerator() {
        return new SingleEnumerator(ref this);
    }
}

在这个测试中:
    // Fastest among seqs, but still 30x times slower than direct sum
    // 49 mops vs 37 mops for yield, or c.30% faster
    [Test]
    public void SingleSequenceStructForEach() {
        var sw = new Stopwatch();
        sw.Start();
        long sum = 0;
        for (var i = 0; i < 100000000; i++) {
            foreach (var single in new SingleSequence<int>(i)) {
                sum += single;
            }
        }
        sw.Stop();
        Console.WriteLine($"Elapsed {sw.ElapsedMilliseconds}");
        Console.WriteLine($"Mops {100000.0 / sw.ElapsedMilliseconds * 1.0}");
    }

2023年由@antiduh编辑 - 我在benchmark dotnet中重新运行了这些测试,使用了三种实现方式:
    [Benchmark]
    public void UsingNewArray()
    {
        long sum = 0;
        for( var i = 0; i < iterations; i++ )
        {
            var seq = EnumerableEx.One( i );
            foreach( var single in seq ) { sum += single; }
        }
    }

    [Benchmark]
    public void UsingSingleSequenceBoxed()
    {
        long sum = 0;
        for( var i = 0; i < iterations; i++ )
        {
            IEnumerable<int> seq = new SingleSequence<int>( i );
            foreach( var single in seq ) { sum += single; }
        }
    }

    [Benchmark]
    public void UsingSingleSequenceUnboxed()
    {
        long sum = 0;
        for( var i = 0; i < iterations; i++ )
        {
            var seq = new SingleSequence<int>( i );
            foreach( var single in seq ) { sum += single; }
        }
    }

在这里,EnumerableEx.One 只是使用 new[] { item }

结果:

方法 平均值 误差 标准差 Gen0 分配内存
使用新数组 185.8 毫秒 2.59 毫秒 2.42 毫秒 102000.0000 612.15 MB
使用单个序列(装箱) 181.6 毫秒 3.47 毫秒 3.25 毫秒 76333.3333 459.11 MB
使用单个序列(非装箱) 144.4 毫秒 2.02 毫秒 1.89 毫秒 38250.0000 229.56 MB
SingleSequence的速度与新数组相当,但更加高效利用堆内存。

1
谢谢,为这种情况创建一个结构体可以减少在紧密循环中的GC负担,这是有意义的。 - vgru
3
当它们被返回时,枚举器和可枚举对象都将被装箱。 - Stephen Drew
2
不要使用 Stopwatch 进行比较。请使用 Benchmark.NET https://github.com/dotnet/BenchmarkDotNet - NN_
7
刚刚测量了一下,使用new [] { i }选项的速度约为您的选项的3.2倍。同时,您的选项又比带有yield return的扩展快大约1.2倍。 - lorond
正如回答中指出的文章所述,C#编译器针对IEnumerable<T>实现了一个foreach优化,以避免在可能的情况下通过接口进行循环。此回答有大量代码利用了这个foreach优化,但目的是创建单例,因此不太可能以一种有用的方式在foreach中使用。 - NetMage
显示剩余3条评论

9

我同意@EarthEngine对原帖的评论,即'AsSingleton'是更好的名称。点此查看维基百科条目。因此,根据单例模式的定义,如果将null值传递作为参数,则'AsSingleton'应该返回一个包含单个null值的IEnumerable,而不是空的IEnumerable,这将解决if (item == null) yield break;的争议。我认为最好的解决方案是拥有两个方法:'AsSingleton'和'AsSingletonOrEmpty';在将null作为参数传递时,'AsSingleton'将返回单个null值,而'AsSingletonOrEmpty'将返回空的IEnumerable。就像这样:

public static IEnumerable<T> AsSingletonOrEmpty<T>(this T source)
{
    if (source == null)
    {
        yield break;
    }
    else
    {
        yield return source;
    }
}

public static IEnumerable<T> AsSingleton<T>(this T source)
{
    yield return source;
}

这些方法或多或少类似于IEnumerable上的“First”和“FirstOrDefault”扩展方法,这感觉很自然。


我喜欢这个解决方案的简洁和清晰。现在我们有可空引用类型,第一个签名可能是public static IEnumerable<T> AsSingletonOrEmpty<T>(this T? source) - Giles
抱歉 - 我错过了之前评论的结尾:where T : notnull - Giles

9

正如之前所说的那样,

MyMethodThatExpectsAnIEnumerable(new[] { myObject });

或者

MyMethodThatExpectsAnIEnumerable(Enumerable.Repeat(myObject, 1));

作为一个附注,如果您想要一个匿名对象的空列表,最后一个版本也可以很好地实现,例如:
var x = MyMethodThatExpectsAnIEnumerable(Enumerable.Repeat(new { a = 0, b = "x" }, 0));

谢谢,虽然Enumerable.Repeat是在.Net 3.5中新增的,但它似乎行为类似于上面的辅助方法。 - vgru

8
对于那些关心性能的人,虽然 @mattica 在上面提到的类似的问题中提供了一些基准测试信息,但是我的基准测试提供了不同的结果。在.NET 7中,yield return valuenew T[] { value }快约9%,并且分配的内存量只有75%。在大多数情况下,这已经是超高性能的,并且是你所需要的最好的性能。
我很好奇是否自定义单个集合实现会更快或更轻量级。结果证明,因为yield return被实现为IEnumerator<T>IEnumerable<T>,要在分配方面击败它的唯一方法就是在我的实现中也这样做。
如果您将 IEnumerable<> 传递给外部库,除非您非常熟悉正在构建的内容,否则我强烈建议不要这样做。话虽如此,我制作了一个非常简单(不可重用)的实现,它能够比 yield 方法快5ns,并且只分配了数组的一半大小。
由于所有测试都通过了 IEnumerable<T>,值类型通常表现不如引用类型。我最好的实现实际上是最简单的 - 您可以查看 gist I linked to 中的 SingleCollection 类。(这比 yield return 快2ns,但分配的内存量为数组分配的75%,而相比之下,分配的内存量为88%。)

简而言之:如果你关心速度,请使用yield return item。如果你非常关心速度,请使用SingleCollection


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