上传/下传和序列化

时间:2013-09-23 09:24:31

标签: c# list serialization downcast upcasting

只是玩弄铸造。假设我们有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”。嗯,公平,因为它的阵列基础。

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

4 个答案:

答案 0 :(得分:4)

如果您希望它与序列化一起使用,您需要告诉序列化程序有关继承的信息。在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>

答案 1 :(得分: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};

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

Base b2 = i1;

这会复制引用,并将该引用分配给b2。请注意,我们尚未复制对象。我们仍然只有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,因此这里没有任何问题。最后,我们得到了问题(来自评论(:

  

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

绝对不是。因为它们是引用类型,所以列表实际上并不包含InhBase 对象 - 它只包含引用。该引用只是一个数字 - 例如120934813940。一个内存地址,基本上。我们是否认为120934813940指向BaseInh并不重要 - 我们在任何一个术语中谈论它都不会影响位于的实际对象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始终存在!我们无法看到它的唯一原因是我们只假设itemBase。要访问b,我们需要转换值。这里有三个常用的重要操作:

  • obj is Foo - 如果值为非null并且可以作为该类型轻易分配,则执行返回true的类型测试,否则false
  • obj as Foo - 执行类型测试,将参考输入为Foo ,如果它是非空且匹配,或null否则< / LI>
  • (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);
    }
}

答案 2 :(得分:1)

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

首先,有value types (structs)

  • 它们存储在stack上(好吧,它可能是一个“实施细节”,但恕我直言,我们可以安全地假设它是事情的方式),
  • 他们不支持继承(没有虚拟方法),
  • 值类型的实例仅包含其字段的值

这意味着struct中的所有方法和属性基本上都是static methodsthis结构引用被隐式地作为参数传递(同样,有一两个例外,如{{ 1}},但大部分都是不相关的。)

所以,当你这样做时:

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 ,则不存在可能必须调用的不同(覆盖)方法,并且编译器静态地知道实际函数。

Reference types (classes)有点复杂。

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

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

那么,当你投射到另一种类型时会发生什么?就内存布局而言,没什么。

如果您的对象定义如上所述:

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

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

// 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) i1都包含一个指针(在此示例中为b2),唯一的区别是编译器将允许0x20000004变量仅引用该内存区域中的第一个字段(Base字段),无法进一步通过该实例。

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

所以如果你写这个:

b

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

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

    一个。获取i1(i1

    的地址

    湾添加4个字节的偏移量以获取0x20000004a

    的地址

    ℃。获取该地址的值(0x20000008

  2. 2

    一个。获取b2(b2

    的地址

    湾添加4个字节的偏移量以获取0x20000004a

    的地址

    ℃。获取该地址的值(0x20000008

  3. 因此,2的唯一实例在内存中,未经修改,通过执行转换,您只需告诉编译器如何表示在该内存位置找到的数据 。与普通C相比,如果您尝试转换为不在继承层次结构中的对象,C#将在运行时失败,但是普通的C程序将很乐意返回在已知固定偏移处的任何内容。您实例中的某个字段。唯一的区别是C#检查你正在做什么是有意义的,但是变量的类型只能用于允许在同一个对象实例中走动。

    您甚至可以将其投放到Inh

    Object

    同样,内存实例是未经修改的,但除了向下转发它之外,对Object o1 = i1; // <-- this still points to `0x20000004` // Hm. Ok, that worked, but now what? 变量没有什么可以做的。

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

答案 3 :(得分:0)

b2是Inh,但对于编译器来说它是Base,因为你是这样声明的。

但是,如果你做(b2 as Inh).b = 2,它会起作用。然后,编译器知道将其视为Inh,并且CLR已经知道它实际上已经是Inh

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