(抱歉标题模糊;想不出更好的事情。请随意改写。)
因此,假设我的函数或属性返回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()
或数组也是安全的吗?
许多人花了很多精力回答这个问题。感谢他们所有人。然而,这个问题的要点仍然没有答案。让我详细介绍一下:
我理解foreach
不要求目标对象是数组或列表。它甚至不需要是任何类型的集合。它需要的目标对象是实现枚举。但是,如果我检查目标对象的值,它会显示实际的基础对象是List<T>
(就像它在检查盒装字符串对象时显示object (string)
一样)。这就是混乱开始的地方。谁进行了这种实现?我检查了底层(Where()
函数的源代码),看起来这些函数看起来并不像这样。
所以我的问题在于两个层面。
List<T>
的代理以及它如何决定应该选择List
而不是Array
以及是否有办法从我们的代码中影响该决策。 List<T>
吗?我需要使用一些List<T>
功能,我想知道例如((List<Person>)Adults).BinarySearch()
是否和Adults.ToList().BinarySearch()
一样安全吗?我也明白,即使我明确地调用ToList()
,它也不会造成任何性能损失。我只是想了解它是如何工作的。无论如何,再次感谢时间;我想我花了太多时间。
答案 0 :(得分:2)
一般而言,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()
。
答案 1 :(得分:1)
任何接口都没有默认具体类型这样的东西 接口的整个要点是保证属性,方法,事件或索引器,用户无需了解实现它的具体类型。
使用界面时,您可以知道的是此界面声明的属性,方法,事件和索引器,以及您实际需要知道的所有内容。这只是封装的另一个方面 - 与使用类的方法时相同,您不需要知道该方法的内部实现。
在评论中回答你的问题:
谁决定具体类型以防我们不这样做,就像我上面那样?
创建实现该接口的实例的代码。
既然你不能var Adults = new IEnumerable<Person>
- 它必须是某种具体类型。
据我在source code中看到linq的可枚举扩展程序 - where
会返回Iterator<TSource>
的实例或WhereEnumerableIterator<TSource>
的实例。我没有进一步检查这些类型究竟是什么,但我几乎可以保证他们都实现了IEnumerable
,或者微软的人们正在使用不同的c#编译器然后我们其他人...: - )
答案 2 :(得分:1)
以下代码有希望强调为什么您和编译器都不能假设基础集合:
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个随机数,这可能是一个更好的例子,现在我想起来了)))
答案 3 :(得分:0)