IEnumerable的默认具体类型是什么

(对不起,标题模糊;没想到更好。请随意改写。)

所以假设我的函数或属性返回一个IEnumerable

 public IEnumerable Adults { get { return _Members.Where(i => i.Age >= 18); } } 

如果我在这个属性上运行foreach而没有实际实现返回的可枚举:

 foreach(var Adult in Adults) { //... } 

是否存在一个规则来控制IEnumerable是否将实现为数组或列表或其他内容?

AdultsList或Array而不调用ToList()ToArray()吗?

编辑

许多人花了很多精力回答这个问题。 感谢他们所有人。 然而,这个问题的要点仍然没有答案。 让我详细介绍一下:

我知道foreach不要求目标对象是数组或列表。 它甚至不需要是任何类型的集合。 它需要的目标对象就是实现枚举。 但是,如果我检查目标对象的值,它会显示实际的底层对象是List (就像它在检查一个盒装的字符串对象时显示object (string) )。 这就是混乱开始的地方。 谁进行了这种实现? 我检查了底层( Where()函数的源),看起来这些函数看起来并不像这样。

所以我的问题在于两个层面。

  • 第一个是纯粹的理论。 与物理学和生物学等许多其他学科不同,在计算机科学中,我们总是清楚地知道某些东西是如何起作用的(回答@zzxyz的最后评论); 所以我试图挖掘创建List的代理以及它如何决定它应该选择List而不是Array以及是否有一种方法可以从我们的代码中影响该决策。
  • 我的第二个原因是实际的。 我可以依赖实际底层对象的类型并将其强制转换为List吗? 我需要使用一些Listfunction,我想知道是否例如((List)Adults).BinarySearch()Adults.ToList().BinarySearch()一样安全Adults.ToList().BinarySearch()

我也明白即使我明确地调用ToList()也不会造成任何性能损失。 我只是想了解它是如何工作的。 无论如何,再次感谢时间; 我想我花了太多时间。

一般而言, foreach工作所需要的只是拥有一个带有可访问的GetEnumerator()方法的对象,该方法返回一个具有以下方法的对象:

 void Reset() bool MoveNext() T Current { get; private set; } // where `T` is some type. 

你甚至不需要IEnumerableIEnumerable

这段代码可以在编译器找出它需要的所有内容时起作用

 void Main() { foreach (var adult in new Adults()) { Console.WriteLine(adult.ToString()); } } public class Adult { public override string ToString() => "Adult!"; } public class Adults { public class Enumerator { public Adult Current { get; private set; } public bool MoveNext() { if (this.Current == null) { this.Current = new Adult(); return true; } this.Current = null; return false; } public void Reset() { this.Current = null; } } public Enumerator GetEnumerator() { return new Enumerator(); } } 

具有适当的可枚举使得该过程更容易且更稳健地工作。 以上代码的惯用版本更为:

 public class Adults { private class Enumerator : IEnumerator { public Adult Current { get; private set; } object IEnumerator.Current => this.Current; public void Dispose() { } public bool MoveNext() { if (this.Current == null) { this.Current = new Adult(); return true; } this.Current = null; return false; } public void Reset() { this.Current = null; } } public IEnumerator GetEnumerator() { return new Enumerator(); } } 

这使得Enumerator成为私有类,即private class Enumerator 。 接口然后完成所有艰苦的工作 – 甚至不可能在Adults之外获得对Enumerator类的引用。

关键是你在编译时不知道类的具体类型是什么 – 如果你这样做,你甚至可能无法强制转换它。

界面就是你所需要的,如果你考虑我的第一个例子,即使这并不严格。

如果需要ListAdult[] ,则必须分别调用.ToList().ToArray()

任何接口都没有默认的具体类型
接口的整个要点是保证属性,方法,事件或索引器,而无需用户需要了解实现它的具体类型。

使用接口时,您可以知道的是此接口声明的属性,方法,事件和索引器,这就是您实际需要知道的全部内容。 这只是封装的另一个方面 – 与使用类的方法时相同,您不需要知道该方法的内部实现。

在评论中回答您的问题:

如果我们不这样做,谁决定具体类型,就像我上面做的那样?

这是创建实现接口的实例的代码。 既然你不能做var Adults = new IEnumerable – 它必须是某种具体类型。

至于我在linq的Enumerable扩展的源代码中看到的 – where返回Iterator的实例或WhereEnumerableIterator的实例。 我不打算进一步检查这些类型到底是什么,但我几乎可以保证他们都实现了IEnumerable ,或者微软的人正在使用不同的c#编译器然后我们其余的人…… 🙂

以下代码有希望强调为什么您和编译器都不能假设底层集合:

 public class OneThroughTen : IEnumerable { private static int bar = 0; public IEnumerator GetEnumerator() { while (true) { yield return ++bar; if (bar == 10) { yield break; } } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } class Program { static void Main(string[] args) { IEnumerable x = new OneThroughTen(); foreach (int i in x) { Console.Write("{0} ", i); } } } 

输出当然是:

1 2 3 4 5 6 7 8 9 10

注意,上面的代码在调试器中的表现非常差。 我不知道为什么。 这段代码表现得很好:

 public IEnumerator GetEnumerator() { while (bar < 10) { yield return ++bar; } bar = 0; } 

(我使用static bar来强调,不仅OneThroughTen没有特定的集合,它没有任何集合,实际上没有任何实例数据。我们可以很容易地返回10个随机数,这将'是一个更好的例子,现在我认为它:))

从您编辑的问题和评论中,您听起来就像了解使用IEnumerable的一般概念,并且您不能假设“列表对象支持所有IEnumerable对象”。 你真正的问题是在调试器中让你困惑的事情,但是我们真的无法准确理解你所看到的是什么。 也许截图有帮助?

这里我有5个IEnumerable变量,我以各种方式分配这些变量,以及“Watch”窗口如何描述它们。 这表明你有困惑吗? 如果没有,你能构建一个类似的短程序和截图吗?

在此处输入图像描述