一.起缘
故事缘于一位朋友的一道题:
朋友四人玩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起身(释放坐椅) } }</div>
部分运行结果:
说明:
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();</div>
而这样的代码通常的逻辑是执行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); } }</div>
结构更合理,逻辑更清晰,但是一堆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, 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); } }</div>
五.解耦
这种调用方式,是满意了。但是运算框架与具体的算法耦合在一起,如果能把运算框架提取出来,以后写具体的算法也方便许多。于是经过苦逼的提取,测试,踩坑,最终出现了第4版:
//运算框架 class ComputeLink<T> where T : ISeat { ComputeLink<T> Parent { get; set; } //父节点,即上一级节点