探究C#中的IEnumerable、IEnumerator、Yield
Case1:IEnumerable(可枚举/迭代的)
如果一个类实现了IEnumerable接口,则称为此类为可迭代的,要实现IEnumerable接口就要实现IEnumerable中定义的GetEnumerator()的方法生成一个迭代器(IEnumerator)。
IEnumerable是所有集合类型接口的基接口,如ICollection、IList;IEnumerable只能进行读操作,其具有延迟执行的特性,ICollection、IList的实现类支持增删改查,IList的实现类支持的操作更多、更加灵活如插入元素、移除元素等。
Case2:IEnumerator(迭代器)
如果我们把实现IEnumerable[可迭代的]看做一个类可以进行迭代的标记,那么IEnumerator[迭代器]就相当于迭代时的工作引擎,迭代中的主要工作都需要依靠迭代器进行。
Case3:Foreach相关
-
Case3-1 什么样的类可以进行Foreach迭代?
实现IEnumerable接口的类。换一个更强的定义,实现了IEnumerable接口同时实现了接口中的GetEnumerator方法。
-
Case3-2 Foreach究竟做了哪些事情?
var lst = new List<int> { 1, 2, 3 }; foreach (var item in lst) { Console.WriteLine(item); } Console.WriteLine("----------Equal-----------"); using var enumerator = lst.GetEnumerator();//获得迭代器,using声明确保迭代器用完就Dispose while (enumerator.MoveNext()) //迭代器开始工作 { Console.WriteLine(enumerator.Current);//获取当前项 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
Case4:自定义的可迭代类
// 可迭代对象 标记此类可迭代
public class SpecificEnumerable: IEnumerable
{
private readonly object[] _dataSource;
public SpecificEnumerable(object[] values)
{
_dataSource = values;
}
public IEnumerator GetEnumerator()
{
return new SpecificEnumerator(_dataSource);
}
// 迭代器 工作引擎
internal class SpecificEnumerator: IEnumerator
{
private int _position; // 指针
private readonly object[] _data; // 数据源
public SpecificEnumerator(object[] data)
{
_position = -1;
_data = data;
}
public bool MoveNext()
{
if(_position != _data.Length)
{
_position++;
}
return _position >= 0 && _position < _data.Length;
}
public void Reset()
{
_position = -1;
}
public object Current
{
get
{
if(_position == -1 || _position == _data.Length)
{
throw new IndexOutOfRangeException();
}
return _data[_position];
}
}
}
}
class Program
{
static void Main(string[] args)
{
# region 自定义可枚举类
var myEnumerable = new SpecificEnumerable(new object[]
{
"A", "B", "C", "D"
});
foreach(var item in myEnumerable)
{
Console.WriteLine(item);
}
Console.WriteLine("=========================>");
var myEnumerator = myEnumerable.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine(myEnumerator.Current);
}
#endregion
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
Case5:使用yield语法糖快速生成迭代器
通过Case4我们可以看到,整个创建自定义可迭代类的时候较为繁琐的过程就是生成工作引擎迭代器,需要手工实现迭代器接口中的MoveNext、Current属性等等,那么为了方便我们快速的生成枚举器,C#提供了一个yield语法糖来帮助我们简化这一过程,请注意,这仅仅是一个语法糖。
public class SpecificYieldEnumerable: IEnumerable
{
private readonly Random _random = new();
private readonly int _capacity;
public SpecificYieldEnumerable(int count)
{
_capacity = count;
}
public IEnumerator GetEnumerator()
{
for(var i = 0; i < _capacity; i++)
{
yield return _random.Next(100);
}
}
}
class Program
{
static void Main(string[] args)
{
var myEnumerable = new SpecificYieldEnumerable(10);
foreach(var item in myEnumerable)
{
Console.WriteLine(item);
}
Console.WriteLine("=================>");
var myEnumerator = myEnumerable.GetEnumerator();
while(myEnumerator.MoveNext())
{
Console.WriteLine(myEnumerator.Current);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
实际包含yield return的返回值为IEnumerator的方法,系统会帮我们自动生成一个hidden class实现了IEnumerator接口,这就是yield语法糖背后做的工作。
Case6:了解yield背后的故事
- 包含yield的方法返回值只能为IEnumerable和IEnumerator。
- 返回值为IEnumerable的方法,系统会自动生成一个hidden class实现了IEnumerable和一个迭代器IEnumerator
- 返回值为IEnumerator的方法,系统会自动生成一个hidden class仅仅实现了迭代器IEnumerator。
我们来验证一下上述结论:
我们反编译了解一下YieldTest1和YieldTest2在应用编译后的代码。
可以看出上述两个包含yield的方法编译后分别返回了一个实例,我们再对应看下系统生成的两个类<YieldTest1>d__0和<YieldTest2>d__1的定义。
进一步分析yield的运行过程,我们以YieldTest2方法为例分析:
整个迭代的主要工作都是在MoveNext中进行,类YieldTest2__1中一共有三个字段:
- state:迭代的执行状态,分别维护三个值0[迭代开始]、-1[迭代结束]、1[迭代进行中]
- current:当前迭代项的值
- i:迭代次序
那么此代码的运行过程大致如下:
var enumerator = YieldTest2(); //并没有去执行 我们从编译后的代码中也可以看到 此处只是得到了一个迭代器对象
while (enumerator.MoveNext()) // 调用MoveNext()真正开始执行
{
Console.WriteLine(enumerator.Current);
}
- 1
- 2
- 3
- 4
- 5
Step1:调用MoveNext()迭代开始,state状态赋值为0,迭代开始,current赋值,state状态赋值为1,迭代继续。
Step2:Loop MoveNext()判断state是否为1,为1,迭代继续。state置为-1,指针i++,判断i是否越界,若不越界,state再次置为1,继续迭代重复Step2;如已越界,执行Step3;
Step3:return false 迭代结束。
Case7: 一个案例阐述包含yield的迭代器在迭代时的运行顺序
public static IEnumerable YieldEnumerable() { Console.WriteLine("Start"); for (var i = 1; i <= 3; i++) { Console.WriteLine($"Item Start:{i}"); yield return i; Console.WriteLine("Item End"); }
<span class="token keyword">yield</span> <span class="token keyword">return</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">;</span> Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token string">"End"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
}
static void Main(string[] args)
{
var enumerable = YieldEnumerable();
foreach (var item in enumerable)
{
Console.WriteLine(
"
F
o
r
e
a
c
h
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
i
n
t
e
r
p
o
l
a
t
i
o
n
"
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
<
!
−
−
−
−
>
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
e
x
p
r
e
s
s
i
o
n
l
a
n
g
u
a
g
e
−
c
s
h
a
r
p
"
>
i
t
e
m
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
<
/
s
p
a
n
>
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
s
t
r
i
n
g
"
>
S
t
a
r
t
"
<
/
s
p
a
n
>
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
)
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
;
<
/
s
p
a
n
>
C
o
n
s
o
l
e
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
.
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
f
u
n
c
t
i
o
n
"
>
W
r
i
t
e
L
i
n
e
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
(
<
/
s
p
a
n
>
i
t
e
m
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
)
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
;
<
/
s
p
a
n
>
C
o
n
s
o
l
e
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
.
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
f
u
n
c
t
i
o
n
"
>
W
r
i
t
e
L
i
n
e
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
p
u
n
c
t
u
a
t
i
o
n
"
>
(
<
/
s
p
a
n
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
i
n
t
e
r
p
o
l
a
t
i
o
n
−
s
t
r
i
n
g
"
>
<
s
p
a
n
c
l
a
s
s
=
"
t
o
k
e
n
s
t
r
i
n
g
"
>
"Foreach </span><span class="token interpolation"><span class="token punctuation">{<!-- --></span><span class="token expression language-csharp">item</span><span class="token punctuation">}</span></span><span class="token string"> Start"</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span>item<span class="token punctuation">)</span><span class="token punctuation">;</span> Console<span class="token punctuation">.</span><span class="token function">WriteLine</span><span class="token punctuation">(</span><span class="token interpolation-string"><span class="token string">
"Foreach</span><spanclass="tokeninterpolation"><spanclass="tokenpunctuation"><!−−−−></span><spanclass="tokenexpressionlanguage−csharp">item</span><spanclass="tokenpunctuation"></span></span><spanclass="tokenstring">Start"</span></span><spanclass="tokenpunctuation">)</span><spanclass="tokenpunctuation">;</span>Console<spanclass="tokenpunctuation">.</span><spanclass="tokenfunction">WriteLine</span><spanclass="tokenpunctuation">(</span>item<spanclass="tokenpunctuation">)</span><spanclass="tokenpunctuation">;</span>Console<spanclass="tokenpunctuation">.</span><spanclass="tokenfunction">WriteLine</span><spanclass="tokenpunctuation">(</span><spanclass="tokeninterpolation−string"><spanclass="tokenstring">“Foreach {
item} End”);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
运行结果
我们结合前面的分析,实际上就很容易理解上述代码的运行顺序。包含yield的函数内部的执行逻辑就像被分割了几个部分一样,当调用MoveNext()时,开始执行,遇到yield时函数内部就像按下了暂停键,调用逻辑继续向后执行。当再次调用MoveNext()时,函数内部按下开始键,继续向后执行,当MoveNext()返回false,迭代停止。
Case8:yield的实际应用
yield目前应用十分广泛,不仅仅是C#,包括Python、JavaScript等语言中均有它的身影出现。
比如C#中TAP异步编程模型中的async、await语法糖的背后原理中也有yield参与工作、Dapper的源码中也出现了很多yield的身影;Python中也支持yield,可以使用yield+Event Loop实现协程以便进行协同式多任务处理;JavaScript中ES6中的Generator生成器中使用yield,内部模拟一个状态机式的管理机制实现更加优雅的异步编程。
最后我们给出一个JavaScript Generator 的一个小案例:
const funcAsync = (i) =>
new Promise((resovle, reject) => {
if (i === 3) {
reject('Wow!')
}
setTimeout(() => {
resovle('Hello World' + i)
}, 2000)
})
function* foo() {
console.log(‘Func1 Start’)
yield funcAsync(1)
console.log(‘Func2 Start’)
yield funcAsync(2)
console.log(‘Func3 Start’)
return funcAsync(3)
}
const generator = foo()
generator
.next()
.value.then((res) => {
console.log(res)
return generator.next().value
})
.then((res) => {
console.log(res)
return generator.next().value
})
.then((res) => {
console.log(res)
})
.catch((err) => {
console.error(err)
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35