C#.NET 拾遺補(bǔ)漏:理解 yield 關(guān)鍵字
在 C# 代碼中,尤其是基礎(chǔ)庫(kù)的 API 中,我們經(jīng)??梢钥吹胶芏喾椒ǚ祷氐氖?IEnumerable 類型,為什么要返回 IEnumerable 而不是 IList、ICollection 等類型呢?從字面上理解,IEnumerable 表示該集合中的元素可以被遍歷。要完全理解 IEnumerable 類型對(duì)象如何被遍歷,就要先理解 yield 關(guān)鍵字。
在 C# 中,大多數(shù)方法都是通過(guò) return 語(yǔ)句把計(jì)算得到的結(jié)果返回給調(diào)用者,同時(shí)把控制權(quán)交回給調(diào)用者。比如下面這樣一個(gè)獲取斐波那契數(shù)列的方法:
IEnumerable<int> nums = Fibonacci(10);
foreach (var n in nums)
{
Console.Write("{0} ", n);
}
List<int> Fibonacci(int count)
{
int prev = 1;
int curr = 1;
List<int> result = new();
for (int i = 0; i < count; i++)
{
result.Add(prev);
int temp = prev + curr;
prev = curr;
curr = temp;
}
return result;
}輸出:
1 1 2 3 5 8 13 21 34 55在 Console.Write() 打印結(jié)果之前,變量 nums 已經(jīng)裝載了完整的數(shù)據(jù),所以運(yùn)行后在打印結(jié)果的時(shí)候可以瞬間把結(jié)果全部輸出,這看起來(lái)沒(méi)有什么問(wèn)題。
現(xiàn)在換個(gè)場(chǎng)景,假設(shè) Fibonacci() 方法內(nèi)部每次計(jì)算得到下一個(gè)數(shù)都需要耗費(fèi)較長(zhǎng)的時(shí)間。我們用 Thread.Sleep() 模擬一下所需的耗時(shí),如下:
...
for (int i = 0; i < count; i++)
{
result.Add(prev);
????Thread.Sleep(1000);
int temp = prev + curr;
prev = curr;
curr = temp;
}
return result;
}再次運(yùn)行,你會(huì)發(fā)現(xiàn),大概等待 10 秒后所有數(shù)字被瞬間打印出來(lái)。而在等待的這段時(shí)間,你無(wú)法了解程序運(yùn)算的進(jìn)展,期間是沒(méi)有反饋的。
可以通過(guò) yield 關(guān)鍵字很好地解決這個(gè)問(wèn)題。yield 可以把每一步計(jì)算推遲到它程序?qū)嶋H需要的時(shí)候再執(zhí)行,也就是說(shuō),你不必等所有結(jié)果都計(jì)算完才執(zhí)行下文代碼。
下面使用 yield 關(guān)鍵字改造一下 Fibonacci() 方法:
IEnumerable<int> Fibonacci(int count)
{
int prev = 1;
int curr = 1;
for (int i = 0; i < count; i++)
{
yield?return prev;
Thread.Sleep(1000);
int temp = prev + curr;
prev = curr;
curr = temp;
}
}再次運(yùn)行后,你會(huì)看到,每隔 1 秒會(huì)輸出一個(gè)數(shù)字,直到所有數(shù)字全部輸出。雖然總的等待時(shí)間是一樣的,但對(duì)于圖形用戶界面來(lái)說(shuō),這種即時(shí)響應(yīng)的用戶體驗(yàn)明細(xì)要好于之前的“漫長(zhǎng)”等待。
yield 關(guān)鍵字的用途是把指令推遲到程序?qū)嶋H需要的時(shí)候再執(zhí)行,這個(gè)特性允許我們更細(xì)致地控制集合每個(gè)元素產(chǎn)生的時(shí)機(jī)。它的好處之一是,可以像上面演示的那樣盡可能即時(shí)地給用戶響應(yīng)。還有一個(gè)好處是,可以提高內(nèi)存使用效率。當(dāng)我們有一個(gè)方法要返回一個(gè)集合時(shí),而作為方法的實(shí)現(xiàn)者我們并不清楚方法調(diào)用者具體在什么時(shí)候要使用該集合數(shù)據(jù)。如果我們不使用 yield 關(guān)鍵字,則意味著需要把集合數(shù)據(jù)裝載到內(nèi)存中等待被使用,這可能導(dǎo)致數(shù)據(jù)在內(nèi)存中占用較長(zhǎng)的時(shí)間。
通過(guò) yield 返回的 IEnumerable 類型,表示這是一個(gè)可以被遍歷的數(shù)據(jù)集合。它之所以可以被遍歷,是因?yàn)樗鼘?shí)現(xiàn)了一個(gè)標(biāo)準(zhǔn)的 IEnumerable 接口。一般,我們把像上面這種包含 yield 語(yǔ)句并返回 IEnumerable 類型的方法稱為迭代器(Iterator)。
注意:包含 yield 語(yǔ)句的方法的返回類型也可以是 IEnumerator,它比迭代器更低一個(gè)層級(jí),迭代器是列舉器的一種實(shí)現(xiàn)。
迭代器方法和普通的方法相比,普通方法是通過(guò) return 語(yǔ)句立即把程序的控制權(quán)交回給調(diào)用者,同時(shí)也會(huì)把方法內(nèi)的本地(局部)資源釋放掉。迭代器方法則是在依次返回多個(gè)值給調(diào)用者的期間保留本地資源,等所有值都返回結(jié)束時(shí)再釋放掉本地資源,這些返回的值將形成一組序列被調(diào)用者使用。
在 C# 中,迭代器可以用于方法、屬性或索引器中。迭代器中的 yield 語(yǔ)句分為兩種:
yeild return,把程序控制權(quán)交回調(diào)用者并保留本地狀態(tài),調(diào)用者拿到返回的值繼續(xù)往后執(zhí)行。yeild break,用于告訴程序當(dāng)前序列已經(jīng)結(jié)束,相當(dāng)于正常代碼塊的return語(yǔ)句(迭代器中直接使用return是非法的)。
實(shí)際場(chǎng)景中,我們一般很少直接寫(xiě)迭代器,因?yàn)榇蟛糠中枰膱?chǎng)景都是數(shù)組、集合和列表等,而這些類型內(nèi)部已經(jīng)封裝好了所需的迭代器。
