8

コード コントラクトを使い始める前は、コンストラクター チェーンを使用するときに、パラメーターの検証に関連して厄介なことに遭遇することがありました。

これは (不自然な) 例で説明するのが最も簡単です:

class Test
{
    public Test(int i)
    {
        if (i == 0)
            throw new ArgumentOutOfRangeException("i", i, "i can't be 0");
    }

    public Test(string s): this(int.Parse(s))
    {
        if (s == null)
            throw new ArgumentNullException("s");
    }
}

Test(string)コンストラクターにコンストラクターをチェーンさせたいTest(int)ので、 を使用しますint.Parse()

もちろん、int.Parse()null 引数を持つのは好きではないので、sが null の場合、検証行に到達する前にスローされます。

if (s == null)
    throw new ArgumentNullException("s");

そのチェックは役に立たなくなります。

それを修正する方法は?さて、私は時々これをしていました:

class Test
{
    public Test(int i)
    {
        if (i == 0)
            throw new ArgumentOutOfRangeException("i", i, "i can't be 0");
    }

    public Test(string s): this(convertArg(s))
    {
    }

    static int convertArg(string s)
    {
        if (s == null)
            throw new ArgumentNullException("s");

        return int.Parse(s);
    }
}

これは少し面倒です。失敗した場合のスタック トレースは理想的ではありませんが、機能します。

さて、Code Contracts が登場したので、私はそれらを使い始めました。

class Test
{
    public Test(int i)
    {
        Contract.Requires(i != 0);
    }

    public Test(string s): this(convertArg(s))
    {
    }

    static int convertArg(string s)
    {
        Contract.Requires(s != null);
        return int.Parse(s);
    }
}

すべて順調です。それは正常に動作します。しかし、私はこれができることを発見しました:

class Test
{
    public Test(int i)
    {
        Contract.Requires(i != 0);
    }

    public Test(string s): this(int.Parse(s))
    {
        // This line is executed before this(int.Parse(s))
        Contract.Requires(s != null);
    }
}

そして、もしそうならvar test = new Test(null)、は の前にContract.Requires(s != null)実行されます。これは、テストを完全に廃止できることを意味します。 this(int.Parse(s))convertArg()

それで、私の実際の質問に進みます:

  • この動作はどこかに文書化されていますか?
  • このような連鎖コンストラクターのコード コントラクトを記述するときに、この動作に依存できますか?
  • これに近づくべき他の方法はありますか?
4

1 に答える 1

7

短い答え

はい、動作は「前提条件」の定義と、への呼び出しなしの従来の検証 (if/then/throw) のContract.EndContractBlock処理方法で文書化されています。

を使用したくない場合はContract.Requires、コンストラクターを次のように変更できます。

public Test(string s): this(int.Parse(s))
{
    if (s == null)
        throw new ArgumentNullException("s");
    Contract.EndContractBlock();
}

長い答え

Contract.*コード内で呼び出しを行う場合、実際には名前空間内のメンバーを呼び出しているわけではありませんSystem.Diagnostics.Contracts。たとえば、次のようにContract.Requires(bool)定義されます。

[Conditional("CONTRACTS_FULL")]
public static void Requires(bool condition) 
{
    AssertMustUseRewriter(ContractFailureKind.Precondition, "Requires"); 
}

AssertMustUseRewriterは無条件に をスローするContractExceptionため、コンパイルされたバイナリを書き換えないと、CONTRACTS_FULLが定義されている場合にコードがクラッシュするだけです。定義されていない場合、属性Requiresが存在するために C# コンパイラによって への呼び出しが省略されるため、事前条件はチェックされません。[Conditional]

リライター

プロジェクト プロパティで選択された設定に基づいて、Visual Studio は適切な IL を定義CONTRACTS_FULLして呼び出しccrewrite、実行時にコントラクトをチェックします。

契約例:

private string NullCoalesce(string input)
{
    Contract.Requires(input != "");
    Contract.Ensures(Contract.Result<string>() != null);

    if (input == null)
        return "";
    return input;
}

でコンパイルするとcsc program.cs /out:nocontract.dll、次のようになります。

private string NullCoalesce(string input)
{
    if (input == null)
        return "";
    return input;
}

コンパイルしcsc program.cs /define:CONTRACTS_FULL /out:prerewrite.dllて実行するccrewrite -assembly prerewrite.dll -out postrewrite.dllと、実際に実行時チェックを実行するコードが得られます。

private string NullCoalesce(string input)
{
    __ContractRuntime.Requires(input != "", null, null);
    string result;
    if (input == null)
    {
        result = "";
    }
    else
    {
        result = input;
    }
    __ContractRuntime.Ensures(result != null, null, null);
    return input;
}

最も興味深いのは、Ensures(事後条件) がメソッドの一番下に移動し、Requires(前提条件) が既にメソッドの一番上にあったため、実際には移動しなかったことです。

これは、ドキュメントの定義に適合します:

[前提条件] は、メソッドが呼び出されたときの世界の状態に関する契約です。
...
事後条件は、メソッドが終了したときの状態に関する契約です。つまり、条件はメソッドを終了する直前にチェックされます。

さて、あなたのシナリオの複雑さは、まさに前提条件の定義に存在します。上記の定義に基づいて、前提条件はメソッドの実行前に実行されます。問題は、C# 仕様で、コンストラクター初期化子 (連鎖コンストラクター) をコンストラクター本体の直前に呼び出す必要があると規定されていることです[CSHARP 10.11.1]。これは前提条件の定義と矛盾しています。

魔法はここに住んでいる

したがって、生成されるコードをccrewriteC# として表現することはできません。言語は、チェーン コンストラクターの前にコードを実行するメカニズムを提供しないためです (チェーン コンストラクターのパラメーター リストで静的メソッドを呼び出す場合を除きます)。 ccrewrite、定義で必要に応じて、コンストラクターを取ります

public Test(string s)
    : this(int.Parse(s))
{
    Contract.Requires(s != null);
}

としてコンパイルされます

上記のコンパイル済みコードの MSIL

そして、連鎖コンストラクタへの呼び出しの前に requires への呼び出しを移動します。

上記のコードの msil がコントラクト リライターを介して渡される

つまり...

引数の検証を行う静的メソッドに頼る必要を回避する方法は、コントラクト リライターを使用することです。Contract.Requiresを使用するか、コードのブロックを で終わらせて前提条件であることを示すことで、リライターを呼び出すことができますContract.EndContractBlock();。そうすることで、リライタはコンストラクタ初期化子への呼び出しの前に、メソッドの先頭にそれを配置します。

于 2014-02-15T00:18:14.533 に答える