自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
本篇是接續前一篇,一樣是講型別推斷,但是焦點放在 VB 型別推斷使用上該注意的地方。
前一篇文章(LINQ自學筆記-打地基-區域型別推斷01)已經說明關於型別推斷的用法,這篇則會把焦點放在 VB 型別推斷使用上該注意的地方。
先回顧型別推斷最重要的限制條件:必須是【區域變數】!所以方法參數不能當做區域變數,但是前一篇我們有帶到,若 VB 方法參數不加型別,「看起來」執行結果也是正確的:
這是為什麼呢?因為 VB 在 Net 3.5 以前,雖然沒有型別推斷的功能,但是仍可允許在定義變數時,不宣告型別,但這個特性和型別推斷大不同!因為這些沒有明確宣告型別的變數,其實 VB 編譯器都會把它們視為 Object 型別,也就是此類變數執行時期都是以 Object 型別儲存變數值,並利用隱含轉換的方式把值輸出,所以上述程式可以成功執行,其實是因為 p1、p2 都被當做 Object 型別,且在 Console.WriteLine 時,被隱含轉換為 String 輸出,並非是型別推斷。
利用這種方式宣告的變數,有幾個缺點:
IDE 的 IntelliSense 功能只會帶出 Object 型別可用的成員資訊(方法、屬性、欄位、事件等等)。
VB 編譯器無法在編譯時期檢查該變數成員的真實性,因此有可能在程式中呼叫了該變數並未擁有的成員,造成執行時期的 MissingMemberException。
因為都是以 Object 型別儲存執行時期真實的值,程式若未控制妥當,該變數所存放的值有可能會一下是 Integer,沒多久又變成 String、DateTime,但是因為 Object 型別是所有型別的父類別,所以都可以收納,這樣有可能執行轉型或調用型別方法時,造成執行時期的 InvalidCaseException 或 MissingMemberException。
因為用 Object 收納變數值,使用時都會產生龐大的轉型成本(不論是隱含轉型或明確轉型)!
這個語法特性叫晚期繫結(Late Binding),雖然有上述的缺陷,但是在某些應用上還頗方便,因此到 .Net 3.5 以後版本仍保留下來,所以當我們以為使用型別推斷時,有可能其實是在用晚期繫結,導致程式會有上述的缺陷存在。
那怎麼識別呢?很簡單,確定我們定義變數的程式碼符合型別推論的限制:必須是區域變數,且宣告變數同時要賦予值。除此之外,其實還有三個 VB 專屬的編譯選項會和型別推斷、晚期繫結產生影響:
Option Explicit:強制檔案中的所有變數都需明確宣告。預設為 On。
Option Strict:將隱含資料型別轉換限制為只能「擴展」轉換。預設為 Off。(白話文 = 是否允許晚期繫結)
Option Infer:在宣告變數中啟用區域型別推斷。此選項為 VS2008(.Net 3.5)新增,所以若是全新的 .Net 3.5 專案,預設為 On,但若是從轉舊的專案升版上來,則預設為 Off。
PS. 和編譯選項有關的程式碼範例,無法使用 LINQPad,因此我會用 VS2008 來展現。
第一個要注意的是 Option Explicit,若設定為 Off 時,我們可以不宣告就直接使用變數(此類變數又稱之為隱含定義變數,Implicitly Defined Variables),但是隱含定義變數無法使用型別推斷,就算它是區域變數且第一次使用就有賦值,仍然會被編譯器視為 Object 型別:
Option Explicit Off
Option Strict Off
Option Infer On
Module Module1
Sub Main()
MyVar = "Test by Leo."
Console.WriteLine(MyVar)
Console.ReadLine()
End Sub
End Module
因為第一行的 Option Explicit 是 Off,所以程式碼中的 MyVar 並沒有用 Dim 宣告就可直接使用,而第三行 Option Infer 是 On,的確有啟用型別推斷,但是因為隱含定義變數不支援型別推斷,所以我們會發現其實該變數還是使用 Object 型別封裝了 Test by Leo 的字串型態之變數值。(監看式第一行 MyVar 型別是 Object,但是值是 Test by Leo. 的 String)
接著我們把 Option Explicit 改回預設值 On,並且將 MyVar 加上 Dim,再看看結果:
MyVar 變數順利的被推斷出型別為 String,不再是 Object 了。所以要使用型別推斷功能,請務必留心此選項。當然,並不是說要用型別推論一定要把 Option Explicit 設為 On,因為關鍵是:隱含定義變數不支援型別推斷,所以避開就行了(加上 Dim 即可)。
第二個選項 Option Strict 和第三個選項 Option Infer 要一起講:當型別推斷啟用時,只要符合區域變數和宣告同時賦值,編譯器就一定會給這個變數一個明確的型別(強型別),但是舊的程式中,可能真的有利用同一變數在不同情況下儲存不同型別值的狀況,這時就會編譯就會失敗:
Option Explicit On
Option Strict On
Option Infer On
Module Module1
Sub Main()
Console.WriteLine(TestFunc("Qty"))
Console.WriteLine(TestFunc("Date"))
Console.WriteLine(TestFunc(""))
Console.ReadLine()
End Sub
Public Function TestFunc(ByVal para As String) As String
Dim result = "Default content"
If para = "Qty" Then
result = 5
ElseIf para = "Date" Then
result = Today
End If
Return result
End Function
End Module
這個問題在我工作環境中也有遇到,解法有兩種:
關掉型別推斷。但是這樣寫 LINQ 會非常痛苦,要不斷在 LINQ 中宣告各種型別。
明確定義要晚期繫結的變數為 Object 型別。這才是最好的解法。
所以上述程式碼,只要把 Dim result = … 改成 Dim result As Object,並把 Function 回傳改成 Object 即可解決:
最後,其實使用型別推斷非常簡單(VB 就不要寫型別/ C# 型別改用var關鍵字),特別是寫 LINQ 時更方便,但是在 VB 中使用時,請特別注意不要誤用了晚期繫結卻以為是在使用型別推斷喔!