IEnumerable的默认具体类型是什么
(对不起,标题模糊;没想到更好。请随意改写。)
所以假设我的函数或属性返回一个IEnumerable
:
public IEnumerable Adults { get { return _Members.Where(i => i.Age >= 18); } }
如果我在这个属性上运行foreach
而没有实际实现返回的可枚举:
foreach(var Adult in Adults) { //... }
是否存在一个规则来控制IEnumerable
是否将实现为数组或列表或其他内容?
将Adults
为List
或Array而不调用ToList()
或ToArray()
吗?
编辑
许多人花了很多精力回答这个问题。 感谢他们所有人。 然而,这个问题的要点仍然没有答案。 让我详细介绍一下:
我知道foreach
不要求目标对象是数组或列表。 它甚至不需要是任何类型的集合。 它需要的目标对象就是实现枚举。 但是,如果我检查目标对象的值,它会显示实际的底层对象是List
(就像它在检查一个盒装的字符串对象时显示object (string)
)。 这就是混乱开始的地方。 谁进行了这种实现? 我检查了底层( Where()
函数的源),看起来这些函数看起来并不像这样。
所以我的问题在于两个层面。
- 第一个是纯粹的理论。 与物理学和生物学等许多其他学科不同,在计算机科学中,我们总是清楚地知道某些东西是如何起作用的(回答@zzxyz的最后评论); 所以我试图挖掘创建
List
的代理以及它如何决定它应该选择List
而不是Array
以及是否有一种方法可以从我们的代码中影响该决策。 - 我的第二个原因是实际的。 我可以依赖实际底层对象的类型并将其强制转换为
List
吗? 我需要使用一些List
function,我想知道是否例如((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.
你甚至不需要IEnumerable
或IEnumerable
。
这段代码可以在编译器找出它需要的所有内容时起作用
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
类的引用。
关键是你在编译时不知道类的具体类型是什么 – 如果你这样做,你甚至可能无法强制转换它。
界面就是你所需要的,如果你考虑我的第一个例子,即使这并不严格。
如果需要List
或Adult[]
,则必须分别调用.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”窗口如何描述它们。 这表明你有困惑吗? 如果没有,你能构建一个类似的短程序和截图吗?