Closed8

Effective C# 6.0/7.0 のメモ(第2章)

だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目11 .NETのリソース管理を理解する

  • GCがメモリを解放するとき、穴が開いたメモリ領域をぎゅっとまとめて整列させる(コンパクション)
  • メモリ解放の仕組みにはファイナライザとIDisposableインターフェースの2つのメカニズムがある。IDisposableを選択したほうがよい。
  • ファイナライザのデメリット
    • いつ呼ばれるか管理できない
    • パフォーマンスを下げる。ファイナライザを実行する必要がある場合、その実行のためにメモリに余分に長く留まることになる。
    • それは1周期余分に留まるのではない。一度GCを乗り越えたオブジェクトは、1世代昇格し、次のチェックの周期が回ってくるのはざっくり10回先になる。(その次にさらに乗り越えてしまうと100回先)
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目12 メンバには割り当て演算子よりもオブジェクト初期化子を使用すること

  • メンバ変数は、コンストラクター内ではなく、宣言した時点で初期化してしまうとよい。(メンバ変数の宣言時の初期化が呼ばれた後に、コンストラクタの初期化が呼ばれる)
    • コンストラクタがいくつ追加されたとしても、メンバ変数が適切に初期化される(未初期化状態になることを防ぐ)
public class Program
{
    private List<string> labels = new();

    この後にコンストラクタ
}
  • それに当てはまらないケース
    • 0またはnullに初期化する場合(システム既定の初期化処理で、0で初期化されるため、無駄な処理になるから)
    • コンストラクタ内で別で初期化される場合(ex: コンストラクタの引数を元にListのsizeを変えるなど)。オブジェクト初期化子での初期化がすぐガベージとなるので無駄。
    • 初期化時にtryブロックで囲みたい場合
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目13 staticメンバを適切に初期化すること

  • staticメンバの初期化方法には、オブジェクト初期化子を用いる方法と、staticコンストラクタを用いる方法の2種類がある。
  • オブジェクト初期化子でのstaticメンバの初期化
    • この場合、newをする際の例外処理はできない
public class MySingleton
{
    private static readonly MySingleton theOneAndOnly = new MySingleton();
    
    public static MySingleton TheOnly
    {
        get { return theOneAndOnly; }
    }

    private MySingleton()
    {
        Console.WriteLine("MySingleton - Constructor");
    }

    public void DoSomething()
    {
        Console.WriteLine("MySingleton - DoSomething");
    }
}

public class SingletonUser
{
    public static void Main()
    {
        MySingleton.TheOnly.DoSomething();
    }
}

出力

MySingleton - Constructor
MySingleton - DoSomething
  • staticコンストラクタを利用することで、初期化時の例外をハンドリングすることができる
public class MySingleton
{
    private static readonly MySingleton theOneAndOnly;
    
    static MySingleton()  // static constructorにはアクセス修飾子は設置できない
    {
        Console.WriteLine("MySingleton - Static Constructor");
        try
        {
            theOneAndOnly = new MySingleton();
        }
        catch
        {
           // 例外処理 
        }
    }
    public static MySingleton TheOnly
    {
        get { return theOneAndOnly; }
    }

    private MySingleton()
    {
        Console.WriteLine("MySingleton - Constructor");
    }
    
    public void DoSomething()
    {
        Console.WriteLine("MySingleton - DoSomething");
    }
}

public class SingletonUser
{
    public static void Main()
    {
        MySingleton.TheOnly.DoSomething();
    }
}

出力

MySingleton - Static Constructor
MySingleton - Constructor
MySingleton - DoSomething
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目14 初期化ロジックの重複を最小化する

  • オーバーロードでコンストラクタを作成する場合、全ての引数の組み合わせ分作る必要がある
    • 例、引数が2つであれば、それぞれが有る・無しの場合があるので2 * 2通り
オーバーロードでコンストラクタを実装する例
public class MyClass
{
    private List<int> _numList;
    private string _name;

    // 組み合わせ(×, ×)
    public MyClass() : this(0, "")
    {
        Console.WriteLine("Constructor: MyClass()");
    }

    // 組み合わせ(〇, ×)
    public MyClass(int initCount) : this(initCount, "")
    {
        Console.WriteLine("Constructor: MyClass(int initCount)");
    }
    
    // 組み合わせ(×, 〇)
    public MyClass(string name) : this(0, name)
    {
        Console.WriteLine("Constructor: MyClass(string name)");
    }

    // 組み合わせ(〇, 〇)
    public MyClass(int initCount, string name)
    {
        Console.WriteLine("Constructor: MyClass(int initCount, string name)");
        _numList = (initCount > 0) ? new List<int>(initCount) : new List<int>();
        _name = name;
    }

    public void Show()
    {
        Console.WriteLine($"List Capacity: {_numList.Capacity}");
        Console.WriteLine($"Name: {_name}");
    }
}

public class Program
{
    public static void Main()
    {
        // var myClass = new MyClass();
        // var myClass = new MyClass(3);
        // var myClass = new MyClass("SampleName");
        var myClass = new MyClass(3, "SampleName");
        myClass.Show();
    }
}

出力

- new MyClass(3)の場合
Constructor: MyClass(int initCount, string name)
Constructor: MyClass(int initCount)
List Capacity: 3
Name:

- new MyClass(3, "SampleName")の場合
Constructor: MyClass(int initCount, string name)
List Capacity: 3
Name: SampleName

  • デフォルト引数を使う場合、new制約を満たすためのコンストラクタと、デフォルト引数を使うコンストラクタの2つだけでよい。
    • 引数の名前とデフォルト値の変更が他に影響を及ぼすことが要注意
デフォルト引数でコンストラクタを実装する例
public class SecondClass
{
    private List<int> _numList;
    private string _name;

    public SecondClass() : this(0, "")
    {
        Console.WriteLine("Constructor: SecondClass()");
    }

    public SecondClass(int initCount = 0, string name = "")
    {
        Console.WriteLine("Constructor: SecondClass(int initCount = 0, string name = )");
        _numList = (initCount > 0) ? new List<int>(initCount) : new List<int>();
        _name = name;
    }
    
    public void Show()
    {
        Console.WriteLine($"List Capacity: {_numList.Capacity}");
        Console.WriteLine($"Name: {_name}");
    }
}

public class Program
{
    public static void Main()
    {
        // var secondClass = new SecondClass();
        // var secondClass = new SecondClass(3);
        // var secondClass = new SecondClass(name: "SampleName"); // nameのラベルを書く必要がある
        var secondClass = new SecondClass(3, "SampleName");
        secondClass.Show();
    }
}

出力

- new SecondClass()の場合
Constructor: SecondClass(int initCount = 0, string name = )
Constructor: SecondClass()
List Capacity: 0
Name:

- new SecondClass(3)の場合
Constructor: SecondClass(int initCount = 0, string name = )
List Capacity: 3
Name:

- new SecondClass(3, "SampleName")の場合
Constructor: SecondClass(int initCount = 0, string name = )
List Capacity: 3
Name: SampleName
  • どちらかというとオーバーロードより名前付き引数のコンストラクタを使う方が良い。
  • コンストラクタ内で別のメソッドを呼んで初期化しようとすると、ILコードが冗長になるだけでなく、readonly変数が初期化できない(readonlyはコンストラクタ内に直接書かれた場合のみ初期化される)
    • コンストラクタは、先にthisの方が呼ばれる(変数の上書きの実験から)
コンストラクタの順番
public class MyClass
{
    private List<int> _numList;
    private readonly string _name;

    public MyClass(string name) : this(0, name)
    {
        Console.WriteLine("Constructor: MyClass(string name)");
        _name = "SampleName2";
    }

    public MyClass(int initCount, string name)
    {
        Console.WriteLine("Constructor: MyClass(int initCount, string name)");
        _numList = (initCount > 0) ? new List<int>(initCount) : new List<int>();
        _name = name;
    }

    public void Show()
    {
        Console.WriteLine($"List Capacity: {_numList.Capacity}");
        Console.WriteLine($"Name: {_name}");
    }
}

public class Program
{
    public static void Main()
    {
        var MyClass = new MyClass("SampleName");
        MyClass.Show();
    }
}

出力(readonlyの_nameが上書きされている)

Constructor: MyClass(int initCount, string name)
Constructor: MyClass(string name)
List Capacity: 0
Name: SampleName2

重要、初期化実行順

  1. static変数のメモリストレージが0に初期化
  2. static変数の初期化子
  3. 親クラスのstaticコンストラクタ
  4. staticコンストラクタ
  5. インスタンス変数のメモリストレージが0に初期化
  6. インスタンス変数の初期化子
  7. 適切な親クラスのコンストラクタ
  8. インスタンスコンストラクタ
  • 型の初期化は1度だけであるため、同じ型の別インスタンスを作るときは5から始まる
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目15 不必要なオブジェクトの生成を避けること

  • 高頻度で同じものが生成されるような場合は、「ローカル変数からメンバ変数に昇格させる」「staticオブジェクトとして用意する」のような回避策をとる
  • そのメンバ変数にするオブジェクトがIDisposableインターフェースを実装していたら、メンバ変数にしたクラス自体もIDisposableインターフェースを実装する
  • staticオブジェクトとして用意するとは、以下のような例
public class Brashes
{
    private static Brush blackBrush;

    // 他のclassで、BlackなBrushが何度も利用される
    public static Brush Black
    {
        get
        {
            if (blackBrush == null)
            {
                blackBrush = new Brush(Color.Black);
            }
            return blackBrush;
        }
    }
}
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目16 コンストラクタ内では仮想メソッドを呼ばないこと

  • 子クラスのコンストラクタ内でのメンバ変数の初期化が完了する前に、親クラスのコンストラクタで仮想メソッドが実行されてしまう。これにより完全には(オブジェクト初期化子では初期化されるがコンストラクタ内の初期化が完了していない)初期化がなされていないメンバ変数が利用されてしまう(仮想メソッドをoverrideした子クラスのメソッドが呼ばれ、そのメソッドの中でメンバ変数を用いていた場合に問題が発生する)
  • readonlyなメンバ変数でも、初期化子で設定した値がコンストラクタを通過し終わるまでは変わりうる。
検証用コード
public class Parent
{
    protected Parent()
    {
        Console.WriteLine("Parent - コンストラクタ - VFunc呼び出し前");
        VFunc();
        Console.WriteLine("Parent - コンストラクタ - VFunc呼び出し後");
    }
    
    protected virtual void VFunc()
    {
        Console.WriteLine("Parent - VFunc");
    }
}

public class Child : Parent
{
    private readonly string _msg = "初期化子で設定";

    public Child(string msg)
    {
        Console.WriteLine("Child - コンストラクタ");
        Console.WriteLine($"_msg = msg前の_msg: {_msg}");
        NormalFunc();
        
        _msg = msg;
        
        Console.WriteLine($"_msg = msg後の_msg: {_msg}");
        NormalFunc();
    }
    
    protected override void VFunc()
    {
        Console.WriteLine($"Child - VFuncの_msg: {_msg}");
    }

    private void NormalFunc()
    {
        Console.WriteLine($"Child - NormalFuncの_msg: {_msg}");
    }
}

public class Program
{
    public static void Main()
    {
        var child = new Child("Main内のコンストラクタで指定");
    }
}

出力

Parent - コンストラクタ - VFunc呼び出し前
Child - VFuncの_msg: 初期化子で設定
Parent - コンストラクタ - VFunc呼び出し後
Child - コンストラクタ
_msg = msg前の_msg: 初期化子で設定
Child - NormalFuncの_msg: 初期化子で設定
_msg = msg後の_msg: Main内のコンストラクタで指定
Child - NormalFuncの_msg: Main内のコンストラクタで指定
  • ちなみにRiderでもこれは怒られる。Why?
だーら(Flamers / Memotia)だーら(Flamers / Memotia)

項目17 標準的なDisposeパターンを実装する

  • IDisposableを実装したクラスに継承関係がある場合には、Disposeメソッドのオーバーロードを作成する
public class MyDisposableParent : IDisposable
{
    private bool _disposed = false;
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool isDisposing)
    {
        if (_disposed) return;
        if (isDisposing)
        {
            // マネージドリソースの解放
        }
        // アンマネージドリソースの解放
        _disposed = true;
    }
}

public class MyDisposableChild : MyDisposableParent
{
    private bool _disposed = false;
    protected override void Dispose(bool isDisposing)
    {
        if (_disposed) return;
        
        if (isDisposing)
        {
            // マネージドリソースの解放
        }
        // アンマネージドリソースの解放
        
        base.Dispose(isDisposing);
        
        _disposed = true;
    }
}
このスクラップは2023/09/12にクローズされました