星空网 > 软件开发 > ASP.net

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

情节故事得有情节,不喜欢情节的朋友可看第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的时候,就能调用下一级的方法。于是我想到了一个“回调链”

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

所以,执行链式方法是在构造回调链,最后的方法再通过调用链头(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     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<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的算法题都可以比较方便的实现了。

你都看到这里了,为我点个赞吧,能说一下看法就更好了。

完整代码:

C#用链式方法表达循环嵌套C#用链式方法表达循环嵌套
 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     Action<Seat2> Method { get; set; }199     public Seat2(string name, Seat2 parent, Action<Seat2> method)200     {201       this.Name = name;202       this.Parent = parent;203       if (parent != null)204         parent.Child = this;205       this.Method = method;206     }207     /// <summary>208     /// 耦合的版本209     /// </summary>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, Action<Seat2> method)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<T> where T : ISeat3289   {290     ComputeLink<T> Parent { get; set; }   //父节点,即上一级节点291     ComputeLink<T> Child { get; set; }   //子节点,即下一级节点292     T Obj { get; set; }   //当前节点对应的算法对象,可以看作业务对象293     public ComputeLink(T obj, ComputeLink<T> parent, Action<T> 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<T> New(T obj, Action<T> method)307     {308       return new ComputeLink<T>(obj, null, method);309     }310 311     public ComputeLink<T> Do(T obj, Action<T> method)312     {313       return new ComputeLink<T>(obj, this, method);314     }315     public ComputeLink<T> 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<ISeat3> 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     /// <summary>344     /// 解耦的版本345     /// </summary>346     public static void Run()347     {348       var sql = ComputeLink<Seat3>349         .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<ISeat3> 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 }

View Code

 




原标题:C#用链式方法表达循环嵌套

关键词:C#

C#
*特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: admin#shaoqun.com (#换成@)。

马来西亚海运到中国价格:https://www.goluckyvip.com/tag/92308.html
中国到马来西亚海运价格:https://www.goluckyvip.com/tag/92309.html
马来西亚海运价格表:https://www.goluckyvip.com/tag/92310.html
广州至马来西亚散货海运价格:https://www.goluckyvip.com/tag/92311.html
广州到马来西亚海运价格:https://www.goluckyvip.com/tag/92312.html
广州海运到马来西亚价格:https://www.goluckyvip.com/tag/92313.html
武陵山大裂谷周围景点 武陵山大裂谷周围景点图片:https://www.vstour.cn/a/411233.html
南美旅游报价(探索南美洲的旅行费用):https://www.vstour.cn/a/411234.html
相关文章
我的浏览记录
最新相关资讯
海外公司注册 | 跨境电商服务平台 | 深圳旅行社 | 东南亚物流