只是玩弄铸造。假设我们有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字段吗?
答案 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
中。我将引用这个词用斜体显示有一个原因:它不是那里的对象。该对象在托管堆上是任意的。基本上b1
和i1
只是将内存地址保存到实际对象。旁注:“参考”,“地址”和“指针”之间存在细微的技术差异,但它们在这里起着相同的作用。
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
的数组中?会被丢弃吗?
绝对不是。因为它们是引用类型,所以列表实际上并不包含Inh
或Base
对象 - 它只包含引用。该引用只是一个数字 - 例如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
- 如果值为非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中存储引用类型实例的信息可能会有所帮助。
这意味着struct
中的所有方法和属性基本上都是static methods,this
结构引用被隐式地作为参数传递(同样,有一两个例外,如{{ 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 (class
es)有点复杂。
如果一个类方法是非虚拟的,那么它的行为基本上就像一个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
编译代码在两种情况下都是相同的(简化,无类型检查,只是寻址):
Console.WriteLine(i1.a);
Console.WriteLine(b2.a);
:
一个。获取i1(i1
)
湾添加4个字节的偏移量以获取0x20000004
(a
)
℃。获取该地址的值(0x20000008
)
2
:
一个。获取b2(b2
)
湾添加4个字节的偏移量以获取0x20000004
(a
)
℃。获取该地址的值(0x20000008
)
因此,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序列化,你将需要使用每个继承类型的声明来装饰基类。