情节故事得有情节,不喜欢情节的朋友可看第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)的某个方法启动真正要执行的整个逻辑。