情節故事得有情節,不喜歡情節的朋友可看第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, 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。
六.第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 null 102 ) 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 else 123 { 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 else 147 { 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 Current 180 { 181 get 182 { 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 Seat2 193 { 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 Head 236 { 237 get 238 { 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 : ISeat3 289 { 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 get 318 { 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 ISeat3 331 { 332 ISeat3 Child { get; set; } 333 Action<ISeat3> Method { get; set; } 334 } 335 class Seat3 : ISeat3 336 { 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