一.起缘
故事缘于一位朋友的一道题:
朋友四人玩LOL游戏。第一局,分别选择位置:中单,上单,ADC,辅助;第二局新加入的伙伴要选上单,四人可选位置变为:中单,打野,ADC,辅助;要求,第二局四人每人不得选择和第一局相同的位置,请问两局综合考虑有多少种位置选择方式?
对于像我这边不懂游戏的人来讲,看不懂。于是有了这个版本:
有4个人,4只椅子,第一局每人坐一只椅子,第二局去掉第2只椅子,增加第5只椅子,每人坐一只椅子,而且每个人不能与第一局坐相同的椅子。问两局综合考虑,共有多少种可能的情况?
我一开始的想法是这样的,4个人就叫ABCD:第1局可能数是4*3*2*1=24,如果A第1局选了第2张椅,则A有4种可能,否则A有3种可能。对B来讲,如果A选了B第一局的椅,则B有3种可能,否则B有2种可能(排队自己第一局和A第二局已选)……想到这里我就晕了,情况越分越多。
二.原始的for嵌套
本来是一道数学题,应该由知识算出来有多少种,但我突然有个想法,不如用计算机穷举出出来。一来可以为各种猜测提供一个正确的答案,二来或许可以从答案反推出(数学上的)计算方法。然后就写了第1版:
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
|
static Seat data = new Seat(); public static void Run() { for ( int a = 0; a < 4; a++) { if (data.IsSelected(0, a)) //第1局编号0。如果已经被人坐了。 continue ; data.Selected(0, a, "A" ); //第1局编号0。A坐a椅。 for ( int b = 0; b < 4; b++) { if (data.IsSelected(0, b)) continue ; data.Selected(0, b, "B" ); for ( int c = 0; c < 4; c++) { if (data.IsSelected(0, c)) continue ; data.Selected(0, c, "C" ); for ( int d = 0; d < 4; d++) { if (data.IsSelected(0, d)) continue ; data.Selected(0, d, "D" ); for ( int a2 = 0; a2 < 5; a2++) { if (a2 == 1) continue ; if (data.IsSelected(1, a2)) //第2局编号1 continue ; if (data.IsSelected(0, a2, "A" )) //如果第1局A坐了a2椅 continue ; data.Selected(1, a2, "A" ); for ( int b2 = 0; b2 < 5; b2++) { if (b2 == 1) continue ; if (data.IsSelected(1, b2)) continue ; if (data.IsSelected(0, b2, "B" )) continue ; data.Selected(1, b2, "B" ); for ( int c2 = 0; c2 < 5; c2++) { if (c2 == 1) continue ; if (data.IsSelected(1, c2)) continue ; if (data.IsSelected(0, c2, "C" )) continue ; data.Selected(1, c2, "C" ); for ( int d2 = 0; d2 < 5; d2++) { if (d2 == 1) continue ; if (data.IsSelected(1, d2)) continue ; if (data.IsSelected(0, d2, "D" )) continue ; data.Selected(1, d2, "D" ); data.Count++; //可能的情况数加1 Console.WriteLine( "{0,5} {1}" , data.Count, data.Current); data.UnSelected(1, d2); } data.UnSelected(1, c2); } data.UnSelected(1, b2); } data.UnSelected(1, a2); } data.UnSelected(0, d); } data.UnSelected(0, c); } data.UnSelected(0, b); } data.UnSelected(0, a); //A起身(释放坐椅) } } |
部分运行结果:
说明:
1.ABCD是人名
2.“.”代表没有人
3.位置是是座位
4.-左边是第1局,右边是第2局
5.数字是序号
1 A B C D .-B . A C D
2 A B C D .-C . A B D
3 A B C D .-D . A B C
4 A B C D .-D . A C B
5 A B C D .-B . D A C
6 A B C D .-C . B A D
7 A B C D .-D . B A C
8 A B C D .-C . D A B
9 A B C D .-B . D C A
10 A B C D .-D . B C A
11 A B C D .-C . D B A
12 A B D C .-B . A D C
...
262 D C B A .-B . C D A
263 D C B A .-B . D C A
264 D C B A .-C . D B A
算出来是264种。从答案上来看是每11种是一组,一组中第1局的坐法是相同的,也就是说对于第一局的每一种情况,第2局都是有11种不同的可能。而第一局的可能性是24,所以答案是24*11=264。而第2局为什么是11种可能,后面再说。
三.想要链式写法
主题来了,像刚才的第1版的写法太死板太麻烦了。
如果能像这样写代码就爽了:
1
|
obj.Try( "A" ).Try( "B" ).Try( "C" ).Try( "D" ).Try2( "A" ).Try2( "B" ).Try2( "C" ).Try2( "D" ).Write(); |
而这样的代码通常的逻辑是执行Try("A")方法,然后执行Try("A")它return的对象的Try("B")方法……,即是Try("B")方法只被执行1次,而我希望的是Try("B")方法被Try("A")内部循环调用n次,Try("C")方法又被Try("B")方法调用m次。想想第1版的for套for不难明白为什么要追求这样的效果。如果Try("A")执行完了,再去执行Try("B"),那么Try("B")肯定不会被调用多次,所以得延迟Try("A")的执行,同理也延迟所有Try和Try2的执行。由于lambda表达天生有延迟计算的特性,于是很快写出了第2版:
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
|
public static void Run2() { Try( "A" , () => Try( "B" , () => Try( "C" , () => Try( "D" , () => Try2( "A" , () => Try2( "B" , () => Try2( "C" , () => Try2( "D" , null ) ) ) ) ) ) ) ); } public static void Try( string name, Action action) //第1局 { for ( int i = 0; i < 4; i++) { if (data.IsSelected(0, i)) continue ; data.Selected(0, i, name); if (action == null ) { Console.WriteLine(data.Current); } else { action(); } data.UnSelected(0, i); } } public static void Try2( string name, Action action) //第2局 { for ( int i = 0; i < 5; i++) { if (i == 1) continue ; if (data.IsSelected(1, i)) continue ; if (data.IsSelected(0, i, name)) continue ; data.Selected(1, i, name); if (action == null ) { data.Count++; Console.WriteLine( "{0,5} {1}" , data.Count, data.Current); } else { action(); } data.UnSelected(1, i); } } |
结构更合理,逻辑更清晰,但是一堆lambda嵌套,太丑了,也不是我要的效果,我要的是类似这样的:
obj.Try("A").Try("B").Try("C").Try("D").Try2("A").Try2("B").Try2("C").Try2("D").Write();
四.继续向目标逼近。
由于要延迟,所以必须先把要被调用的方法的引用“告诉”上一级,当上一级执行for的时候,就能调用下一级的方法。于是我想到了一个“回调链”
所以,执行链式方法是在构造回调链,最后的方法再通过调用链头(Head)的某个方法启动真正要执行的整个逻辑。
延迟计算是从Linq借鉴和学习来的,构造Linq的过程并没有执行,等到了执行ToList, First等方法时才真正去执行。
我想构造回调链每一步都是一个固定的方法,这里随便起用了T这个极短名称,而每一步后期计算时要执行的方法可灵活指定。于是有了第3版:
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
|
static Seat data = new Seat(); //借用Seat保存数据 public Seat2( string name, Seat2 parent, Action<Seat2> method) { this .Name = name; this .Parent = parent; if (parent != null ) parent.Child = this ; this .Method = method; } public static void Run() { new Seat2( "A" , null , me => me.Try()) .T( "B" , me => me.Try()) .T( "C" , me => me.Try()) .T( "D" , me => me.Try()) .T( "A" , me => me.Try2()) .T( "B" , me => me.Try2()) .T( "C" , me => me.Try2()) .T( "D" , me => me.Try2()) .P().Start(); } public Seat2 T( string name, Action<Seat2> method) { return new Seat2(name, this , method); } public void Try() { for ( int i = 0; i < 4; i++) { if (data.IsSelected(0, i)) continue ; data.Selected(0, i, this .Name); if ( this .Child != null ) { this .Child.Method( this .Child); } data.UnSelected(0, i); } } public void Try2() { for ( int i = 0; i < 5; i++) { if (i == 1) continue ; if (data.IsSelected(1, i)) continue ; if (data.IsSelected(0, i, this .Name)) continue ; data.Selected(1, i, this .Name); if ( this .Child != null ) { this .Child.Method( this .Child); } data.UnSelected(1, i); } } |
五.解耦
这种调用方式,是满意了。但是运算框架与具体的算法耦合在一起,如果能把运算框架提取出来,以后写具体的算法也方便许多。于是经过苦逼的提取,测试,踩坑,最终出现了第4版:
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
|
//运算框架 class ComputeLink<T> where T : ISeat { ComputeLink<T> Parent { get ; set ; } //父节点,即上一级节点 ComputeLink<T> Child { get ; set ; } //子节点,即下一级节点 T Obj { get ; set ; } //当前节点对应的算法对象,可以看作业务对象 public ComputeLink(T obj, ComputeLink<T> parent, Action<T> method) { if (obj == null ) throw new ArgumentNullException( "obj" ); this .Obj = obj; this .Obj.Method = x => method((T)x); if (parent != null ) { this .Parent = parent; parent.Child = this ; parent.Obj.Child = this .Obj; } } public static ComputeLink<T> New(T obj, Action<T> method) { return new ComputeLink<T>(obj, null , method); } public ComputeLink<T> Do(T obj, Action<T> method) { return new ComputeLink<T>(obj, this , method); } public ComputeLink<T> Head //链表的头 { get { if ( null != this .Parent) return this .Parent.Head; return this ; } } public void Action() //启动(延迟的)整个计算 { var head = this .Head; head.Obj.Method(head.Obj); } } interface ISeat { ISeat Child { get ; set ; } Action<ISeat> Method { get ; set ; } } |
p.s.为什么第4版是ISeat3而不是ISeat4呢,因为我本不把第1版当作1个版本,因为太原始了,出现第2版后,我就把第1版给删除了。为了写这篇文章才重新去写第1版。于是原本我当作第3版的ISeat3自然地排到了第4版。
具体的"算法"就很简单了:
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
|
class Seat3 : ISeat3 { static Seat data = new Seat(); string Name { get ; set ; } public Seat3( string name) { this .Name = name; } /// <summary> /// 解耦的版本 /// </summary> public static void Run() { var sql = ComputeLink<Seat3> .New( new Seat3( "A" ), m => m.Try()) .Do( new Seat3( "B" ), m => m.Try()) .Do( new Seat3( "C" ), m => m.Try()) .Do( new Seat3( "D" ), m => m.Try()) .Do( new Seat3( "A" ), m => m.Try2()) .Do( new Seat3( "B" ), m => m.Try2()) .Do( new Seat3( "C" ), m => m.Try2()) .Do( new Seat3( "D" ), m => m.Try2()) .Do( new Seat3( "" ), m => m.Print()); sql.Action(); } public Action<ISeat3> Method { get ; set ; } public ISeat3 Child { get ; set ; } public void Try() { for ( int i = 0; i < 4; i++) { if (data.IsSelected(0, i)) continue ; data.Selected(0, i, this .Name); if ( this .Child != null ) { this .Child.Method( this .Child); } data.UnSelected(0, i); } } public void Try2() { for ( int i = 0; i < 5; i++) { if (i == 1) continue ; if (data.IsSelected(1, i)) continue ; if (data.IsSelected(0, i, this .Name)) continue ; data.Selected(1, i, this .Name); if ( this .Child != null ) { this .Child.Method( this .Child); } data.UnSelected(1, i); } } public void Print() { data.Count++; Console.WriteLine( "{0,5} {1}" , data.Count, data.Current); } } |
Seat3写起来简单,(Run方法内部)看起来舒服。通过链式写法达到嵌套循环的效果。对,这就是我要的!
它很像linq,所以我直接给变量命名为sql。
•对于Try和Try2来讲,要调用的方法最好从参数传来,但是这样就会增加Run方法中New和Do的参数复杂性,破坏了美感,所以经过权衡,Child和Method通过属性传入。这个我也不确定这样做好不好,请各位大侠指点。
•还有一个细节,就是ComputeLink构造方法中的(行号12的)代码 this.Obj.Method = x => method((T)x); 。我原来是这样写的 this.Obj.Method = method; 编译不通过,原因是不能把 Action<ISeat3> 转化为 Action<T> ,虽然T一定实现了ISeat3,强制转化也不行,想起以前看过的一篇文章里面提到希望C#以后的版本能拥有的一特性叫“协变”,很可能指的就是这个。既然这个 Action<ISeat3> 不能转化为 Action<T> 但是ISeat3是可以强制转化为T的,所以我包了一层薄薄的壳,成了 this.Obj.Method = x => method((T)x); ,如果有更好的办法请告诉我。
六.第2局为什么是11种可能
回过头来解决为什么对于一个确定的第1局,第2局有11种可能。
不妨假设第1局的选择是A选1号椅,B选2号椅,C选3号椅,D选4号椅。
第2局分为两大类情况:
如果B选了第5号椅
则只有2种可能:
A B C D .-D . A C B
A B C D .-C . D A B
如果B选了不是第5号椅,
则ACD都有可能选第5号椅,有3种可能。B有3种选的可能(1,3,4号椅),B一旦确定,A和C也只有一种可能
所以11 = 2 + 3 * 3
七.结论
由一道数学题牵引出多层循环嵌套,最终通过封装达到了我要的链式调用的效果,我是很满意的。这也是我第一次设计延迟计算,感觉强烈。如果新的场景需要用到延迟计算我想有了这次经验写起来会顺手许多。如果是需要多层for的算法题都可以比较方便的实现了。
你都看到这里了,为我点个赞吧,能说一下看法就更好了。
完整代码:
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
|
using System; using System.Linq; using System.Diagnostics; namespace ConsoleApplication { class Seat { static Seat data = new Seat(); public static void Run() { //Seat.Run(); //return; for ( int a = ; a < ; a++) { if (data.IsSelected(, a)) //第局编号。如果已经被人坐了。 continue ; data.Selected(, a, "A" ); //第局编号。A坐a椅。 for ( int b = ; b < ; b++) { if (data.IsSelected(, b)) continue ; data.Selected(, b, "B" ); for ( int c = ; c < ; c++) { if (data.IsSelected(, c)) continue ; data.Selected(, c, "C" ); for ( int d = ; d < ; d++) { if (data.IsSelected(, d)) continue ; data.Selected(, d, "D" ); for ( int a = ; a < ; a++) { if (a == ) continue ; if (data.IsSelected(, a)) //第局编号 continue ; if (data.IsSelected(, a, "A" )) //如果第局A坐了a椅 continue ; data.Selected(, a, "A" ); for ( int b = ; b < ; b++) { if (b == ) continue ; if (data.IsSelected(, b)) continue ; if (data.IsSelected(, b, "B" )) continue ; data.Selected(, b, "B" ); for ( int c = ; c < ; c++) { if (c == ) continue ; if (data.IsSelected(, c)) continue ; if (data.IsSelected(, c, "C" )) continue ; data.Selected(, c, "C" ); for ( int d = ; d < ; d++) { if (d == ) continue ; if (data.IsSelected(, d)) continue ; if (data.IsSelected(, d, "D" )) continue ; data.Selected(, d, "D" ); data.Count++; //可能的情况数加 Console.WriteLine( "{,} {}" , data.Count, data.Current); data.UnSelected(, d); } data.UnSelected(, c); } data.UnSelected(, b); } data.UnSelected(, a); } data.UnSelected(, d); } data.UnSelected(, c); } data.UnSelected(, b); } data.UnSelected(, a); //A起身(释放坐椅) } } public static void Run() { Try( "A" , () => Try( "B" , () => Try( "C" , () => Try( "D" , () => Try( "A" , () => Try( "B" , () => Try( "C" , () => Try( "D" , null ) ) ) ) ) ) ) ); } public static void Try( string name, Action action) { for ( int i = ; i < ; i++) { if (data.IsSelected(, i)) continue ; data.Selected(, i, name); if (action == null ) { Console.WriteLine(data.Current); } else { action(); } data.UnSelected(, i); } } public static void Try( string name, Action action) { for ( int i = ; i < ; i++) { if (i == ) continue ; if (data.IsSelected(, i)) continue ; if (data.IsSelected(, i, name)) continue ; data.Selected(, i, name); if (action == null ) { data.Count++; Console.WriteLine( "{,} {}" , data.Count, data.Current); } else { action(); } data.UnSelected(, i); } } public Seat() { seats[, ] = "." ; seats[, ] = "." ; } private string [,] seats = new string [, ]; public void UnSelected( int game, int i) { Debug.Assert(game == && i != || game == && i != ); Debug.Assert(seats[game, i] != null ); seats[game, i] = null ; } public void Selected( int game, int i, string name) { Debug.Assert(game == && i != || game == && i != ); Debug.Assert(seats[game, i] == null ); seats[game, i] = name; } public bool IsSelected( int game, int a) { return seats[game, a] != null && seats[game, a] != "." ; } public bool IsSelected( int game, int a, string name) { return seats[game, a] == name; } public string Current { get { return string .Format( "{} {} {} {} {}-{} {} {} {} {}" , seats[, ], seats[, ], seats[, ], seats[, ], seats[, ], seats[, ], seats[, ], seats[, ], seats[, ], seats[, ]); } } public int Count { get ; set ; } } class Seat { static Seat data = new Seat(); //借用Seat保存法的数据 Seat Parent { get ; set ; } Seat Child { get ; set ; } string Name { get ; set ; } Action<Seat> Method { get ; set ; } public Seat( string name, Seat parent, Action<Seat> method) { this .Name = name; this .Parent = parent; if (parent != null ) parent.Child = this ; this .Method = method; } /// <summary> /// 耦合的版本 /// </summary> public static void Run() { new Seat( "A" , null , me => me.Try()) .T( "B" , me => me.Try()) .T( "C" , me => me.Try()) .T( "D" , me => me.Try()) .T( "A" , me => me.Try()) .T( "B" , me => me.Try()) .T( "C" , me => me.Try()) .T( "D" , me => me.Try()) .P().Start(); } public Seat T( string name, Action<Seat> method) { return new Seat(name, this , method); } public Seat P() { return new Seat( "Print" , this , me => me.Print()); } public void Start() { var head = this .Head; head.Method(head); } public Seat Head { get { if ( null != this .Parent) return this .Parent.Head; return this ; } } public void Try() { for ( int i = ; i < ; i++) { if (data.IsSelected(, i)) continue ; data.Selected(, i, this .Name); if ( this .Child != null ) { this .Child.Method( this .Child); } data.UnSelected(, i); } } public void Try() { for ( int i = ; i < ; i++) { if (i == ) continue ; if (data.IsSelected(, i)) continue ; if (data.IsSelected(, i, this .Name)) continue ; data.Selected(, i, this .Name); if ( this .Child != null ) { this .Child.Method( this .Child); } data.UnSelected(, i); } } public void Print() { data.Count++; Console.WriteLine( "{,} {}" , data.Count, data.Current); } public override string ToString() { return this .Name.ToString(); } } class ComputeLink<T> where T : ISeat { ComputeLink<T> Parent { get ; set ; } //父节点,即上一级节点 ComputeLink<T> Child { get ; set ; } //子节点,即下一级节点 T Obj { get ; set ; } //当前节点对应的算法对象,可以看作业务对象 public ComputeLink(T obj, ComputeLink<T> parent, Action<T> method) { if (obj == null ) throw new ArgumentNullException( "obj" ); this .Obj = obj; this .Obj.Method = x => method((T)x); if (parent != null ) { this .Parent = parent; parent.Child = this ; parent.Obj.Child = this .Obj; } } public static ComputeLink<T> New(T obj, Action<T> method) { return new ComputeLink<T>(obj, null , method); } public ComputeLink<T> Do(T obj, Action<T> method) { return new ComputeLink<T>(obj, this , method); } public ComputeLink<T> Head //链表的头 { get { if ( null != this .Parent) return this .Parent.Head; return this ; } } public void Action() //启动(延迟的)整个计算 { var head = this .Head; head.Obj.Method(head.Obj); } } interface ISeat { ISeat Child { get ; set ; } Action<ISeat> Method { get ; set ; } } class Seat : ISeat { static Seat data = new Seat(); string Name { get ; set ; } public Seat( string name) { this .Name = name; } /// <summary> /// 解耦的版本 /// </summary> public static void Run() { var sql = ComputeLink<Seat> .New( new Seat( "A" ), m => m.Try()) .Do( new Seat( "B" ), m => m.Try()) .Do( new Seat( "C" ), m => m.Try()) .Do( new Seat( "D" ), m => m.Try()) .Do( new Seat( "A" ), m => m.Try()) .Do( new Seat( "B" ), m => m.Try()) .Do( new Seat( "C" ), m => m.Try()) .Do( new Seat( "D" ), m => m.Try()) .Do( new Seat( "" ), m => m.Print()); sql.Action(); } public Action<ISeat> Method { get ; set ; } public ISeat Child { get ; set ; } public void Try() { for ( int i = ; i < ; i++) { if (data.IsSelected(, i)) continue ; data.Selected(, i, this .Name); if ( this .Child != null ) { this .Child.Method( this .Child); } data.UnSelected(, i); } } public void Try() { for ( int i = ; i < ; i++) { if (i == ) continue ; if (data.IsSelected(, i)) continue ; if (data.IsSelected(, i, this .Name)) continue ; data.Selected(, i, this .Name); if ( this .Child != null ) { this .Child.Method( this .Child); } data.UnSelected(, i); } } public void Print() { data.Count++; Console.WriteLine( "{,} {}" , data.Count, data.Current); } public override string ToString() { return this .Name.ToString(); } } } |
以上内容是小编给大家介绍的C#用链式方法表达循环嵌套的相关知识,希望对大家有所帮助!