迭代器方法 简单写一个迭代器方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void Start (){ foreach (var e in Iterator ()) { Console.WriteLine(e); } } IEnumerable<int > Iterator () { Console.WriteLine("00" ); yield return 1 ; Console.WriteLine("11" ); yield return 2 ; Console.WriteLine("22" ); }
上面的迭代器方法,在内部,编译器会生成一个匿名类,代码如下:
仔细观察会发现,这个匿名类同时继承IEnumerator和IEnumerable,意味着它同时负责生产迭代器和迭代。
状态点数字的含义:-1代表正在执行/当前不处于挂起点;-2代表尚未枚举或者已经Dispose;大于等于0代表各个恢复点
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 public class Program { public void Start () { IEnumerator<int > enumerator = this .Iterator().GetEnumerator(); try { while (enumerator.MoveNext()) Console.WriteLine(enumerator.Current); } finally { if (enumerator != null ) enumerator.Dispose(); } } [NullableContext(1) ] [IteratorStateMachine(typeof (Program.<Iterator>d__2)) ] private IEnumerable<int > Iterator () { Program.<Iterator>d__2 iteratorD2 = new Program.<Iterator>d__2(-2 ); iteratorD2.<>4 __this = this ; return (IEnumerable<int >) iteratorD2; } [CompilerGenerated ] private sealed class <Iterator >d__2 : IEnumerable <int >, IEnumerable , IEnumerator <int >, IEnumerator , IDisposable { private int <>1 __state; private int <>2 __current; private int <>l__initialThreadId; public Program <>4 __this; [DebuggerHidden ] public <Iterator>d__2(int _param1) { base ..ctor(); this .<>1 __state = _param1; this .<>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden ] void IDisposable.Dispose() { this .<>1 __state = -2 ; } bool IEnumerator.MoveNext() { switch (this .<>1 __state) { case 0 : this .<>1 __state = -1 ; Console.WriteLine("00" ); this .<>2 __current = 1 ; this .<>1 __state = 1 ; return true ; case 1 : this .<>1 __state = -1 ; Console.WriteLine("11" ); this .<>2 __current = 2 ; this .<>1 __state = 2 ; return true ; case 2 : this .<>1 __state = -1 ; Console.WriteLine("22" ); return false ; default : return false ; } } int IEnumerator<int >.Current { [DebuggerHidden ] get { return this .<>2 __current; } } [DebuggerHidden ] void IEnumerator.Reset() { throw new NotSupportedException(); } object IEnumerator.Current { [DebuggerHidden ] get { return (object ) this .<>2 __current; } } [DebuggerHidden ] [return: Nullable(1) ] IEnumerator<int > IEnumerable<int >.GetEnumerator() { Program.<Iterator>d__2 enumerator; if (this .<>1 __state == -2 && this .<>l__initialThreadId == Environment.CurrentManagedThreadId) { this .<>1 __state = 0 ; enumerator = this ; } else { enumerator = new Program.<Iterator>d__2(0 ); enumerator.<>4 __this = this .<>4 __this; } return (IEnumerator<int >) enumerator; } [DebuggerHidden ] [return: Nullable(1) ] IEnumerator IEnumerable.GetEnumerator() { return (IEnumerator) this .System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator(); } } }
yield break 的行为 如果在迭代器方法中执行yield break,MoveNext会直接return false,不会再执行后续代码:
1 2 3 4 5 6 7 IEnumerable<int > Demo () { yield return 1 ; if (someCondition) yield break ; yield return 2 ; }
本质上,编译器同样会为yield break分配一个状态点,只不过该状态点的逻辑就是直接return false——等价于提前走到了方法末尾。
GetEnumerator 中的线程安全复用 注意上面的GetEnumerator方法里有这样一个判断:
1 2 3 4 5 if (this .<>1 __state == -2 && this .<>l__initialThreadId == Environment.CurrentManagedThreadId){ this .<>1 __state = 0 ; enumerator = this ; }
只有在state==-2(已Dispose或尚未开始)且当前线程等于创建线程时,才会复用当前实例。跨线程调用时永远new新实例,避免了线程安全问题。
Unity协程 协程的方法体返回类型是IEnumerator而不是IEnumerable,因为它不需要重复产生枚举对象,枚举对象是一次性的,每次StartCoroutine(Cor())(Cor()的时候就已经分配 )的时候都会new一个新的迭代器状态机:
1 2 3 4 5 6 7 [IteratorStateMachine(typeof (TempScript.<Cor>d__3)) ] private IEnumerator Cor (){ TempScript.<Cor>d__3 corD3 = new TempScript.<Cor>d__3(0 ); corD3.<>4 __this = this ; return (IEnumerator) corD3; }
这个迭代器状态机不可缓存来复用,内部状态已经用过就污染了。但是像是WaitForSeconds这类YieldInstruction还是可以复用的,Unity协程调度器并不会改变它们内部的状态,状态由调度器自己保存。但是有些比如WaitForSecondsRealtime这类CustomYieldInstruction是会被改变内部状态的,但是它们同时拥有Reset()重置的功能。可以将WaitForEndOfFrame, WaitForFixedUpdate, WaitForSeconds[0.1f/0.5f/1f]它们作为单例去复用
鸭子类型 编译期鸭子类型(compile-time duck typing),一种比接口更加宽松的模式匹配。
foreach 编译器尝试寻找
返回的对象内再去寻找有没有
1 2 bool MoveNext ()T Current { get ; }
只要同时满足以上条件,就可以进行foreach:
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 public struct MyEnumerator{ int _i; public bool MoveNext () { _i++; return _i <= 3 ; } public int Current => _i; } public class MyCollection { public MyEnumerator GetEnumerator () { return new MyEnumerator(); } } public static Main (){ foreach (var x in new MyCollection ()) { Console.WriteLine(x); } }
C#明明是静态语言,为什么会有鸭子类型?因为C#编译器在少数语法糖里会进行模式匹配成员查找,而不是接口检查, 可以看作是一种特殊的编译器规则。为什么要有这些?一方面为了避免装箱,因为如果通过接口承接迭代器,迭代器如果是结构体 就会发生装箱。
foreach支持ref返回 1 2 public ref int Current => ref _current;foreach (ref var x in iterator);
编译期间的不同版本处理 前面我们知道,编译器对List执行foreach会进行如下代码展开:
1 2 3 4 5 6 7 8 9 List<T>.Enumerator enumerator1 = new List<T>.GetEnumerator(); try { while (enumerator1.MoveNext()) Handle(enumerator1.Current); } finally { enumerator1.Dispose(); }
但对数组T[]执行foreach,则会是另一种展开:
1 2 3 T[] numArray = new T[3 ]; for (int index = 0 ; index < numArray.Length; ++index) Console.WriteLine(numArray[index]);
foreach的行为取决于编译时类型,而不是运行时类型 我们有如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { List<int > arr = new List<int >(); foreach (var e in arr) { Console.WriteLine(e); } } { IList<int > arr = new List<int >(); foreach (var e in arr) { Console.WriteLine(e); } }
编译器展开后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 List<int >.Enumerator enumerator1 = new List<int >().GetEnumerator(); try { while (enumerator1.MoveNext()) Console.WriteLine(enumerator1.Current); } finally { enumerator1.Dispose(); } IEnumerator<int > enumerator2 = ((IEnumerable<int >) new List<int >()).GetEnumerator(); try { while (enumerator2.MoveNext()) Console.WriteLine(enumerator2.Current); } finally { if (enumerator2 != null ) enumerator2.Dispose(); }
前者,其编译期类型就是List<int>,编译器直接去匹配List<int>.GetEnumerator(),它返回的是List<int>.Enumerator, 这是一个结构体,直接生成代码List<int>.Enumerator e = list.GetEnumerator();,整个过程没有接口、装箱,非常干净。
后者,其编译期类型是IList<int>,编译器不知道它的实际类型是什么,虽然我这简单写的就是赋值,但是代码里可能是通过一个复杂函数, 返回各种不同容器,比如List<int>, int[], ReadOnlyCollection<int>, 自定义容器等等,编译器只能按照IList<int>的类型 公事公办。但是IList<int>没有GetEnumerator,但是它继承自IEnumerable<T>,所以编译器只能生成IEnumerator<int> e = ((IEnumerable<int>)list).GetEnumerator(); 由于List<T>.Enumerator是结构体,因此赋值给接口后就会发生装箱
根据以上描述,加深了一个印象:foreach优先进行模式匹配,其次才是走接口。
await 并不要求被await的一定是Task,只要求它具有
且返回值里包含:
1 2 3 IsCompleted OnCompleted ()GetResult ()
任何满足上述条件的都可以被await。
同样的模式也出现在await foreach(C# 8):
1 await foreach (var x in asyncEnumerable)
编译器会去找GetAsyncEnumerator(),返回的对象里再找MoveNextAsync()和Current。
deconstruction解构
要求实现
using 旧版要求必须继承IDisposable,但是目前会优先进行模式匹配识别
fixed
会找: