首页 > 代码库 > .NET教程:.NET 面试题之IEnumerable(二)

.NET教程:.NET 面试题之IEnumerable(二)

  .NET教程,这篇文章还是接着上文介绍的第二部分!多的不说,直接献上内容!

      使用yield关键字实现方法GetEnumerator

  如果iterator本身有实现IEnumerator接口(本例就是一个数组),则可以有更容易的方法:

  public IEnumerator GetEnumerator()

  {

  return _people.GetEnumerator();

  }

  注意,这个方法没有Foreach的存在,所以如果你改用for循环去迭代这个集合,你得自己去呼叫MoveNext,然后获得集合的下一个成员。而且会出现一个问题,就是你无法知道集合的大小(IEnumerable没有Count方法,只有IEnumerable才有)。

  此时,可以做个试验,如果我们知道一个集合有3个成员,故意迭代多几次,比如迭代10次,那么当集合已经到达尾部时,将会抛出InvalidOperationException异常。

  class Program

  {

  static void Main(string[] args)

  {

  Person p1 = new Person("1");

  Person p2 = new Person("2");

  Person p3 = new Person("3");

  People p = new People(new Person[3]{p1, p2, p3});

  var enumerator = p.GetEnumerator();

  //Will throw InvalidOperationException

  for (int i = 0; i < 5; i++)

  {

  enumerator.MoveNext();

  if (enumerator.Current != null)

  {

  var currentP = (Person) enumerator.Current;

  Console.WriteLine("current is {0}", currentP.Name);

  }

  }

  Console.ReadKey();

  }

  }

  public class Person

  {

  public string Name { get; set; }

  public Person(string name)

  {

  Name = name;

  }

  }

  public class People : IEnumerable

  {

  private readonly Person[] _persons;

  public People(Person[] persons)

  {

  _persons = persons;

  }

  public IEnumerator GetEnumerator()

  {

  return _persons.GetEnumerator();

  }

  }

  使用yield关键字配合return,编译器将会自动实现继承IEnumerator接口的类和上面的三个方法。而且,当for循环遍历超过集合大小时,不会抛出异常,Current会一直停留在集合的最后一个元素。

  public IEnumerator GetEnumerator()

  {

  foreach (Person p in _people)

  yield return p;

  }

  如果我们在yield的上面加一句:

  public IEnumerator GetEnumerator()

  {

  foreach (var p in _persons)

  {

  Console.WriteLine("test");

  yield return p;

  }

  }

  我们会发现test只会打印三次。后面因为已经没有新的元素了,yield也就不执行了,整个Foreach循环将什么都不做。

  yield的延迟执行特性 – 本质上是一个状态机

  关键字yield只有当真正需要迭代并取到元素时才会执行。yield是一个语法糖,它的本质是为我们实现IEnumerator接口。

  static void Main(string[] args)

  {

  IEnumerable items = GetItems();

  Console.WriteLine("Begin to iterate the collection.");

  var ret = items.ToList();

  Console.ReadKey();

  }

  static IEnumerable GetItems()

  {

  Console.WriteLine("Begin to invoke GetItems()");

  yield return "1";

  yield return "2";

  yield return "3";

  }

  在上面的例子中,尽管我们呼叫了GetItems方法,先打印出来的句子却是主函数中的句子。这是因为只有在ToList时,才真正开始进行迭代,获得迭代的成员。我们可以使用ILSpy察看编译后的程序集的内容,并在View -> Option的Decompiler中,关闭所有的功能对勾(否则你将仍然只看到一些yield),然后检查Program类型,我们会发现编译器帮我们实现的MoveNext函数,实际上是一个switch。第一个yield之前的所有代码,统统被放在了第一个case中。

  bool IEnumerator.MoveNext()

  {

  bool result;

  switch (this.<>1__state)

  {

  case 0:

  this.<>1__state = -1;

  Console.WriteLine("Begin to invoke GetItems()");

  this.<>2__current = "1";

  this.<>1__state = 1;

  result = true;

  return result;

  case 1:

  this.<>1__state = -1;

  this.<>2__current = "2";

  this.<>1__state = 2;

  result = true;

  return result;

  case 2:

  this.<>1__state = -1;

  this.<>2__current = "3";

  this.<>1__state = 3;

  result = true;

  return result;

  case 3:

  this.<>1__state = -1;

  break;

  }

  result = false;

  return result;

  }

  如果某个yield之前有其他代码,它会自动包容到它最近的后续的yield的“统治范围”:

  static IEnumerable GetItems()

  {

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  yield return "1";

  Console.WriteLine("Begin to invoke GetItems()");

  yield return "2";

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  yield return "3";

  }

  它的编译结果也是可以预测的:

  case 0:

  this.<>1__state = -1;

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  this.<>2__current = "1";

  this.<>1__state = 1;

  result = true;

  return result;

  case 1:

  this.<>1__state = -1;

  Console.WriteLine("Begin to invoke GetItems()");

  this.<>2__current = "2";

  this.<>1__state = 2;

  result = true;

  return result;

  case 2:

  this.<>1__state = -1;

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  Console.WriteLine("Begin to invoke GetItems()");

  this.<>2__current = "3";

  this.<>1__state = 3;

  result = true;

  return result;

  case 3:

  this.<>1__state = -1;

  break;

  这也就解释了为什么第一个打印出来的句子在主函数中,因为所有不是yield的代码统统都被yield吃掉了,并成为状态机的一部分。而在迭代开始之前,代码是无法运行到switch分支的。

  令人瞩目的是,编译器没有实现reset方法,这意味着不支持多次迭代:

  void IEnumerator.Reset()

  {

  throw new NotSupportedException();

  }

  yield只返回,不赋值

  下面这个例子。不过我认为Artech大大分析的不是很好,我给出自己的解释。

  class Program

  {

  static void Main(string[] args)

  {

  IEnumerable vectors = GetVectors();

  //Begin to call GetVectors

  foreach (var vector in vectors)

  {

  vector.X = 4;

  vector.Y = 4;

  }

  //Before this iterate, there are 3 members in vectors, all with X and Y = 4

  foreach (var vector in vectors)

  {

  //But this iterate will change the value of X and Y BACK to 1/2/3

  Console.WriteLine(vector);

  }

  }

  static IEnumerable GetVectors()

  {

  yield return new Vector(1, 1);

  yield return new Vector(2, 3);

  yield return new Vector(3, 3);

  }

  }

  public class Vector

  {

  public double X { get; set; }

  public double Y { get; set; }

  public Vector(double x, double y)

  {

  this.X = x;

  this.Y = y;

  }

  public override string ToString()

  {

  return string.Format("X = {0}, Y = {1}", this.X, this.Y);

  }

  }

  我们进行调试,并将断点设置在第二次迭代之前,此时,我们发现vector的值确实变成4了,但第二次迭代之后,值又回去了,好像被改回来了一样。但实际上,并没有改任何值,yield只是老老实实的吐出了新的三个vector而已。Yield就像一个血汗工厂,不停的制造新值,不会修改任何值。

  从编译后的代码我们发现,只要我们通过foreach迭代一个IEnumerable,我们就会跑到GetVectors方法中,而每次运行GetVectors方法,yield都只会返回全新的三个值为(1,1),(2,2)和(3,3)的vector,仿佛第一次迭代完全没有运行过一样。原文中,也有实验证明了vector创建了六次,实际上每次迭代都会创建三个新的vector。

  解决这个问题的方法是将IEnumerable转为其子类型例如List或数组。

  在迭代的过程中改变集合的状态

  foreach迭代时不能直接更改集合成员的值,但如果集合成员是类或者结构,则可以更改其属性或字段的值。不能在为集合删除或者增加成员,这会出现运行时异常。For循环则可以。

  var vectors = GetVectors().ToList();

  foreach (var vector in vectors)

  {

  if (vector.X == 1)

  //Error

  //vectors.Remove(vector);

  //This is OK

  vector.X = 99;

  Console.WriteLine(vector);

  }

  IEnumerable的缺点

  IEnumerable功能有限,不能插入和删除。

  访问IEnumerable只能通过迭代,不能使用索引器。迭代显然是非线程安全的,每次IEnumerable都会生成新的IEnumerator,从而形成多个互相不影响的迭代过程。

  在迭代时,只能前进不能后退。新的迭代不会记得之前迭代后值的任何变化。

      

.NET教程:.NET 面试题之IEnumerable(二)