[VBA] IsError関数によるエラー処理

2017年2月13日月曜日

Excel VBA

VBAには On Error によるエラートラップ以外にIsError関数によるエラー判定方法が存在する。

"On Error" or "IsError" ?

例えば、引数が負数であればエラーとする処理は以下のようになる。
Function MustBePositiveOnErr(ByVal v As Long) As Variant
    MustBePositiveOnErr = v

    If v < 0 Then
        Err.Raise 5 ' Invalid Procedure call or argument.'
    End If
End Function

例外処理に慣れたプログラマであれば、なんということもない処理だね。

CVErrは整数値をErr型に変換してくれる。
ただし、CVErrを使った関数は戻り値の型をVariantにする必要がある。
Function MustBePositiveCVErr(ByVal v As Long) As Variant
    MustBePositiveCVErr = v

    If v < 0 Then
        MustBePositiveCVErr = CVErr(5)
    End If
End Function

こちらはCのAPIでよくある正常時は0を返して、異常時はエラー番号を返すような処理に似ている。
呼び出し側はIsErrorを使うことによって、エラーが起きたかどうかを判定することができる。
    Debug.Print IsError(MustBePositiveCVErr(1))
 ' False'
    Debug.Print IsError(MustBePositiveCVErr(-1))
 ' True'

Excelのユーザー定義関数

CVErrとIsErrorの組み合わせは戻り値でエラーを表現する方法をより一般化したものと言える。

なんで、こんな機能が言語レベルでサポートされているんだろう?
て思ったんだけど、VBAの中で使うのではなく、Excelのユーザー定義関数としてなら、これはかなり便利。
というより、ユーザー定義関数でいちいちErr.Raiseされたらひどいことになるよね。

※ユーザー定義関数ってのはVBAで自分で作った関数をSUMのようにExcelのセルの中から呼べる機能ね。

この場合、Err型の変数を返せば、Excelがセルに"#N/A"とか"#DIV0!"とか表示してくれる。
Excel VBA リファレンス セルのエラー値
確かにExcelではどんな数値や文字列だったら、エラーだなんて言えないからね。

まあ、実際になんでVBAにIsErrorが導入されたかの経緯はわかりませんが...

ラッパークラス、Nullable、Errorable?

IsErrorの考え方はループの中で全件処理して、どの値が不正だったかを調べる場合に応用できそう。
こんなときに値を格納するための変数とエラーフラグ用の変数を用意するのはめんどくさいよね。
ループの中でいちいち例外処理をするのはもっと煩わしい!

というわけ(?)で、Javaの場合はプリミティブ型ではなく、Integerのようなラッパークラスを使えば、nullを返すことができる。
C#だと、Nullableにすれば、nullを返すことができる。

でも、nullでエラーを表現するってのも、あまりかっこよくないね。
Errorableみたいなラッパークラスを作って、isErrorメソッドを用意するといいかもしれない。
あとは評価すると元のエラーに該当する例外が発生するメソッドとか。

C#で書くと、こんな感じだろうか。
class Errorable
{
    private T val;
    private Exception e;

    public Errorable(T val) : this(val, null)
    {
    }

    public Errorable(T val, Exception e)
    {
        this.val = val;
        this.e = e;
    }

    public T Value
    {
        get
        {
            return val;
        }
    }

    public Exception exception
    {
        get
        {
            return e;
        }
    }

    public bool IsError()
    {
        return e != null;
    }

    public T GetValue()
    {
        if (IsError())
        {
            throw e;
        }
        return val;
    }
}
# ここまで書いて、気づいたんだけど、これって非同期処理の一般形式ですね。

Java8であれば、Optionalがあるので、それを使えばいいのかもしれませんが...