程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則6:區別值類型數據和引用類型數據

Effective C#原則6:區別值類型數據和引用類型數據

編輯:關於C#

值類型數據還是引用類型數據?結構還是類?什麼你須要使用它們呢?這不 是C++,你可以把所有類型都定義為值類型,並為它們做一個引用。這也不是 Java,所有的類型都是值類型。你在創建每個類型實例時,你必須決定它們以什 麼樣的形式存在。這是一個為了取得正確結果,必須在一開始就要面對的重要決 定。(一但做也決定)你就必須一直面對這個決定給你帶來的後果,因為想在後 面再對它進行改動,你就不得不在很多細小的地方強行添加很多代碼。當你設計 一個類型時,選擇struct或者class是件簡單的小事情,但是,一但你的類型發 生了改變,對所有使用了該類型的用戶進行更新卻要付出(比設計時)多得多的工 作。

這不是一個簡單的非此及彼的選擇。正確的選擇取決於你希望你的 新類型該如何使用。值類型不具備多態性,但它們在你的應用程序對數據的存取 卻是性能有佳;引用類型可以有多態性,並且你還可以在你的應用程序中為它們 定義一些表現行為。考慮你期望給你的類型設計什麼樣的職能,並根據這些職能 來決定設計什麼樣的類型。結構存儲數據,而類表現行為。

因為很多的 常見問題在C++以及Javaj裡存在,因此.Net和C#對值類型和引用類型的做了區分 。在C++裡,所有的參數和返回值都是以值類型的進行傳遞的。以值類型進行傳 遞是件很有效率的事,但不得不承受這樣的問題:對象的淺拷貝(partial copying)(有時也稱為slicing object)。如果你對一個派生的對象COPY數據時, 是以基類的形式進行COPY的,那麼只有基類的部分數據進行了COPY。你就直接丟 失了派生對象的所有信息。即使時使用基類的虛函數。

而Java語言呢, 在放棄了值類型數據後,或多或少有些表現吧。Javs裡,所有的用戶定義類型都 是引用類型,所有的參數及返回數據都是以引用類型進行傳遞的。這一策略在( 數據)一致性上有它的優勢,但在性能上卻有缺陷。讓我們面對這樣的情況,有 些類型不是多態性的--它們並不須要。Java的程序員們為所有的變量准備了一個 內存堆分配器和一個最終的垃圾回收器。他們還須要為每個引用變量的訪問花上 額外的時間,因為所有的變量都是引用類型。在C#裡,你或者用struct聲明一個 值類型數據,或者用class聲明一個引用類型數據。值類型數據應該比較小,是 輕量級的。引用類型是從你的類繼承來的。這一節將練習用不同的方法來使用一 個數據類型,以便你給掌握值類型數據和引用類型數據之間的區別。

我 們開始了,這有一個從一個方法上返回的類型:

private MyData _myData;
public MyData Foo()
{
return _myData;
}
// call it:
MyData v = Foo();
TotalSum += v.Value;

如果MyData是一個值類型,那麼回返值會被COPY到V中 存起來。而且v是在棧內存上的。然而,如果MyData是一個引用類型,你就已經 把一個引用導入到了一個內部變量上。同時,

你也違犯了封裝原則(見原 則23)。

或者,考慮這個變量:

private MyData _myData;
public MyData Foo()
{
return _myData.Clone( ) as MyData;
}
// call it:
MyData v = Foo();
TotalSum += v.Value;

現在,v是原始數據_myData的一個COPY 。做為一個引用類型,兩個對象都是在內存堆上創建的。你不會因為暴露內部數 據而遇到麻煩。取而代之的是你會在堆上建立了一個額外的數據對象。如果v是 局部變量,它很快會成為垃圾,而且Clone要求你在運行時做類型檢測。總而言 之,這是低效的。

以公共方法或屬性暴露出去的數據應該是值類型的。 但這並不是說所有從公共成員返回的類型必須是值類型的。對前面的代碼段做一 個假設,MyData有數據存在,它的責任就是保存這些數據。

但是,可以 考慮選擇下面的代碼段:

private MyType _myType;
public IMyInterface Foo()
{
return _myType as IMyInterface;
}
// call it:
IMyInterface iMe = Foo();
iMe.DoWork( );

變量_myType還是從Foo方法返回。但這次不同的是,取而代之 的是訪問返回值的內部數據,通過調用一個定義好了的接口上的方法來訪問對象 。你正在訪問一個MyType的對象,而不是它的具體數據,只是使用它的行為。該 行為是IMyInterface展示給我們的,同時,這個接口是可以被其它很多類型所實 現的。做為這個例子,MyType應該是一個引用類型,而不是一個值類型。MyType 的責任是考慮它周圍的行為,而不是它的數據成員。

這段簡單的代碼開 始告訴你它們的區別:值類型存儲數據,引用類型表現行為。現在我們深入的看 一下這些類型在內存裡是如何存儲的,以及在存儲模型上表現的性能。考慮下面 這個類:

public class C
{
 private MyType _a = new MyType( );
 private MyType _b = new MyType( );
 // Remaining implementation removed.
}
C var = new C ();

多少個對象被創建了?它們占用多少內存?這還不好說。如 果MyType是值類型,那麼你只做了一次堆內存分配。大小正好是MyType大小的2 倍。然而,如果MyType是引用類型,那麼你就做了三次堆內存分配:一次是為C 對象,占8字節(假設你用的是32位的指針)(譯注:應該是4字節,可能是筆誤), 另2次是為包含在C對象內的MyType對象分配堆內存。之所以有這樣不同的結果是 因為值類型是以內聯的方式存在於一個對象內,相反,引用類型就不是。每一個 引用類型只保留一個引用指針,而數據存儲還須要另外的空間。

為了理 解這一點,考慮下面這個內存分配:

MyType [] var = new MyType[ 100 ];

如果MyType是一個值類型數據,一次就分配出 100個MyType的空間。然而,如果MyType是引用類型,就只有一次內存分配。每 一個數據元素都是null。當你初始化數組裡的每一個元素時,你要上演101次分 配工作--並且這101次內存分配比1次分配占用更多的時間。分配大量的引用類型 數據會使堆內存出現碎片,從而降低程序性能。如果你創建的類型意圖存儲數據 的值,那麼值類型是你要選擇的。

采用值類型數據還是引用類型數據是 一個很重要的決定。把一個值類型數據轉變為類是一個深層次的改變。考慮下面 這種情況:

public struct Employee
{
 private string _name;
 private int   _ID;
 private decimal _salary;
 // Properties elided
 public void Pay( BankAccount b )
 {
  b.Balance += _salary;
 }
}

這是個很清楚的例子,這個類型包含一個方法,你可以用它 為你的雇員付薪水。時間流逝,你的系統也公正的在運行。接著,你決定為不同 的雇員分等級了:銷售人員取得擁金,經理取得紅利。你決定把這個Employee類 型改為一個類:

public class Employee
{
 private string _name;
 private int   _ID;
 private decimal _salary;
 // Properties elided
 public virtual void Pay( BankAccount b )
 {
  b.Balance += _salary;
 }
}

這擾亂了很多已經存在並使用了你設計的結構的代碼。返回 值類型的變為返回引用類型。參數也由原來的值傳遞變為現在的引用傳遞。下面 代碼段的行為將受到重創:

Employee e1 = Employees.Find( "CEO" );
e1.Salary += Bonus; // Add one time bonus.
e1.Pay( CEOBankAccount );

就是這個一次性的在工資中添加 紅利的操作,成了持續的提升。曾經是值類型COPY的地方,如今都變成了引用類 型的引用。編譯器很樂意為你做這樣的改變,你的CEO更是樂意這樣的改變。另 一方面,你的CEO將會給你報告BUG。

你還是沒能改變對值類型和引用類 型的看法,以至於你犯下這樣的錯誤還不知道:它改變了行為!

出現這個 問題的原因就是因為Employee已經不再遵守值類型數據的的原則。

另外 ,定義為Empolyee的保存數據的元素,在這個例子裡你必須為它添加一個職責: 為雇員付工資。職責是屬於類范圍內的事。類可以被定義多態的,從而很容易的 實現一些常見的職責;而結構則不充許,它應該僅限於保存數據。

在值 類型和引用類型間做選擇時,.Net的說明文檔建議你把類型的大小做為一個決定 因素來考慮。而實際上,更多的因素是類型的使用。簡單的結構或單純的數據載 體是值類型數據優秀的候選對象。事實表明,值類型數據在內存管理上有很好的 性能:它們很少會有堆內存碎片,很少會有垃圾產生,並且很少間接訪問。

(譯注:這裡的垃圾,以及前面提到過的垃圾,是指堆內存上“死 ”掉的對象,用戶無法訪問,只等著由垃圾回收器來收集的對象,因此認 為是垃圾。在.net裡,一般說垃圾時,都是指這些對象。建議看一下.net下垃圾 回收器的管理模型)

更重要是:當從一個方法或者屬性上返回時,值類型 是COPY的數據。這不會有因為暴露內部結構而存在的危險。But you pay in terms of features. 值類型在面向對象技術上的支持是有限的。你應該把所有 的值類型當成是封閉的。你可以建立一個實現了接口的值類型,但這須要裝箱, 原則17會給你解釋這會帶來性能方面的損失。把值類型就當成是一個數據的容器 吧,不再感覺是OO裡的對象。

你創建的引用類型可能比值類型要多。如 果你對下面所有問題回答YES,你應該創建值類型數據。把下面的問題與前面的 Employee例子做對比:

1、類型的最基本的職責是存儲數據嗎?

2 、它的屬性上有定義完整的公共接口來訪問或者修改數據成員嗎?

3、我 對類型決不會有子類自信嗎?

4、我對類型決不會有多太性自信嗎?

把值類型當成一個低層次的數據存儲類型,把應用程序的行為用引用類 型來表現。

你會在從類暴露的方法那取得安全數據的COPY。你會從使用 內聯的值類型那裡得到內存使用高率的好處。並且你可以用標准的面向對象技術 創建應用程序邏輯。當你對期望的使用拿不准時,使用引用類型。

=================================

小結:這一原則有點長, 花的時間也比較多一點,本想下班後,兩三個小時就搞定的,因為我昨天已經翻 譯了一些的,結果,還是一不小心搞到了11點。

最後說明一個,這一原 則還是沒有說明白什麼是引用類型什麼是值類型。當然,用class說明的類型一 定是引用類型,用struct說明的是值類型。還要注意其它一些類型的性質:例如 :枚舉是什麼類型?委托是什麼類型?事件呢?

返回教程目錄

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