web-dev-qa-db-ja.com

<T>から<U>への暗黙的な変換演算子が<T?>を受け入れるのはなぜですか?

これは私が理解できない奇妙な振る舞いです。私の例では、クラス_Sample<T>_と、Tから_Sample<T>_への暗黙的な変換演算子があります。

_private class Sample<T>
{
   public readonly T Value;

   public Sample(T value)
   {
      Value = value;
   }

   public static implicit operator Sample<T>(T value) => new Sample<T>(value);
}
_

この問題は、_int?_などのTにNULL入力可能値タイプを使用する場合に発生します。

_{
   int? a = 3;
   Sample<int> sampleA = a;
}
_

重要な部分は次のとおりです
Sample<int>_はintから_Sample<int>_への変換を定義しますが、_int?_から_Sample<int>_への変換は定義しないため、これはコンパイルすべきではありません。 ただし、正常にコンパイルおよび実行されます!(つまり、変換演算子が呼び出され、readonlyフィールドに_3_が割り当てられます。)

そしてさらに悪化します。ここでは、変換演算子は呼び出されず、sampleBnullに設定されます。

_{
   int? b = null;
   Sample<int> sampleB = b;
}
_

すばらしい答えはおそらく2つの部分に分かれます。

  1. 最初のスニペットのコードがコンパイルされるのはなぜですか?
  2. このシナリオでコードがコンパイルされないようにすることはできますか?
52
Noel Widmer

コンパイラがこのコードをどのように下げるかを見ることができます。

int? a = 3;
Sample<int> sampleA = a;

into this

int? nullable = 3;
int? nullable2 = nullable;
Sample<int> sample = nullable2.HasValue ? ((Sample<int>)nullable2.GetValueOrDefault()) : null;

なぜならSample<int>は、そのインスタンスにnull値を割り当てることができるクラスであり、そのような暗黙の演算子を使用すると、null許容オブジェクトの基本型も割り当てることができます。したがって、これらのような割り当ては有効です。

int? a = 3;
int? b = null;
Sample<int> sampleA = a; 
Sample<int> sampleB = b;

Sample<int>structになりますが、もちろんエラーになります。

EDIT:では、なぜこれが可能ですか?これは意図的な仕様違反であり、これは下位互換性のためにのみ保持されているため、仕様で見つけることができませんでした。 code でそれについて読むことができます:

意図的な仕様違反:
ネイティブコンパイラは、変換の戻り値の型がnull不可の値型ではない場合でも、「リフト」変換を許可します。たとえば、構造体Sから文字列への変換がある場合、Sから「リフト」変換されますか? to stringは、ネイティブコンパイラによって「s.HasValue?(string)s.Value:(string)null」のセマンティクスで存在すると見なされます。 Roslynコンパイラは、後方互換性のためにこのエラーを永続化します。

それが、この「エラー」がRoslynで 実装 である方法です:

それ以外の場合、変換の戻り値の型がNULL入力可能な値型、参照型、またはポインター型Pである場合、これを次のように下げます。

temp = operand
temp.HasValue ? op_Whatever(temp.GetValueOrDefault()) : default(P)

つまり、 spec に従って、特定のユーザー定義の変換演算子T -> U持ち上げられた演算子が存在するT? -> U?ここで、TUは、null不可の値型です。ただし、このようなロジックは、上記の理由により、Uが参照型である変換演算子にも実装されています。

PART 2このシナリオでコードがコンパイルされないようにする方法は?方法があります。 nullable型専用の追加の暗黙的な演算子を定義し、属性Obsoleteで修飾できます。それには、型パラメーターTstructに制限する必要があります。

public class Sample<T> where T : struct
{
    ...

    [Obsolete("Some error message", error: true)]
    public static implicit operator Sample<T>(T? value) => throw new NotImplementedException();
}

この演算子は、より具体的なため、null許容型の最初の変換演算子として選択されます。

そのような制限を行うことができない場合は、各値タイプに対して各演算子を個別に定義する必要があります(reallyが決定した場合、テンプレートを使用してリフレクションとコード生成を利用できます):

[Obsolete("Some error message", error: true)]
public static implicit operator Sample<T>(int? value) => throw new NotImplementedException();

コード内の任意の場所で参照すると、エラーが発生します。

エラーCS0619「Sample.implicit operator Sample(int?)」は廃止されました:「Some error message」

42
arekzyla

コンバージョンオペレーターが機能しなくなったと思います。仕様によると:

Null不可の値型SからNull不可の値型Tに変換するユーザー定義の変換演算子がある場合、Sから変換するリフト変換演算子が存在しますか? Tへ?この解除された変換演算子は、Sからのアンラップを実行しますか? SからTへのユーザー定義の変換、それに続くTからT?へのラッピングが続きます。ただし、ヌル値のS?ヌル値のT?に直接変換します。

タイプSは値タイプ(int)ですが、タイプTは値タイプ(Sampleクラス)ではないため、ここでは適用できないようです。ただし、Roslynリポジトリの この問題 は、実際には仕様のバグであると述べています。そして、Roslyn code ドキュメンテーションはこれを確認します:

上記のように、ここでは2つの方法で仕様とは異なります。最初に、通常のフォームが適用できない場合にのみ、解除されたフォームをチェックします。第二に、変換パラメーターと戻り値の型がbothnull不可の値型である場合にのみ、リフティングセマンティクスを適用することになっています。

実際、ネイティブコンパイラは、以下に基づいてリフトされたフォームをチェックするかどうかを決定します。

  • 最終的に変換可能な型は、null許容値型から変換されますか?
  • 変換のパラメータータイプは、null不可の値タイプですか?
  • 最終的に変換可能な型は、null許容値型、ポインター型、または参照型に変換されますか?

これらすべての質問に対する答えが「はい」の場合、null許容値に引き上げ、結果の演算子が適用可能かどうかを確認します。

コンパイラが仕様に従う場合-この場合、期待どおりにエラーが発生します(一部の古いバージョンではエラーが発生します)が、現在はエラーになりません。

まとめると、コンパイラーはリフト演算子の暗黙の演算子を使用していると思います。これは仕様によっては不可能ですが、ここではコンパイラーが仕様から逸脱しています。

  • これは、コンパイラではなく仕様のバグと見なされます。
  • 古い、roslyn前のコンパイラによって仕様に既に違反しており、後方互換性を維持するのは良いことです。

持ち上げられた演算子がどのように機能するかを説明する最初の引用で説明したように(Tを参照型にすることができます)-あなたのケースで何が起こるかを正確に説明していることに注意してください。 null valued Sint?)は、変換演算子なしでTSample)に直接割り当てられ、null以外はintにラップ解除されます。演算子を実行します(Tが参照型の場合、T?へのラップは明らかに不要です)。

19
Evk

最初のスニペットのコードがコンパイルされるのはなぜですか?

見つけることができる_Nullable<T>_のソースコードからのコードサンプル here

_[System.Runtime.Versioning.NonVersionable]
public static explicit operator T(Nullable<T> value) {
    return value.Value;
}

[System.Runtime.Versioning.NonVersionable]
public T GetValueOrDefault(T defaultValue) {
    return hasValue ? value : defaultValue;
}
_

Struct _Nullable<int>_には明示的なオーバーライド演算子とメソッドGetValueOrDefaultがあり、これら2つのうちの1つは_int?_をTに変換するためにコンパイラによって使用されます。

その後、implicit operator Sample<T>(T value)を実行します。

何が起こるかの大まかな画像は次のとおりです。

_Sample<int> sampleA = (Sample<int>)(int)a;
_

_Sample<T>_暗黙の演算子の内部でtypeof(T)を出力すると、_System.Int32_と表示されます。

2番目のシナリオでは、コンパイラーは_implicit operator Sample<T>_を使用せず、単にnullsampleBに割り当てます。

8
Fabjan