我記得我剛開始學C#的時候,先碰到了迴圈,假設有一個數列,我希望將數列中的每一項加1:
var array = new int[] { 1, 2, 3, 4, 5 };
for (int i = 0; i < array.Length; i++)
{
array[i] = array[i] + 1;
}
後來我學到了foreach,所以上面這段程式碼就變成了
foreach(var item in array)
{
item = item + 1;
}
然後程式就壞掉了……….
在FP的世界裡,大多的變數都是不可變的,你可能覺得很奇怪,如果不可變的話到底要怎麼寫程式呢?就以昨天的例子來說Stack本身就是可變的阿?其實在C#裡面,你可能比你想像中的更常使用不可變的變數,應該說,不可變的資料。回到上面的程式碼,如果你用的IDE很聰明,通常會告訴你這段程式碼有問題,item是immutable的,到底是因為它是immutable的所以不可變?還是因為它不可變才會被叫immutable呢?實際上在foreach裡面,你可以改變變數的參考,但是不能改變變數本身。
如果變數都是不可變的,這樣要怎麼設計程式呢,其實很簡單,讓每次的操作都回傳一個新的物件就好了阿,其實我們經常使用的Linq就是這麼處裡的
var rawList = Enumerable.Range(1, 10).ToArray();
var list = rawList.Where(x => x > 5);
Console.WriteLine(rawList.Length);
// 10
Console.WriteLine(list.Count());
// 5
where並沒有改變原來的rawList,而是回傳了一個新的結果給list。回傳一個新的結果有多麼重要呢?我們可以看一下下面的範例
var list = Enumerable.Range(-10000, 20001).Reverse().ToList();
var immutableList = list.ToImmutalbeList();
var sum = () => Console.WriteLine(list.Sum());
var sort = () => list.Sort();
Parallel.Invoke(sum,sort);
// 28894511
var _sum = () => Console.WriteLine(immutalbeList);
var order = () => immutableList.OrderBy();
// 不可變集合無法使用sort
Parallel.Invoke(_sum,order)
// 0
理論上list.Sum()的結果應該是0,但上面的程式每次跑出來的結果都會不一樣,因為Sort方法是對原本的數列做排序,而Sum則是將list從頭遍歷一次加總,當這兩個動作平行化進行的時候,就會發生Sort移動了Sum還沒有計算的值,導致最後加總的結果異常,如果改用Linq中的OrderBy,因為沒有修改到原來的list,所以平行化排序跟加總出來結果就不會有問題。
我覺得以FP的觀點,其實不是變數不可變,而是函數在操作上不會去修改原始的資料,畢竟定義域與值域概念上本來就是兩個不同的集合,而不可變性帶來的好處就是能夠避免意料之外的情況,尤其在有競爭行為的時候最害怕的就是同時改到相同的資料。
今天初步講到了不可變性,如果能夠避免變數被更改,就能夠減少競爭行為導致的意外。在C#中其實經常使用到不可變的資料,也就是我們常用的Linq。等等,說到不可變的資料?那昨天範例裡面的stack是怎麼回事?明天我們就來聊聊這件事吧!