構造型のoutパラメータを割り当てる必要はありません


9

コードレビュー中に関数の行を誤ってコメントアウトすると、コードに奇妙な動作が見られます。再現は非常に困難でしたが、ここでは同様の例を示します。

私はこのテストクラスを持っています:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

GetOut通常はエラーをスローする電子メールへの割り当てはありません。

コントロールが現在のメソッドを離れる前に、出力パラメーター 'email'を割り当てる必要があります

ただし、EmailAddressが別のアセンブリの構造体にある場合、エラーは発生せず、すべてが正常にコンパイルされます。

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

コンパイラが電子メールの割り当てを強制しないのはなぜですか?構造体が別のアセンブリで作成されている場合、このコードはコンパイルされますが、構造体が既存のアセンブリで定義されている場合はコンパイルされないのはなぜですか?


2
クラスを使用している場合は、オブジェクトのインスタンスを「新規作成」する必要があります。構造体には必要ありません。docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…(このページでこのテキストを具体的に検索してください:クラスとは異なり、構造体は新しい
オペラ

1
あなたの犬の構造体は、変数を取得するとすぐにそれがコンパイルされません:)
アンドレ・サンソン

この例ではstruct Dog{}、ですべて順調です。
Henk Holterman、

2
@ johnny5次に例を示します。
アンドレ・サンソン

1
OK、これは面白いです。Core 3 Consoleアプリと.Standardクラスlibで再現。
ヘンクホルターマン

回答:


12

TLDR:これは、長期にわたる既知のバグです。私は2010年にそれについて最初に書きました:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assignment-anomaly/

それは無害であり、安全に無視でき、ややあいまいなバグを発見したことを祝福します。

コンパイラーはEmail確実に割り当てなければならないことを強制しないのはなぜですか?

ああ、そういう風に。後で説明するように、変数が確実に割り当てられていることをどの条件が意味するかについては、誤った考えがあります。

構造体が別のアセンブリで作成されている場合、このコードはコンパイルされますが、構造体が既存のアセンブリで定義されている場合はコンパイルされないのはなぜですか?

それがバグの核心です。このバグは、C#コンパイラが構造体の明確な割り当てチェックを行う方法と、コンパイラがライブラリからメタデータをロードする方法の共通部分の結果です。

このことを考慮:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

さて、この時点で私たちは何を知っていますか? fはタイプの変数のエイリアスですFoo。そのため、ストレージはすでに割り当てられており、少なくともストレージアロケータから出た状態にあります。呼び出し元によって変数に配置された値があった場合、その値はそこにあります。

何が必要ですか?私たちはf、コントロールがM正常に終了するすべてのポイントで確実に割り当てられることを要求します。したがって、次のようなものが期待されます。

void M(out Foo f)
{
  f = new Foo();
}

どのセットf.xf.yそのデフォルト値に。しかし、これはどうですか?

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

それも大丈夫です。しかし、ここにキッカーがあります。なぜデフォルト値を割り当てて、すぐにそれらを吹き飛ばす必要があるのでしょうか。 C#の明確な割り当てチェッカーは、すべてのフィールドが割り当てられているかどうかを確認します。これは合法です:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

そしてなぜそれが合法ではないのですか?値型です。 fは変数であり、既に typeの有効な値が含まれているFooので、フィールドを設定するだけで完了です。

正しい。それで、バグは何ですか?

あなたが発見したバグは次のとおりです: コスト削減のため、C#コンパイラーは参照ライブラリーにある構造体のプライベートフィールドのメタデータをロードしません。そのメタデータは巨大になる可能性があり、毎回それをすべてメモリにロードすることはほとんど成功しないため、コンパイラの速度が低下します。

これで、発見したバグの原因を推測できるはずです。コンパイラーは、outパラメーターが確実に割り当てられているかどうかを確認するときに、既知のフィールドの数を、初期化された明確なフィールドの数と比較し、プライベートフィールドのメタデータが読み込まれなかったため、ゼロのパブリックフィールドのみを認識します。。コンパイラーは、「ゼロフィールドが必要で、ゼロフィールドが初期化されているので問題ない」と結論付けています。

私が言ったように、このバグは10年以上前からあり、あなたのような人々は時々それを再発見して報告します。それは無害であり、修正する利点はほとんどありませんが、パフォーマンスコストが大きいため、修正される可能性はほとんどありません。

そしてもちろん、このバグは、プロジェクトのソースコードにある構造体のプライベートフィールドを再現しません。明らかに、コンパイラはすでにプライベートフィールドに関する情報を持っているためです。


@ johnny5:エラーが発生しないようにする必要があります。dotnetfiddle.net/ZEKiUkを参照してください。簡単な再現を投稿できますか?
Eric Lippert

1
フィドルのおかげでそれは私がメンバーの代わりにプロパティとしてxとyを定義したためでした
ジョニー

1
@ johnny5:通常のC#1.0スタイルのプロパティを定義しただけの場合、明確な代入チェッカーの観点から見ると、これはフィールドではなくメソッドです。C#3.0+スタイルの自動プロパティを定義した場合、コンパイラはそれを裏付けるプライベートフィールドがあることを認識します。そのことの明確な割り当てのルールは長年にわたって微調整されており、正確なルールを思い出しません。
Eric Lippert

あなたが使用している場合はSystem.TimeSpan代わりに、エラーが来るんerror CS0269: Use of unassigned out parameter 'email'error CS0177: The out parameter 'email' must be assigned to before control leaves the current method。の非静的フィールドは1つだけTimeSpan、つまり_ticksです。それはinternalその組立mscorlibに。このアセンブリは特別ですか?と同じSystem.DateTime、その分野はprivate
Jeppe Stig Nielsen

@JeppeStigNielsen:どうしたのかわからない!わかったら教えてください。
Eric Lippert

1

それはバグのように見えますが、それはある程度の意味があります。

「欠落エラー」は、クラスライブラリを使用している場合にのみ表示されます。また、クラスライブラリがVB.Netなどの別の.net言語で記述されている可能性もあります。「明確な割り当ての追跡」はC#の機能であり、フレームワークの機能ではありません。

だから全体としてはバグだとは思いませんが、これについての正式な声明については知りません。


アセンブリをC#にインポートする場合、構造体が別の言語で記述されたアセンブリ内にある場合でも、それを使用するコードはまだc#にあるため、明確な割り当て追跡を使用する必要はありません。
ジョニー

1
必ずしも。C#では、単一化された(ローカル)変数を使用できませんが、同時にフレームワークはそれが0に設定されることを保証します(default(T))。したがって、メモリの安全性などの違反はありません。
Henk Holterman、

3
私はそのような権威ある発言をすることができます。:)これは、長年の既知のバグです。
Eric Lippert

1
@EricLippertおかげで、私はあなたがいつかこれを見るであろうことを望んでいました
ジョニー5
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.