程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 改善C#編程的50個建議(16-20)

改善C#編程的50個建議(16-20)

編輯:C#入門知識

16 避免創建不必要的對象
GC(垃圾回收)為我們管理內存,以一種比較有效的方式移除不使用的對象。但是不管怎樣分配和銷毀基於堆管理的對象
都會占用不少處理器時間,所以請不要加重GC的負擔,如下是一種比較糟糕的方式來分配GDI對象:

    protected override void OnPaint(PaintEventArgs e)
    {
        // Bad. Created the same font every paint event.
        using (Font MyFont = new Font("Arial", 10.0f))
        {
            e.Graphics.DrawString(DateTime.Now.ToString(),
            MyFont, Brushes.Black, new PointF(0, 0));
        }
        base.OnPaint(e);
    }

OnPaint事件會被頻繁地調用,而每調用一次該事件都會創建一個Font對象,GC需要每次都清理這些對象,這是非常沒有效率的。
如果我們此時能把Font改寫為成員變量將會更好:
    private readonly Font myFont =new Font("Arial", 10.0f);
    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.DrawString(DateTime.Now.ToString(),
        myFont, Brushes.Black, new PointF(0, 0));
        base.OnPaint(e);
    }

對於string對象,我們也應注意string的內容是不變的,一旦你創建一個string對象,其內容將不能被修改,當你好像在修改其內容時,實際是創建了新的對象,而舊對象做為
垃圾被回收,如下:
string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString();
這段代碼對應:
string msg = "Hello, ";
// Not legal, for illustration only:
string tmp1 = new String(msg + thisUser.Name);
msg = tmp1; // "Hello " is garbage.
string tmp2 = new String(msg + ". Today is ");
msg = tmp2; // "Hello " is garbage.
string tmp3 = new String(msg + DateTime.Now.ToString());
msg = tmp3; // "Hello . Today is " is garbage.


tmp1,tmp2,tmp3以及原始的msg對象都成為垃圾被GC回收了。string上的+=操作符實際是創建了一個新的string對象並返回。
此時你應該使用string.Format()方法來避免創建新對象:
string msg = string.Format("Hello, {0}. Today is {1}",thisUser.Name, DateTime.Now.ToString());
對於更復雜的string操作,你可以使用StringBuilder類型:
StringBuilder msg = new StringBuilder("Hello, ");
msg.Append(thisUser.Name);
msg.Append(". Today is ");
msg.Append(DateTime.Now.ToString());
string finalMsg = msg.ToString();
GC雖然有效地管理內存,但是創建多余的對象還是會損耗處理器時間,所以應避免創建過多的對象,不要創建你不需要的對象。


17 實現標准的Dispose模式
一個標准的模式是通過.NET框架來處理非托管資源。使用你的類的用戶也期望你能按照這個標准模式來處理。標准模式就是使用IDisposable接口來釋放你的非托管資源,當客戶端忘記這樣做的時候會使用finalizer被動的釋放。它和GC一起來保證你的對象在必要的時候被終結。這是處理非托管資源的正確方式,所以你應該徹底地理解它。
類層次的根基類應該實現IDisposable接口來釋放資源。該類還應該增加一個finalizer來作為被動防御機制。慣例方法就是在虛方法內執行釋放資源的委托方法,這樣派生類能重寫該方法以釋放它們自己的資源,而且派生類必須記得調用基類版本的虛方法來釋放基類的相關資源。
如果有非托管資源,你的類則必須有一個finalizer方法。你不應總是去信任客戶端來調用Dispose()方法來處理資源釋放。如果忘記Dispose()而你又沒有finalizer方法,則可能會導致內存洩漏。
當GC運行時,它從內存移除任何沒有終結器(有finalizer方法)的垃圾對象。那些有終結器的對象仍舊在內存裡。這些對象被添加到一個終結隊列,然後GC會在這些對象上生成一個新線程來運行終結器。在終結器線程完成它的工作後,GC才能從內存移除這些對象,所以那些需要終結的對象會比不需要終結的對象在內存中呆的時間更長。但是你不用為這些性能問題擔心。
實現IDisposable是一個標准的方式,它通知用戶運行時系統必須及時地釋放持有資源的對象。IDisposable接口只包含一個方法:
public interface IDisposable
{
void Dispose();
}
實現IDisposable.Dispose()方法是為了4個任務:
1.釋放所有非托管資源
2.釋放所有托管資源(包含未掛鉤的事件)
3.設置一個狀態標志來指示對象是否被釋放。你需要檢查這個狀態,如果釋放後再次釋放該對象,則拋出ObjectDisposed異常。
4.禁止終結器。調用GC.SuppressFinalize(this)來停止使用終結。
實現IDisposable,你也完成了兩個任務,一是提供了客戶端來及時釋放所有托管資源的機制,二是避免了客戶端去執行終結器。
簡單示例:
public class MyResourceHog : IDisposable
{
    // Flag for already disposed
    private bool alreadyDisposed = false;
    // Implementation of IDisposable.
    // Call the virtual Dispose method.
    // Suppress Finalization.
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    // Virtual Dispose method
    protected virtual void Dispose(bool isDisposing)
    {
        // Don't dispose more than once.
        if (alreadyDisposed)
            return;
        if (isDisposing)
        {
            // elided: free managed resources here.
        }
        // elided: free unmanaged resources here.
        // Set disposed flag:
        alreadyDisposed = true;
    }
    public void ExampleMethod()
    {
        if (alreadyDisposed)
            throw new ObjectDisposedException(
            "MyResourceHog",
            "Called Example Method on Disposed object");
        // remainder elided.
    }
}

如果派生類需要執行額外的清理,它將實現基類的虛擬方法,如下:
public class DerivedResourceHog : MyResourceHog
{
    // Have its own disposed flag.
    private bool disposed = false;
    protected override void Dispose(bool isDisposing)
    {
        // Don't dispose more than once.
        if (disposed)
            return;
        if (isDisposing)
        {
            // TODO: free managed resources here.
        }
        // TODO: free unmanaged resources here.
        // Let the base class free its resources.
        // Base class is responsible for calling
        // GC.SuppressFinalize( )
        base.Dispose(isDisposing);
        // Set derived class disposed flag:
        disposed = true;
    }
}

注意基類和派生類都包含了disposed狀態標志,並且上面的類都沒有實現終結方法(因為沒有包含非托管資源)。
下面是一個終結器(析構函數)使用代碼:
public class BadClass
{
    // Store a reference to a global object:
    private static readonly List finalizedList =
    new List();
    private string msg;
    public BadClass(string msg)
    {
        // cache the reference:
        msg = (string)msg.Clone();
    }
    ~BadClass()
    {
        // Add this object to the list.
        // This object is reachable, no
        // longer garbage. It's Back!
        finalizedList.Add(this);
    }
}

此時當類執行終結方法時,它會將它自己的引用放到一個全局列表中,它只是為了使自己可及,於是它又存活了。如果你實際需要終結一個恢復的對象,它將不會被終結。GC將不會從內存中移除任何只有通過終結器隊列才能到達的對象,但是可能對象已經被終結,如果這樣,這些對象幾乎可以肯定是不再使用了。雖然BadClass的成員仍然在內存中,但是它們像被disposed或finalized。沒有辦法控制終結器的順序,這種工作將是不可靠的。
在托管環境,你不需要創建終結器,你只需要在包含了非托管資源的類中或包含了實現IDisposable的成員類中創建終結器。


18 值類型和引用類型的區別
值類型或引用類型,結構或類,你應該使用哪個?這不是C++,C++裡所有類型都做為值類型來定義。這也不是JAVA,JAVA裡所有類型都是引用類型。當你創建類型時,在選擇struct或class關鍵字是簡單的,但是如果你之後更改了類,你將會做更多的工作來更新那些使用了你的類的客戶端應用。
這裡不是簡單的喜歡用哪個或另一個。正確與否取決於你如何去使用你新創建的類型。值類型不是多態的,它們更適合存儲應用操作的數據。引用類型能多態並且應用來定義應用的行為。
在C#中你使用struct或class關鍵字來聲明類型是否是值類型或引用類型。值類型應是輕量級、小類型。引用類型形成你的類層次結構。
如下代碼:
private MyData myData;
public MyData Foo()
{
return myData;
}
// call it:
MyData v = Foo();
TotalSum += v.Value;
如果MyData是值類型,那麼返回值會復制一份然後賦值給V.
如果MyData是引用類型,你引用了一個內部變量。這違法了封裝性。更改代碼如下:
public MyData Foo2()
{
return myData.CreateCopy();
}
// call it:
MyData v = Foo();
TotalSum += v.Value;
現在v是myData的一份副本。作為引用類型,兩個對象都創建在堆上。你不用擔心暴露內部數據,相反,你已經創建了一個額外的對象在堆上。
類型通過公共方法和屬性來導出值類型數據。
考慮下面代碼段:
private MyType myType;
public IMyInterface Foo3()
{
return myType as IMyInterface;
}
// call it:
IMyInterface iMe = Foo3();
iMe.DoWork();
myType變量仍然通過方法Foo3來返回,但是現在不是直接訪問內部數據,而是通過調用已定義的接口的方法。你現在不是為了MyType對象的數據內容,而是獲得它的行為(接口主要定義類的行為方法)。通過這些代碼,我們可以知道值類型存儲值,而引用類型存儲行為。
現在我們來深入了解一下這些類型是如何在內存中存儲以及與性能相關的存儲模型。考慮如下類:
public class C
{
private MyType a = new MyType();
private MyType b = new MyType();
// Remaining implementation removed.
}
C cThing = new C();
上面代碼一共創建了多少對象?每個有多大?這都視情況而定。如果MyType是值類型,你做了一次分配,分配了兩倍MyType的大小;如果是引用類型,做了三次分配,一次是為對象C(8個字節),另外2次是c裡的2個MyType對象。產生這不同的結果是因為值類型內聯存儲在一個對象裡,而引用類型不是。每個引用類型變量都包含一個引用,需要額外的空間來存儲。
為了能透徹地理解,考慮如下分配:
MyType[] arrayOfTypes = new MyType[100];
如果MyType是值類型,一次性就分配100個MyType對象。但如果MyType對象是引用類型,只分配了一次,數組每個元素都是null。當你初始化數組的每個元素時,你將執行101次分配(101次分配將會比一次性分配要花費更多時間).分配大量的引用類型會導致堆碎片,並且使你的應用變慢。如果你創建這些大量的類型是為了存儲數據,你應當選擇值類型。
決定使用值類型還是引用類型是比較重要的。把值類型變為類類型是一個深遠的改變。考慮如下類:
public struct Employee
{
    // Properties elided
    public string Position
    {
        get;
        set;
    }
    public decimal CurrentPayAmount
    {
        get;
        set;
    }
    public void Pay(BankAccount b)
    {
        b.Balance += CurrentPayAmount;
    }
}

這個相當簡單的類包含了一個支付方法。時過境遷,系統運行越來越好,然後你決定有不同類型的員工:銷售人員可以獲得提成,管理人員可以獲得獎金。如是你決定將Employee結構改為類:
public class Employee2
{
    // Properties elided
    public string Position
    {
        get;
        set;
    }
    public decimal CurrentPayAmount
    {
        get;
        set;
    }
    public virtual void Pay(BankAccount b)
    {
        b.Balance += CurrentPayAmount;
    }
}

這將會打破現在使用了你的Employee結構的代碼,返回值變成了返回引用。參數從傳值變成了傳引用。如下代碼將發生很大改變:
Employee e1 = Employees.Find(e => e.Position == "CEO");
BankAccount CEOBankAccount = new BankAccount();
decimal Bonus = 10000;
e1.CurrentPayAmount += Bonus; // Add one time bonus.
e1.Pay(CEOBankAccount);
現在一次性增加獎金現在變成了永久的增加了。改變類型的同時,影響了行為的改變。
類類型可以定義公共職責的多種不同的實現,而結構只應用來存儲數據。值類型在內存管理上會更有效率,它有較少的堆碎片,較少的垃圾,較少的迂回。更重要的是當從方法或屬性返回時,值類型返回的是副本。但是考慮擴展性,值類型對面向對象技術的支持很有限。你應該考慮所有的值類型都是sealed。
如果你對下面幾個問題都回答YES,那麼你應創建一個值類型。
1.這個類型的主要責任釋放是存儲數據?
2.它的訪問數據成員的公共接口完全由屬性來定義?
3.你確信這個類型永遠不會有子類?
4.你確信這個類型永遠不會有多種形態?
建立底層的數據存儲使用值類型,建立應用的行為使用引用類型。


19 確保0是一個有效的值類型狀態
缺省的.NET系統會將所有對象都設置為0.因為你不能阻止其他語言使用0來初始化值類型,所以用0來作為你的類型缺省值吧。
一個比較特殊的例子是枚舉。不要創建一個不把0作為有效狀態的枚舉類型。所有的枚舉類型都是從System.ValueType繼承來的。默認的枚舉值是從0開始,但是你可以修改默認行為:
public enum Planet
{
    // Default starts at 0 otherwise.
    Mercury = 1,
    Venus = 2,
    Earth = 3,
    Mars = 4,
    Jupiter = 5,
    Saturn = 6,
    Neptune = 7,
    Uranus = 8
    // First edition included Pluto.
}
Planet sphere = new Planet();

sphere是0,但它不是一個有效的值。當你創建你自己的枚舉類型時,請確保0是其中的一個有效的值。

20 偏愛不變的原子的值類型
內容不變的類型是簡單的,一旦創建,它們就是常量。不變的類型也是線程安全的。多個線程可以訪問同樣的內容,絕對不會看到不一致的內容。不變類型的內容是不能改變的,它會很好地工作在基於hash的集合裡。對於內容不變的類型Object.GetHashCode()返回的值是相同的。但是要使每個類型都內容不變,這是很難的。你需要不斷的克隆新對象來修改其內容狀態。分解你的類型為自然形成的單一實體的結構,即原子的類型。一個客戶類不是一個原子類,因為它可能包含很多信息(如:地址、電話等),任何一個獨立的信息也都可能會改變。原子類是一個單一的實體,如果改變它的組成字段,將會引發異常。
如果你需要將地址類作為一個struct,請讓它不變。任何更改實例字段都設置為只讀的,如下:
public struct Address2
{
    // remaining details elided
    public string Line1
    {
        get;
        private set;
    }
    public string Line2
    {
        get;
        private set;
    }
    public string City
    {
        get;
        private set;
    }
    public string State
    {
        get;
        private set;
    }
    public int ZipCode
    {
        get;
        private set;
    }
}

現在你有一個基於公共接口的不變的類型了。為了使它更有用,你需要增加必要的構造函數來初始化完整的地址結構信息。但請記住struct已有一個缺省的構造函數,缺省所有string類型成員都為Null,ZIPCode為0:
public Address2(string line1,
	string line2,
	string city,
	string state,
	int zipCode) :
	this()
{
	Line1 = line1;
	Line2 = line2;
	City = city;
	ValidateState(state);
	State = state;
	ValidateZip(zipCode);
	ZipCode = zipCode;
}

使用不可變類型需要一點稍微不同的調用順序來修改它的狀態.你的修改是通過創建一個新對象而不是修改已存在的實例來完成.
// Create an address:
Address2 a2 = new Address2("111 S. Main","", "Anytown", "IL", 61111);
// To change, re-initialize:
a2 = new Address2(a1.Line1,a1.Line2, "Ann Arbor", "MI", 48103);
一旦一個新的地址對象被構造了,它的值就是永遠固定了.這是相當安全的,如果a2在構造中出現了異常,它不會影響到原來的值。
上面的Address2結構並不是嚴格的不可變類型。私有的setter仍然可以改變內部數據狀態。改為真正不可變類型如下:
public struct Address3
    {
        // remaining details elided
        public string Line1
        {
            get { return Line1; }
        }
        private readonly string line1;
        public string Line2
        {
            get { return line2; }
        }
        private readonly string line2;
        public string City
        {
            get { return city; }
        }
        private readonly string city;
        public string State
        {
            get { return state; }
        }
        private readonly string state;
        public int ZipCode
        {
            get { return zip; }
        }
        private readonly int zip;
        public Address3(string line1,
        string line2,
        string city,
        string state,
        int zipCode) :
            this()
        {
            this.line1 = line1;
            this.line2 = line2;
            this.city = city;
            ValidateState(state);
            this.state = state;
            ValidateZip(zipCode);
            this.zip = zipCode;
        }
    }

值類型不支持派生,所以你不能期望用派生類來修改其字段。但應注意不可變類型中的引用類型,它可能導致能修改內部數據,從而失去不可變的特性。
如:
public struct PhoneList
{
	private readonly Phone[] phones;
	public PhoneList(Phone[] ph)
	{
	  phones = ph;
	}
	public IEnumerable Phones
	{
	  get
	  {
	    return phones;
	  }
	}
}
Phone[] phones = new Phone[10];
// initialize phones
PhoneList pl = new PhoneList(phones);
// Modify the phone list:
// also modifies the internals of the (supposedly)
// immutable object.
phones[5] = Phone.GeneratePhoneNumber();

因為數組是一個引用類型,所以修改元素的值直接破壞了其內部數據,為了解決該問題,你需要復制一份該數組的副本.
// Immutable: A copy is made at construction.
public struct PhoneList2
{
	private readonly Phone[] phones;
	public PhoneList2(Phone[] ph)
	{
	    phones = new Phone[ph.Length];
	    // Copies values because Phone is a value type.
	    ph.CopyTo(phones, 0);
	}
	public IEnumerable Phones
	{	
	  get
	  {
	    return phones;
	  }
	}
}
Phone[] phones2 = new Phone[10];
// initialize phones
PhoneList p2 = new PhoneList(phones);
// Modify the phone list:
// Does not modify the copy in pl.
phones2[5] = Phone.GeneratePhoneNumber();

不可變類型編寫簡單且容易維護。不要盲目地為每個屬性創建get和set訪問器。你存儲數據的第一選擇應是不可變的原子的值類型。

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