🕌

Effective C# 6.0/7.0 メモ - 第 2 章 リソース管理

2023/08/29に公開

この記事は「Effective C# 6.0/7.0」の読書メモとして、私的プラクティスをまとめています。特に重要だと感じた項目のみ簡潔にまとめています。より詳細な内容に興味のある方は、原著を読んでみることをお勧めします。

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

.NET のメモリ管理

.NET では基本的にメモリ管理をガベージコレクタ(GC)にお任せします。GC は未使用のオブジェクトを探し出して、破棄します。しかし一部のリソース(ウィンドウやファイル、データベース接続、ネットワーク接続など…)は GC の管理外に置かれ、自分で管理しなければいけません。

アンマネージリソース

このようなリソースはアンマネージリソースと呼ばれ、不必要になったら明示的に破棄を行わなければいけません。

アンマネージリソースを破棄する方法は以下の二つです。

  • using ステートメント
  • ファイナライザ

using ステートメントによる破棄

using ステートメントを使用してリソースを定義することで、「ブロック内でリソースを使用し、ブロックを抜けたらリソースを破棄する」という表現が可能になります。

// ResourceはSystem.IDisposableを実装している必要がある
using(Resource r = new Resource())
{
	// リソースに対する操作
}

ファイナライザによる破棄

ファイナライザはクラスの破棄時に確実に呼ばれるメソッドです。アンマネージリソースを含むクラスを定義する際には、ファイナライザを定義する必要があります。

クラスのコンストラクタ名の先頭に~を付けたメソッドがファイナライザです。C++のデストラクタに近い文法ですが、動作としては Java のファイナライザに近いようです。

class MyClass
{
	// ファイナライザ(デストラクタと呼ばれることもあるが、正しくはファイナライザだそう)
	~MyClass()
	{
		// アンマネージリソースを破棄
	}
}

ファイナライザの問題

ファイナライザは高コストなため、扱いには注意が必要です。必要な時だけ使用するようにしましょう。

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

すべてのコンストラクタに共通の初期化は宣言時に行う

宣言時の初期化は、コンパイラによりコンストラクタの先頭に自動的に追加されます。そのため、すべてのコンストラクタに共通する初期化は宣言時に行うことを推奨します。

// NG
public class MyClass
{
	private List<string> labels;
	public MyClass()
	{
		labels = new List<string>();
	}
}

// OK
public class MyClass
{
	private List<string> labels = new List<string>();
}

宣言時に初期化をしない方が良いケース

以下の場合、宣言時に初期化をすべきではありません(できません)。

  • 0 または null に初期化する場合
  • すべてのコンストラクタに共通していない初期化をする場合
  • 初期化時に例外処理をしたい場合

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

宣言時に初期化するか static コンストラクタを使用する

static メンバは、インスタンスの作成前に初期化されるべきです。宣言時に初期化するか static コンストラクタを使用しましょう。

// 宣言時初期化
public class MyClass
{
	private static readonly MyClass theOneAndOnly = new MyClass();
}

// staticコンストラクタ
public class MyClass
{
	private static readonly MyClass theOneAndOnly;

	static MyClass()
	{
		theOneAndOnly = new MyClass();
	}
}

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

コンストラクタ共通の処理は、メソッドではなくコンストラクタに切り出す

メソッドではなくコンストラクタに切り出す理由は以下の 2 つによるものです。

  • コンパイラにより冗長なコードが生成される
  • コンストラクタ内でしか readonly フィールドを初期化できない
// NG
public class MyClass
{
    public MyClass()
    {
        commonConstructor(0, "");
    }

    public MyClass(int count)
    {
        commonConstructor(count, "");
    }

    public MyClass(int count, string name)
    {
        commonConstructor(count, name);
    }

    private void commonConstructor(int count, string name)
    {
        // ...
    }
}

// OK
public class MyClass
{
    public MyClass() : this(0, "")
    {
    }

    public MyClass(int count) : this(count, "")
    {
    }

    public MyClass(int count, string name)
    {
        // ...
    }
}

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

使いまわせるものはメンバ変数に

処理のたびに毎回オブジェクトを生成してしまうと GC にプレッシャーを与えることになる。使いまわして問題のないものはメンバ変数に宣言してください。

// NG
protected override void OnPaint(PaintEventArgs e)
{
	using (Font MyFont = new Font("Arial", 10.0f))
	{
		// ...
	}
}

// OK
private readonly Font myFont = new Font("Arial", 10.0f);
protected override void OnPaint(PaintEventArgs e)
{
		// ...
}

インスタンス生成時に初期化したくない場合は遅延評価

使わない可能性があるインスタンスは必要になってから作成するのも有効です。

private static Brush blackBrush;
public static Brush Black
{
	get
	{
		if (blackBrush == null)
		{
			blackBrush = new SolidBrush(Color.Black);
		}
		return blackBrush;
	}
}

文字列の連結は 文字列補間 or StringBuilder

文字列は ++= で連結すると、裏で不要なオブジェクトが生成されてしまいます。

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

// OK
string msg = $"Hello, {thisUser.Name}. Today is {System.DateTime.Now.ToString()}";

// OK
StringBuilder msg = new StringBuilder("Hello, ");
msg.Append(thisUser.Name);
msg.Append(". Today is ");
msg.Append(System.DateTime.Now.ToString());
string finalMsg = msg.ToString();

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

リソースを扱うクラスは Dispose パターンを実装しましょう。

Dispose パターン

  • リソースを解放するために IDisposable インターフェースを実装する
  • クラスがアンマネージリソースをメンバに持つ場合に限り、ファイナライザを実装する(それ以外では不要)
  • マネージリソースの破棄はシステムに任せるべきなので、ファイナライザからの呼び出しでは破棄しない
  • Dispose メソッドで GC.SuppressFinalize を呼び出すことで、不要なファイナライザを抑制
  • Dispose メソッドとファイナライザはいずれも、派生クラスでリソース管理を独自にオーバーライドできるよう、仮想メソッドに処理を委ねる
public class MyResource : IDisposable
{
	// すでに破棄済みかどうかを表すフラグ
	private bool disposed = false;

	public void Dispose()
	{
		Dispose(true);
		GC.SuppressFinalize(this);
	}

	protected virtual void Dispose(bool disposing)
	{
		// 2回以上の破棄処理を行わないようにする
		if (disposed)
		{
			return;
		}
		if (disposing)
		{
			// ここでマネージリソースを解放する
		}
		// ここでアンマネージリソースを解放する
		disposed = true;
	}

	// ファイナライザ(デストラクタと呼ばれることもあるが、正しくはファイナライザだそう)
    ~MyResource()
    {
        Dispose(false);
    }
}

派生クラスの Dispose パターン

  • 独自のリソースを解放する必要がある場合に限って仮想メソッドをオーバーライドする
  • 親クラスの仮想メソッドを必ず呼ぶ
public class DerivedResource : MyResource
{
	// 派生クラス固有の破棄フラグを用意する
	private bool disposed = false;

	protected override void Dispose(bool disposing)
	{
		// 2回以上は破棄処理は行わないようにする
		if (disposed)
		{
			return;
		}
		if (disposing)
		{
			// ここでマネージリソースを解放する
		}
		// ここでアンマネージリソースを解放する
		// 親クラスにリソースを解放させる
		base.Dispose(isDisposing);
		disposed = true;
	}
}

参考

Discussion