情节故事得有情节,不喜欢情节的朋友可看第1版代码,然后直接跳至“三.想要链式写法”
一.起缘
故事缘于一位朋友的一道题:
朋友四人玩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版:
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版的写法太死板太麻烦了。
如果能像这样写代码就爽了: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版:
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版:static Seat data = new Seat(); //借用Seat保存数据public Seat2(string name, Seat2 parent, Actionmethod){ 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 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 class ComputeLinkwhere T : ISeat3 3 { 4 ComputeLink Parent { get; set; } //父节点,即上一级节点 5 ComputeLink Child { get; set; } //子节点,即下一级节点 6 T Obj { get; set; } //当前节点对应的算法对象,可以看作业务对象 7 public ComputeLink(T obj, ComputeLink parent, Action method) 8 { 9 if (obj == null)10 throw new ArgumentNullException("obj");11 this.Obj = obj;12 this.Obj.Method = x => method((T)x);13 if (parent != null)14 {15 this.Parent = parent;16 parent.Child = this;17 parent.Obj.Child = this.Obj;18 }19 }20 public static ComputeLink New(T obj, Action method)21 {22 return new ComputeLink (obj, null, method);23 }24 25 public ComputeLink Do(T obj, Action method)26 {27 return new ComputeLink (obj, this, method);28 }29 public ComputeLink Head //链表的头30 {31 get32 {33 if (null != this.Parent)34 return this.Parent.Head;35 return this;36 }37 }38 public void Action() //启动(延迟的)整个计算39 {40 var head = this.Head;41 head.Obj.Method(head.Obj);42 }43 }44 interface ISeat345 {46 ISeat3 Child { get; set; }47 Action Method { get; set; }48 }
p.s.为什么第4版是ISeat3而不是ISeat4呢,因为我本不把第1版当作1个版本,因为太原始了,出现第2版后,我就把第1版给删除了。为了写这篇文章才重新去写第1版。于是原本我当作第3版的ISeat3自然地排到了第4版。
具体的"算法"就很简单了:
class Seat3 : ISeat3{ static Seat data = new Seat(); string Name { get; set; } public Seat3(string name) { this.Name = name; } ////// 解耦的版本 /// public static void Run() { var sql = ComputeLink.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 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 using System; 2 using System.Linq; 3 using System.Diagnostics; 4 5 namespace ConsoleApplication1 6 { 7 class Seat 8 { 9 static Seat data = new Seat(); 10 public static void Run() 11 { 12 //Seat2.Run(); 13 //return; 14 for (int a = 0; a < 4; a++) 15 { 16 if (data.IsSelected(0, a)) //第1局编号0。如果已经被人坐了。 17 continue; 18 data.Selected(0, a, "A"); //第1局编号0。A坐a椅。 19 for (int b = 0; b < 4; b++) 20 { 21 if (data.IsSelected(0, b)) 22 continue; 23 data.Selected(0, b, "B"); 24 for (int c = 0; c < 4; c++) 25 { 26 if (data.IsSelected(0, c)) 27 continue; 28 data.Selected(0, c, "C"); 29 for (int d = 0; d < 4; d++) 30 { 31 if (data.IsSelected(0, d)) 32 continue; 33 data.Selected(0, d, "D"); 34 for (int a2 = 0; a2 < 5; a2++) 35 { 36 if (a2 == 1) 37 continue; 38 if (data.IsSelected(1, a2)) //第2局编号1 39 continue; 40 if (data.IsSelected(0, a2, "A")) //如果第1局A坐了a2椅 41 continue; 42 data.Selected(1, a2, "A"); 43 for (int b2 = 0; b2 < 5; b2++) 44 { 45 if (b2 == 1) 46 continue; 47 if (data.IsSelected(1, b2)) 48 continue; 49 if (data.IsSelected(0, b2, "B")) 50 continue; 51 data.Selected(1, b2, "B"); 52 for (int c2 = 0; c2 < 5; c2++) 53 { 54 if (c2 == 1) 55 continue; 56 if (data.IsSelected(1, c2)) 57 continue; 58 if (data.IsSelected(0, c2, "C")) 59 continue; 60 data.Selected(1, c2, "C"); 61 for (int d2 = 0; d2 < 5; d2++) 62 { 63 if (d2 == 1) 64 continue; 65 if (data.IsSelected(1, d2)) 66 continue; 67 if (data.IsSelected(0, d2, "D")) 68 continue; 69 data.Selected(1, d2, "D"); 70 71 data.Count++; //可能的情况数加1 72 Console.WriteLine("{0,5} {1}", data.Count, data.Current); 73 74 data.UnSelected(1, d2); 75 } 76 data.UnSelected(1, c2); 77 } 78 data.UnSelected(1, b2); 79 } 80 data.UnSelected(1, a2); 81 } 82 data.UnSelected(0, d); 83 } 84 data.UnSelected(0, c); 85 } 86 data.UnSelected(0, b); 87 } 88 data.UnSelected(0, a); //A起身(释放坐椅) 89 } 90 } 91 public static void Run2() 92 { 93 Try("A", 94 () => Try("B", 95 () => Try("C", 96 () => Try("D", 97 () => Try2("A", 98 () => Try2("B", 99 () => Try2("C",100 () => Try2("D",101 null102 )103 )104 )105 )106 )107 )108 )109 );110 }111 public static void Try(string name, Action action)112 {113 for (int i = 0; i < 4; i++)114 {115 if (data.IsSelected(0, i))116 continue;117 data.Selected(0, i, name);118 if (action == null)119 {120 Console.WriteLine(data.Current);121 }122 else123 {124 action();125 }126 data.UnSelected(0, i);127 }128 }129 130 public static void Try2(string name, Action action)131 {132 for (int i = 0; i < 5; i++)133 {134 if (i == 1)135 continue;136 if (data.IsSelected(1, i))137 continue;138 if (data.IsSelected(0, i, name))139 continue;140 data.Selected(1, i, name);141 if (action == null)142 {143 data.Count++;144 Console.WriteLine("{0,5} {1}", data.Count, data.Current);145 }146 else147 {148 action();149 }150 data.UnSelected(1, i);151 }152 }153 public Seat()154 {155 seats[0, 4] = ".";156 seats[1, 1] = ".";157 }158 private string[,] seats = new string[2, 5];159 public void UnSelected(int game, int i)160 {161 Debug.Assert(game == 0 && i != 4 || game == 1 && i != 1);162 Debug.Assert(seats[game, i] != null);163 seats[game, i] = null;164 }165 public void Selected(int game, int i, string name)166 {167 Debug.Assert(game == 0 && i != 4 || game == 1 && i != 1);168 Debug.Assert(seats[game, i] == null);169 seats[game, i] = name;170 }171 public bool IsSelected(int game, int a)172 {173 return seats[game, a] != null && seats[game, a] != ".";174 }175 public bool IsSelected(int game, int a, string name)176 {177 return seats[game, a] == name;178 }179 public string Current180 {181 get182 {183 return string.Format("{0} {1} {2} {3} {4}-{5} {6} {7} {8} {9}",184 seats[0, 0], seats[0, 1], seats[0, 2], seats[0, 3], seats[0, 4],185 seats[1, 0], seats[1, 1], seats[1, 2], seats[1, 3], seats[1, 4]);186 }187 }188 189 public int Count { get; set; }190 }191 192 class Seat2193 {194 static Seat data = new Seat(); //借用Seat保存法的数据195 Seat2 Parent { get; set; }196 Seat2 Child { get; set; }197 string Name { get; set; }198 ActionMethod { get; set; }199 public Seat2(string name, Seat2 parent, Action method)200 {201 this.Name = name;202 this.Parent = parent;203 if (parent != null)204 parent.Child = this;205 this.Method = method;206 }207 /// 208 /// 耦合的版本209 /// 210 public static void Run()211 {212 new Seat2("A", null, me => me.Try())213 .T("B", me => me.Try())214 .T("C", me => me.Try())215 .T("D", me => me.Try())216 .T("A", me => me.Try2())217 .T("B", me => me.Try2())218 .T("C", me => me.Try2())219 .T("D", me => me.Try2())220 .P().Start();221 }222 public Seat2 T(string name, Actionmethod)223 {224 return new Seat2(name, this, method);225 }226 public Seat2 P()227 {228 return new Seat2("Print", this, me => me.Print());229 }230 public void Start()231 {232 var head = this.Head;233 head.Method(head);234 }235 public Seat2 Head236 {237 get238 {239 if (null != this.Parent)240 return this.Parent.Head;241 return this;242 }243 }244 public void Try()245 {246 for (int i = 0; i < 4; i++)247 {248 if (data.IsSelected(0, i))249 continue;250 data.Selected(0, i, this.Name);251 if (this.Child != null)252 {253 this.Child.Method(this.Child);254 }255 data.UnSelected(0, i);256 }257 }258 public void Try2()259 {260 for (int i = 0; i < 5; i++)261 {262 if (i == 1)263 continue;264 if (data.IsSelected(1, i))265 continue;266 if (data.IsSelected(0, i, this.Name))267 continue;268 data.Selected(1, i, this.Name);269 if (this.Child != null)270 {271 this.Child.Method(this.Child);272 }273 data.UnSelected(1, i);274 }275 }276 public void Print()277 {278 data.Count++;279 Console.WriteLine("{0,5} {1}", data.Count, data.Current);280 }281 282 public override string ToString()283 {284 return this.Name.ToString();285 }286 }287 288 class ComputeLink where T : ISeat3289 {290 ComputeLink Parent { get; set; } //父节点,即上一级节点291 ComputeLink Child { get; set; } //子节点,即下一级节点292 T Obj { get; set; } //当前节点对应的算法对象,可以看作业务对象293 public ComputeLink(T obj, ComputeLink parent, Action method)294 {295 if (obj == null)296 throw new ArgumentNullException("obj");297 this.Obj = obj;298 this.Obj.Method = x => method((T)x);299 if (parent != null)300 {301 this.Parent = parent;302 parent.Child = this;303 parent.Obj.Child = this.Obj;304 }305 }306 public static ComputeLink New(T obj, Action method)307 {308 return new ComputeLink (obj, null, method);309 }310 311 public ComputeLink Do(T obj, Action method)312 {313 return new ComputeLink (obj, this, method);314 }315 public ComputeLink Head //链表的头316 {317 get318 {319 if (null != this.Parent)320 return this.Parent.Head;321 return this;322 }323 }324 public void Action() //启动(延迟的)整个计算325 {326 var head = this.Head;327 head.Obj.Method(head.Obj);328 }329 }330 interface ISeat3331 {332 ISeat3 Child { get; set; }333 Action Method { get; set; }334 }335 class Seat3 : ISeat3336 {337 static Seat data = new Seat();338 string Name { get; set; }339 public Seat3(string name)340 {341 this.Name = name;342 }343 /// 344 /// 解耦的版本345 /// 346 public static void Run()347 {348 var sql = ComputeLink349 .New(new Seat3("A"), m => m.Try())350 .Do(new Seat3("B"), m => m.Try())351 .Do(new Seat3("C"), m => m.Try())352 .Do(new Seat3("D"), m => m.Try())353 .Do(new Seat3("A"), m => m.Try2())354 .Do(new Seat3("B"), m => m.Try2())355 .Do(new Seat3("C"), m => m.Try2())356 .Do(new Seat3("D"), m => m.Try2())357 .Do(new Seat3(""), m => m.Print());358 sql.Action();359 }360 public Action Method { get; set; }361 public ISeat3 Child { get; set; }362 public void Try()363 {364 for (int i = 0; i < 4; i++)365 {366 if (data.IsSelected(0, i))367 continue;368 data.Selected(0, i, this.Name);369 if (this.Child != null)370 {371 this.Child.Method(this.Child);372 }373 data.UnSelected(0, i);374 }375 }376 public void Try2()377 {378 for (int i = 0; i < 5; i++)379 {380 if (i == 1)381 continue;382 if (data.IsSelected(1, i))383 continue;384 if (data.IsSelected(0, i, this.Name))385 continue;386 data.Selected(1, i, this.Name);387 if (this.Child != null)388 {389 this.Child.Method(this.Child);390 }391 data.UnSelected(1, i);392 }393 }394 public void Print()395 {396 data.Count++;397 Console.WriteLine("{0,5} {1}", data.Count, data.Current);398 }399 400 public override string ToString()401 {402 return this.Name.ToString();403 }404 }405 }