Effective C# Item 6: Distinguish Between Value Types and Reference Types

來源:互聯網
上載者:User

Item 6: Distinguish Between Value Types and Reference Types

     實值型別還是參考型別?結構體還是類?我們應當用哪個?這不是C++,把所有的類型都定義為實值型別,之後我們可以再為其建立引用;這也不是java,所有的類型都是參考型別。當我們建立類型的時候我們必須明確它的行為,這對我們正確能選擇值還是引用非常重要。我們必須仔細推敲我們的決定,因為一旦改變就可能在一些不顯眼的地方造成錯誤。當我們建立一個類型的時候,將其聲明為class還是struct是一個經常會遇到的問題。但是同事後修改代碼相比,仔細考慮這個問題還是非常值得的。

     選擇哪個並不僅僅是喜歡這個不喜歡那個這麼簡單。正確的選擇取決於我們期望新類型如何發揮作用。實值型別沒有多態性,它們更適於在應用程式中儲存資料之類的工作。參考型別有多態性,,適於定義應用程式的行為模式。我們應當通過分析新類型的作用來決定選擇哪一種。結構體儲存資料,類定義行為。

     在.Net和C#中我們要區分實值型別和參考型別。在C++中所有的參數和傳回值都是通過實值型別來進行的。傳值非常高效,但是也會遇到一些問題:例如局部拷貝(partial copying有的地方叫做截斷效應(slicing problem))。當我們使用一個衍生類別的對象作為基類對象來進行傳遞時,只有基類的部分被傳遞了,而衍生類別中的一部分就被截斷開來。

     Java中所有使用者自訂類型都為參考型別,所有的參數和傳回值都是通過引用來傳遞。這種處理策略具有一致性,但是在效率上有時會有些消耗。有些類型根本就不存在也不可能存在多態性。在建立和銷毀這些類型執行個體時會有多餘的消耗。在C#中,我們可以選擇使用struct或class來決定使用實值型別還是參考型別。實值型別更輕量級,參考型別有多態性。合理使用不同作用的類型要求我們必須瞭解實值型別和參考型別之間的區別。

     下面的例子中的方法返回一個MyData型的對象。

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

     如果MyData是一個實值型別,那麼返回的是一個儲存在v中的值的拷貝。如果它是參考型別,則返回了一個內部成員的引用,這也違反了類密封的要求。

     再考慮下面這段代碼:

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

     現在v是_myData的一個拷貝。對於參考型別來說建立了兩個對象,我們不會再引發內部成員暴露的問題。為此我們付出的代價就是建立了一個額外的對象。如果v是一個局部變數,它很快就會實效,而使用Clone方法則需要在運行時對類型進行檢驗。這些都會降低效率。

     通過公有方法和屬性向外部傳遞資料的類型應當使用實值型別。但是這並不是說所有從公有方法和屬性返回的類型都應當為實值型別。我們假設上例中MyData類型包含了一些資料,它的職責就是儲存這些資料。

     但是考慮下面的代碼:

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

     _myType變數依然從Foo方法返回,但是這次不是包含資料的值,而是通過定義介面調用對象內部定義的方法。這次我們得到的不是MyType的資料而是它的行為。這些行為是通過IMyInterface介面表現出來的,這將帶來很大的變化,例入,MyType應當是個參考型別而不是實值型別,應當負責表現類型的行為而不是儲存資料。

     這些簡單的代碼向我們展示了實值型別和參考型別之間的區別:實值型別儲存資料,參考型別定義行為。現在我們來看一下它們在記憶體中是如何儲存的。考慮下面的類

public class C
{
   private MyType _a = new MyType( );
   private MyType _b = new MyType( );
}
C var = new C();

     我們一共建立了多少個對象呢?它們都是多大?答案取決於MyType的類型。如果它是實值型別,那麼我們在棧中分配了一塊記憶體空間,大小為MyType的兩倍。如果它是參考型別,我們要分配三塊記憶體空間:一塊為C的對象,為8byte,兩塊為C對象內的MyType型對象。這種結果的原因是因為實值型別將儲存的值內嵌在類型內,而參考型別不是。每個參考型別的中的變數僅保留引用,其儲存的值在另外的記憶體塊中。

     選擇使用實值型別還是參考型別是非常重要的。一旦發生改變那就不僅僅是將struct換成class那麼簡單的了。考慮下面這段代碼

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

     這個簡單的類型中包含了一個為員工支付工資的方法,而且運轉正常。後來你決定將其換為類,因為有不同的員工:銷售人員獲得傭金,經理獲得獎金。

public class Employee
{
   private string _name;
   private int _ID;
   private decimal _salary;

   public virtual void Pay( BankAccount b )
   {
      b.Balance += _salary;
   }
}

     這個改動會影響到許多你使用結構體的地方。原本傳回值類型的地方現在返回參考型別了。原本傳遞實值型別參數的地方現在傳遞引用型參數了。這種變化會在下面的代碼產生完全不同的表現。

Employee e1 = Employees.Find( "CEO" );
e1.Salary += Bonus; 
e1.Pay( CEOBankAccount );

     應當是值增加一次的獎金變為永久增加了,因為原本是值的地方現在變成引用了。編譯器並不會提出異議,但這是個bug。我們必須注意這並不是簡單的替換一下的問題,這種改變也改變了程式的行為。

     造成問題產生的原因是Employee類型沒有遵循實值型別的規則。定義Employee時除了儲存資料外,還為它添加了支付員工工資的責任。這是種責任應當使用類類型。類可以使用多態性,能很靈活的完成這些人物,而結構不可以。結構應當僅僅用來儲存資料。

     在.Net的協助性文檔中還提到我們應當將類型的大小作為使用值或引用的原因之一。事實上,更重要的還是這個類型的應用。對於簡單的結構和資料來說,實值型別是不二之選。的確,實值型別在記憶體管理上的效率更高:產生更少的片段,更少的垃圾,更直接。最重要的是當從方法和屬性返回時實值型別返回的是原值的拷貝,這樣不會有暴露內部成員的危險。當然它也有局限性,它不能實現多態性,無法從實值型別中繼承,就彷彿它們是sealed一樣。我們可以使用實值型別來實現介面,但是這需要boxing,這會降低效率。我們應當將實值型別考慮為簡單的儲存容器,而不是OO中的object。

     我們應當更多的使用參考型別。下面有幾個問題,如果的答案都是yes的話,我們就應當建立實值型別。

     1. 這個類型是否僅負責資料存放區?

     2. 這個類型的公有介面是否是用來傳遞或修改自身的資料成員?

     3. 是否確定這個類型不會有子類型?

     4. 是否確定這個類型不需要多態性?

     建立消耗較低的實值型別來儲存資料,建立參考型別類為你的應用程式定義行為。當拿不準使用什麼的時候,那就用參考型別吧。

譯自   Effective C#:50 Specific Ways to Improve Your C#                      Bill Wagner著

P.S. 由於我對Java一竅不通,所以本文中對Java的一些解釋可能會不到位。
       雖說是單純用來如果儲存資料,但是如果結構十分複雜的話,那樣傳值還會比傳引用高效嗎?恐怕會浪費大量的時間在應對struct中的資料吧。這樣的話是不是應當傳引用比較好呢?我也不是很清楚了。
       雖說將傳值和傳引用用錯會造成一些麻煩,但相比起來將參考型別錯用成實值型別的錯誤會大的多,把實值型別錯定義成參考型別會造成內部成員暴露,降低效率,但是還不會造成致命傷。但是一旦發現將參考型別錯定義成實值型別,那可真是好多都白乾了。實值型別的局限太大了,還是謹慎使用的好。就像書裡最後說的,不知道用什麼就用參考型別。。。。。

下面是MSDN說的:
      資料類型分隔為實值型別和參考型別。實值型別要麼是堆棧分配的,要麼是在結構中以內聯方式分配的。參考型別是堆分配的。參考型別和實值型別都是從最終的基類 Object 派生出來的。當實值型別需要充當對象時,就在堆上分配一個封裝(該封裝能使實值型別看上去像引用對象一樣),並且將該實值型別的值複製給它。該封裝被加上標記,以便系統知道它包含一個實值型別。這個進程稱為裝箱,其反向進程稱為unboxing。裝箱和unboxing能夠使任何類型像對象一樣進行處理。

      回到目錄

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.