程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#基礎知識 >> 《Effective C#》之減少裝箱和拆箱

《Effective C#》之減少裝箱和拆箱

編輯:C#基礎知識
  為了便於文章的開展,首先介紹裝箱(Boxing)和拆箱(Unboxing)這兩個名詞。.Net的類型分為兩種,一種是值類型,另一種是引用類型。這兩個類型的本質區別,值類型數據是分配在棧中,而引用類型數據分配在堆上。那麼如果要把一個值類型數據放到堆上,就需要裝箱操作;反之,把一個放在堆上的值類型數據取出來,則需要進行拆箱操作。

  例如,對於如下簡單的裝箱和拆箱操作語句。

  int i = 123;
  object obj = i;//Boxing
  if( obj is int )
  int j = (int) obj;//Unboxing
  為了,更好的诠釋裝箱和拆箱操作,我借用MSDN關於“Boxing”的解釋圖,具體如下。

  

  明白了這兩名詞的意思,現在說說為什麼要減少裝箱和拆箱操作。

  原因有兩個,主要是關於效率:一個就是對於堆的操作效率比較低;另一個就是對於堆上分配的內存資源,需要GC來回收,從而降低程序效率。

  考慮到這兩點因素,那麼需要在程序中減少裝箱和拆箱操作。

  如何減少呢,涉及到這兩個操作比較多的是,格式化輸出操作,例如:String.Format,Console.WriteLine之類的語句。

  例如:

Console.WriteLine( "Number list:{0}, {1}, {2}",1,2,3 );  
  對於“1,2,3”來說,相當於前面的“123”一樣,需要經過裝箱和拆箱兩個操作。那麼如何避免呢,其實只要向WriteLine傳遞引用類型數據即可,也就是按照如下的方式。

  Console.WriteLine( "Number list:{0}, {1}, {2}", 1.ToString(),2.ToString(),3.ToString() );
  由於“1.ToString()”的結果是String類型,屬於引用類型,因此不牽扯裝箱和拆箱操作。

  其次,牽扯到裝箱和拆箱操作比較多的就是在集合中,例如:ArrayList或者HashTable之類。

  把值類型數據放到集合中,可能會出現潛在錯誤。例如:

public struct Person
{
 private string _Name;
 public string Name
 {
  get{ return _Name; }
  set{ _Name = value; }
 }
 public Person( string PersonName )
 {
  _Name = PersonName;
 }

 public override string ToString()
 {
  return _Name;
 }
}

// Using the person in a collection

ArrayList arrPersons = new ArrayList();
Person p = new Person( "OldName" );
arrPersons.Add( p );

// Try to change the name

p = ( Person ) arrPersons[0] ;
p.Name = "NewName";
Debug.WriteLine( ( (Person ) arrPersons[0] ).Name );//It's "OldName"
  這個問題其實在前面的文章中已經講過了。有人可能會說,是否可以按照如下的方式去修改呢。

( (Person ) arrPersons[0] ).Name = "NewName";//Can't be compiled  
  很不幸,如上操作不能通過編譯。為什麼呢,對於“( (Person ) arrPersons[0] )”來說,是系統用一個臨時變量來接收拆箱後的值類型數據,那麼由於值類型是分配在棧上,那麼操作是對實體操作,可是系統不允許對一個臨時值類型數據進行修改操作。

// Using the person in a collection

ArrayList arrPersons = new ArrayList();
Person p = new Person( "OldName" );
arrPersons.Add( p );

// Try to change the name

p = ( Person ) arrPersons[0] ;
p.Name = "NewName";
arrPersons.RemoveAt( 0 );//Remove old data first
arrPersons.Insert( 0, p );//Add new data
Debug.WriteLine( ( (Person ) arrPersons[0] ).Name );//It's "NewName"
  其實,這樣操作會產生過多裝箱和拆箱操作。那麼更好的方法,可以通過接口來完成,從而減少裝箱和拆箱操作。對於這個例子的接口實現應該如下。

public interface IPersonName
{
 string Name{ get;set;}
}

public struct Person:IPersonName
{
 private string _Name;
 public string Name
 {
  get{ return _Name; }
  set{ _Name = value; }
 }
 public Person( string PersonName )
 {
  _Name = PersonName;
 }

 public override string ToString()
 {
  return _Name;
 }
}

// Using the person in a collection

ArrayList arrPersons = new ArrayList();
Person p = new Person( "OldName" );

arrPersons.Add( p );
// Change the name

( (IPersonName)arrPersons[0] ).Name = "NewName";
Debug.WriteLine( ( (Person ) arrPersons[0] ).Name );//It's "NewName"
  很多人就問,為什麼值類型不能修改,即

( (Person ) arrPersons[0] ).Name = "NewName";//Can't be compiled
  而如上的接口類型就能修改呢,即

( (IPersonName)arrPersons[0] ).Name = "NewName";
  這是由於產生的臨時變量的類型不同,前者已經在前面進行說明了,後者由於產生的臨時變量的類型為IPersonName,屬於引用類型,那麼相當於臨時變量就是原對象的引用,那麼對於對於它的修改會直接修改到原對象,因此是可以的。可以說這裡的不同本身在於產生臨時對象的類型不同,從而造成本質的區別。

  通過接口來改寫,這樣就減少了裝箱和拆箱操作,同時也保證了修改的正確性。不過要注意的是,這裡接口對於的是引用類型,如果接口訪問的或者返回的是值類型,那麼用接口雖說能實現了,但是對於裝箱和拆箱操作來說,並沒有減少。

  對於裝箱和拆箱操作來說,基本上就講完了,只要記住頻繁裝箱和拆箱操作會降低程序效率,因此在編寫的時候要盡量避免。

  查閱關於 C# 的全部文檔
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved