# C# 重新认识一下 IEnumerable<T>,IAsyncEnumerable<T> 以及搭配异步可能遇到的问题
C# 重新認(rèn)識一下 IEnumerable<T>,IAsyncEnumerable<T> 以及搭配異步可能遇到的問題
前言
為啥會想到寫這個(gè)
為了這碟醋,包了這頓餃子
作為老鳥不免犯迷糊
因?yàn)?在使用異步中使用IEnumerable<T>,IAsyncEnumerable<T>遇到了一些細(xì)節(jié)(對于我之前來說)上沒注意到問題.
什么是IEnumerable<T>
IEnumerable<T> 繼承自 System.Collections.IEnumerable
namespace System.Collections.Generic
{
//
// 摘要:
// Exposes the enumerator, which supports a simple iteration over a collection of
// a specified type.
//
// 類型參數(shù):
// T:
// The type of objects to enumerate.
public interface IEnumerable<out T> : IEnumerable
{
//
// 摘要:
// Returns an enumerator that iterates through the collection.
//
// 返回結(jié)果:
// An enumerator that can be used to iterate through the collection.
IEnumerator<T> GetEnumerator();
}
}
以下引用自 微軟官方文檔
IEnumerable<T>是 命名空間中System.Collections.Generic集合(例如 、 Dictionary<TKey,TValue>和 Stack<T> )List<T>和其他泛型集合(如 ObservableCollection<T> 和 ConcurrentStack<T>)的基接口。 可以使用 語句枚舉實(shí)現(xiàn) IEnumerable<T> 的 foreach 集合。
有關(guān)此接口的非泛型版本,請參閱 System.Collections.IEnumerable。
IEnumerable<T> 包含實(shí)現(xiàn)此接口時(shí)必須實(shí)現(xiàn)的單個(gè)方法; GetEnumerator,返回 IEnumerator<T> 對象。 返回的 IEnumerator<T> 提供通過公開 Current 屬性循環(huán)訪問集合的功能。
粗俗的說,就是我們可以通過實(shí)現(xiàn)了 IEnumerable<T> 接口的容器提高數(shù)據(jù)處理的效率,因?yàn)橥ㄟ^它 我們可以方便的使用 foreach 關(guān)鍵字 遍歷容器內(nèi)的元素,而我們所熟知的大部分的容器,例如,List<T>,Dictionary<TKey,TValue> 等等都是實(shí)現(xiàn)了 IEnumerable<T> 的.
除了快速遍歷以外,作為返回值 IEnumerable<T> 也有著強(qiáng)大的優(yōu)勢,因?yàn)槿绻莻鹘y(tǒng)的數(shù)組遍歷的話如果我想要找到多個(gè)數(shù)組中指定的元素,我必須等到找到所有符合的元素的時(shí)候才能將數(shù)據(jù)返回,調(diào)用方才能開始進(jìn)行操作,而返回結(jié)果為 IEnumerable<T> 的方法可以通過 yield 關(guān)鍵字提前將當(dāng)前符合條件的 T 值返回給調(diào)用方然后返回到之前執(zhí)行的地方繼續(xù)查找符合條件的元素.
使用方式
1. 通過 GetEnumerator() 方法訪問成員元素
IEnumerable和IEnumerable<T>接口提供了GetEnumerator()方法讓我們獲取迭代器,通過MoveNext()方法返回的bool值提供是否可以進(jìn)行下一次迭代,然后通過Current屬性獲取當(dāng)前元素.
// 快速生成0-100, Enumerable 提供了很多方便的靜態(tài)方法
IEnumerable<int> arr = Enumerable.Range(0, 100);
var enumerator = arr.GetEnumerator();
while(enumerator.MoveNext())
{
enumerator.Current.Dump();
}
2. 通過 foreach 關(guān)鍵字快速遍歷成員元素
foreach關(guān)鍵字提供了快速遍歷成員元素的操作,其也是通過生成第一個(gè)例子的代碼迭代,省去了反復(fù)書寫冗余代碼的步驟.
微軟官方建議使用 foreach,而不是直接操作枚舉數(shù)
(這里是一個(gè)鴨子類型)只要擁有GetEnumerator方法都可以通過foreach關(guān)鍵字進(jìn)行遍歷,所可以通過一些黑魔法(擴(kuò)展函數(shù)Range類型實(shí)例GetEnumerator)實(shí)現(xiàn) foreach (var i in 1..10) 這樣的語法.
IEnumerable<int> arr = Enumerable.Range(0, 100);
// 遍歷打印成員
foreach (int element in arr)
{
Console.WriteLine(arr.ToString());
}
3. 作為同步方法返回值時(shí)通過 yield 關(guān)鍵字即時(shí)返回成員
當(dāng)使用IEnumerable<T>作為同步方法的返回值時(shí),我們可以對外隱藏返回值具體的實(shí)現(xiàn),比如List<T> 實(shí)現(xiàn)了IEnumerable<T>,Dictionary<TKey,TValue>實(shí)現(xiàn)了IEnumerable<KeyValuePair<TKey,TValue>>.
當(dāng)需要返回值時(shí),方法內(nèi)可以是一個(gè)整體結(jié)果返回,也可以利用yield關(guān)鍵字逐個(gè)成員結(jié)果返回.
public void Main(string[] args)
{
// 通過IEnumerable<char> 逐個(gè)char 打印
foreach (var task in GetTasksFromIEnumerable(5))
{
Console.WriteLine(task);
Console.WriteLine($"處理完:{task}");
}
IEnumerable<int> GetTasksFromIEnumerable(int count)
{
for (int i = 0; i < count; i++)
{
yield return HeavyTask(i);
Console.WriteLine($"已返回當(dāng)前值:{i},準(zhǔn)備下一次");
}
}
// 模擬比較重的任務(wù)
int HeavyTask(int i)
{
// 模擬耗時(shí)
Thread.Sleep(1000);
return i;
}
}
以上代碼我們可以得到以下輸出,可以看到每次調(diào)用方當(dāng)前循環(huán)體結(jié)束后,迭代器又會回到當(dāng)前運(yùn)行的地方準(zhǔn)備執(zhí)行下一次迭代;
0
處理完:0
已返回當(dāng)前值:0,準(zhǔn)備下一次
1
處理完:1
已返回當(dāng)前值:1,準(zhǔn)備下一次
2
處理完:2
已返回當(dāng)前值:2,準(zhǔn)備下一次
3
處理完:3
已返回當(dāng)前值:3,準(zhǔn)備下一次
4
處理完:4
已返回當(dāng)前值:4,準(zhǔn)備下一次
4. 作為異步方法返回值時(shí)通過 yield 關(guān)鍵字即時(shí)返回成員
在如今異步方法大行其道的今天,我們的實(shí)際使用中異步方法已經(jīng)稀疏平常了,但 C# 中的異步方法關(guān)鍵字 async , await 具有傳染性,只有我們方法中使用到了異步方法并希望使用 await 等待結(jié)果的時(shí)候當(dāng)前的方法必須使用 async 關(guān)鍵字標(biāo)記并且將返回值使用 Task<T> 包裹.所以,通過正常途徑我們無法獲得一個(gè)只返回 IEnumerable<T> 結(jié)果的異步方法,因?yàn)樗冀K被 Task 包裹,除非我們在方法中等待所有的結(jié)果完成后作為異步方法的結(jié)果返回,但顯然這不是我們希望的結(jié)果.那么我們?nèi)绾尾拍芟M屯椒椒ㄖ幸粯蛹磿r(shí)返回當(dāng)前的結(jié)果且不阻塞呢? 答案是使用它的異步類型接口 IAsyncEnumerable<T>.
可以使整個(gè)結(jié)果返回,無法將單個(gè)結(jié)果即時(shí)返回
public async Task<IEnumerable<int>> GetNumbersAsync()
{
// 模擬需要執(zhí)行的異步任務(wù)
await Task.Delay(1000);
var result = Enumerable.Range(0, 100);
return result; // 返回整個(gè)結(jié)果
}
public async Task<IEnumerable<int>> GetNumbersAsync()
{
for(int i = 0; i < 5 ; i++ )
{
yield return await GetSignleNumberAsync(); // 編譯錯(cuò)誤
//CS1624: The body of 'GetNumbersAsync()' cannot be an iterator block because 'Task<IEnumerable<int>>' is not an iterator interface type
}
}
5. IAsyncEnumerable<T>
當(dāng)使用 IAsyncEnumerable<T> 時(shí)異步方法的返回值可以直接使用它作為返回值的類型例如
public async Task Main(string[]args)
{
Console.WriteLine($"當(dāng)前線程:{Environment.CurrentManagedThreadId}");
// 通過await foreach 立即進(jìn)行迭代
await foreach (var number in GetNumbersAsync())
{
Console.WriteLine($"當(dāng)前線程:{Environment.CurrentManagedThreadId}");
Console.WriteLine(number);
}
}
async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 0; i < 5; i++)
{
yield return await GetSignleNumberAsync(); // 編譯通過
}
}
async Task<int> GetSignleNumberAsync()
{
// 模擬耗時(shí)
await Task.Delay(1000);
return Random.Shared.Next();
}
得到輸出結(jié)果
當(dāng)前線程:1
當(dāng)前線程:6
809282356
當(dāng)前線程:6
696341357
當(dāng)前線程:6
872147671
當(dāng)前線程:6
791323674
當(dāng)前線程:6
1961595625
當(dāng)前線程:6
我們也可以通過 ToBlockingEnumerable() 方法將對應(yīng)的 IAsyncEnumerable<int> 的結(jié)果轉(zhuǎn)為同步阻塞的 IEnumerable<T>
// 通過 ToBlockingEnumerable 轉(zhuǎn)為同步阻塞的 IEnumerable<T>
var result = GetNumbersAsync().ToBlockingEnumerable();
// 將以同步代碼執(zhí)行
Console.WriteLine($"當(dāng)前線程:{Environment.CurrentManagedThreadId}");
foreach (var element in result)
{
Console.WriteLine($"當(dāng)前線程:{Environment.CurrentManagedThreadId}");
Console.WriteLine(element);
}
得到以下輸出結(jié)果
當(dāng)前線程:1
當(dāng)前線程:1
1933649614
當(dāng)前線程:1
1975509029
當(dāng)前線程:1
1303323564
當(dāng)前線程:1
1618007076
當(dāng)前線程:1
503278324
IEnumerable 到底做了什么
我們可以通過 sharplab.io 這個(gè)網(wǎng)站來看看 通過 yield + foreach 關(guān)鍵字為我們生成最終的代碼的樣子
源代碼
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public class C
{
public void M()
{
foreach(var item in GetTasksFromIEnumerable(15))
{
Console.WriteLine(item);
}
}
IEnumerable<int> GetTasksFromIEnumerable(int count)
{
for (int i = 0; i < count; i++)
{
yield return HeavyTask(i);
Console.WriteLine($"已返回當(dāng)前值:{i},準(zhǔn)備下一次");
}
}
// 模擬比較重的任務(wù)
int HeavyTask(int i)
{
// 模擬耗時(shí)
Thread.Sleep(1000);
return i;
}
}
生成后的代碼
// 省略部分無關(guān)代碼
public class C
{
[CompilerGenerated]
private sealed class <GetTasksFromIEnumerable>d__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
private int <>1__state;
private int <>2__current;
private int <>l__initialThreadId;
private int count;
public int <>3__count;
public C <>4__this;
private int <i>5__1;
int IEnumerator<int>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <GetTasksFromIEnumerable>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
}
private bool MoveNext()
{
int num = <>1__state;
if (num != 0)
{
if (num != 1)
{
return false;
}
<>1__state = -1;
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(13, 1);
defaultInterpolatedStringHandler.AppendLiteral("已返回當(dāng)前值:");
defaultInterpolatedStringHandler.AppendFormatted(<i>5__1);
defaultInterpolatedStringHandler.AppendLiteral(",準(zhǔn)備下一次");
Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());
<i>5__1++;
}
else
{
<>1__state = -1;
<i>5__1 = 0;
}
if (<i>5__1 < count)
{
<>2__current = <>4__this.HeavyTask(<i>5__1);
<>1__state = 1;
return true;
}
return false;
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
[DebuggerHidden]
[return: System.Runtime.CompilerServices.Nullable(1)]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
<GetTasksFromIEnumerable>d__1 <GetTasksFromIEnumerable>d__;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
<GetTasksFromIEnumerable>d__ = this;
}
else
{
<GetTasksFromIEnumerable>d__ = new <GetTasksFromIEnumerable>d__1(0);
<GetTasksFromIEnumerable>d__.<>4__this = <>4__this;
}
<GetTasksFromIEnumerable>d__.count = <>3__count;
return <GetTasksFromIEnumerable>d__;
}
[DebuggerHidden]
[return: System.Runtime.CompilerServices.Nullable(1)]
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<int>)this).GetEnumerator();
}
}
public void M()
{
IEnumerator<int> enumerator = GetTasksFromIEnumerable(15).GetEnumerator();
try
{
while (enumerator.MoveNext())
{
int current = enumerator.Current;
Console.WriteLine(current);
}
}
finally
{
if (enumerator != null)
{
enumerator.Dispose();
}
}
}
[System.Runtime.CompilerServices.NullableContext(1)]
[IteratorStateMachine(typeof(<GetTasksFromIEnumerable>d__1))]
private IEnumerable<int> GetTasksFromIEnumerable(int count)
{
<GetTasksFromIEnumerable>d__1 <GetTasksFromIEnumerable>d__ = new <GetTasksFromIEnumerable>d__1(-2);
<GetTasksFromIEnumerable>d__.<>4__this = this;
<GetTasksFromIEnumerable>d__.<>3__count = count;
return <GetTasksFromIEnumerable>d__;
}
private int HeavyTask(int i)
{
Thread.Sleep(1000);
return i;
}
}
// 省略部分無關(guān)代碼
在 調(diào)用方 M() 方法中
foreach關(guān)鍵字 為我們生成了通過GetTasksFromIEnumerable().GetEnumerator() 方法返回的 IEnumerator<int> 類型的結(jié)果 的迭代器 ,然后通過try-finally包裹了原來forech中的方法塊finally最終會釋放獲取到的迭代器.GetTasksFromIEnumerable() 方法中為我們生成了一個(gè)狀態(tài)機(jī) <GetTasksFromIEnumerable>d__1 初始化狀態(tài)為 -2 ,然后將 當(dāng)前所處的實(shí)例 this 和 入?yún)?count 作為字段
通過 <GetTasksFromIEnumerable>d__1 中的 IEnumerable<int>.GetEnumerator() 方法實(shí)現(xiàn)該狀態(tài)機(jī)的初始化,其中還包含了對調(diào)用方線程與迭代器初始化線程是否一致的判斷,如果不一致的話會將其重置為當(dāng)前線程.
然后通過 MoveNext 不斷獲取當(dāng)前迭代的值 ,可以看到原來的
yield return HeavyTask(i);
轉(zhuǎn)化成了
if (<i>5__1 < count) // 原來?xiàng)l件
{
<>2__current = <>4__this.HeavyTask(<i>5__1);
<>1__state = 1; // 將 state 標(biāo)記為 1, 使其走到上面對應(yīng)的 if 語句
return true; // 并表示可以繼續(xù)移動
}
return false; // 結(jié)束
state 改變?yōu)?1 之后 , 執(zhí)行原 yield 后的代碼塊
if (num != 0)
{ if (num != 1)
{
return false;
} // 重新標(biāo)記為 -1
<>1__state = -1; // 對應(yīng)原來的 Console.WriteLine($"已返回當(dāng)前值:{i},準(zhǔn)備下一次");
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(13, 1);
defaultInterpolatedStringHandler.AppendLiteral("已返回當(dāng)前值:");
defaultInterpolatedStringHandler.AppendFormatted(<i>5__1);
defaultInterpolatedStringHandler.AppendLiteral(",準(zhǔn)備下一次");
Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear()); // 循環(huán)遍歷累加
<i>5__1++;
}
else
{
<>1__state = -1;
// 這里為啥會重置為 0 ?
<i>5__1 = 0;
}
問題
上面說了為了這碟醋包了這頓餃子,那么這頓餃子是什么呢?
其實(shí)后面發(fā)現(xiàn)不是 IEnumerable 或者IAsyncEnumerable 的問題 而是對于異步中對象的生命周期的理解問題.
之前再寫一個(gè)解析網(wǎng)頁元素項(xiàng)的輔助方法時(shí),本著能少寫一個(gè)少寫一個(gè)的原則(哈哈哈,偷懶),想將傳入的 html 字符串轉(zhuǎn)成流 然后調(diào)用另一個(gè)寫好的 Stream 解析的函數(shù).
/// 偷懶的函數(shù)
public static IAsyncEnumerable<TTableRow> ParseSimpleTable<TTableRow>(string html, string tableSelector, string rowSelector, Func<IElement, ValueTask<TTableRow>> rowParseFunc)
{
// 出于直覺 在這里 using
using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(html));
return ParseSimpleTable(stream, tableSelector, rowSelector, rowParseFunc);
}
/// <summary>
/// 解析簡單表格
/// </summary>
/// <typeparam name="TTableRow">解析結(jié)果項(xiàng)</typeparam>
/// <param name="stream">要解析的流</param>
/// <param name="tableSelector">table選擇器</param>
/// <param name="rowSelector">行選擇器</param>
/// <param name="rowParseFunc">行解析方法委托</param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static async IAsyncEnumerable<TTableRow> ParseSimpleTable<TTableRow>(Stream stream, string tableSelector, string rowSelector, Func<IElement, ValueTask<TTableRow>> rowParseFunc)
{
IBrowsingContext browsingContext = BrowsingContext.New();
var htmlParser = browsingContext.GetService<IHtmlParser>();
if (htmlParser == null)
throw new ArgumentException(nameof(htmlParser));
using IDocument document = await htmlParser.ParseDocumentAsync(stream);
var tableElement = document.QuerySelector(tableSelector);
if (tableElement == null)
yield break;
var rowsElement = tableElement.QuerySelectorAll(rowSelector);
if (rowsElement == null || !rowsElement.Any())
yield break;
foreach (var rowElement in rowsElement)
{
yield return await rowParseFunc(rowElement);
}
}
由于出于直覺的 using 了這個(gè)流,下意識的以為這個(gè) Stream 會在這個(gè)函數(shù)執(zhí)行后釋放, 然后就...異常了
Cannot access a closed Stream.
Data = <enumerable Count: 0>
HelpLink = <null>
HResult = -2146232798
InnerException = <null>
Message = Cannot access a closed Stream.
ObjectName =
Source = System.Private.CoreLib
StackTrace = at System.IO.MemoryStream.get_Length()
at Program.<<Main>$>g__GetBytes|0_1(Stream stream)+MoveNext() in :line 20
at Program.<<Main>$>g__GetBytes|0_1(Stream stream)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
at Program.<Main>$(String[] args) in :line 3
at Program.<Main>$(String[] args) in :line 3
at Program.<Main>(String[] args)
TargetSite = Void ThrowObjectDisposedException_StreamClosed(System.String)
一般流報(bào)這個(gè)異常都是被提前釋放的問題,我一想噢應(yīng)該時(shí)異步的問題,然后我去看生成后的代碼,恍然大悟.
// 模擬場景
private IAsyncEnumerable<byte> ParseSimpleTable<TTableRow>(string s)
{
MemoryStream memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(s));
try
{
// 這里是一個(gè)異步方法,但是我并沒有等待完成,而是轉(zhuǎn)交給了調(diào)用方等待
return ParseSimpleTable(memoryStream);
}
finally
{
if (memoryStream != null)
{
// 沒有等待所以這里 memoryStream 被釋放了 ,但是 GetBytes 方法還在執(zhí)行
((IDisposable)memoryStream).Dispose();
}
}
}
生成后的代碼 一目了然,memoryStream 被提前釋放了.
解決錯(cuò)誤方式很簡單
等待完成 await ParseSimpleTable 后釋放,在當(dāng)前方法塊中等待完成,但是無法直接返回 IAsyncEnumerable了,必須配合
yield關(guān)鍵字在最終調(diào)用 Stream 的函數(shù)中
using或 調(diào)用 Close() ,也就是在具體 yield 方法塊之后調(diào)用 ,但是在最底層釋放來自調(diào)用方的流感覺有點(diǎn)怪怪的(不排除調(diào)用方的流還要重用...這里給他關(guān)閉了就會顯得坑!)不偷懶了,手動寫一個(gè) 基于 string html 解析的函數(shù)(哈哈),就沒有上述問題了,也避免了重復(fù)創(chuàng)建流對象的問題(滑稽).
總結(jié)
在異步中使用一些需要釋放的資源的時(shí)候需要注意對象的生命周期,不然可能造成內(nèi)存泄漏或者代碼異常.
尤其是編寫一些底層一點(diǎn)點(diǎn)的代碼時(shí),往往為了優(yōu)化而不會同步等待資源到位,而是通過異步的方式訪問,這個(gè)時(shí)候關(guān)注對象的生命周期就顯得尤為重要了.
總結(jié)
以上是生活随笔為你收集整理的# C# 重新认识一下 IEnumerable<T>,IAsyncEnumerable<T> 以及搭配异步可能遇到的问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅述超声波液位计的故障排除方法
- 下一篇: 杯式法铝箔水蒸气透过率测定仪的工作原理