程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET之我見系列 - 類型系統(下)

.NET之我見系列 - 類型系統(下)

編輯:關於.NET

1,泛型

泛型在.NET中的重要價值已經無需用過多的語言來描述了。從.NET2.0提出泛型開始,這個東西已經被開發人員廣為稱道,頗有相見恨晚之意。可以想象得到,.NET在設計之初就想實現這種特性。微軟把這一希望寄托在了System.Object上,但事後證明,後者所帶來的性能開銷是開發人員所無法接受的。因此紛紛棄用這一雞肋。為了解決這一問題,在.NET2.0上,微軟全力攻關,終於搞出了一個完美的解決方案,就是泛型。實際上,泛型是一種虛擬意義上的“泛型”,它的原理也很容易理解,就是在第一次編譯時,使用一個占位符代替原來應使用類型的位置,當第二此編譯時再用代碼中指定的類型替換這個占位符。記得以前大學裡上課時,經常有的寢室只派一個人來搶座位,把書或包放在空位置上,就表示這個位置已經有人,其他人不能再坐。等到上課前一分鐘,這些人才睡眼惺惺地跑來搶現成的座位。大概想出泛型的這位老大也是受了大學裡的啟發(純屬猜測)。

這一過程用代碼這樣表示:

源程序中的代碼:

class Human<T>
{
public void Say(T word)
{
Console.WriteLine(word.ToString());
}
}

這個泛型類表示了一個人類的類別,在類名Human後用<T>表示泛型的引入,其中T就是一個占位符,在此處,它可以表示任何的類型。而在Say方法中,T作為參數的類型,從而用於接收不同類型的值。

當第一編譯結束後,以上程序的IL代碼變成如下所示:

圖中我們可以看到,無論是在Human類的後面和Say方法的參數中,都使用T來作為類型標識,當然T只是安裝微軟的規范,這裡你完全可以用另一個字符或單詞來表示,效果是同樣的。但此時,類和方法還並不清楚自己是什麼類型。直到調用該類的Main方法中出現下面的代碼:

此時,通過智能感知提示,我們已經很清楚的看到,Say方法後的參數類型被指定為string型。表明泛型類在初始化時,指明了類型為string,並用string替換了占位符T。實際上這一轉換發生在二次編譯之時,也就是JIT編譯時。由此我們可以得知,.NET的泛型是一種虛擬意義的泛型,只是利用了一個占位符為中介,讓程序達到了泛型的效果。這也是一種較完美的解決方案。

順便提一下,泛型的實現原理在王濤先生的《你必須知道的.NET》一書中第10章裡講得很清楚了,對這一概念仍不大清楚的朋友還可以去好好學習一下。

2.實現

了解泛型的原理,再來使用泛型也就不是那麼難的事了。我們首先要知道的是,泛型給我們帶來的最大的好處就是解決了類型轉換所引起的性能損失,也避免了這種轉換所引起的各種錯誤。因此我們就明白了,在經常發生類型的轉換的地方就需要使用泛型。那麼哪些地方經常會發生類型轉換啦,當然就數集合了。我們看如下示例:

using System;
using System.Collections;
namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
ArrayList al = new ArrayList();
al.Add("abc");
al.Add(123);
al.Add('a');
al.Add(true);
foreach (string str in al)
{
Console.WriteLine(str.ToString());
}
}
}
}

這個示例中使用了集合類ArrayList,由於未規范類型的使用,導致此程序中在向集合內添加記錄時,發生了3次裝箱操作,將極大降低程序的性能。這還不算什麼,關鍵是最後使用foreach循環時,由於集合類中的數據類型各異,導致在讀取記錄時發生類型轉換的錯誤,但此錯誤在編譯期間卻無法檢測出來,原因是沒有安全的使用集合類。

當然,我們實際開發中很少這樣SB地使用集合類,但一個優秀的程序員總是希望能用最安全最穩妥的辦法來消除一切可能存在的隱患,決不允許有這樣的不穩定因素遺留在程序中,因此,.NET在System.Collections.Generic這個命名空間中為我們提供了泛型的集合類實現。

using System;
using System.Collections;
using System.Collections.Generic;
namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int>();
list.Add("abc"); //此處是錯誤的添加集合數據,程序將不能通過編譯檢查。
list.Add(123);
}
}
}

再調試或編譯這段程序就會報錯,原因是使用了泛型的List集合,在定義之初就鎖定了類型為int型。若添加集合數據時不匹配的操作,則程序無法編譯通過檢查。也就避免了可能出現的問題。

3.泛型約束

就這上面的話題,順便講到做開發的一個原則:“錯誤要盡量早的發現”。要將一切可能出現的錯誤扼殺在搖籃中,用一句經典的反派台詞來說就是”另可錯殺一千,不可放過一個“。(做程序員難道都注定要變成冷血動物麼?)

那麼要遵循這條原則,就要給泛型加上約束了。約束是對泛型類更進一步的規范。看下面這個例子:

using System;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Sword sword=new Sword();
Hero<Sword> hero = new Hero<Sword>();
Console.WriteLine("英雄當前的攻擊值為{0}:", hero.AttackValue);
//為英雄裝備倚天劍
hero.AddArm(sword);
Console.WriteLine("裝備武器後,英雄的攻擊值為{0}:",hero.AttackValue);
}
}
/// <summary>
/// 物品的接口
/// </summary>
interface IWeapon
{
//物品名稱
string WeaponName
{get;}
//物品效果值
int WeaponValue
{ get;}
}
/// <summary>
/// 倚天劍的類
/// </summary>
class Sword : IWeapon
{
public Sword()
{
this._WeaponName = "倚天劍";
this._WeaponValue = 500;
}
private string _WeaponName;
/// <summary>
/// 劍的名稱
/// </summary>
public string WeaponName
{
get { return _WeaponName; }
}
private int _WeaponValue;
//劍的攻擊效果
public int WeaponValue
{
get { return _WeaponValue; }
}
}
/// <summary>
/// 英雄的主類
/// </summary>
/// <typeparam name="T">泛型參數,接受物品類型</typeparam>
class Hero<T> where T : IWeapon, new()
{
private int _AttackValue;
/// <summary>
/// 攻擊值
/// </summary>
public int AttackValue
{
get { return _AttackValue; }
set { _AttackValue = value; }
}
public Hero()
{
//生命值的初始化
this._AttackValue = 100;
}
/// <summary>
/// 裝備武器
/// </summary>
/// <param name="goods">武器類型</param>
public void AddArm(T weapon)
{
this._AttackValue += ((IWeapon)weapon).WeaponValue;
}
}
}

這個例子中 ,英雄類是這樣定義的:class Hero<T> where T : Weapon, new() ,表明英雄類定義了一個泛型參數T,用於裝備武器的AddArm方法。但為這個參數定義了兩個約束,一個是必須為Weapon接口的實現,一個是必須有無參的構造函數。當然你還可以為英雄定義更多的武器,但前提是必須實現Weapon接口。該程序的運行結果如下:

除了上面所講到的約束外,泛型約束還有以下幾種:

T:結構 類型參數必須是值類型。可以指定除 Nullable 以外的任何值類型. T:類 類型參數必須是引用類型,包括任何類、接口、委托或數組類型。 T:new() 類型參數必須具有無參數的公共構造函數。當與其他約束一起使用時,new() 約束必須最後指定。 T:<基類名> 類型參數必須是指定的基類或派生自指定的基類。 T:<接口名稱> 類型參數必須是指定的接口或實現指定的接口。可以指定多個接口約束。約束接口也可以是泛型的。 T:U 為 T 提供的類型參數必須是為 U 提供的參數或派生自為 U 提供的參數。這稱為裸類型約束。

這些泛型約束在使用上可配合使用,但也有一定的限制 ,比如T:class 不可和T:struct合用,這個很容易理解,因為一個是值類型,一個是引用類型嘛。還有一些其它的限制,我們這裡不一一列舉了,在實踐中一試即知。本文主要的目的就是要弄懂原理。

最後總結一下。泛型其實並不是一個很難理解的東西,關鍵是弄清楚它的設計目的,就是為了解決類型轉換所帶來的不安全因素和性能損失的問題,而原理就是在進行二次編譯之前用一個占位符替換類型的定義,再由JIT將占位符一一替換成指定的類型,最後編譯。所以我稱之為偽泛型!

講到這裡突然又想到一點,VS2008中搞了一個隱式類型var,這個能不能看成真正的泛型啦?大家給點意見吧!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved