null可能な値のタイプをよく覚えていますか?私たちは「ボンネットの下」に見えます

image1.png


最近、null可能な参照タイプが話題になっています。ただし、古き良きnull可能値タイプはなくなっておらず、現在も積極的に使用されています。彼らと一緒に働くことのニュアンスをよく覚えていますか?この記事を読んで、知識を更新またはテストすることをお勧めします。サンプルのC#およびILコード、CLI仕様への参照、およびCoreCLRコードが含まれています。私は興味深い問題から始めることを提案します。



。ヌル可能参照タイプに興味がある場合は、私の同僚の記事のいくつかをチェックしてください:「C#8.0および静的分析のヌル可能参照タイプ」、「ヌル可能参照は保護しない、そしてここに証明があります。」



以下のサンプルコードを見て、コンソールに出力される内容に答えてください。そして、同じように重要なのは、その理由です。コンパイラのヒント、ドキュメント、文献の閲覧などを行わずに、そのまま回答することにすぐに同意しましょう。 :)



static void NullableTest()
{
  int? a = null;
  object aObj = a;

  int? b = new int?();
  object bObj = b;

  Console.WriteLine(Object.ReferenceEquals(aObj, bObj)); // True or False?
}


image2.png


さて、少し考えてみましょう。私には、起こり得ると思われるいくつかの主要な考え方を考えてみましょう。



1. int? -参照タイプ。



このように推論しましょう、intとは何ですか?参照タイプです。この場合、値が書き込まれるヌル、それはまた記録され、AOBJ割り当て後。あるオブジェクトへ参照bに書き込まます。また、割り当て後bObj書き込まれます。その結果、Object.ReferenceEqualsnullnull以外のオブジェクト参照を引数として受け取るため、...当然のことながら、答えはFalseです。







2. int? -重要なタイプ。



または多分あなたはそのintを疑う参照タイプですか?そして、int式にもかかわらずこれを確信していますか? a = null?さて、反対側から行って、intとは何かから始めましょう。 -重要なタイプ。



この場合、式int? a = nullは少し奇妙に見えますが、C#で再び砂糖が上に注がれたと仮定します。ある種のオブジェクトを格納していることがわかりましたbはある種のオブジェクトも格納します。変数aObjおよびbObjを初期化すると、aおよびbに格納されいるオブジェクトがパックされます。、その結果、aObjbObjに異なる参照が書き込まれます。Object.ReferenceEqualsは、さまざまなオブジェクトへの参照を引数として取ることが判明しました。したがって、...



すべてが明白であり、答えはFalseです。



3.ここではNullable <T>が使用されていると仮定します



上記のオプションが気に入らなかったとしましょう。あなたはintがないことを完全によく知っているので実際にはそうではありませんが、値のタイプはNullable <T>であり、この場合はNullable <int>が使用されます。また、実際にはabでそれを理解しています同一のオブジェクトがあります。同時に、aObjbObjに値を書き込むときにパッキングが発生し、その結果、異なるオブジェクトへの参照が取得されることを忘れないでくださいObject.ReferenceEqualsはさまざまなオブジェクトへの参照を受け入れるため、...



当然のことながら、答えはFalseです。



4。;)



値の種類から始めた人のために-参照の比較について突然疑問がある場合は、docs.microsoft.comObject.ReferenceEqualsのドキュメントを参照してください。..。特に、値のタイプとボックス化/ボックス化解除のトピックにも触れています。確かに、重要なタイプのインスタンスがメソッドに直接渡され、パッケージを個別に取り出した場合を説明していますが、本質は同じです。



値のタイプを比較する場合。objAとobjBが値タイプの場合、ReferenceEqualsメソッドに渡される前にボックス化されます。これは、objAとobjBの両方が値タイプの同じインスタンスを表す場合でも、次の例に示すように、ReferenceEqualsメソッドはfalseを返すことを意味します。



ここで記事を完成させることができるように思われますが、正解はTrueです。



さて、それを理解しましょう。



理解



シンプルと面白いの2つの方法があります。



簡単な方法



int?あるのNullable <int型>Nullable <T>ドキュメントを開きます。ここでは、「ボクシングとアンボクシング」セクションを確認します。原則として、それだけです-動作はそこで説明されています。しかし、詳細が必要な場合は、興味深い方法でご招待します。;)



面白い方法



このパスに関する十分なドキュメントはありません。彼女はその振る舞いについて説明しますが、「なぜ」という質問には答えません。



実際にはintとは何ですか?そしてヌル適切な文脈で?なぜこのように機能するのですか?ILコードは異なるコマンドを使用しますか?CLRレベルでの動作は異なりますか?他の魔法はありますか?int



エンティティを解析することから始めましょう基本を覚えて、徐々に元のケースの分析に取り掛かります。C#はかなり「甘美な」言語であるため、定期的にILコードを参照して、物事の本質を調べます(はい、C#のドキュメントは今日の私たちのやり方ではありません)。



int?、Nullable <T>



ここでは、原則としてnull可能な値タイプの基本(それらが何であるか、ILで何にコンパイルされるかなど)を見ていきます。課題からの質問への回答については、次のセクションで説明します。



コードの一部を見てみましょう。



int? aVal = null;
int? bVal = new int?();
Nullable<int> cVal = null;
Nullable<int> dVal = new Nullable<int>();


これらの変数の初期化はC#では異なって見えますが、すべての変数に対して同じILコードが生成されます。



.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
              valuetype [System.Runtime]System.Nullable`1<int32> V_1,
              valuetype [System.Runtime]System.Nullable`1<int32> V_2,
              valuetype [System.Runtime]System.Nullable`1<int32> V_3)

// aVal
ldloca.s V_0
initobj  valuetype [System.Runtime]System.Nullable`1<int32>

// bVal
ldloca.s V_1
initobj  valuetype [System.Runtime]System.Nullable`1<int32>

// cVal
ldloca.s V_2
initobj  valuetype [System.Runtime]System.Nullable`1<int32>

// dVal
ldloca.s V_3
initobj  valuetype [System.Runtime]System.Nullable`1<int32>


ご覧のとおり、C#では、すべてに心臓からの構文上の砂糖が振りかけられているため、実際、あなたと私はより良く生きています。



  • int?-重要なタイプ。
  • int?-Nullable <int>と同じです。ILコードはNullable <int32>で機能しています。
  • int?aVal = nullは、Nullable <int> と同じです。aVal= new Nullable <int>()ILでは、これはinitobjステートメントに展開され、ロードされたアドレスでデフォルトの初期化を実行します。


次のコードについて考えてみます。



int? aVal = 62;


デフォルトの初期化を理解しました-上記の対応するILコードを見ました。aValを62に初期化する場合、ここで何が起こりますか?



ILコードを見てみましょう。



.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0)
ldloca.s   V_1
ldc.i4.s   62
call       instance void valuetype 
           [System.Runtime]System.Nullable`1<int32>::.ctor(!0)


繰り返しますが、複雑なことは何もありません。アドレスaValが評価スタックと値62にロードされ、その後、Nullable <T>(T)署名を持つコンストラクターが呼び出されます。つまり、次の2つの式は完全に同一になります。



int? aVal = 62;
Nullable<int> bVal = new Nullable<int>(62);


ILコードをもう一度見ると、同じことがわかります。



// int? aVal;
// Nullable<int> bVal;
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
              valuetype [System.Runtime]System.Nullable`1<int32> V_1)

// aVal = 62
ldloca.s   V_0
ldc.i4.s   62
call       instance void valuetype                           
           [System.Runtime]System.Nullable`1<int32>::.ctor(!0)

// bVal = new Nullable<int>(62)
ldloca.s   V_1
ldc.i4.s   62
call       instance void valuetype                             
           [System.Runtime]System.Nullable`1<int32>::.ctor(!0)


検査はどうですか?たとえば、次のコードは実際にはどのように見えますか?



bool IsDefault(int? value) => value == null;


そうです、理解のために、対応するILコードにもう一度目を向けましょう。



.method private hidebysig instance bool
IsDefault(valuetype [System.Runtime]System.Nullable`1<int32> 'value')
cil managed
{
  .maxstack  8
  ldarga.s   'value'
  call       instance bool valuetype 
             [System.Runtime]System.Nullable`1<int32>::get_HasValue()
  ldc.i4.0
  ceq
  ret
}


想像のとおり、実際にnullありません。発生するのはNullable <T> .HasValueプロパティの呼び出しだけです。つまり、C#の同じロジックは、次のように使用されるエンティティに関してより明示的に記述できます。



bool IsDefaultVerbose(Nullable<int> value) => !value.HasValue;


ILコード:



.method private hidebysig instance bool 
IsDefaultVerbose(valuetype [System.Runtime]System.Nullable`1<int32> 'value')
cil managed
{
  .maxstack  8
  ldarga.s   'value'
  call       instance bool valuetype 
             [System.Runtime]System.Nullable`1<int32>::get_HasValue()
  ldc.i4.0
  ceq
  ret
}




要約しましょう:



  • Nullable値タイプは、Nullable <T>タイプを犠牲にして実装されます。
  • int?-実際には、汎用値タイプNullable <T>の構築されたタイプ
  • int?a = null-デフォルト値を使用したNullable <int>タイプのオブジェクトの初期化。実際はここにnullありません
  • if(a == null) -繰り返しますが、nullはなくNullable <T> .HasValueプロパティへの呼び出しがあります


Nullable <T>タイプ のソースコードは、たとえば、dotnet /ランタイムリポジトリのGitHub(ソースコードファイルへの直接リンク)で表示できますそこにはコードがあまりないので、興味を引くために一読することをお勧めします。そこから、次の事実を学ぶ(または覚える)ことができます。



便宜上、Nullable <T>タイプは以下を定義します。



  • TからNullable <T>への暗黙の変換演算子;
  • Nullable <T>からTへの明示的な変換演算子


作業の主なロジックは、2つのフィールド(および対応するプロパティ)を介して実装されます。



  • T値-Nullable <T>でラップされた値自体
  • bool hasValueは、ラッパーに値が含まれているかどうかを示すフラグです。引用符で囲まれている場合、実際にはNullable <T>には常にタイプTの値が含まれます


null可能な値のタイプについて復習したので、パッケージの状態を見てみましょう。



ヌル可能な<T>パッキング



値タイプのオブジェクトをパックすると、ヒープ上に新しいオブジェクトが作成されることを思い出してください。次のコードスニペットは、この動作を示しています。



int aVal = 62;
object obj1 = aVal;
object obj2 = aVal;

Console.WriteLine(Object.ReferenceEquals(obj1, obj2));


2つのボクシング操作が発生し、2つのオブジェクトが作成され、その参照がobj1obj2に書き込まれたため、参照を 比較した結果はfalseであると予想されます ここで、intNullable <int>に変更します。







Nullable<int> aVal = 62;
object obj1 = aVal;
object obj2 = aVal;

Console.WriteLine(Object.ReferenceEquals(obj1, obj2));


結果はまだ期待されています-false



そして今、62の代わりにデフォルト値を書きます。



Nullable<int> aVal = new Nullable<int>();
object obj1 = aVal;
object obj2 = aVal;

Console.WriteLine(Object.ReferenceEquals(obj1, obj2));


Iii ...結果は突然真になります2つのオブジェクトを作成し、2つの異なるオブジェクトにリンクする、すべて同じ2つのパッキング操作があるように見えますが、結果は正しいです。



ええ、それはおそらく再び砂糖であり、ILコードレベルで何かが変更されました!どれどれ。



例N1。



C#コード:



int aVal = 62;
object aObj = aVal;


ILコード:



.locals init (int32 V_0,
              object V_1)

// aVal = 62
ldc.i4.s   62
stloc.0

//  aVal
ldloc.0
box        [System.Runtime]System.Int32

//     aObj
stloc.1


例N2。



C#コード:



Nullable<int> aVal = 62;
object aObj = aVal;


ILコード:



.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
              object V_1)

// aVal = new Nullablt<int>(62)
ldloca.s   V_0
ldc.i4.s   62
call       instance void
           valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0)

//  aVal
ldloc.0
box        valuetype [System.Runtime]System.Nullable`1<int32>

//     aObj
stloc.1


例N3。



C#コード:



Nullable<int> aVal = new Nullable<int>();
object aObj = aVal;


ILコード:



.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
              object V_1)

// aVal = new Nullable<int>()
ldloca.s   V_0
initobj    valuetype [System.Runtime]System.Nullable`1<int32>

//  aVal
ldloc.0
box        valuetype [System.Runtime]System.Nullable`1<int32>

//     aObj
stloc.1


ご覧のとおり、パッキングはどこでも同じ方法で行われます-ローカル変数の値が評価スタックにロードされ(ldloc命令)、その後、boxコマンドを呼び出すことによってパッキング自体が実行され、どのタイプをパッキングするかが示されます。



私たちは、に変わり共通言語基盤の仕様の記述を見ボックスのコマンドおよびNULL可能なタイプについての興味深いノートを見つける:



typeTokが値型の場合、箱の命令変換は、その箱入りフォームにVAL ...。null可能なタイプの場合、これはvalのHasValueプロパティを検査することによって行われます。falseの場合、null参照がスタックにプッシュされます。それ以外の場合、ボクシングvalのValueプロパティの結果はスタックにプッシュされます。



ここから、「i」に点在するいくつかの結論があります。



  • Nullable <T>オブジェクトの状態が考慮されます(前に検討したHasValueフラグがチェックされます)。場合のNullable <T>は(値が含まれていないhasValueはである)、ボックスがになりますヌル
  • 場合NULL可能<T>は値を含む(hasValueは-真の)、次いで包装目的ではないのNullable <T> のインスタンスTに格納され、値のタイプのNullable <T>
  • パッキングを処理するための特定のロジックNullable <T>は、C#レベルでは実装されておらず、ILレベルでも実装されていません。CLRに実装されています。


上で説明 したNullable <T>の戻りましょう



最初:



Nullable<int> aVal = 62;
object obj1 = aVal;
object obj2 = aVal;

Console.WriteLine(Object.ReferenceEquals(obj1, obj2));


梱包前のアイテムの状態:



  • T- > int ;
  • -> 62 ;
  • hasValue- > true


値62は2回パックされ(この場合、intタイプのインスタンスがパックされNullable <int>ではないことに注意してください)、2つの新しいオブジェクトが作成され、異なるオブジェクトへの2つの参照が取得され、その結果はfalseです。



2番目:



Nullable<int> aVal = new Nullable<int>();
object obj1 = aVal;
object obj2 = aVal;

Console.WriteLine(Object.ReferenceEquals(obj1, obj2));


梱包前のアイテムの状態:



  • T- > int ;
  • value- > default(この場合、0intのデフォルト値です);
  • hasValue- > false


以来hasValueはである偽の、何のオブジェクトはヒープ上に作成されていない、とボクシングの動作を返しますヌルに書き込まれ、OBJ1およびOBJ2予想どおり、これらの値を比較すると、trueが得られます



記事の冒頭にあった元の例では、まったく同じことが起こります。



static void NullableTest()
{
  int? a = null;       // default value of Nullable<int>
  object aObj = a;     // null

  int? b = new int?(); // default value of Nullable<int>
  object bObj = b;     // null

  Console.WriteLine(Object.ReferenceEquals(aObj, bObj)); // null == null
}


楽しみのために、前述のdotnet /ランタイムリポジトリからのCoreCLRソースコードを見てみましょうobject.cppファイル、具体的には、必要なロジックを含むNullable :: Boxメソッド関心があります



OBJECTREF Nullable::Box(void* srcPtr, MethodTable* nullableMT)
{
  CONTRACTL
  {
    THROWS;
    GC_TRIGGERS;
    MODE_COOPERATIVE;
  }
  CONTRACTL_END;

  FAULT_NOT_FATAL();      // FIX_NOW: why do we need this?

  Nullable* src = (Nullable*) srcPtr;

  _ASSERTE(IsNullableType(nullableMT));
  // We better have a concrete instantiation, 
  // or our field offset asserts are not useful
  _ASSERTE(!nullableMT->ContainsGenericVariables());

  if (!*src->HasValueAddr(nullableMT))
    return NULL;

  OBJECTREF obj = 0;
  GCPROTECT_BEGININTERIOR (src);
  MethodTable* argMT = nullableMT->GetInstantiation()[0].AsMethodTable();
  obj = argMT->Allocate();
  CopyValueClass(obj->UnBox(), src->ValueAddr(nullableMT), argMT);
  GCPROTECT_END ();

  return obj;
}


これが私たちが上で話したすべてです。値を保存しない場合、NULLを返します



if (!*src->HasValueAddr(nullableMT))
    return NULL;


それ以外の場合は、パッケージを作成します。



OBJECTREF obj = 0;
GCPROTECT_BEGININTERIOR (src);
MethodTable* argMT = nullableMT->GetInstantiation()[0].AsMethodTable();
obj = argMT->Allocate();
CopyValueClass(obj->UnBox(), src->ValueAddr(nullableMT), argMT);


結論



興味を引くために、記事の最初から同僚や友人に例を示すことを提案します。彼らは正しい答えを出し、それを実証することができるでしょうか?そうでない場合は、記事を読むように招待してください。彼らができるなら-まあ、私の尊敬!



小さいけれど楽しい冒険だったと思います。:)



PS誰かが質問をするかもしれません:このトピックへの没頭はどのように始まったのですか?Object.ReferenceEqualsが引数で機能し、そのうちの1つが重要な型で表されるという事実について、PVS-Studioで新しい診断ルールを作成しました突然、Nullable <T>を使用すると、パッキング動作に予期しない瞬間があることが判明しました私たちは、ILコードを見て-ボックスなどのボックス..。CLI仕様をご覧ください-ええ、それだけです!これはかなり興味深いケースのようで、一度だけ言う価値があります。-そして記事はあなたの目の前にあります。





この記事を英語を話す聴衆と共有したい場合は、翻訳リンクSergeyVasilievを使用してください。null可能な値のタイプをどのように覚えているかを確認してください。ボンネットの下を覗いてみましょう



PPSちなみに、最近私はTwitterでもう少し活動的になり、興味深いコードスニペットを投稿したり、.NETの世界からの興味深いニュースをリツイートしたりしています。興味があれば、確認することをお勧めします-サブスクライブ(プロファイルへのリンク)。



All Articles