Chapter 2 - Item 15 : Avoid Creating Unnecessary Objects

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

承 Item 11,雖然有 GC 在背景負責回收記憶體,幾乎不需要開發者手動處理;但是在撰寫程式時,應該避免創建過多記憶體垃圾,降低 GC 介入的次數進而減少效能消耗。

1. 將可重覆使用的區域變數提升至類別成員。

考慮以下程式碼:

protected override void OnPaint( PaintEventArgs e )
{
    using ( var font = new Font( "Consolas", 12.0f ) )
    {
        e.Graphics.DrawString( DateTime.Now.ToString( ),
            font, Brushes.Black,
            new PointF( 0, 0 ) );
    }

    base.OnPaint( e );
}

我們可以注意到,font 這個區域變數不斷的創建以及釋放(Font 為類別);而 OnPaint 這個方法很頻繁的被呼叫,造成 heap 中的記憶體垃圾不斷產生。導致 GC 介入的次數大增。
    
為了改善這個情況,可以將 font 提升至類別成員,使其可以重複使用。
    
修改後程式碼:

private Font _font = new Font( "Consolas", 12.0f );

protected override void OnPaint( PaintEventArgs e )
{
    e.Graphics.DrawString( DateTime.Now.ToString( ),
        _font, Brushes.Black,
        new PointF( 0, 0 ) );

    base.OnPaint( e );
}

此舉讓 _font 可以被重複使用;減少 GC 介入的次數,增加效率。

Note:using 區塊有自動呼叫物件 Dipose 的作用(e.g. try finally),提升至類別成員後需記得設計 _font 呼叫 Dispose 的時機。

2. 利用靜態成員減少相同物件創建的次數。

承 1.,.NET framework 將 Brush 設計為讓整個應用程式可共用。

public static Brush Black
{
    [ResourceExposure( ResourceScope.Process )]
    [ResourceConsumption( ResourceScope.Process | ResourceScope.AppDomain,
    ResourceScope.Process | ResourceScope.AppDomain )]
    get
    {
        Brush black = ( Brush ) SafeNativeMethods.Gdip.ThreadData [ BlackKey ];
        if ( black == null )
        {
            black = new SolidBrush( Color.Black );
            SafeNativeMethods.Gdip.ThreadData [ BlackKey ] = black;
        }
        return black;
    }
}

這裡用了 Lazy initiation 的技巧,當應用程式欲取得指定顏色筆刷時;檢查字典內有無該物件,有則從字典取值後回傳、無則創建物件並加入字典後回傳。此舉減少了無用筆刷被創建的機會,進一步減少記憶體中無用的物件。

3. 使用 StringBuilder 串接複雜的字串。

考慮一般串接字串的方式。

string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString( );

此舉相當於(編譯會失敗,只是為了解釋如何運作):
    
string msg = "Hello, ";
    
string tmp1 = new string( msg + thisUser.Name );
msg = tmp1; // "Hello, " is garbage.
    
string tmp2 = new string( msg + ". Today is " );
msg = tmp2; // "Hello, <user>" is garbage.
    
string tmp3 = new string( msg + System.DateTime.Now.ToString( ) );
msg = tmp3; // "Hello, <user>. Today is " is garbage. 
    
由於 string 為 immutable,每一次的字串串接都會產生新的字串;在這個例子中,原始字串與 tmp1, tmp2 都成為了記憶體垃圾。

Note:可以使用 iterpolated strings 達到以上效果:
string msg = $"Hello, {thisUser.Name}. Today is {System.DateTime.Now.ToString( )}";

為了減少串接字串的記憶體垃圾,使用 stringBuilder 重複利用字串。

StringBuilder msg= new StringBuilder( "Hello, " );
msg.Append( thisUser.Name );
msg.Append(". Today is " );
msg.Append( DateTime.Now.ToString( ) );
string finalMsg = msg.ToString( );
結論:
1. 盡量避免創建無用的物件,可使用輔助的程式幫忙檢查(e.g. FxCop)。

2. 將可重覆使用的區域變數(只考慮 reference type)提升至類別成員,或是靜態成員。

3. 針對 immutable type 提供 builder class。