您好,登錄后才能下訂單哦!
.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,從而形成多個互相不影響的迭代過程。
在迭代時,只能前進不能后退。新的迭代不會記得之前迭代后值的任何變化。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。