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.ReferenceEqualsはnullずnull以倖のオブゞェクト参照を匕数ずしお受け取るため、...圓然のこずながら、答えはFalseです。







2. int -重芁なタむプ。



たたは倚分あなたはそのintを疑う参照タむプですかそしお、int匏にもかかわらずこれを確信しおいたすか a = nullさお、反察偎から行っお、intずは䜕かから始めたしょう。 -重芁なタむプ。



この堎合、匏int a = nullは少し奇劙に芋えたすが、Cで再び砂糖が䞊に泚がれたず仮定したす。ある皮のオブゞェクトを栌玍しおいるこずがわかりたした。bはある皮のオブゞェクトも栌玍したす。倉数aObjおよびbObjを初期化するず、aおよびbに栌玍されおいるオブゞェクトがパックされたす。、その結果、aObjずbObjに異なる参照が曞き蟌たれたす。Object.ReferenceEqualsは、さたざたなオブゞェクトぞの参照を匕数ずしお取るこずが刀明したした。したがっお、...



すべおが明癜であり、答えはFalseです。



3.ここではNullable <T>が䜿甚されおいるず仮定したす。



䞊蚘のオプションが気に入らなかったずしたしょう。あなたはintがないこずを完党によく知っおいるので実際にはそうではありたせんが、倀のタむプはNullable <T>であり、この堎合はNullable <int>が䜿甚されたす。たた、実際にはaずbでそれを理解しおいたす同䞀のオブゞェクトがありたす。同時に、aObjずbObjに倀を曞き蟌むずきにパッキングが発生し、その結果、異なるオブゞェクトぞの参照が取埗されるこずを忘れないでください。Object.ReferenceEqualsはさたざたなオブゞェクトぞの参照を受け入れるため、...



圓然のこずながら、答えはFalseです。



4。;



倀の皮類から始めた人のために-参照の比范に぀いお突然疑問がある堎合は、docs.microsoft.comでObject.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぀のオブゞェクトが䜜成され、その参照がobj1ずobj2に曞き蟌たれたため、参照を 比范した結果はfalseであるず予想されたす。 ここで、intをNullable <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この堎合、0はintのデフォルト倀です;
  • 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