集合與泛型之日常生活範例

突然靈光一閃,有通的感覺。

剛接觸「泛型」時,實在看不懂、聽不通,但接擉時間久一些,才慢慢了解「泛型」原來如此。一般在談到泛型之前都會先談集合,談完集合在談泛型。使用一樣的過程,我想用一些生活中的範例來舉例,讓大家更了解什麼是泛型。

我們生活上有很多「集合」名詞,例如:「人」、「鳥」、「動物」、「植物」、「昆蟲」、「魚」…等,以英文來解釋集合名詞:「一群相似也相關的個體結合而成的集合體,稱為集合名詞。」

例如:

  • 「人」有:男人、女人、白種人、黃種人、黑人、大人、小孩、老人…這些都都稱為「人」,所以人是一個集合名詞。
  • 「鳥」:依網路查詢的結果,全世界鳥的種類有9000多種,但我們可以通稱這9000多種的鳥類為「鳥」,什麼鳥?不知道!但我知道它是鳥。
所以程式上的集合也是用這樣的觀念來進行。

假設有一天我們要開一間溫泉旅館,但溫泉分男湯、女湯,這時候你就不能用集合的觀念來操作,你不能說你是人,所以要進去女湯,妳也一樣不能說妳是人,所以要進去男湯,這時候就必須以更明確分類,來分類這個集合。只有男人的集合,可以進去男湯,女人的集合,可以進去女湯,男人可以是大人、老人、小孩、白種人、黃種人、黑人…只要是男人;女人同義。

用這樣的思考來想泛型,應該就能想通了,我可以這樣使用泛型:

Dim man As New ArrayList(Of SuperMan)()

我希望這個泛型集合不只是男人,而且是「男人中的男人」。

或是我們這樣說:

  • 集合:較無限制。
  • 泛型:有限制性。
「人」的集合,管你什麼人,活的、死的…通通可以包進來。但如果我經營「葬儀社」,很明顯,我要的集合只有一種,有呼吸的我都不要,沒呼吸的我都想要,這時候就是泛型出馬,把沒呼吸通通包進來,而不是「人」我都想要,泛型就可以解決我的問題。

泛型型別效率測試

在接觸物件(Object)觀念後,都會談到boxing及unboxing的動作,以Visual Basic來說,就是像使用CType()來轉換型別的動作,而且大多數都會談到boxing及unboxing會影響效能,所以都會建議盡量使用「泛型型別」。

不過經過以下測試後,效率應該不能說是使用泛型型別的理由!理由應該是「安全型別」,就像我們常看到的舉例:ArrayList及ArrayList(Of T)、IList或IList(Of T),也就是說ArrayList、IList是個什麼型別都可以包含進去的陣列。假設我程式是在做數值運算,但你放了一個「Hello World!」ArrayList、IList是接受的,這稱「不安全型別」。

而ArrayList(Of T)與IList(Of T)就不同,在宣告時可以指定任意型別給ArrayList(Of T)與IList(Of T),也就是那個T,例如:ArrayList(Of Integer)、IList(Of String),這樣宣告之後,當你在使用時,Visual Studio會自動幫你除錯,不用等到建置之後再來除錯,例如你在ArrayList放了一個「Hello World!」的字串,但ArrayList的宣告只能放Integer,所以你或Visual Studio都能很明確找出問題!

泛型型別 V.S 非泛型型別


以下使用主控台應用程式測試:
  1. 新增「主控台應用程式」
  2. 新增一個類別,使用Object來進行處理
  3. 新增一個類別,使用泛型來進行處理
  4. 在主控台應用程式撰寫測試程式碼

測試環境

軟體:
  • XP SP3
  • Visual Studio 2008
  • .Net Framework 2 (3.5 SP1)
硬體:
  • CPU:賽揚 2.0
  • RAM:2 GB
ps:我的家中的硬體算舊且效能平平,都測不出有很大的差異,如果硬體新一點,我看差異更小。

類別Obj(使用Object)

Public Class Obj
    Public _v1 As Object
    Public _v2 As Object
    Public Sub New(ByVal v1 As Object, ByVal v2 As Object)
        _v1 = v1
        _v2 = v2
    End Sub
End Class

類別Gen(使用泛型)

Module Module1

    Sub Main()
        Dim i As Integer = 0
        Dim v1 As String
        Dim v2 As String

        ' st:Star Time
        Dim st As DateTime = Now
        ' boxing、unboxing1百萬次,兩個數變共2百萬次
        For i = 0 To 1000000
            Dim oa As New Obj(1, 2)
            v1 = CType(oa._v1, String)
            v2 = CType(oa._v2, String)
        Next
        'et:End Time
        Dim et As DateTime = Now

        Console.WriteLine("Obj1:{0}", (et - st).ToString)


        st = Now
        ' 泛型型別1百萬次,兩個數變共2百萬次
        For i = 0 To 1000000
            Dim ga As New Gen(Of String, String)("1", "2")
            v1 = ga._v1
            v2 = ga._v2
        Next
        et = Now

        Console.WriteLine("Gen1:{0}", (et - st).ToString)

        st = Now
        For i = 0 To 2000000
            Dim oa As New Obj(1, 2)
            v1 = CType(oa._v1, String)
            v2 = CType(oa._v2, String)
        Next
        et = Now

        Console.WriteLine("Obj2:{0}", (et - st).ToString)

        st = Now
        For i = 0 To 2000000
            Dim ga As New Gen(Of String, String)("1", "2")
            v1 = ga._v1
            v2 = ga._v2
        Next
        et = Now

        Console.WriteLine("Gen2:{0}", (et - st).ToString)

        st = Now
        For i = 0 To 3000000
            Dim oa As New Obj(1, 2)
            v1 = CType(oa._v1, String)
            v2 = CType(oa._v2, String)
        Next
        et = Now

        Console.WriteLine("Obj3:{0}", (et - st).ToString)

        st = Now
        For i = 0 To 3000000
            Dim ga As New Gen(Of String, String)("1", "2")
            v1 = ga._v1
            v2 = ga._v2
        Next
        et = Now

        Console.WriteLine("Gen3:{0}", (et - st).ToString)

        st = Now
        For i = 0 To 4000000
            Dim oa As New Obj(1, 2)
            v1 = CType(oa._v1, String)
            v2 = CType(oa._v2, String)
        Next
        et = Now

        Console.WriteLine("Obj4:{0}", (et - st).ToString)

        st = Now
        For i = 0 To 4000000
            Dim ga As New Gen(Of String, String)("1", "2!")
            v1 = ga._v1
            v2 = ga._v2
        Next
        et = Now

        Console.WriteLine("Gen4:{0}", (et - st).ToString)

        st = Now
        For i = 0 To 5000000
            Dim oa As New Obj(1, 2)
            v1 = CType(oa._v1, String)
            v2 = CType(oa._v2, String)
        Next
        et = Now

        Console.WriteLine("Obj5:{0}", (et - st).ToString)

        st = Now
        For i = 0 To 5000000
            Dim ga As New Gen(Of String, String)("1", "2!")
            v1 = ga._v1
            v2 = ga._v2
        Next
        et = Now

        Console.WriteLine("Gen5:{0}", (et - st).ToString)
    End Sub

End Module

測試數據


Obj1:00:00:01.7968750
Gen1:00:00:00.1562500
Obj2:00:00:03.7343750
Gen2:00:00:00.2812500
Obj3:00:00:05.5000000
Gen3:00:00:00.4218750
Obj4:00:00:07.2968750
Gen4:00:00:00.5468750
Obj5:00:00:09.1093750
Gen5:00:00:00.6875000

我們以最後那個5百萬次的測試來看,中間有兩個變數,所以共進行了一千萬次,很明顯的,泛型型別的效率比需boxing、unboxing的效率快很多倍,但也看的出來.Net Framework在型別處理上是「非常快速」。

結論


我想很少有一支程式必須進行一千萬次的boxing與unboxing,平常的影響連可能連0.1秒都不到。數據證明一切,泛型能提供的率效是高於一般非泛型的多倍,但此差距只有在非常大量(千萬次以上)才明顯,但泛型能帶來型別安全才是我們主要考量因素。

將陣列指示項放在變數上,不要放在型別上之討論

在前一篇「MSDN的範例程式碼不見了」談到了一個小主題是「將陣列指示項放在變數上,不要放在型別」算是解了我一個Visual Basic上的疑問。但今天在寫一段程式碼時出現問題,讓我把這個「陣列指示項」完全搞清楚了。

' 宣告a是一個String陣列
Dim a As String()
' 宣告b是一個String陣列
Dim b() As String

以上是兩種宣告陣列的方法,在Visual Basic正式的用法是下方「變數b()」,「將陣列指示項放在變數上」方法。

ArrayList

但在寫一段程式碼時:
'Dim coll As New ArrayList()
Dim coll() As New ArrayList

很直覺的把第一行的程式碼改為第二行,但改了第二行後,程式都還沒執行Visual Studio就已經提示有問題:

修改陣列指示項
奇怪,MSDN的建議馬上破功,到底是怎麼回事?在反覆查資料及思考後,終於看到關鍵點了「ArrayList 建構函式」:

ArrayList() 建構函式
原來ArrayList()不是陣列,而是Sub New()無參數建構式,也就是說原始第一行「Dim coll As New ArrayList()」建立一個型別ArrayList的coll的Instance並初始化,那個最後的()不是陣列的意思,而是初始化。

再舉個例子,我們常用的System.Data.SqlClient中,常使用的SqlCommand類別也有一個無參數建構式:

' SqlConnection() 無參數建構式
' SqlConnection() 無參數建構式
Dim conn As New SqlConnection()
conn.ConnectionString = "..."

' SqlCommand() 無參數建構式
Dim cmd As New SqlCommand()
cmd.Connection = conn
)

我們不一定要一開始就使用帶參數建構式,也能先使用無參數的建構式,後面再來設定各參數或屬性。我們在設定無參數建構式時,不一定要在最後面使用(),但使用()是比較嚴僅的方法,例如使用ToString,你可以直接使用ToString不加(),但使用ToString()是比較好的方法,因為看到()就知道這是個方法。這是一個不錯的經驗,讓我完全了解「()」在不同地方的用法,這部份在MSDN上完全沒有討論哦。

在IIS 7.x上使用ASP + Access資料庫

環境:Windows Server 2008 R2 + IIS 7.x + Access

為了測試hicloud的穩定度,把一個ASP+Access的網站放到hicloud,不過在OS及IIS上有很多必須小心的地方,不然第一個ASP跑不起來,第二個ASP+Access也跑不起來。

ODBC


你不能使用「系統管理工具」裡的ODBC,因為那是64位元版本,雖然你在「工作管理員」看到的是「odbcad32.exe」。
  1. 在64位元作業系統裡,所有32位元版本的程式,最後面都會加上「odbcad32.exe *32」,最後面那個「*.32」才是說這個是32位元程式。
  2. 在「系統管理工具」裡的ODBC是找不到MDB,只有SQL Server。
  3. 找「C:\Windows\SysWOW64」裡「odbcad32.exe」才是32位元版本的ODBC,才有辦法與Access連接。
ps:「C:\Windows\SysWOW64」裡是作業系統相關程式32位元版本集散地。

角色


IIS角色必須包含「ASP」。

站台


在ASP站台中設定「ASP」,
啟用上層路徑:True;

應用程式集區


在ASP的「應用程式集區」「進階設定」「(一般)」
啟用32位元應用程式:True

「寫入」權限


在這裡注意兩個地方的寫入權限:
  1. IIS暫存目錄
    設定「IIS_IUSRS群組」有寫入權限。
  2. Access目錄
    資料庫所在目錄,設定「IUSRS帳戶」有寫入權限。
總之,上了Windows Server 2008後,設定上麻煩許多。

參考:

    MSDN的範例程式碼不見了

    Visual Basic 編碼慣例

    好文一篇,推!

    最近覺得奇怪,自從MSDN改版後,總是有些怪怪的現象,例如TreeView改的很奇怪,但還好找到了設定,修改為「傳統式」後,就回覆原來的樣子。

    但還是一直有個疑問?

    最近有些未翻譯的文章,都會出現右邊是英文、左邊是中文排版,但內容看了一下都是出現「如下例範列…」之類的文字後,就「空白」一片,想說是否還沒寫好,因為我看設定是顯示所有Code的:

    All Code

    本來是想整理這篇文章,結果發現我「複製、貼上」時出現了「程式碼」,程式碼怎麼被「隱藏」起來了,找了許久終於找到了顯示程式碼的設定:

    語言篩選
    選擇你要的語言就會出現程式碼了,此這改版真的是抱怨多於誇獎。不過還是怪怪的,程式碼裡多了一堆<span>的Tags!真是…傻眼!

    文章中有幾項建議:
    • 善加利用區域變數的型別推斷
      請參考我的介紹:區域型別推斷。
      也就是說Visual Basic很推薦使用區域型別推斷。
      但我個人認為,在必要時才使用比較好,在使用區域型別推斷會有一定的效率折扣,它還是多一道判斷型別的手續,而且在程式可讀性也不好,而且如果全使用區域型別推斷,我認為會多了一個除錯上的麻煩,如果正是型別造成的錯誤,你怎麼除錯?

    • 將陣列指示項放在變數上,不要放在型別上。
      Dim Names() as String 或 Dim Names as String(),這兩種用法我都常看到,查MSDN也查不出這兩種用法有什麼差異,我猜又是雙語的人材(VB,C#),由C#轉VB時把這樣的語法給帶進來使用,因為C#是使用「int[] array1 = new int[] { 1, 3, 5, 7, 9 };」它們是把陣列指示項放在型別上的(int[ ]的那個[ ]),雖然都可以正常運作,但有個依據總是好的,以後看到有人在Visual Basic用型別宣告陣列,就把這篇Mail給他看,然後付上一句「依MSDN上的建議…」^_^。
    • 優先使用 IsNot 關鍵字,而非 Not...Is Nothing。
    • 在執行比較時,若要略過不必要的程式碼以避免例外狀況並提升效能,請使用 AndAlso 代替 And 並使用 OrElse 代替 Or。
      這個東西很多人還不知道,而且也少有書會談這個,這是個好東西,可以讓我們在比較運算時加速我們的程式效率。
      例如:

      And:兩個都True才True,不管如何And都會等到所有結果出來才給最後結果;
      AndAlso:X AndAlso Y AndAlso Z,只要X為False,就得到結果為False,不用等Y/Z;
      Or:任一為True則True;但還是會等到所有結果出來才給最後結果;
      OrElse:X OrElse Y OrElse Z OrElse,只要X為True,就得到結果為True,不用等Y/Z;
     這篇文章還有很多很好的建議,Visual Basic的使用者細讀之。