【问题标题】:Upcast/Downcast and serializationUpcast/Downcast 和序列化
【发布时间】:2013-09-23 09:24:31
【问题描述】:

只是在玩选角。假设,我们有 2 个类

public class Base
{
    public int a;
}

public class Inh : Base
{
    public int b;
}

实例化他们两个

        Base b1 = new Base {a = 1};
        Inh i1 = new Inh {a = 2, b = 2};

现在,让我们尝试向上转型

        // Upcast
        Base b2 = i1;

似乎 b2 仍然持有字段 b,该字段仅在 Inh 类中出现。让我们通过向下转换来检查它。

        // Downcast
        var b3 = b2;
        var i2 = b2 as Inh;
        var i3 = b3 as Inh;

        bool check = (i2 == i3);

这里检查是真的(我猜,因为 i2 和 i3 引用同一个实例 i1)。 好的,让我们看看,它们将如何存储在数组中。

        var list = new List<Base>();

        list.Add(new Base {a = 5});
        list.Add(new Inh {a = 10, b = 5});

        int sum = 0;
        foreach (var item in list)
        {
            sum += item.a;
        }

一切都很好,总和是 15。但是当我尝试使用 XmlSerializer 序列化数组时(只是想看看里面有什么),它返回 InvalidOperationException“类型 ConsoleApplication1.Inh 不是预期的”。好吧,很公平,因为它的 Bases 数组。

那么,实际上 b2 是什么?我可以序列化一组 Bases 和 Inhs 吗?我可以通过从反序列化数组中向下转换项目来获取 Inhs 字段吗?

【问题讨论】:

    标签: c# list serialization downcast upcasting


    【解决方案1】:

    如果您希望它与序列化一起使用,您需要告诉序列化程序有关继承的信息。对于XmlSerializer,这是:

    [XmlInclude(typeof(Inh))]
    public class Base
    {
        public int a;
    }
    
    public class Inh : Base
    {
        public int b;
    }
    

    然后以下工作正常:

    var list = new List<Base>();
    
    list.Add(new Base { a = 5 });
    list.Add(new Inh { a = 10, b = 5 });
    
    var ser = new XmlSerializer(list.GetType());
    var sb = new StringBuilder();
    using (var xw = XmlWriter.Create(sb))
    {
        ser.Serialize(xw, list);
    }
    string xml = sb.ToString();
    Console.WriteLine(xml);
    using (var xr = XmlReader.Create(new StringReader(xml)))
    {
        var clone = (List<Base>)ser.Deserialize(xr);
    }
    

    clone 具有预期的 2 个不同类型的对象。 xml 是(重新格式化以提高可读性):

    <?xml version="1.0" encoding="utf-16"?><ArrayOfBase
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <Base><a>5</a></Base>
        <Base xsi:type="Inh"><a>10</a><b>5</b></Base>
    </ArrayOfBase>
    

    【讨论】:

    • 然后,另一个问题(更详细):如何将 Inh 存储在 Bases 数组中? b 会是 :) 刚刚删除吗?
    • @VitaliiVasylenko 我正在写一个长而详细的答案......给我1分钟
    • 但是等等.. 当我们序列化时,我们是在告诉 Base 类是 typeof Inherited?也许反之亦然?
    • @VitaliiVasylenko 我们告诉序列化程序的唯一一件事是typeof(List&lt;Base&gt;),通过list.GetType()。从Base,序列化程序可以推断它还需要知道Inh
    • @VitaliiVasylenko 不,您不能将Base 向下转换为Inh。如果实际对象(不是变量)是Base,那么它不是Inh,在任何情况下都不能被视为一个。如果您想将异构项目存储在同一个列表中,在这种情况下很好 - 您只需使用 List&lt;Base&gt;。 Inh 是 Base,但 Base 不是 Inh
    【解决方案2】:

    其实问题是关于内存中发生了什么

    所以;不是序列化,那么。 K.

    让我们从顶部开始,然后:

    public class Base
    {
        public int a;
    }
    
    public class Inh : Base
    {
        public int b;
    }
    

    这里我们有两个引用类型(类);它们是引用类型这一事实非常重要,因为这直接会影响实际存储在数组/变量中的内容。

    Base b1 = new Base {a = 1};
    Inh i1 = new Inh {a = 2, b = 2};
    

    这里我们创建了 2 个对象; Base 类型之一,Inh 类型之一。每个对象的 reference 分别存储在 b1 / i1 中。我将单词 reference 斜体是有原因的:它不是存在的 object。该对象在托管堆上的某个地方是任意的。本质上b1 和i1 只是将内存地址保存到实际对象。旁注:“引用”、“地址”和“指针”之间存在细微的技术差异,但它们在这里的用途相同。

    Base b2 = i1;
    

    这会复制引用,并将该引用分配给b2。请注意,我们没有复制 object。我们仍然只有 2 个对象。我们复制的只是恰好代表内存地址的数字。

    var b3 = b2;
    var i2 = b2 as Inh;
    var i3 = b3 as Inh;
    bool check = (i2 == i3);
    

    在这里我们反过来做同样的事情。

    var list = new List<Base>();
    
    list.Add(new Base {a = 5});
    list.Add(new Inh {a = 10, b = 5});
    
    int sum = 0;
    foreach (var item in list)
    {
        sum += item.a;
    }
    

    这里的列表是引用的列表。对象在托管堆上仍然是任意的。所以是的,我们可以遍历它们。因为所有Inh 也是Base,所以这里没有任何问题。所以最后,我们得到了这个问题(来自 cmets(:

    然后,另一个问题(更详细):Inh 将如何存储在Bases 的数组中? b 会被丢弃吗?

    绝对不是。因为它们是引用类型,所以列表实际上从不包含 Inh 或 Base objects - 它只包含引用。参考只是一个数字 - 例如 120934813940。基本上是一个内存地址。无论我们认为 120934813940 指向 Base 还是 Inh 都无关紧要 - 我们以任何一种方式谈论它都不会影响 位于 120934813940 的实际对象。需要做的是执行强制转换,这意味着:不要将 120934813940 视为 Base,而将其视为 Inh - 这涉及到类型测试以确认它是我们所怀疑的。例如:

    int sum = 0;
    foreach (var item in list)
    {
        sum += item.a;
        if(item is Inh)
        {
           Inh inh = (Inh)item;
           Console.WriteLine(inh.b);
        }
    }
    

    所以b 一直都在!我们看不到它的唯一原因是我们只假设item 是Base。要访问b,我们需要转换值。这里常用的三个重要操作:

    • obj is Foo - 执行类型测试返回 true 如果值是非 null 并且可以简单地分配为该类型,否则 false
    • obj as Foo - 执行类型测试,如果它为非空且匹配,则返回 类型为 Foo 的引用,否则返回 null
    • (Foo)obj - 执行类型测试,如果是null,则返回null,如果匹配,则引用类型为Foo,否则抛出异常

    所以这个循环也可以写成:

    int sum = 0;
    foreach (var item in list)
    {
        sum += item.a;
        Inh inh = item as Inh;
        if(inh != null)
        {
           Console.WriteLine(inh.b);
        }
    }
    

    【讨论】:

    • 感谢您提供详细的回答,这让事情变得更加清晰。因此,当我们将 Inh 的引用转换为 Base 时,我们只是在说“通常是 Inh,但您可以尝试将存储在该地址的对象作为 Base”?
    • @VitaliiVasylenko 将Inh 转换为Base 是无操作的;编译器和 CLI 已经知道任何Inh 肯定是Base。所做的只是丢掉一点信息(事实上我们也知道它是Inh)——将可用操作限制在Base 上可用的操作。
    【解决方案3】:

    为了阐明从一种类型转换为另一种类型时实际发生的情况,提及一些有关引用类型实例如何存储在CLR 中的信息可能会有所帮助。

    首先有value types (structs)。

    • 它们存储在stack 上(嗯,它可能是一个“实现细节”,但恕我直言,我们可以放心地假设事情就是这样),
    • 它们不支持继承(无虚拟方法),
    • 值类型的实例仅包含其字段的值。

    这意味着struct 中的所有方法和属性基本上都是static methods,其中this 结构引用作为参数被隐式传递(同样,有一个或两个例外,例如ToString,但大多不相关) .

    所以,当你这样做时:

    struct SomeStruct 
    {
        public int Value;
        public void DoSomething()
        {
            Console.WriteLine(this.Value);
        }
    }
    
    SomeStruct c; // this is placed on stack
    c.DoSomething();
    

    这在逻辑上与使用static 方法并将引用传递给SomeStruct 实例在逻辑上相同(引用部分很重要,因为它允许该方法通过直接写入该堆栈内存区域来改变结构内容,无需装箱):

    struct SomeStruct 
    {
        public int Value;
        public static void DoSomething(ref SomeStruct instance)
        {
            Console.WriteLine(instance.Value);
        }
    }
    
    SomeStruct c; // this is placed on stack
    SomeStruct.DoSomething(ref c); // this passes a pointer to the stack and jumps to the method call
    

    如果您在结构上调用DoSomething,则不存在可能必须调用的不同(覆盖)方法,并且编译器静态地知道实际函数。

    Reference types (classes) 有点复杂。

    • 引用类型的实例存储在heap上,并且某个引用类型的所有变量或字段仅持有一个reference指向堆上的对象。将一个变量的值分配给另一个变量以及强制转换,只需复制 reference 周围的内容,而使实例保持不变。
    • 他们支持inheritance (virtual methods)
    • 引用类型的实例包含其字段的值,以及与GC、同步、AppDomain 标识和类型相关的一些额外行李。

    如果一个类方法是非虚拟的,那么它的行为基本上就像struct 方法:它在编译时是已知的并且不会改变,所以编译器可以发出一个直接的函数调用,就像它一样传递对象引用用结构做。

    那么,当你转换为不同的类型时会发生什么?就内存布局而言,没什么。

    如果你像你提到的那样定义了你的对象:

    public class Base
    {
        public int a;
    }
    
    public class Inh : Base
    {
        public int b;
    }
    

    然后你实例化一个Inh,然后将其转换为Base:

    Inh i1 = new Inh() { a = 2, b = 5 };
    Base b2 = i1;    
    

    堆内存将包含单个对象实例(例如,地址0x20000000):

    // simplified memory layout of an `Inh` instance
    [0x20000000]: Some synchronization stuff
    [0x20000004]: Pointer to RTTI (runtime type info) for Inh
    [0x20000008]: Int32 field (a = 2)
    [0x2000000C]: Int32 field (b = 5)
    

    现在,引用类型的所有变量都指向 RTTI 指针的位置(实际对象的内存区域早 4 个字节开始,但这并不那么重要)。

    i1 和 b2 都包含一个指针(在此示例中为 0x20000004),唯一的区别是编译器将允许 Base 变量仅引用该内存区域中的第一个字段( a 字段),无法进一步了解实例。

    对于Inh 实例i1,相同的字段位于完全相同的偏移量处,但它也可以访问位于第一个字段之后 4 个字节的下一个字段b(距RTTI 指针)。

    所以如果你这样写:

    Console.WriteLine(i1.a);
    Console.WriteLine(b2.a);
    

    编译后的代码在两种情况下都是相同的(简化,没有类型检查,只是寻址):

    1. 对于i1:

      一个。获取i1的地址(0x20000004)

      b.加上4个字节的偏移量得到a(0x20000008)的地址

      c。获取该地址的值 (2)

    2. 对于b2:

      一个。获取b2的地址(0x20000004)

      b.加上4字节的偏移量得到a(0x20000008)的地址

      c。获取该地址的值 (2)

    因此,Inh 的唯一实例在内存中,未修改,并且通过执行转换,您只是告诉编译器如何表示在该内存位置找到的数据。与纯 C 相比,如果您尝试强制转换为不在继承层次结构中的对象,C# 将在运行时失败,但纯 C 程序会愉快地返回位于 known fixed offset 的任何内容您实例中的某个字段。唯一的区别是 C# 会检查您所做的事情是否有意义,但变量的类型仅用于允许在同一个对象实例中走动。

    您甚至可以将其转换为 Object:

    Object o1 = i1; // <-- this still points to `0x20000004`    
    // Hm. Ok, that worked, but now what? 
    

    同样,内存实例是未修改的,但是您对Object 的变量无能为力,除了再次向下转换它。

    Virtual methods 更有趣,因为它们涉及编译器跳过提到的 RTTI 指针以到达该类型的 virtual method table(允许类型覆盖基类型的方法)。这再次意味着编译器将简单地为特定方法使用固定偏移量,但派生类型的实际实例将在表中的该位置具有适当的方法实现。

    【讨论】:

    • 只是一个快速的问题:那么我们可以使用静态 class= struct 吗?
    • @VitaliiVasylenko:不,类始终是引用类型(在堆上实例化,并且类变量始终包含引用或指向该内存的“指针”),而 struct 始终是值类型(在堆栈上实例化,并且传递结构总是复制其全部内容)。在 .NET 中很少推荐使用结构,除了微小的结构(例如 System.Drawing.Point),即便如此,最好使它们不可变(使用 readonly 支持字段)。
    【解决方案4】:

    b2 是一个Inh,但对于编译器来说它是一个Base,因为你声明了它。

    不过,如果您使用(b2 as Inh).b = 2,它会起作用。然后编译器知道将其视为Inh,而CLR 知道它实际上已经是Inh。

    正如 Marc 指出的那样,如果您使用 XML 序列化,您将需要使用每个继承类型的声明来装饰基类。

    【讨论】:

    • 问题中的问题似乎与序列化有关——而不仅仅是内存中发生的事情。
    • @MarcGravell 感谢您的回答。实际上,问题在于内存中发生了什么 - xmlserialization 只是一种查看数组内部的方法。无论如何,感谢您提供额外的信息,这也很有用。
    • @VitaliiVasylenko 好吧,xml 序列化是您描述实际问题的唯一时间。但是不,xml 序列化 不是 一种查看数组内部内容的方法。那是完全不相关的。
    猜你喜欢
    • 2016-03-23
    • 2017-07-18
    • 1970-01-01
    • 2012-03-17
    • 2018-07-09
    • 2013-04-19
    相关资源
    最近更新 更多