C#用链式方法表达循环嵌套

一.起源

故事缘于一位朋友的一道题:

朋友四人玩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, 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 class ComputeLink<T> where T : ISeat3
 3 {
 4     ComputeLink<T> Parent { get; set; }     //父节点,即上一级节点
 5     ComputeLink<T> Child { get; set; }      //子节点,即下一级节点
 6     T Obj { get; set; }     //当前节点对应的算法对象,可以看作业务对象
 7     public ComputeLink(T obj, ComputeLink<T> parent, Action<T> 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<T> New(T obj, Action<T> method)
21     {
22         return new ComputeLink<T>(obj, null, method);
23     }
24
25     public ComputeLink<T> Do(T obj, Action<T> method)
26     {
27         return new ComputeLink<T>(obj, this, method);
28     }
29     public ComputeLink<T> Head      //链表的头
30     {
31         get
32         {
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 ISeat3
45 {
46     ISeat3 Child { get; set; }
47     Action<ISeat3> 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;
    }
    /// <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的算法题都可以比较方便的实现了。

时间: 2024-08-07 00:35:59

C#用链式方法表达循环嵌套的相关文章

C#用链式方法

C#用链式方法表达循环嵌套 情节故事得有情节,不喜欢情节的朋友可看第1版代码,然后直接跳至“三.想要链式写法” 一.起缘 故事缘于一位朋友的一道题: 朋友四人玩LOL游戏.第一局,分别选择位置:中单,上单,ADC,辅助:第二局新加入的伙伴要选上单,四人可选位置变为:中单,打野,ADC,辅助:要求,第二局四人每人不得选择和第一局相同的位置,请问两局综合考虑有多少种位置选择方式? 对于像我这边不懂游戏的人来讲,看不懂.于是有了这个版本: 有4个人,4只椅子,第一局每人坐一只椅子,第二局去掉第2只椅子

Java链式方法

http://blog.csdn.net/lemon_shenzhen/article/details/6358537 有两种情况可运用链式方法: 第一种  除最后一个方法外,每个方法都返回一个对象 object2 = object1.method1(); object3 = object2.method2(); object3.method3(); 以上三行代码等价于如下链式方法 object1.method1().method2().method3(); 第二种 调用同一个对象的多个方法 u

简谈 JavaScript、Java 中链式方法调用大致实现原理

相信,在 JavaScript .C# 中都见过不少链式方法调用,那么,其中实现该类链式调用原理,大家有没有仔细思考过?其中 JavaScript 类库:jQuery 中就存在大量例子,而在 C# 中,其中 lambda 表达式的大量使用以及扩展方法的新增,使链式调用也见的不少. 首先,就谈谈 JavaScript 中链式调用,其实,也是就是运用之前提及的 this . var Person=function(name,age){ this.Name=name; this.Age=age; };

c语言描述-链式队列与循环队列

我真的不喜欢写代码 队列的特点 先进先出,即只能从队尾插入元素,从队头删除元素 队列的链式存储结构 #include<stdio.h> #include <stdlib.h> #include<malloc.h> typedef struct QNode { int date; struct QNode *next; }QNode ,*QueuePtr; typedef struct { int size; //记录队列长度 QueuePtr front; //头指针

通过JavaScript中的链式方法实现JQuery的链式调用

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>实现JQuery的链式调用</title> </head> <body> <script> function A(){} A.prototype.v = 2; A.prototype.set = function(num)

【线性表5】线性表的链式实现:循环单链表

简介 循环单链表 是在在单链表的基础上,用最后的一个结点的指针域指向头结点形成的.因此它在逻辑上是一个环形结构. 循环单链表在实际编程中很少用. 要点:1.遍历结束的标志是 p == [头结点地址],而不是p==NULL 2.表为空的判断标志是:   if( head->next == head   ) 3.在单循环链表中,从任一结点出发都可访问到表中所有结点 循环链表一般还使用尾指针rear保存最后一个结点的地址,因为使用尾指针既可以快速找到 最后一个结点,也可以快速找到头结点. 简单的代码实

android studio: 对齐成员变量及链式方法

"File"-"Settings"-"Editor"-"Java"-"Wrapping and Braces"-"Group declarations"-"Align fields in columns" "File"-"Settings"-"Editor"-"Java"-"Wr

ruby中的链式访问和方法嵌套

先看一道题,这道题是codewars上的一道题,我很早就看到了,但是不会写.等到又看到这道题的时候,我刚看完元编程那本书,觉得是可以搞定它的时候了.废话不多说,先看这道题,题目最开始是为JavaScript写的,但是也放在了ruby语言里面,这个没有关系.题目内容是有一个类Calc,通过链式方法调用,可以实现加减乘除.如图给的四个例子.数字只有0-9,运算只有加减乘除,而且每个运算只有一个操作符.(可以先不看下面,自己先想一下怎么写) 首先,每一个例子都是同样的结构---类名和四个方法.拿第一个

PHP 设计模式 笔记与总结(4)PHP 链式操作的实现

PHP 链式操作的实现 $db->where()->limit()->order(); 在 Common 下创建 Database.php. 链式操作最核心的地方在于:在方法的最后 return $this; Database.php: <?php namespace Common; class Database{ function where($where){ return $this; //链式方法最核心的地方在于:在每一个方法之后 return $this } functio