如果我可以定义一个GetEnumerator,为什么要实现IEnumerable(T)?

Dan*_*Tao 13 .net c# ienumerable ienumerator foreach

更新:我感谢所有的评论,这些评论基本上都是一致的反对意见.虽然提出的每一项反对意见都是有效的,但我觉得棺材中的最终指甲是Ani的精明观察,最终,即使是这个想法表面上提供的一个 微不足道的好处 - 消除样板代码 - 也被这个事实所否定.想法本身需要自己的样板代码.

所以,是的,请相信我说:这将是一个坏主意.

而且只是为了有点挽救我的尊严:我可能是出于争吵的缘故而玩弄它,但我从来没有真正把这个想法卖掉 - 只是好奇地听到别人不得不说的话.诚实.


在你将这个问题视为荒谬之前,我请你考虑以下问题:

  1. IEnumerable<T>从继承*IEnumerable,这意味着实现任何类型的IEnumerable<T>通常必须实现两个 IEnumerable<T>.GetEnumerator (明确地)IEnumerable.GetEnumerator.这基本上等于样板代码.
  2. 只要该方法使用方法和属性返回某种类型的对象,就可以foreach覆盖任何具有GetEnumerator方法的类型.因此,如果您的类型使用签名定义了一个方法,那么使用它来枚举它是合法的.MoveNextCurrentpublic IEnumerator<T> GetEnumerator()foreach
  3. 显然,有很多代码需要IEnumerable<T>接口 - 例如,基本上所有的LINQ扩展方法.幸运的是,使用C#通过关键字提供的自动迭代器生成,从可以使用的类型foreach转变IEnumerable<T>为简单的类型yield.

所以,把这一切放在一起,我有这个疯狂的想法:如果我只是定义我自己的界面,如下所示:

public interface IForEachable<T>
{
    IEnumerator<T> GetEnumerator();
}
Run Code Online (Sandbox Code Playgroud)

然后每当我定义一个我想要枚举的类型时,我实现这个接口而不是IEnumerable<T>,不需要实现两个GetEnumerator方法(一个显式).例如:

class NaturalNumbers : IForEachable<int>
{
   public IEnumerator<int> GetEnumerator()
   {
       int i = 1;
       while (i < int.MaxValue)
       {
           yield return (i++);
       }
   }

   // Notice how I don't have to define a method like
   // IEnumerator IEnumerable.GetEnumerator().
}
Run Code Online (Sandbox Code Playgroud)

最后,为了使这种类型的代码兼容是期望的IEnumerable<T>界面,我可以定义一个扩展方法从任何去IForEachable<T>IEnumerable<T>像这样:

public static class ForEachableExtensions
{
    public static IEnumerable<T> AsEnumerable<T>(this IForEachable<T> source)
    {
        foreach (T item in source)
        {
            yield return item;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在我看来,这样做使我能够设计出各种方式可用的类型作为实现IEnumerable<T>,但IEnumerable.GetEnumerator每个方案中没有那个讨厌的显式实现.

例如:

var numbers = new NaturalNumbers();

// I can foreach myself...
foreach (int x in numbers)
{
    if (x > 100)
        break;

    if (x % 2 != 0)
        continue;

    Console.WriteLine(x);
}

// Or I can treat this object as an IEnumerable<T> implementation
// if I want to...
var evenNumbers = from x in numbers.AsEnumerable()
                  where x % 2 == 0
                  select x;

foreach (int x in evenNumbers.TakeWhile(i => i <= 100))
{
    Console.WriteLine(x);
}
Run Code Online (Sandbox Code Playgroud)

你们怎么看待这个想法?我错过了为什么这会是一个错误的原因吗?

我意识到这可能看起来像是一个过于复杂的解决方案,开始时没有那么大的交易(我怀疑任何人真的非常关心必须明确定义IEnumerable接口); 但它只是突然出现在我脑海中,我没有看到这种方法会带来任何明显的问题.

一般情况下,如果我能写的代码量适中一次对自己保存需要编写的代码量小的麻烦很多次,对我来说,这是值得的.

*这是正确的术语吗?我总是犹豫要说一个界面"继承"另一个界面,因为这似乎没有正确捕捉它们之间的关系.但也许它是正确的.

Ree*_*sey 13

你错过了一件大事 -

如果你实现自己的接口而不是IEnumerable<T>,你的类将无法使用期望的框架方法IEnumerable<T>- 主要是,你将完全无法使用LINQ,或使用你的类来构造一个List<T>或许多其他有用的抽象.

正如您所提到的,您可以通过单独的扩展方法完成此操作 - 但是,这需要付出代价.通过使用扩展方法转换为a IEnumerable<T>,您需要添加另一个抽象级别才能使用您的类(您将比编写类更频繁地执行FAR),并且会降低性能(您的扩展方法将会,实际上,在内部生成一个新的类实现,这实际上是不必要的).最重要的是,你班级的任何其他用户(或者你以后)将不得不学习一个什么都不做的新API - 你不使用标准接口使你的课程更难以使用,因为它违反了用户的期望.


Jon*_*eet 9

你是对的:对于一个非常简单的问题来说,似乎是一个过于复杂的解决方案.

它还为迭代的每个步骤引入了额外的间接级别.可能不是性能问题,但当我认为你没有真正获得任何非常重要的东西时,仍然有点不必要.

此外,虽然您的扩展方法允许您将any转换IForEachable<T>为an IEnumerable<T>,但这意味着您的类型本身不会满足这样的通用约束:

public void Foo<T>(T collection) where T : IEnumerable
Run Code Online (Sandbox Code Playgroud)

等等.基本上,不必进行转换,你失去治疗单一对象的能力两者的实现IEnumerable<T> 真正的具体类型.

此外,通过不实现IEnumerable,您将自己计算在集合初始化器之外.(我有时只是IEnumerable明确地实现选择进入集合初始化,从中抛出异常.)GetEnumerator()

哦,你已经介绍了一些额外的基础设施,这是世界上其他人都不熟悉的,与已经了解的大量C#开发人员相比IEnumerable<T>.


Rex*_*x M 5

在我们的代码库中,可能有超过100个这个完全代码段的实例:

IEnumerator IEnumerable.GetEnumerator()
{
    return this.GetEnumerator();
}
Run Code Online (Sandbox Code Playgroud)

我真的很好.为与所有其他.NET代码完全兼容而付出的代价是微不足道的;)

您提出的解决方案基本上要求新接口的每个消费者/调用者都记得在它有用之前调用特殊的扩展方法.


Ani*_*Ani 5

您是不是只是将样板移动到其他地方 - 从IEnumerable.GetEnumerator每个类的方法编写到AsEnumerable每次IEnumerable<T>预期调用您的扩展?通常情况下,我希望使用可枚举的类型来查询的次数远远超过它的写入次数(这恰好是一次).这意味着这种模式平均会导致更多样板.