Debug.Assertの遅延評価

以前にC#によるDesign by Contractのやり方を紹介した.簡単におさらいしておくと以下のように実装する.

  using System.Diagnostics

  public class Foo
  {
    public void Bar(object baz)
    {
      Invalid()  //不変条件
      //事前条件
      Debug.Assert(baz != null);

      //処理
      //・・・

      //事後条件
      Debug.Assert("処理の結果・・・");
      Invalid()  //不変条件
    }
  }

  //不変条件
  protected void Invalid()
  {
    Debug.Assert(...);
    ...
  }

これで,コンパイル定数にDEBUGが定義されているときのみ,Contractのチェックが行われる.リリース時にはDebug.Assertのコードが評価されないため,速度の劣化の心配もない*1


さて,以前遅延評価をネタにしようとした時に問題としてあげたのが,Debug.Assertの先行評価である.Debug.Assertはリリース時には評価を行わないことで速度の劣化を防ぐのだが,上記コードの

  Debug.Assert(buz != null)

の部分でbuz != nullが評価されてしまうと,その分速度の劣化へとつながってしまう.この程度の評価ならたいした事はないかもしれないが,ちりも積もれば山となることもあるし,評価によってはそれ自体それなりの負荷がかかるケースもある*2


そこで,Debug.Assertの引数部分を関数でクロージングし,デバッグ時のみそれを評価するようにして遅延評価を実装してみる.

using System;
using System.Diagnostics;

namespace Sample
{

  public delegate Boolean Assertion();
  
  public class LazyDebug
  {
    public static void Assert(Assertion func)
    {
      #if DEBUG
        Debug.Assert(func());
      #endif
    }
  }
  
  public class LazyEvaluation
  {
    public int LazyAssert()
    {
      int i = 0;
      LazyDebug.Assert(delegate() { return ++i == 1; });
      LazyDebug.Assert(delegate() { return ++i == 1; });  //(*)Debag時にはAssertに引っかかる
      return i;
    }
    
    public static void Main()
    {
      LazyEvaluation le = new LazyEvaluation();
      
      Console.WriteLine(le.LazyAssert());
    }
  }
}

上記コードは,デバッグでは(*)行でAssertに引っかかり,エラーを発行する.しかし,リリースでは正常に動作し,画面には0が表示される.すなわち,Assertのみならず ++i == 1 も評価されていないことが分かる.また,(*)行をコメントアウトしてデバッグで実行すると,画面には1が表示される.デバッグとリリースで評価が異なるため取り扱いに注意は必要だが,デリゲート内部にどんな処理を書いてもリリース時には実行されないため,思う存分条件のチェックを入れることができる.


・・・とは言うものの,この程度の評価だと実は上記のコードは全く無意味だったりする.実は,C#のDebug.Assertは既に遅延評価(のようなもの)が実装されているからだ*3
確認のために,以下のコードを実行してみよう.

using System;
using System.Diagnostics;

namespace Sample
{
  public class LazyEvaluation
  {
    public int DefaultAssert()
    {
      int i = 0;
      Debug.Assert(++i == 1);
      Debug.Assert(++i == 1);  //(*)Debag時にはAssertに引っかかる
      return i;
    }
    
    public static void Main()
    {
      LazyEvaluation le = new LazyEvaluation();
      
      Console.WriteLine(le.DefaultAssert());
    }
  }
}

先ほどのコードと同じように,デバッグでは(*)行でAssrtに引っかかり,リリースでは画面に0が表示される.実際のところは遅延評価というより,リリース時にはDebug.Assertの行を引数ごと取っ払ってコンパイルしているのではないかと推測される.


というわけで,Booleanの比較のみで十分ならわざわざ小細工を労するまでもなく真っ当に使用すればよい.が,ここまで考察しといてその結論ではちょっと癪なので,小細工を入れてみる.

using System;
using System.Diagnostics;

namespace Sample
{

  public delegate void Assertion();
  
  public class LazyDebug
  {
    public static void Run(Assertion func)
    {
      #if DEBUG
        func();
      #endif
    }
  }
  
  public class LazyEvaluation
  {
    public void LazyAssert()
    {
      Invalid();  //不変条件
      LazyDebug.Run(delegate() {
        //事前条件
        Debug.Assert(...);
      });
      
      //処理
      //.....

      LazyDebug.Run(delegate() {
        //事後条件
        Debug.Assert(...);
      });
      Invalid();  //不変条件
    }
   
    //不変条件
    public void Invalid()
    {
      LazyDebug.Run(delegate() {
        Debug.Assert(...);
      });
    }
    
    public static void Main()
    {
      LazyEvaluation le = new LazyEvaluation();
      
      Console.WriteLine(le.LazyAssert());
    }
  }
}

上記のように条件の評価そのものではなく条件を評価する一連の行をクロージングしてやれば,それ自体がリリース時には評価されなくなる.評価するものを取得するためにロジックが必要な場合などに若干の効果を発揮することもあるような気がする.デリゲートによる見栄えの悪化や若干の行数の増加,見慣れぬやり方強制等のデメリットをカバーできる価値があるかどうかは疑問が残るところではある.

*1:最も,Design by Contractの機能が言語に組み込まれているものと比較すると多少の見栄えの悪さは否めない

*2:特に不変条件と事後条件のチェックはまともにやると概ねそうなりがち

*3:このネタを書くまで全く気がつかなかった.Microsoftのヘルプはいつもながら重要な事項が抜けてないか?ある意味トラップになりかねんぞ.