web-dev-qa-db-ja.com

ValueTypeはどのようにObject(ReferenceType)から派生し、それでもValueTypeのままですか?

C#では、構造体がクラスから派生することはできませんが、すべてのValueTypeはObjectから派生します。この区別はどこで行われますか?

CLRはこれをどのように処理しますか?

78
Joan Venge

C#では、構造体がクラスから派生することはできません

あなたの声明は正しくないので、あなたの混乱。 C#does構造体がクラスから派生することを許可します。すべての構造体は、System.Objectから派生する同じクラスSystem.ValueTypeから派生します。そして、すべての列挙型はSystem.Enumから派生します。

更新:いくつかの(現在は削除されている)コメントに混乱がありました。追加の質問をいくつかします。

構造体は基本型から派生していますか?

明らかにそうです。これは、仕様の最初のページを読むことで確認できます。

Intやdoubleなどのプリミティブ型を含むすべてのC#型は、単一のルートオブジェクト型から継承されます。

ここで、仕様はここでのケースを過大評価していることに注意してください。ポインター型はオブジェクトから派生しないため、インターフェイス型と型パラメーター型の派生関係は、このスケッチが示すよりも複雑です。ただし、すべての構造体型が基本型から派生しているのは明らかです。

構造体型が基本型から派生していることを知っている他の方法はありますか?

承知しました。構造体型はToStringをオーバーライドできます。基本型の仮想メソッドでない場合、それは何をオーバーライドしていますか?したがって、基本型が必要です。その基本型はクラスです。

選択したクラスからユーザー定義の構造体を派生できますか?

はっきりと違います。 これは、構造体がクラスから派生しないことを意味しません。構造体はクラスから派生するため、そのクラスの継承可能なメンバーを継承します。実際、構造体は特定のクラスから派生するために必須です。列挙型はEnumから派生する必要があり、構造体はValueTypeから派生する必要があります。これらは必須であるため、C#言語forbidsは、派生関係をコードで記述しないでください。

なぜそれを禁止するのですか?

関係が必須の場合、言語デザイナーには次のオプションがあります。(1)ユーザーに必要な呪文の入力を要求する、(2)オプションにする、または(3)禁止する。それぞれに長所と短所があり、C#言語の設計者はそれぞれの詳細に応じて異なる方法を選択しています。

たとえば、constフィールドは静的である必要がありますが、そうすることは、そうすることが最初に無意味な言い回しであり、2番目に、非静的constフィールドがあることを意味するためです。ただし、開発者に選択の余地はありませんが、オーバーロードされた演算子は静的としてマークする必要があります。それ以外の場合、オペレーターのオーバーロードはインスタンスメソッドであると開発者が信じるのは簡単すぎます。これは、「静的」が「仮想」も可能性があることを意味することをユーザーが信じるようになるかもしれないという懸念を無効にします。

この場合、ユーザーに構造体がValueTypeから派生していると言うことを要求することは単なる過剰な言い回しのように見え、構造体couldが別のタイプから派生していることを意味します。これらの問題を両方とも解消するために、C#は、構造体が基本型から派生していることをコード内でillegalと記述していますが、これは明白です。

同様に、すべてのデリゲート型はMulticastDelegateから派生しますが、C#ではnotとする必要があります。

つまり、これでC#のすべての構造体はクラスから派生であることが確立されました。

継承クラスからの派生の関係は何ですか?

C#の継承関係に多くの人が混乱しています。継承関係は非常に単純です。構造体、クラス、またはデリゲート型Dがクラス型Bから派生している場合、Bの継承可能なメンバーはDのメンバーでもあります。これは単純です。

構造体がValueTypeから派生すると言うとき、それは継承に関してどういう意味ですか?単純に、ValueTypeのすべての継承可能なメンバーは、構造体のメンバーでもあります。これは、たとえば、構造体がToStringの実装を取得する方法です。構造体の基本クラスから継承されます。

すべての継承可能なメンバー?きっとそうではない。プライベートメンバーは継承可能ですか?

はい。基本クラスのすべてのプライベートメンバーは、派生型のメンバーでもあります。呼び出しサイトがメンバーのアクセシビリティドメインにない場合、これらのメンバーを名前で呼び出すことはもちろん違法です。メンバーがいるからといって、それを利用できるわけではありません。

ここで、元の回答を続けます。


CLRはこれをどのように処理しますか?

とても良い。 :-)

値タイプを値タイプにするのは、そのインスタンスが値によってコピーされるであることです。参照型を参照型にするのは、そのインスタンスが参照によってコピーであることです。値型と参照型の間のinheritanceの関係はどういうわけか特別で異例であると信じているようですが、私はその考えが何であるか理解していません。 継承は、物事がどのようにコピーされるかとは関係ありません。

このように見てください。私があなたに次の事実を言ったとしましょう:

  • ボックスには、赤いボックスと青いボックスの2種類があります。

  • 赤いボックスはすべて空です。

  • O、V、Eと呼ばれる3つの特別な青いボックスがあります。

  • Oはどのボックス内にもありません。

  • VはOの内側にあります。

  • EはVの中にいます。

  • Vの中に他の青い箱はありません。

  • Eの中に青いボックスはありません。

  • すべての赤いボックスはVまたはEのいずれかにあります。

  • O以外のすべての青いボックスは、それ自体が青いボックス内にあります。

青いボックスは参照タイプ、赤いボックスは値タイプ、OはSystem.Object、VはSystem.ValueType、EはSystem.Enumであり、「内部」関係は「派生」です。

これは完全に一貫性のある単純なルールのセットであり、段ボールが多く、忍耐力がある場合は、簡単に自分で実装できます。箱が赤か青かは、箱の中身とは関係ありません。現実の世界では、赤い箱を青い箱の中に入れることは完全に可能です。 CLRでは、System.ValueTypeまたはSystem.Enumである限り、参照型から継承する値型を作成することは完全に合法です。

だからあなたの質問を言い換えましょう:

ValueTypeはどのようにObject(ReferenceType)から派生し、それでもValueTypeのままですか?

なので

すべての赤いボックス(値の型)がボックスO(System.Object)の内側(派生)であり、青いボックス(参照の型)でありながら赤いボックス(値の型)である可能性はありますか。

そんなふうに言ったら自明だと思います。青色のボックスOの内側にあるボックスVの中に赤いボックスを配置することを妨げるものは何もありません。なぜあるのでしょうか?


追加の更新:

Joanの最初の質問は、値型が参照型から派生するということについてpossibleについてでした。私の元の答えは、CLRがまったく異なる表現を持つ2つのものの間の派生関係を持っているという事実を説明するためにCLRが使用するメカニズムを実際には説明していませんでした-つまり、参照先データにオブジェクトヘッダーがあるかどうか、 syncブロック、ガベージコレクションの目的で独自のストレージを所有しているかどうかなど。これらのメカニズムは複雑で、1つの答えで説明するには複雑すぎます。 CLR型システムの規則は、C#で見られるやや単純化されたフレーバーよりもかなり複雑です。たとえば、型のボックス化バージョンと非ボックス化バージョンが明確に区別されていません。ジェネリックの導入により、CLRにかなりの複雑さが追加されました。詳細については、CLI仕様を参照してください。ボクシングと制約付き仮想呼び出しのルールに特に注意してください。

99
Eric Lippert

これは、すべての型をSystem.Objectとして扱うことができるようにするために、CLRによって維持されるやや人工的な構造です。

値の型は、System.Objectから System.ValueType を介して派生します。これは、特別な処理が行われる場所です(つまり、CLRは、ValueTypeから派生するすべての型のボックス化/ボックス化解除などを処理します)。

19
Reed Copsey

小さな修正、C#は構造体がクラスだけでなく、何かからカスタム派生することを許可しません。構造体でできることは、派生とは非常に異なるインターフェースを実装することだけです。

これに答える最良の方法は、ValueTypeが特別であるということです。これは基本的に、CLR型システムのすべての値型の基本クラスです。 「CLRはこれをどのように処理するのか」というのは、CLRの規則にすぎないため、どのように答えるかはわかりません。

19
JaredPar

あなたの声明は正しくないので、あなたの混乱。 C#では、構造体がクラスから派生することを許可しています。すべての構造体は同じクラスSystem.ValueTypeから派生しています

これを試してみましょう:

 struct MyStruct :  System.ValueType
 {
 }

これもコンパイルされません。コンパイラーは、「インターフェイスリストのタイプ 'System.ValueType'はインターフェイスではありません」と通知します。

構造体であるInt32を逆コンパイルすると、次のようになります。

public struct Int32:IComparable、IFormattable、IConvertible {}、 System.ValueTypeから派生していることは言及していません。しかし、オブジェクトブラウザーでは、Int32がSystem.ValueTypeを継承していることがわかります。

だから、これらすべてが私を信じさせてくれます:

これに答える最善の方法は、ValueTypeが特別であるということです。これは基本的に、CLR型システムのすべての値型の基本クラスです。 「CLRがこれをどのように処理するか」は、CLRのルールにすぎないため、どのように回答するかを知るのは困難です。

5
yi.han

ボックス化された値型は、実質的には参照型です(1のように歩き、1のようにいじくるので、事実上1です)。 ValueTypeは実際には値型の基本型ではなく、Object型にキャストしたときに値型を変換できる基本参照型であることをお勧めします。ボックス化されていない値タイプ自体は、オブジェクト階層の外にあります。

2
supercat

根拠

すべての回答の中で、@ supercatの回答は実際の回答に最も近くなります。他の回答は実際には質問に回答せず、実際には誤った主張をするため(たとえば、値の型は何かから継承されるため)、質問に答えてください。

プロローグ

この回答は、私自身のリバースエンジニアリングとCLI仕様に基づいています。

structおよびclassはC#キーワードです。 CLIに関する限り、すべてのタイプ(クラス、インターフェース、構造体など)はクラス定義によって定義されます。

たとえば、オブジェクトタイプ(C#ではclassとして知られている)は次のように定義されます。

_.class MyClass
{
}
_

インターフェイスは、interfaceセマンティック属性を持つクラス定義によって定義されます。

_.class interface MyInterface
{
}
_

値の型はどうですか?

構造体が_System.ValueType_から継承しても値型である理由は、..継承できないためです。

値タイプは単純なデータ構造です。値タイプはnotから継承anythingであり、それらはcannotインターフェースを実装します。値タイプは、どのタイプのサブタイプでもないため、タイプ情報を持ちません。値型のメモリアドレスが指定されている場合、非表示フィールドに型情報を持つ参照型とは異なり、値型が何を表すかを識別することはできません。

次のC#構造体を想像してみてください。

_namespace MyNamespace
{
    struct MyValueType : ICloneable
    {
        public int A;
        public int B;
        public int C;

        public object Clone()
        {
            // body omitted
        }
    }
}
_

次に、その構造体のクラス定義を示します。

_.class MyNamespace.MyValueType extends [mscorlib]System.ValueType implements [mscorlib]System.ICloneable
{
    .field public int32 A;
    .field public int32 B;
    .field public int32 C;

    .method public final hidebysig newslot virtual instance object Clone() cil managed
    {
        // body omitted
    }
}
_

ここで何が起こっているのでしょうか?それは明らかにオブジェクト/参照タイプである_System.ValueType_を拡張しますandは_System.ICloneable_を実装します。

説明は、クラス定義が_System.ValueType_を拡張する場合、実際には2つの事柄を定義するということです:値の型と、値の型に対応するボックス化された型。クラス定義のメンバーは、値型と対応するボックス型の両方の表現を定義します。拡張して実装するのは値の型ではなく、対応するボックス型です。 extendsおよびimplementsキーワードは、ボックス型にのみ適用されます。

明確にするために、上記のクラス定義は2つのことを行います。

  1. 3つのフィールド(および1つのメソッド)を持つ値タイプを定義します。何も継承せず、インターフェースも実装していません(値タイプはどちらも実行できません)。
  2. _System.ValueType_から継承し、_System.ICloneable_インターフェースを実装する3つのフィールド(および1つのインターフェースメソッドを実装)でオブジェクトタイプ(ボックス型)を定​​義します。

また、sealedキーワードが指定されているかどうかにかかわらず、_System.ValueType_を拡張するすべてのクラス定義も本質的にシールされています。

値型は単純な構造であり、継承も実装もポリモーフィズムもサポートしていないため、残りの型システムで使用することはできません。これを回避するために、CLRは値タイプに加えて、ボックス化タイプと呼ばれる同じフィールドを持つ対応する参照タイプも定義します。したがって、値の型をobjectを受け取るメソッドに渡すことはできませんが、対応するボックス化された型canです。

今、あなたがC#でメソッドを定義するなら

public static void BlaBla(MyNamespace.MyValueType x)

メソッドが_MyNamespace.MyValueType_型の値を取ることを知っています。

上記で、C#のstructキーワードから生じるクラス定義は、実際には値の型とオブジェクトの型の両方を定義することを学びました。 C#では、定義された値の型のみを参照できます。 ただし、これはC#の制限にすぎません。 ILではcanは実際には両方を指します。

ILでタイプを参照する場合、いくつかの制約がサポートされます。その中には、classvaluetypeがあります。 _valuetype MyNamespace.MyValueType_を使用する場合、型をクラス定義の値型に制限します。同様に、_class MyNamespace.MyValueType_を使用して、クラス定義のボックス化された型に型を制約できます。

つまり、.method static void Print(valuetype MyNamespace.MyValueType test) cil managedは、_MyNamespace.MyValueType_クラス定義で定義された値の型を取ります。

一方、.method static void Print(class MyNamespace.MyValueType test) cil managedは、_MyNamespace.MyValueType_クラス定義で定義されたボックス型を受け取ります。

したがって、基本的に、対応するボックス化された型を、C#クラスのように定義されているかのように、ILプログラム全体でインスタンス化して使用できます。これはvaluetypeを初期化します

_newobj void valuetype MyNamespace.MyValueType::.ctor()
_

ランタイムは、次のようにnewobjを通じてボックス型をインスタンス化することを拒否します

_newobj void class MyNamespace.MyValueType::.ctor()
_

ただし、box命令は、値タイプのボックス化されたタイプをインスタンス化し、値タイプから値タイプに値をコピーします。これにより、_System.ValueType_、object、または_class MyNamespace.MyValueType_変数に格納できるボックス化された型のインスタンスが取得され、_System.ValueType_を受け取るすべてのメソッドに渡されます。 、objectまたは_class MyNamespace.MyValueType_を引数として使用すると、すべての目的と目的で、他の参照型と同様に機能します。これは値の型ではなく、値の型に対応するボックス型です。

以下は、これを実証するためにILで作成した小さなプログラムです。私はそれをずっとコメントしました。 valuetypeおよびclassキーワードの使用には特に注意してください。

_// Class definition for a class.
.class MyNamespace.Program
{
    // Entry point method definition.
    .method static void Main() cil managed
    {
        .entrypoint // This is the entry point of the application.
        .maxstack 8
        .locals init
        (
            [0] valuetype MyNamespace.MyValueType, // Local variable able to hold the value type of the MyNamespace.MyValueType class definition.
            [1] class MyNamespace.MyValueType // Local variable able to hold the boxed type of the MyNamespace.MyValueType class definition.
        )

        ldloca.s 0 // Load the address of local variable at index 0 onto the evaluation stack.
        initobj MyNamespace.MyValueType // Set all fields of our value type to 0 (required by the CLI for the Assembly to be verifiable).

        // The following sets fields A, B and C to 1, 2 and 3 in the value type, respectively.
        ldloca.s 0
        ldc.i4 1
        stfld int32 MyNamespace.MyValueType::A
        ldloca.s 0
        ldc.i4 2
        stfld int32 MyNamespace.MyValueType::B
        ldloca.s 0
        ldc.i4 3
        stfld int32 MyNamespace.MyValueType::C

        ldloc.0 // Load a copy of our value type onto the evaluation stack as an argument for the call below.
        call void MyNamespace.Program::Print(valuetype MyNamespace.MyValueType) // Call the overload of Print() that takes a value type.

        ldloc.0 // Load a copy of our value type onto the evaluation stack.
        box MyNamespace.MyValueType // Create the corresponding boxed type of our value type.
        stloc.1 // Store it in the local variable that takes the boxed version of our value type.
        ldloc.1 // Load it back onto the evaluation stack.
        call void MyNamespace.Program::Print(class MyNamespace.MyValueType) // Call the overload of Print() that takes a reference type.
        ret // Return.
    }

    // This method takes the value type of our MyNamespace.MyValueType class definition.
    // This is equivalent to "static void Print(MyNamespace.MyValueType test)" in C#.
    .method static void Print(valuetype MyNamespace.MyValueType test) cil managed
    {
        .maxstack 8
        // Equivalent to "Console.WriteLine(test.ToString()); test.PrintA();"
        ldarga.s test
        constrained. MyNamespace.MyValueType // callvirt prefix that will box 'test' for the ToString() call below. (Remember, 'test' is a value type, which has no ToString() method, but its corresponding boxed type does)
        callvirt instance string [mscorlib]System.Object::ToString()
        call void [mscorlib]System.Console::WriteLine(string)
        ldarga.s test
        call instance string MyNamespace.MyValueType::PrintA()
        ret
    }

    // This method takes the boxed type of our MyNamespace.MyValueType class definition.
    // This is not possible in C#.
    // The closest you can get to this, is to accept the parent class System.ValueType or System.Object,
    // which of course will allow callers to pass ANY boxed value type or object, and won't allow you to call PrintB() directly.
    // This method will only allow the boxed type of this specific value type.
    .method static void Print(class MyNamespace.MyValueType test) cil managed noinlining
    {
        .maxstack 8
        .locals init
        (
            [0] valuetype MyNamespace.MyValueType // Local variable for unboxing operation below.
        )

        ldarg.0
        callvirt instance string [mscorlib]System.Object::ToString() // No 'constrained' prefix to box necessary, since 'test' is already a reference type.
        call void [mscorlib]System.Console::WriteLine(string)

        // Now, methods in value types operate on the value type, not the boxed type,
        // so even though we can call PrintB() directly because 'test' is a class MyNamespace.MyValueType and not a class System.Object,
        // we have to unbox it and call PrintB() on the unboxed type.
        // (without unboxing, the call will still succeed, but since PrintB() expects the value type and not the boxed type, it will read the wrong field offset
        // (because offset 0 of an object is the object header pointer, and offset 0 of a value type is its first field))

        // I'll call PrintB() twice to show two different ways.
        // This first one unboxes, then passes the resulting value type (which is a copy) to PrintB().
        ldarg.0 // Push our MyNamespace.MyValueType boxed type instance onto the evaluation stack.
        unbox.any MyNamespace.MyValueType // Unboxes and pushes a copy of the value type onto the evaluation stack.
        stloc.0 // Store in local variable.
        ldloca.s 0 // Pass the address of the local variable to PrintB() (for the 'this' pointer).
        call instance void MyNamespace.MyValueType::PrintB()

        // Now, the above is a full unboxing, so PrintB() receives a copy of 'test'. So if PrintB() were to make changes to the boxed type, it would be on the copy,
        // not the instance that was passed to Print().
        // This can be fixed by using the 'unbox' instruction instead of the 'unbox.any' instruction.
        // 'unbox.any' will calculate the address of the value type part of the boxed type and then copy the data and Push it onto the evaluation stack.
        // 'unbox' will just calculate the address of the value type part of the boxed type and Push the address onto the evaluation stack.
        // Using just unbox, PrintB will get a pointer to the value type part of the boxed type (object address + size of object header pointer), and thus perform its operations on the actual boxed type instance.
        ldarg.0
        unbox MyNamespace.MyValueType
        call instance void MyNamespace.MyValueType::PrintB()

        ret
    }
}

// Class definition for a value type, defining both the value type and the corresponding boxed type.
.class MyNamespace.MyValueType extends [mscorlib]System.ValueType
{
    .field public int32 A;
    .field public int32 B;
    .field public int32 C;

    .method void PrintA() cil managed
    {
        // Equivalent to "Console.WriteLine(this.B.ToString());"
        // Identical to PrintB, so I've put the comments in that.
        ldarg.0
        ldflda int32 MyNamespace.MyValueType::A
        ldstr "x8"
        call instance string [mscorlib]System.Int32::ToString(string)
        call void [mscorlib]System.Console::WriteLine(string)
        ret
    }

    .method void PrintB() cil managed
    {
        // Equivalent to "Console.WriteLine(this.B.ToString());"
        ldarg.0 // Load the value in the 'this' pointer onto the evaluation stack (Load the address of the struct instance).
        ldflda int32 MyNamespace.MyValueType::B  // Load the address of the field 'B' onto the evaluation stack ('this' pointer for ToString() call below).
        ldstr "x8" // Load constant string "x8" onto the evaluation stack (Format specifier).
        call instance string [mscorlib]System.Int32::ToString(string) // Du'h.
        call void [mscorlib]System.Console::WriteLine(string) // Print it.
        ret
    }
}
_

お気づきかもしれませんが、2つのPrint()メソッドはオーバーロードであり、同じシグネチャを持たないため、オーバーロードできます。 1つは_MyNamespace.MyValueType_クラス定義の値型を受け取り、もう1つは_MyNamespace.MyValueType_クラス定義のボックス型を受け取ります。

上記のコードをアセンブルして、たとえば、 ILSpy(ILとして出力する場合でも、ILSpy(他の逆コンパイラーについてコメントできない)は型に基づいて制約を想定するため、1つのメソッドが参照型を取り、もう1つのメソッドが値を受け取るとしても、混乱する結果になります)タイプ、両方ともvaluetypeキーワードで表示されます。 C#として逆コンパイルすると、両方のオーバーロードシグネチャは同一に見えます。

上記のアセンブルされたILコードのILSpyからの出力は次のとおりです(コメントは私のものです)。

_internal class Program
{
    private static void DoIt()
    {
        MyNamespace.MyValueType myValueType = default(MyNamespace.MyValueType);
        myValueType.A = 1;
        myValueType.B = 2;
        myValueType.C = 3;
        MyNamespace.Program.Print(myValueType); // The overload taking a value type.
        MyNamespace.MyValueType test = (MyNamespace.MyValueType)(object)myValueType; // ILSpy tries to understand the boxing operation as best as it can, but ends up boxing and unboxing, despite the IL only boxing (because C# boxes/unboxes by casting and cannot differentiate between the value type and boxed type).
        MyNamespace.Program.Print(test); // The overload taking the boxed type.
    }

    // The overload taking a value type.
    private static void Print(MyNamespace.MyValueType test)
    {
        Console.WriteLine(test.ToString());
        test.PrintA();
    }

    // The overload taking the boxed type.
    private static void Print(MyNamespace.MyValueType test)
    {
        Console.WriteLine(test.ToString());
        ((MyNamespace.MyValueType)(object)test).PrintB();
        ((MyNamespace.MyValueType)test).PrintB();
    }
}

[StructLayout(LayoutKind.Auto)]
internal struct MyValueType
{
    public int A;
    public int B;
    public int C;

    private void PrintA()
    {
        Console.WriteLine(A.ToString("x8"));
    }

    private void PrintB()
    {
        Console.WriteLine(B.ToString("x8"));
    }
}
_

上記のC#への逆コンパイルをC#で行う方法と混同しないでください。それは不可能であり、逆コンパイルは非常に間違っています。これは単に、ILSpyや他の逆コンパイラが誤解を招く可能性があることを示すためのものです。 (それ自体が含まれているように見える_System.Int32_のようなものを見ると誤解を招く可能性があるのと同じように(実際には_int32_が含まれていますが、これは_System.Int32_の組み込み型ですは対応するCLS型です(2つのオーバーロードメソッドが同じクラス定義の値型とボックス型を受け取ることができるように、2つのオーバーロードは_int32_と_System.Int32_を取り、共存できます(ILSpyはそれらは両方とも「int」をとります)))。

ILDasmは、valuetypeおよびclass制約キーワードを正しく表示します。

概要

つまり、要約すると、質問に答えるには:

値の型はnot参照型であり、notは_System.ValueType_またはその他の型から継承し、それらはcannotインターフェースを実装します。対応するboxedタイプであるalso定義されたdoは_System.ValueType_から継承され、canインターフェースを実装します。

_.class_定義は、状況に応じて異なるものを定義します。

  • interfaceセマンティック属性が指定されている場合、クラス定義はインターフェースを定義します。
  • interfaceセマンティック属性が指定されておらず、定義が_System.ValueType_を拡張していない場合、クラス定義はオブジェクトタイプ(クラス)を定義します。
  • interfaceセマンティック属性が指定されておらず、定義does extend _System.ValueType_の場合、クラス定義は値タイプandに対応するボックス化を定義しますタイプ(構造体)。

メモリレイアウト

このセクションでは、32ビットプロセスを想定しています

すでに述べたように、値型には型情報がないため、値型が何を表すかをメモリの場所から識別することはできません。構造体は単純なデータ型を記述し、定義するフィールドのみを含みます。

_public struct MyStruct
{
    public int A;
    public short B;
    public int C;
}
_

MyStructのインスタンスがアドレス0x1000に割り当てられていると想像すると、これはメモリレイアウトです。

_0x1000: int A;
0x1004: short B;
0x1006: 2 byte padding
0x1008: int C;
_

構造体のデフォルトは順次レイアウトです。フィールドは、独自のサイズの境界に配置されます。これを満たすためにパディングが追加されます。

同じようにクラスを定義すると、次のようになります。

_public class MyClass
{
    public int A;
    public short B;
    public int C;
}
_

同じアドレスを想像すると、メモリレイアウトは次のようになります。

_0x1000: Pointer to object header
0x1004: int A;
0x1008: int C;
0x100C: short B;
0x100E: 2 byte padding
0x1010: 4 bytes extra
_

クラスはデフォルトで自動レイアウトになり、JITコンパイラーはそれらを最適な順序で配置します。フィールドは、独自のサイズの境界に配置されます。これを満たすためにパディングが追加されます。理由はわかりませんが、すべてのクラスの末尾には常に4バイトが追加されています。

オフセット0には、タイプ情報、仮想メソッドテーブルなどを含むオブジェクトヘッダーのアドレスが含まれます。これにより、ランタイムは、値タイプとは異なり、アドレスのデータが何を表すかを識別できます。

したがって、値型は継承、インターフェース、ポリモーフィズムをサポートしていません。

方法

値型には仮想メソッドテーブルがないため、ポリモーフィズムをサポートしていません。 ただし、対応するボックス型する

構造体のインスタンスがあり、_System.Object_で定義されているToString()のような仮想メソッドを呼び出そうとすると、ランタイムは構造体をボックス化する必要があります。

_MyStruct myStruct = new MyStruct();
Console.WriteLine(myStruct.ToString()); // ToString() call causes boxing of MyStruct.
_

ただし、構造体がToString()をオーバーライドする場合、呼び出しは静的にバインドされ、ランタイムはボクシングなしで仮想メソッドテーブルを調べずにMyStruct.ToString()を呼び出します(構造体にはありません) 。このため、ToString()呼び出しをインライン化することもできます。

構造体がToString()をオーバーライドしてボックス化されている場合、呼び出しは仮想メソッドテーブルを使用して解決されます。

_System.ValueType myStruct = new MyStruct(); // Creates a new instance of the boxed type of MyStruct.
Console.WriteLine(myStruct.ToString()); // ToString() is now called through the virtual method table.
_

ただし、ToString()は構造体で定義されているため、構造体の値を操作するため、値の型を想定していることに注意してください。他のクラスと同様に、ボックス型にはオブジェクトヘッダーがあります。 thisのフィールドAにアクセスしようとしたときに、構造体で定義されたToString()メソッドがMyStructポインターのボックス型で直接呼び出された場合、オフセット0にアクセスします。ボックス化されたタイプでは、オブジェクトヘッダーポインターになります。したがって、ボックス型には、ToString()を実際にオーバーライドする隠しメソッドがあります。この非表示のメソッドは、ボックス化された型をボックス化解除(「unbox」のようなアドレス計算のみ)し、構造体で定義されたToString()を静的に呼び出します。

同様に、ボックス化された型には、同じアンボックス化を実行し、構造体で定義されたメソッドを静的に呼び出す、実装されたインターフェイスメソッドごとに非表示メソッドがあります。

CLI仕様

ボクシング

I.8.2.4すべての値タイプについて、CTSはボックス化タイプと呼ばれる対応する参照タイプを定義します。逆は当てはまりません。通常、参照型には対応する値型がありません。ボックス化された型の値(ボックス化された値)の表現は、値型の値を格納できる場所です。ボックス型はオブジェクト型であり、ボックス型値はオブジェクトです。

値タイプの定義

I.8.9.7クラス定義で定義されているすべての型がオブジェクト型であるとは限りません(§I.8.2.3を参照)。特に、値型はオブジェクト型ではありませんが、クラス定義を使用して定義されます。値タイプのクラス定義は、(ボックス化されていない)値タイプと関連するボックス化されたタイプの両方を定義します(§I.8.2.4を参照)。クラス定義のメンバーは、両方の表現を定義します。

II.10.1.3タイプの意味属性は、インターフェース、クラス、または値タイプを定義するかどうかを指定します。インターフェース属性はインターフェースを指定します。この属性が存在せず、定義が(直接的または間接的に)System.ValueTypeを拡張し、定義がSystem.Enumを対象としない場合、値の型が定義されます(§II.13)。それ以外の場合は、クラスを定義する必要があります(§II.11)。

値タイプは継承しません

I.8.9.10ボックス化されていないフォームでは、値の型はどの型からも継承されません。ボックス化された値型は、列挙型でない限り、System.ValueTypeから直接継承します。列挙型の場合、System.Enumから継承します。ボックス化された値タイプは封印されます。

II.13ボックス化されていない値の型は別の型のサブタイプとは見なされず、ボックス化されていない値の型に対してisinst命令(パーティションIIIを参照)を使用することは無効です。ただし、ボックス化された値タイプにはisinst命令を使用できます。

I.8.9.10値型は継承しません。むしろ、クラス定義で指定された基本型は、ボックス型の基本型を定義します。

値型はインターフェースを実装していません

I.8.9.7値型はインターフェースコントラクトをサポートしていませんが、それらに関連付けられたボックス型はサポートしています。

II.13値型は0個以上のインターフェースを実装する必要がありますが、これはボックス化された形式でのみ意味があります(§II.13.3)。

I.8.2.4インターフェースと継承は参照型でのみ定義されます。したがって、値タイプ定義(§I.8.9.7)は、値タイプによって実装される両方のインターフェースと、それが継承するクラス(System.ValueTypeまたはSystem.Enum)を指定できますが、これらはボックス化された値にのみ適用されます。

値タイプとボックス化タイプの参照

II.13.1値タイプのボックス化されていない形式は、タイプ参照が後に続くvaluetypeキーワードを使用して参照されるものとします。値の型のボックス化された形式は、ボックス化されたキーワードとそれに続く型参照を使用して参照されます。

注:ここでは指定が間違っています。boxedキーワードはありません。キーワードはclassです。

エピローグ

値の型がどのように継承されるかの混乱の一部は、C#がキャスト構文を使用してボックス化とボックス化解除を実行するため、キャストを実行しているように見えますが、そうではありません。 C#の_(object)myStruct_は、値型のボックス型の新しいインスタンスを作成します。キャストは行いません。同様に、C#の_(MyStruct)obj_はボックス化された型のボックス化を解除し、値の部分をコピーします。キャストは行いません。

0
MulleDK19