3

只是在玩铸造。假设,我们有 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

4 回答 4

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>
于 2013-09-23T09:27:41.053 回答
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。对每个对象的引用b1分别存储在/i1中。我将引用这个词用斜体表示是有原因的:它不是存在的对象。该对象在托管堆上的某个地方是任意的。本质上b1i1只是将内存地址保存到实际对象。旁注:“引用”、“地址”和“指针”之间存在细微的技术差异,但它们在这里的用途相同。

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;
}

这里的列表是参考列表。对象在托管堆上仍然是任意的。所以是的,我们可以遍历它们。因为 all Inhare also Base,所以这里没有任何问题。所以最后,我们得到了这个问题(来自评论(:

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

绝对不。因为它们是引用类型,所以列表实际上从不包含和InhBase 对象——它只包含引用。参考只是一个数字 - 例如 120934813940。基本上是一个内存地址。Base无论我们认为 120934813940 指向 a还是 an都无关紧要Inh——我们用任何一种术语谈论它都不会影响位于120934813940 的实际对象。我们需要做的就是执行转换,这意味着:而不是将 120934813940 视为 a 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);
    }
}
于 2013-09-23T09:52:21.087 回答
1

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

首先,有值类型(structs)

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

这意味着 a 中的所有方法和属性struct基本上都是静态方法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结构,则不存在可能必须调用的不同(覆盖)方法,并且编译器静态地知道实际函数。

引用类型(classes)有点复杂。

  • 引用类型的实例存储在堆上,并且某个引用类型的所有变量或字段仅保存对堆上对象的引用。将一个变量的值分配给另一个变量以及强制转换,只是简单地复制引用,使实例保持不变。
  • 它们支持继承虚拟方法
  • 引用类型的实例包含其字段的值,以及一些与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字段)中的第一个字段,而无法进一步通过实例。

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

所以如果你写这个:

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

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

  1. 对于i1

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

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

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

  2. 对于b2

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

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

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

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

您甚至可以将其转换为Object

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

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

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

于 2013-09-23T11:10:37.887 回答
0

b2 是 a Inh,但对于编译器来说它是 a Base,因为您将其声明为这样。

不过,如果你这样做(b2 as Inh).b = 2,它会起作用。然后编译器知道将其视为一个Inh,而 CLR 知道它确实是一个Inh已经存在的。

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

于 2013-09-23T09:30:24.303 回答