(对不起,标题模糊;没想到更好。请随意改写。)
所以,假设我的函数或属性返回IEnumerable<T>
:
public IEnumerable<Person> Adults
{
get
{
return _Members.Where(i => i.Age >= 18);
}
}
如果我在这个属性上运行foreach
而没有实际实现返回的可枚举:
foreach(var Adult in Adults)
{
//...
}
是否存在一条规则来管理IEnumerable<Person>
是否将实现为数组或列表或其他内容?
在不调用Adults
或List<Person>
的情况下将ToList()
投射到ToArray()
或Array也是安全的吗?
许多人花了很多精力回答这个问题。感谢他们所有人。然而,这个问题的要点仍然没有答案。让我详细介绍一下:
据我所知,foreach
不要求目标对象是数组或列表。它甚至不需要是任何类型的集合。它需要的目标对象是实现枚举。但是,如果我检查目标对象的值,它会显示实际的底层对象是List<T>
(就像它在检查盒装字符串对象时显示object (string)
一样)。这就是混乱开始的地方。谁进行了这种实现?我检查了底层(Where()
函数的源代码),看起来这些函数看起来并不像这样。
所以我的问题在于两个层面。
List<T>
的代理人以及它如何决定它应该选择List
而不是Array
以及是否有从我们的代码中影响该决定的方法。List<T>
吗?我需要使用一些List<T>
功能,我想知道例如((List<Person>)Adults).BinarySearch()
是否像Adults.ToList().BinarySearch()
一样安全?我也明白即使我明确地调用ToList()
也不会造成任何性能损失。我只是想了解它是如何工作的。无论如何,再次感谢时间;我想我花了太多时间。
一般而言,foreach
工作所需要的只是拥有一个具有可访问的GetEnumerator()
方法的对象,该方法返回一个具有以下方法的对象:
void Reset()
bool MoveNext()
T Current { get; private set; } // where `T` is some type.
你甚至不需要IEnumerable
或IEnumerable<T>
。
这段代码可以在编译器找出它需要的所有内容时运行:
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<Adult>
{
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<Adult> GetEnumerator()
{
return new Enumerator();
}
}
这使得Enumerator
成为私人阶级,即private class Enumerator
。接口然后完成所有艰苦的工作 - 甚至不可能在Enumerator
之外获得对Adults
类的引用。
关键是你在编译时不知道类的具体类型是什么 - 如果你这样做,你甚至可能无法强制转换它。
界面就是你所需要的,如果你考虑我的第一个例子,即使这并不严格。
如果你想要一个List<Adult>
或Adult[]
,你必须分别给.ToList()
或.ToArray()
打电话。
任何接口都没有默认的具体类型。 接口的整个要点是保证属性,方法,事件或索引器,而无需用户需要了解实现它的具体类型。
使用接口时,您可以知道的是此接口声明的属性,方法,事件和索引器,这就是您实际需要知道的全部内容。这只是封装的另一个方面 - 与使用类的方法时相同,您不需要知道该方法的内部实现。
在评论中回答您的问题:
如果我们不这样做,谁决定具体类型,就像我上面做的那样?
这是创建实现接口的实例的代码。既然你不能做var Adults = new IEnumerable<Person>
- 它必须是某种具体类型。
据我在source code中看到linq的可枚举扩展 - where
返回Iterator<TSource>
的实例或WhereEnumerableIterator<TSource>
的实例。我不打算进一步检查这些类型究竟是什么,但我几乎可以保证他们都实现IEnumerable
,或者微软的人正在使用不同的c#编译器然后我们其他人... :-)
以下代码有希望强调为什么您和编译器都不能假设基础集合:
public class OneThroughTen : IEnumerable<int>
{
private static int bar = 0;
public IEnumerator<int> GetEnumerator()
{
while (true)
{
yield return ++bar;
if (bar == 10)
{ yield break; }
}
}
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
}
class Program
{
static void Main(string[] args)
{
IEnumerable<int> x = new OneThroughTen();
foreach (int i in x)
{ Console.Write("{0} ", i); }
}
}
输出当然是:
1 2 3 4 5 6 7 8 9 10
注意,上面的代码在调试器中表现得非常糟糕。我不知道为什么。这段代码表现得很好:
public IEnumerator<int> GetEnumerator()
{
while (bar < 10)
{
yield return ++bar;
}
bar = 0;
}
(我使用static
为bar
强调,不仅OneThroughTen
没有特定的集合,它没有任何集合,事实上没有任何实例数据。我们可以很容易地返回10个随机数,这将'是一个更好的例子,现在我认为它:))