溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

.NET教程:.NET 面試題之IEnumerable(二)

發(fā)布時間:2020-06-17 15:12:54 來源:網(wǎng)絡(luò) 閱讀:394 作者:IT大贏家 欄目:編程語言

  .NET教程,這篇文章還是接著上文介紹的第二部分!多的不說,直接獻上內(nèi)容!

      使用yield關(guān)鍵字實現(xiàn)方法GetEnumerator

  如果iterator本身有實現(xiàn)IEnumerator接口(本例就是一個數(shù)組),則可以有更容易的方法:

  public IEnumerator GetEnumerator()

  {

  return _people.GetEnumerator();

  }

  注意,這個方法沒有Foreach的存在,所以如果你改用for循環(huán)去迭代這個集合,你得自己去呼叫MoveNext,然后獲得集合的下一個成員。而且會出現(xiàn)一個問題,就是你無法知道集合的大小(IEnumerable沒有Count方法,只有IEnumerable才有)。

  此時,可以做個試驗,如果我們知道一個集合有3個成員,故意迭代多幾次,比如迭代10次,那么當集合已經(jīng)到達尾部時,將會拋出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關(guān)鍵字配合return,編譯器將會自動實現(xiàn)繼承IEnumerator接口的類和上面的三個方法。而且,當for循環(huán)遍歷超過集合大小時,不會拋出異常,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;

  }

  }

  我們會發(fā)現(xiàn)test只會打印三次。后面因為已經(jīng)沒有新的元素了,yield也就不執(zhí)行了,整個Foreach循環(huán)將什么都不做。

  yield的延遲執(zhí)行特性 – 本質(zhì)上是一個狀態(tài)機

  關(guān)鍵字yield只有當真正需要迭代并取到元素時才會執(zhí)行。yield是一個語法糖,它的本質(zhì)是為我們實現(xiàn)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方法,先打印出來的句子卻是主函數(shù)中的句子。這是因為只有在ToList時,才真正開始進行迭代,獲得迭代的成員。我們可以使用ILSpy察看編譯后的程序集的內(nèi)容,并在View -> Option的Decompiler中,關(guān)閉所有的功能對勾(否則你將仍然只看到一些yield),然后檢查Program類型,我們會發(fā)現(xiàn)編譯器幫我們實現(xiàn)的MoveNext函數(shù),實際上是一個switch。第一個yield之前的所有代碼,統(tǒng)統(tǒng)被放在了第一個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之前有其他代碼,它會自動包容到它最近的后續(xù)的yield的“統(tǒng)治范圍”:

  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";

  }

  它的編譯結(jié)果也是可以預(yù)測的:

  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;

  這也就解釋了為什么第一個打印出來的句子在主函數(shù)中,因為所有不是yield的代碼統(tǒng)統(tǒng)都被yield吃掉了,并成為狀態(tài)機的一部分。而在迭×××始之前,代碼是無法運行到switch分支的。

  令人矚目的是,編譯器沒有實現(xiàn)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);

  }

  }

  我們進行調(diào)試,并將斷點設(shè)置在第二次迭代之前,此時,我們發(fā)現(xiàn)vector的值確實變成4了,但第二次迭代之后,值又回去了,好像被改回來了一樣。但實際上,并沒有改任何值,yield只是老老實實的吐出了新的三個vector而已。Yield就像一個血汗工廠,不停的制造新值,不會修改任何值。

  從編譯后的代碼我們發(fā)現(xiàn),只要我們通過foreach迭代一個IEnumerable,我們就會跑到GetVectors方法中,而每次運行GetVectors方法,yield都只會返回全新的三個值為(1,1),(2,2)和(3,3)的vector,仿佛第一次迭代完全沒有運行過一樣。原文中,也有實驗證明了vector創(chuàng)建了六次,實際上每次迭代都會創(chuàng)建三個新的vector。

  解決這個問題的方法是將IEnumerable轉(zhuǎn)為其子類型例如List或數(shù)組。

  在迭代的過程中改變集合的狀態(tài)

  foreach迭代時不能直接更改集合成員的值,但如果集合成員是類或者結(jié)構(gòu),則可以更改其屬性或字段的值。不能在為集合刪除或者增加成員,這會出現(xiàn)運行時異常。For循環(huán)則可以。

  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,從而形成多個互相不影響的迭代過程。

  在迭代時,只能前進不能后退。新的迭代不會記得之前迭代后值的任何變化。


向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI