LKY 只有原創內容的 Blog

今之能者,謂能轉貼,至於魯蛇,皆能轉貼。不原創,何以別乎?

LINQ 如何不對參考重新賦值,就使物件對自身有副作用?

Lin, Kao-Yuan's Avatar 2018-09-21

  1. 1. 解釋
  2. 2. 注意
  3. 3. 參考資料

目前工作由於領域的限制,必須要用 C# 來完成以前用 Python + Pandas 做的許多表格操作。在 C# 中,相當於 Python pandas.DataFrame 的型別是 System.Data.DataTable。

C# : System.Data.DataTable == Python : pandas.DataFrame

為了程式碼清爽精簡,我通常不喜歡寫 dataTable = dataTable.LINQ_func(),最好 dataTable.LINQ_func() 就可以對自身產生副作用。


這邊就要介紹一個以為 dataTable.LINQ_func() 可以產生副作用,結果踩到雷的例子。

先看看這段的程式碼, 你認為會輸出什麼 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System;
using System.Data;
using System.Linq;
class Program
{
static void Main(string[] args)
{
// 建立 DataTable
DataTable dataTable = new DataTable();
dataTable.Columns.Add("Col_0", typeof(int));
dataTable.Columns.Add("Col_1", typeof(double));

// 加入 10 個 Row,並在 Col 0 填充 0~9
foreach(int i in Enumerable.Range(0,10))
{
dataTable.Rows.Add(new object[]{i, null});
}

dataTable.Rows.Cast<DataRow>() //for .NetCore, as dataTable.AsEnumerable()
.Select(workRow =>
{//試圖產生副作用
workRow["Col_1"] = (int)workRow["Col_0"]/10.0;
return workRow;
});

// 印出 DataTable 內容展示結果
foreach(DataRow row in dataTable.Rows.Cast<DataRow>())
{
Console.WriteLine(String.Join(", ",row.ItemArray));
}
}
}

結果發現對 Col_0 的寫入沒有成功,沒有副作用,Select 並沒有改變 dataTable 的內容。

1
2
3
4
5
6
7
8
9
10
11
12
> Executing task: dotnet run <

0,
1,
2,
3,
4,
5,
6,
7,
8,
9,

如果加上一個看起來無關痛癢的 .ToList()

1
2
3
4
5
6
7
8
// 前略
dataTable.Rows.Cast<DataRow>() //for .NetCore, as dataTable.AsEnumerable()
.Select(workRow =>
{//試圖產生副作用
workRow["Col_1"] = (int)workRow["Col_0"]/10.0;
return workRow;
}.ToList(); //只有這一行改變,改變是加上 ToList()
//後略

卻改變輸出了,對 Col_0 的寫入成功了。

1
2
3
4
5
6
7
8
9
10
11
12
> Executing task: dotnet run <

0, 0
1, 0.1
2, 0.2
3, 0.3
4, 0.4
5, 0.5
6, 0.6
7, 0.7
8, 0.8
9, 0.9

這是為什麼?為什麼看似沒有給 dataTable 重新賦值得 .ToList() 會有不同結果?

解釋

因為所有的 LINQ 都只是一種「預約命令」,要等到對 IEnumerabe 跑 foreach 才會執行 LINQ。如果要立刻生效,必須使用強制查詢。

  • 強制查詢
    • 方法1:呼叫 ToList() 或 ToArray() 可以強制查詢。
    • 方法2:Count、Max、Average 和 First 這一類「彙總方法(Aggregation Method)」雖然沒有明確呼叫 foreach,但實作要 foreach 才能回傳結果,因此呼叫彙總方法也能完成強制查詢。

注意

但以下這樣是不會生效的。

1
2
3
4
5
6
7
8
9
10
11
12
// 前略
dataTable.Rows.Cast<DataRow>() //for .NetCore, as dataTable.AsEnumerable()
.Select(workRow =>
{//試圖產生副作用
workRow["Col_1"] = (int)workRow["Col_0"]/10.0;
return workRow;
});

dataTable.Rows.Cast<DataRow>().ToList();
//或者
dataTable.Rows.Cast<DataRow>().Max(row=>row.ItemArray.First());
//後略

因為 ToList()、ToArray() 或 Aggregat Method 必須要與 LINQ 寫在同一個敘述式。



但是,這樣又會生效了

1
2
3
4
5
6
7
8
9
10
// 前略
var t = dataTable.Rows.Cast<DataRow>() //for .NetCore, as dataTable.AsEnumerable()
.Select(workRow =>
{//試圖產生副作用
workRow["Col_1"] = (int)workRow["Col_0"]/10.0;
return workRow;
});

t.ToList();
//後略

因為 LINQ 建立的「預約命令」已經被寄託到參考 t 之上。


參考資料

本文最后更新于 天前,文中所描述的信息可能已发生改变