[.NET]快快樂樂學LINQ系列前哨戰-var與匿名型別

[.NET]快快樂樂學LINQ系列前哨戰-var與匿名型別

從C# 3.0開始出現了var,var真是個讓人又愛又恨的東西,用起來很方便,但有些朋友認為可讀性跟偵錯的時候很痛苦。這邊就先對var稍作說明一下。

var的簡介
首先,請參考MSDN的說明:var (C# 參考)

var被稱為隱含型別,在編譯時期宣告成隱含型別的變數,其真實型別由等號右邊的值型別所決定,請記住,是在編譯時期就已經決定其真實型別了。MSDN上其實說的很清楚:

var 關鍵字會指示編譯器從初始化陳述式右側的運算式推斷變數的型別。 推斷的型別可能是內建型別、匿名型別、使用者定義型別,或 .NET Framework 類別程式庫中定義的型別。

也就是用var宣告的變數,其實跟自己宣告型別沒什麼兩樣,差異的地方只是:var是由編譯器來幫你推斷實際型別。例如下面的程式碼,編譯器就會將result的型別,推斷為System.Int32,因為初始化的值為1,1的預設型別為System.Int32。
image

這同時也代表了兩件事:

  1. 為什麼用var宣告變數型別後,要馬上初始化其變數,不然編譯器就會賞你個錯誤。不在同一行,編譯器就無法推斷型別。
    image
  2. 當用var宣告完變數後,後面在撰寫程式碼時,intellisense就可以支援。
    image


那為什麼要發明var?難道只是為了讓開發人員更簡便的宣告型別而已?不,有些地方,一定得要用var,因為有些型別無法由開發者自行宣告。例如:匿名型別

匿名型別的簡介
首先,請參考MSDN的簡介:匿名型別 (C# 程式設計手冊)

匿名型別提供便利的方式將唯讀屬性集封裝至單一物件,而不必先明確定義型別。 型別名稱是由編譯器產生,而且在來源程式碼層級中無法使用。 每個屬性的型別是由編譯器推斷。
您可以使用 new 運算子搭配物件初始設定式來建立匿名型別。

幾個關鍵字highlight一下:

  1. 唯讀屬性
  2. 不必先明確定義型別
  3. 型別名稱由編譯器產生
  4. 屬性型別由編譯器推斷
  5. 來源程式碼無法使用
  6. 使用new運算子來建立


為什麼前文提到,匿名型別無法自行宣告,我們來看下一段程式碼的畫面。
image

從上圖可以看到,result的型別是<>f__AnonymousType0`2[System.Int32,System.String]這樣的東西,如同MSDN所說,這是編譯器所產生的型別名稱,可以當做是暫時產生的名字,因為一出方法界限,又或者可以說這個變數的生命週期結束,這個型別就失去『使用上』的意義。

所以要注意,如果匿名型別的物件參考,需要儲存或傳遞,只能使用object,但用了object,就又變成弱型別了,而無法享用到匿名型別的好處。因此,若需要傳遞或儲存,還是應該要宣告具名結構或類別。

而匿名型別除了不能轉換為object以外的任何型別這一點,就CLR的角度來看,匿名型別與其他的reference type並無不同。從型別名稱上還可以看到,裡面有兩個屬性,第一個型別為System.Int32,第二個型別為System.String,這就是上面所說,屬性的型別由編譯器推斷。
image

由於匿名型別的存在,所以var也有其存在的必要性。

而為什麼var會這麼常被用在LINQ中呢?簡單的說,因為LINQ裡面會回傳的型別是由委派的方法所決定的,而委派方法裡面,也可以回傳匿名型別,加上委派方法回傳內容一改,前面宣告的型別可能又要改,還不如直接用var或不得不用var來宣告。

而當碰到回傳的是IEnumerable<T>的集合時,(T當然也包括了匿名型別),使用上通常都是用foreach去展開與操作處理。這時候foreach裡面宣告每一個item的type,也相對變得不得不用var了,因為開發人員還是無法自行宣告型別。
image

如果new[]裡面的element是不同的屬性結構,編譯器會出現無法推斷。
image

匿名型別用起來,就像SQL裡面Select了一堆table相關的資料集合後,所取用需要的欄位。每次取用的欄位數、名稱可能都不一致,但只要得到結果的人可以使用,就達到其目的。

在LINQ使用中也是如此,可能是針對多個物件的集合,進行交集、聯集、Group、條件判斷等操作後,組出來的結果。這樣的結果組合是無限多種,為了這樣的結果如果需要每個都定義一個具名的結構或類別,那一整個定義不完。所以匿名型別在這樣的需求上,就可以相當簡便的解決問題。

結論
隱含型別與匿名型別的差異,相信各位讀者經過上面的描述,應該都很清楚了。(差異就是它們根本就是不同的東西啊)不過在使用上,以及這兩個東西存在的意義,大家務必要清楚明瞭,因為這都是LINQ的基礎。

而針對var會不會造成可讀性或維護上的困難,這沒有絕對的答案,單純看每個人的習慣來決定。有些人是除了非得用var的地方,才用var,否則不用var。我自己則是習慣另一種:除了不可以用var的地方,否則都用var。我的看法如下:

  1. 針對維護與偵錯上的情況來說,基本上有Visual Studio輔助,型別根本不是問題,當把滑鼠移到var或變數上,都會出現其型別名稱,除非維護與偵錯不透過Visual Studio,那我想說的是,這是維護偵錯的方式錯誤,而不是var的問題。

    而當變數型別需要改變時,用var宣告的好處,就是可以省得再改一次變數型別,把重點放在要修改的部份。
    image
  2. 針對開發上,宣告一個變數,是否要先決定其型別?我的認為是不必的,就像變數名稱不需要把型別定義上去(例如:EmployeeArray這是個糟糕的命名)。重點是變數名稱所表達的意義,把關注點關心在抽象的行為與邏輯上。既然編譯器可以幫我們決定,不會造成開發上的問題,那麼只要關心"等號"右邊的值就夠了。

    用var另一個我認為的好處是,寫code會出現自己的pattern,會加速撰寫的速度,會邊寫邊想。常看我文章的讀者就會發現,我的sample code裡面,超常出現var result = xxxxx; 基本上我可以在打var的時候,想變數名稱的意義,打到等號的時候,想我要回傳的東西是什麼。加上自己的開發習慣,每個function都短短的,var result的初始化到return result,距離不會太遠。所以有時候我都會直接寫var result = 方法回傳的型別instance; return result; 在這段過程中,就會去想內容到底要寫什麼。
     
  3. 什麼時候不用var?對我來說,只有兩種情況:

    第一,要用到多型的時候,尤其是變數的型別為interface,這如果用var,變數型別就直接變成concrete class的型別了,雖說執行上不會有任何問題,但在開發上意義可完全不同,加上使用變數時看到的雜訊,會讓我不太愉悅,所以在運用多型的設計上,我不會用var。

    第二,top-down的設計方式,當我還沒有實際的方法時,我不會用var。因為用Visual Studio的產生功能,會判斷成object,這樣我還要移過去改,太麻煩了,所以這類的宣告,我會想好型別宣告後,再透過產生功能來幫忙產生function的殼。
    image

    產生的結果
    image


針對var的部份,我只是提出我的看法,請各位朋友毋須在本文章上激烈討論,用var有好處,不用var也有好處,沒有誰對誰錯。習慣、上手,可以提昇生產力與正確性,才是最重要的。

我自己就是從不用var那一派,慢慢轉成用var開發的那一派,如果讀者們覺得,我提到的好處有吸引力,不妨試試看,您也可以找出自己開發上的pattern,那樣的code寫起來很過癮啊,搭配Visual Studio的一些快捷鍵,簡直就快升天了。
 

補充
與隱含型別類似的,是在C# 4.0的dynamic型別

dynamic 型別可讓它發生所在的作業略過編譯時期型別檢查。 這些作業會改在執行階段解析。

簡單的說,就是dynamic所宣告的,在編譯時期為object型別,但很酷的是,編譯器不會去檢查這個型別的使用方式,在執行時期才決定其型別。例如下圖的程式碼建置是會通過的:
image

但執行是會錯的:
image

使用變數則如下圖所示:
image

大家可以簡單想像成dynamic就是宣告成object型別,然後編譯時期不會去檢查這個變數怎麼使用,然後run time的時候,.net framework自己幫你cover掉那一整段又臭又長的Reflection。(所以不只是屬性可以直接用,方法也可以直接用唷)


或許您會對下列培訓課程感興趣:

  1. 2019/12/21(六)~2019/12/22(日):演化式設計:測試驅動開發與持續重構 第七梯次(台北)
  2. 2020/1/4(六)~2020/1/5(日):Clean Coder:DI 與 AOP 進階實戰 第三梯次(台北)
  3. 2020/02/08(六):【針對遺留代碼加入單元測試的藝術】 第八梯次(台北)
  4. 2020/02/09(日):【極速開發+】 第九梯次(台北)
  5. 2020/02/28(五)~2020/3/1(日) C#進階設計-從重構學會高易用性與高彈性API設計 第三梯次(台北)

想收到第一手公開培訓課程資訊,或想詢問企業內訓、顧問、教練、諮詢服務的,請洽 Facebook 粉絲專頁:91敏捷開發之路