Effective C# 15.避免建構不必要的物件 Effective C# 15.避免建構不必要的物件 (Avoid Creating Unnecessary Objects)

Published on Friday, October 11, 2024

這個做法提出了建立過多物件會影響效率的概念,雖然我們的系統有 GC 能夠高效的管理記憶體中的物件, 但清除的效率在快還不如不創建無意義的物件,從根本上解決過度使用 GC 的導致的效率問題。

在做法 11 中有提到 GC 的基礎觀念,GC 就是在管理 Heap 記憶體空間,所以要減少 GC 執行的次數,等同於要盡量減少 在 Heap 中分配記憶體的相關操作,也就是所有的參考型別都包含在內。

以下面這段程式碼為例,假設 OnPaint 經常被呼叫的話,相同的 Font 物件就會不斷被創建並且結束後呼叫 Dispose 方法釋放掉記憶體。

public class Draw
{
    protected override void OnPaint(PaintEventArgs e)
    {
        using (Font MyFont = new Font("Arial", 10.0f))
        {
            e.Graphics.DrawString(DateTime.Now.ToString(),
                MyFont, Brushes.Black, new PointF(0, 0));
        }
        base.OnPaint(e);
    }
}

所以有一個技巧是把 Font 物件從區域變數改成成員變數,這樣就只需要初始化一次就能重複使用 Font 物件了,不過這種做法會導致 Font 物件 不會即時釋放掉,所以要把這個類別也實做 IDisposable 才能正確釋放掉。

public class Draw : IDisposable
{
	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);
	}
	public void Dispose()
	{
		myFont.Dispose();
		...
	}
}

上面的做法只適用於非常常用的物件才建議這麼處理,並不代表要把所有區域變數改成成員變數,一般的物件還是使用第一種 using 方法直接釋放掉就好。

第二個技巧是將物件改成靜態,靜態成員在前幾個做法有提到,我們可以透過它只會共用的特性來避免建立過多重複的物件,例如剛剛的程式碼裡面有個 Brushes.Black 取得筆刷的屬性就是個靜態的屬性。

public static Brush Black => GetBrush(s_blackKey, Color.Black);

還有一個是 System.String 類別使用上要注意的問題,在下面的寫法中雖然看起來是將資料拼接到同一個變數,但實際上 System.String 類別是 immutable 代表建構出字串後就不能再次修改了,所以我們下面的寫法實際上是不斷建立新的字串並且取代掉舊的,所以背後建立了多個垃圾變數。

void Main()
{
	string msg = "Hello";
	msg += ". Today is ";
	msg += System.DateTime.Now.ToString();
	Console.WriteLine(msg);
}

所以需要改用字串插值或者 StringBuilder 來減少背後變數建立的數量。

void Main()
{
	string msg = string.Format("Hello. Today is {0}", DateTime.Now.ToString());
	Console.WriteLine(msg);
}

void Main()
{
	StringBuilder msg = new StringBuilder("Hello, ");
	msg.Append(". Today is ");
	msg.Append(DateTime.Now.ToString());
	string finalMsg = msg.ToString();
	Console.WriteLine(msg);
}

Summary

這個做法主要核心是不要建立過多個參考型別變數,因為會導致 GC 運作過於頻繁,並且提出兩種辦法讓變數盡量少創建,不過可能會導致記憶體釋放 不夠即時,最後是 System.String 是一個不變的類型,所以在修改時背後會建立多個隱藏的變數,使用字串插值或 StringBuilder 可以解決相關問題。