15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】面倒なIDisposableの実装を自動化しました

15
Last updated at Posted at 2026-04-09

はじめに

C#にはIDisposableというインタフェースがあり,安全なリソース管理のために欠かせないため,どんなプロジェクトでも遭遇すると思います。
要求しているのはvoid Dispose()の1メソッドだけでありシンプルなようですが,「お行儀の良い方法」で実装すると結構めんどくさいです。
実際に書こうとすると

  • 基底がIDisposableであるか
  • 自分が継承される可能性はあるか
  • アンマネージリソースを解放する必要があるか

といったことを考慮して,場合によって適切な実装方法が変わります。
また,自分が管理するフィールドが増減した場合には解放もそれに応じて変化しますが,いちいちコードを修正して回るのも煩雑です。
ベストプラクティスが面倒というのはよくない状況ですが,サボるとリソースを適切に解放できずバグの原因となります。
そこで,面倒なことはSource Generatorに書かせようということで,IDisposableを自動実装してくれるSGを作成・公開しました。

基本的な機能

クラスや構造体がフィールドとして持っているリソースをすべて解放するDisposeメソッドを生成します。
また,オプションでアンマネージリソースを解放することもできます。

解放の対象となるリソースは

の内,後述する抑止パターンに該当しないすべてのフィールドです。

使い方

パッケージを導入した後,Disposeを実装したい型に[AutoDispose]属性(Dirge名前空間,以下同じ)を付けます。
型の定義内にDisposeを自動実装するために,

  • partialである
  • staticではない
  • 構造体(ref構造体を含む)の場合はreadonlyではない

という制約が課されます。
(違反する場合にはエラーを出してコードは一切生成しません)

シンプルな例

using Dirge;

namespace Test;

[AutoDispose]
internal partial class TestClass
{
    private readonly Stream _stream = new MemoryStream();
}

というコードに対して

namespace Test;

partial class TestClass : IDisposable
{
    private bool __generated_disposed = false;

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

    protected virtual void Dispose(bool disposing)
    {
        if (this.__generated_disposed) return;

        try
        {
            if (disposing)
            {
                this._stream?.Dispose();
            }
        }
        finally
        {
            this.__generated_disposed = true;
        }
    }
}

といったコードが自動的に生成されます。
(実際にはSGのベストプラクティスに従ったさらに丁寧なコードが生成されますが,簡単のために簡略化しています。以下同様。)

解放の抑止

機能上リソースを保持しているけれど自分で解放すべきではないフィールドに対して[DoNotDispose]属性を付けることでDispose内での解放から除外されます。

条件付き解放抑止

同一クラス(または構造体)内のbool型フィールドを条件として,当該フィールドの値が指定した値と一致する場合に解放を抑止します。
[DoNotDisposeWhen(string name, bool value)]という属性をフィールドに付けた場合,nameで指定されフィールドがvalueで指定される値と一致する場合にはリソースを解放しません。
BCLでStreamをコンストラクタ引数で取る型が一緒にbool leaveOpenを取るような場合と同じような用途で使えます。
生成するコードは割と真面目に最適化しており,例えば

[AutoDispose]
internal partial class MyClass
{
    private readonly bool _leaveOpen1;
    private static readonly bool _leaveOpen2;

    private readonly Stream _stream;

    [DoNotDisposeWhen(nameof(_leaveOpen1), true)]
    private readonly Stream _stream1; // _leaveOpen1 == true なら解放しない

    [DoNotDisposeWhen(nameof(_leaveOpen1), true)]
    private readonly Stream _stream2; // _leaveOpen1 == true なら解放しない

    [DoNotDisposeWhen(nameof(_leaveOpen1), false)]
    private readonly Stream _stream3; // _leaveOpen1 == false なら解放しない

    [DoNotDisposeWhen(nameof(_leaveOpen2), false)]
    private readonly Stream _stream4; // _leaveOpen2 == false なら解放しない

    [DoNotDispose]
    private readonly Stream _stream5; // これは常に解放しない
}

という型に対しては,参照するフィールドごとに分岐をまとめて

protected virtual void Dispose(bool disposing)
{
    if (this.__generated_disposed) return;

    try
    {
        if (disposing)
        {
            this._stream?.Dispose();

            if (this._leaveOpen1)
            {
                this._stream3?.Dispose();
            }
            else
            {
                this._stream1?.Dispose();
                this._stream2?.Dispose();
            }

            if (_leaveOpen2)
            {
                this._stream4?.Dispose();
            }
        }
    }
    finally
    {
        this.__generated_disposed = true;
    }
}

というコードを生成します(フィールドごとの分岐は行わない)。
サンプルコードで示しているように参照するフィールドはbool型であればstaticであっても構いませんが,プロパティの参照は今のところ認めていません。1

"NotDispose"に対して条件を付けているので「解放される場合」という視点では条件が逆になってしまいますが,基本の設計思想として「原則として全て解放,オプトアウトで除外」,という方針になっているためこのような設計になっています。

ref構造体

今でこそref構造体はインタフェースを実装することができますが,歴史的経緯からref構造体に限ってはパターンベースのusingの使用が可能であり,アクセス可能なでvoidを返す引数なしのDisposeメソッドを提供する場合にはusingパターンを使うことができます。
この仕様を踏襲し,上記の条件を満たすref構造体をDisposableであるとみなし,さらにref構造体に対してはIDisposableを実装せず,単にpublic void Dispose()を公開するにとどめます。
(言語バージョンによってインタフェースを実装できる場合には実装しても良いのですが,SGのコードが複雑になる割に恩恵が少ないのでこのような仕様になっています2)

アンマネージリソースの解放

さすがに個々のアンマネージリソースの適切な解放方法は知らないので,これを行う方法は利用者の責任で実装する必要があります。
しかし,解放が1つのメソッドとして提供されているのであればこのメソッドの呼び出し(ファイナライザ周りの扱い)はSG側で対応することができます。
[AutoDispose]属性のReleaseUnmanagedResourcesオプションに文字列としてアンマネージリソースを解放するメソッドの名前を渡すことで(実際にはnameofを使うことを強く推奨します),適切に呼び出すコードを生成します。

継承関係

基底がIDisposableを実装せずかつ自分がsealedである場合にはprotected virtual void Dispose(bool)は無駄であるためpublic void Dispose()内で完結します(アンマネージリソース解放する場合を除く)。
基底がオーバーライド可能なvirtual void Dispose(bool)を持つ場合はそれをオーバーライドし,そうでなければ自分で実装します。
また,基底がIDisposableを実装するけれどabstractになっている場合にはそれをオーバーライドする判断も行います。

使えないパターン

予期せぬリークを防ぐため,安全にDisposeメソッドを実装できない場合には安全側に倒すためにエラーを出して何も実装しません。
具体的には

  • 基底がIDisposableを実装しないのにvoid Dispose()メソッドを提供する
  • 基底がDisposableを実装するのにオーバーライド可能なvoid Dispose(bool)を提供しない

といった場合にはエラーを出します。
前者ではDisposeメソッドの役割が明らかでないため安易に呼び出すことができず,後者では新しいメソッドの定義により基底クラス内のリソースを解放できなくなってしまう危険性があるためです。

診断・コード修正

  • partial忘れ
  • [DoNotDisposeWhen]で指定するフィールド名がnameof(...)ではなく文字列リテラル (リファクタリングに追従できないので危険)

に対して診断とコード修正を出します。
partialであれば何も生成されないので予期せぬリークを引き起こす可能性がありますが,[AutoDispose]を付けた時点でエラーが出るので見落とすことはありません(解放すべきリソースを持っていない空クラスでもエラーになります)。

さいごに

ちょっと大きめのプロジェクトでいちいちIDisposableを実装するのが面倒だったので作る始めたSGでしたが,色んなところで使いたくなったため公開することにしました。
機能追加やバグ修正などお送りいただけると嬉しいです。
MITライセンスですので必要に応じて適当に使っていただいてももちろん大丈夫です。
皆さんのC#ライフが少しでもより良いものになれば幸いです。

おまけ: 名前について

"Disposable Implementation Roslyn Generator Extension"のアクロニムで"Dirge"となっており,この語は普通名詞で「葬送歌」といった意味を持ちます。
やや無理のあるアクロニムですが「リソースをちゃんと葬ってあげる」といった雰囲気を込めてみました。

更新履歴

2026-04-14

『【C#】IDisposableの実装,面倒じゃないですか?』より改題。

診断・コード修正を追記。

  1. プロパティのgetterは例外を投げることが可能ですが同じ型内のbool型フィールドの参照が例外を投げることは有り得ません。Disposeメソッド内でやたらと例外を投げられても困るので,プロパティの参照は認めないようにしています。

  2. where T : IDisposableのような制約を持つジェネリクスに入れたい場合には自動生成に頼らず型の定義に: IDisposableと書けば通ります。

15
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?