.NETでBitmap、BitmapDataを扱う場合の例外処理について


# はじめに
これまで2回ほどBitmap、BitmapDataを扱った画像処理の記事を記載してきました。
[.NETによる画像処理の高速化Tips：非unsafe編](http://qiita.com/Nuits/items/4a2fbc0f4e8583bd5531)
[.NETによる画像処理の高速化Tips：unsafe編（改稿：2015/11/08）](http://qiita.com/Nuits/items/da8c11e5b284ad6cb90a)

これらの中では例外処理について意図的に避けてきましたが、今回は例外処理について記載したいと思います。

# 例外処理のポイント
Bitmap及びBitmapDataを扱った画像処理を行った場合、以下の二点に気を配る必要があります。
1. BitmapはIDisposableを実装している
この為オブジェクトが不要となった時点でもれなくDisposeすること
2. BitmapDataは不要となった時点で必ずUnlockBitsを呼び出してメモリのロックを解除すること

1.は気が付きやすいのですが、2.は案外例外時の処理が不十分であるコードが散見されます。
Web上のコードサンプルがそこまで記述されていないものが多いため（私の前のコードもですが）、どうしても見落とされがちになってしまうのかも知れません。

また、気を配っているつもりでも意外と忘れやすいケースがあります。
次節ではサンプルを元に解説したいと思います。

# 例外処理サンプル
まず以下の前提があるとします。
1. 何らかの画像処理を行うメソッドを作成する必要がある
2. 該当メソッドでは引数にBitmapオブジェクトを受け取り、画像処理結果を引数とは異なるBitmapオブジェクトとして返却する
3. 引数として渡されるBitmap及び、戻り値のBitmapのリソース開放はメソッドの呼び出し元が責任を持つ
4. 処理の速度を考慮し、内部処理でBitmapDataを利用する

よくありがちな例だと思います。
具体的なコード例は以下の通りです。

```csharp
public System.Drawing.Bitmap DoWork(System.Drawing.Bitmap src)
{
    // 処理結果となるBitmapを作成する
    var dest = new System.Drawing.Bitmap(src.Width, src.Height, src.PixelFormat);
    // ビットマップをロックする
    var destBitmapData =
        dest.LockBits(
            new System.Drawing.Rectangle(0, 0, src.Width, src.Height), 
            System.Drawing.Imaging.ImageLockMode.ReadOnly, dest.PixelFormat);
    // 何らかの画像処理を実施する
    ...

    // 処理が完了したためBitmapDataをアンロックする
    dest.UnlockBits(destBitmapData);
    return dest;
}
```

さて上のコードは鋭い方はすぐ気が付くかも知れませんが、画像処理ブロックで例外が発生した場合、BitmapDataがアンロックされず、メモリがリークする原因となってしまいます。
というわけで、例外が発生してもきちんとアンロックされるように修正します。
以下の通りです。

```csharp
public System.Drawing.Bitmap DoWork(System.Drawing.Bitmap src)
{
    // 処理結果となるBitmapを作成する
    var dest = new System.Drawing.Bitmap(src.Width, src.Height, src.PixelFormat);
    // ビットマップをロックする
    var destBitmapData =
        dest.LockBits(
            new System.Drawing.Rectangle(0, 0, src.Width, src.Height), 
            System.Drawing.Imaging.ImageLockMode.ReadOnly, dest.PixelFormat);
    try
    {
        // 何らかの画像処理を実施する
        ...
    }
    finally
    {
        // 処理が完了したためBitmapDataをアンロックする
        dest.UnlockBits(destBitmapData);
    }
    return dest;
}
```

ポイントは2点あります。
1. dest.LockBitsで例外が発生した場合は、destBitmapDataは確保されていないためアンロックは必要ない
2. 「tryブロックに入っている」＝「destBitmapData」は必ず存在するためfinally句でnullチェックは必要ない

さて、一見上で問題ないように見える。。。というか、私は見えてやらかしたんですが、以下の問題点があります。
「画像処理ブロックで例外が発生した場合、destのBitmapのDisposeが呼び出されない」 

きっと見落としがちなはず！（私だけじゃないはず！）
戻り値のBitmapは、ちゃんとreturnできた場合は、呼び出し元でリソースの開放責任を持つため、失念しがちです。（きっと！）

そんなわけで、その点も考慮したコードが以下になります。

```csharp
public System.Drawing.Bitmap DoWork(System.Drawing.Bitmap src)
{
    // 処理結果となるBitmapを作成する
    var dest = new System.Drawing.Bitmap(src.Width, src.Height, src.PixelFormat);
    try
    {
        // ビットマップをロックする
        var destBitmapData =
            dest.LockBits(
                new System.Drawing.Rectangle(0, 0, src.Width, src.Height), 
                System.Drawing.Imaging.ImageLockMode.ReadOnly, dest.PixelFormat);
        try
        {
            // 何らかの画像処理を実施する
            ...
        }
        finally
        {
            // 処理が完了したためBitmapDataをアンロックする
            dest.UnlockBits(destBitmapData);
        }
    }
    catch
    {
        dest.Dispose();
        throw;
    }
    return dest;
}
```

ポイントは以下の点です。
1. BitmapのLockBits自体が例外をスローする可能性がある
この為、tryブロックを2重で書かなくてはならずはっきり言って気に入りません。
[MSDN Bitmap.LockBits メソッド](https://msdn.microsoft.com/ja-jp/library/5ey6h79d(v=vs.110).aspx)
2. 画像処理ブロックで例外が発生した場合、戻り値予定であったBitmapオブジェクトのdestは返却されないため、catchブロックでDisposeし、例外は再スローする

上のコードも正直汚くてあまり気に入りませんが、実際の場合、destのBitmapだけではなくsrcのBitmapもロックしてBitmapDataを利用するケースが多いはずです。
こうなると、try・catch・finallyを何重にも記載しなくてはならず、コードの可読性は下がる上に、リソースの開放漏れも気が付きにくくなります。

これらを回避するためには、IDisposableを実装した、BitmapDataのラッパークラスを作成するのが良いかもしれません。（ラッパークラスのサンプルは末尾に記載します。）
すると以下のように比較的きれいに記述できますし、例外処理の漏れも防げます。

```csharp
public System.Drawing.Bitmap DoWork(System.Drawing.Bitmap src)
{
    // 処理結果となるBitmapを作成する
    var dest = new System.Drawing.Bitmap(src.Width, src.Height, src.PixelFormat);

    try
    {
        using (var srcBitmapData = BitmapDataWrapper.LockBits(src))
        using (var destBitmapData = BitmapDataWrapper.LockBits(dest))
        {
            // 何らかの画像処理を実施する
        }
    }
    catch
    {
        dest.Dispose();
        throw;
    }

    return dest;
}
```

IDisposableを実装したBitmapDataのサンプルは以下の通りです。

```csharp
/// <summary>
/// BitmapDataをラップしたヘルパークラス
/// </summary>
public class BitmapDataWrapper : IDisposable
{
    /// <summary>
    /// ロック対象となったBitmap
    /// </summary>
    private System.Drawing.Bitmap bitmap;
    /// <summary>
    /// Bitmapをロックしたオブジェクト
    /// </summary>
    private System.Drawing.Imaging.BitmapData bitmapData;
    /// <summary>
    /// Bitmapをロックしたオブジェクトを取得する
    /// </summary>
    public System.Drawing.Imaging.BitmapData BitmapData
    {
        get { return bitmapData; }
    }
    /// <summary>
    /// コンストラクタは隠蔽し、LockBitsメソッドを利用する
    /// </summary>
    /// <param name="bitmap">ロック対象となったBitmap</param>
    /// <param name="bitmapData">Bitmapをロックしたオブジェクト</param>
    private BitmapDataWrapper(System.Drawing.Bitmap bitmap, System.Drawing.Imaging.BitmapData bitmapData)
    {
        this.bitmap = bitmap;
        this.bitmapData = bitmapData;
    }
    /// <summary>
    /// デストラクタ
    /// </summary>
    ~BitmapDataWrapper()
    {
        Dispose(false);
    }
    /// <summary>
    /// Bitmapをシステムメモリにロックします。
    /// </summary>
    /// <param name="bitmap"></param>
    /// <returns></returns>
    public static BitmapDataWrapper LockBits(System.Drawing.Bitmap bitmap)
    {
        var bitmapData =
            bitmap.LockBits(
                    new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height),
                    System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat);
        return new BitmapDataWrapper(bitmap, bitmapData);
    }
    /// <summary>
    /// リソースを開放する
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    /// <summary>
    /// リソースを開放する
    /// </summary>
    /// <param name="disposing"></param>
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (bitmap != null)
            {
                bitmap.UnlockBits(bitmapData);
                bitmap = null;
                bitmapData = null;
            }
        }
    }
}
```