向上转型及其对堆的影响

9
对于以下类:
public class Parent {
//Parent members
}

public class ChildA : Parent {
//ChildA members
}

public class ChildB : Parent {
//ChildB members
}

如果我将ChildA或ChildB实例向上转型为Parent实例,则无法访问它们的成员,但它们的成员仍然存在,因为如果我向下转型并尝试再次访问它们的成员,我会发现它们仍然有其数据。

我认为这意味着父类实例保留了子类的内存空间。

那么,当我实例化一个父类时,它是否为子类成员分配内存空间,还是只有在转型时才会发生?

如果我们前后移动转型,父类是否可以为多个子类分配内存?


区分对象和变量。当您创建一个实例时,您正在创建一个对象。当您进行强制转换时,您正在转换变量,而不是对象。 - Rotem
4个回答

14
在您所描述的情况中,从基类向子类或从子类向基类进行类型转换时,类型转换不会影响分配的内存。如果您实例化了一个 Parent 对象,那么在内存中就会有一个 Parent 对象。如果您将其转换为任何一个子类,都将导致出现 "InvalidCastException" 异常。如果您实例化任一子类,则在内存中将有一个子类对象。您可以将其转换为 Parent 类型,然后再转回原来的子类类型,这两种情况下内存分配都不会改变。此外,如果您实例化 ChildA,将其强制转换为 Parent,然后尝试将其转换为 ChildB,也将出现 "InvalidCastException" 异常。

@Honey - 对于引用类型(class),变量仅仅持有一个指向分配内存的指针。前后转换不会改变指针,也不会改变分配的内存。它只是改变了如何解释该指针的值。对于值类型(struct,以及基本类型如intfloatbyte等),值存储在变量本身中。由于值类型不能被继承,大多数转换(例如从intfloat)将简单地执行一些转换并将结果存储在目标变量中。 - Vilx-
2
强制类型转换不会影响在任何情况下分配的内存。对于这种类型的强制类型转换(子类->父类,类->实现的接口等),值得注意的是,只有在不存在隐式/显式转换运算符、使用了转换运算符并且分配了内存的情况下才成立。 - tolanj
@Honey - 大多数情况下,除非您在转换运算符中执行某些花哨的操作,否则不会进行额外的内存分配。实际上,对于class来说也是如此 - 如果定义了显式/隐式转换运算符,则所有赌注都取消。最后,值类型可以转换为引用类型object,这涉及到“装箱” - 这实际上在堆上分配了另一个对象的副本,并将指向该副本的指针存储在变量中。然后在引用类型和值类型之间来回转换将涉及内存分配。 - Vilx-
感谢您的评论,我已经修改了我的答案。不过,我不想深入讨论细节,因此没有提及显式转换运算符和装箱,因为它们只会让答案更加混乱。 - Tim Rutter

7
引用类型的“正常”向上转型和向下转型 对于引用类型,强制转换变量不会改变已在堆上分配的对象的类型,它只影响引用该对象的变量的类型。
因此,在使用引用类型(即来自类的对象实例)进行转换时,通常不会有额外的堆开销,前提是没有涉及自定义转换运算符(请参阅下面tolanj的评论)。
考虑以下类层次结构:
public class Fruit
{
    public Color Colour {get; set;}
    public bool Edible {get; set;}
}

public class Apple : Fruit
{
    public Apple { Color = Green; Edible = true; KeepsDoctorAtBay = true;}
    public bool KeepsDoctorAtBay{get; set;}
}

当同时使用向上转型和向下转型时:

Example of variables pointing to same heap object

堆上只有一个分配,即最初的var foo = new Apple()

在各种变量赋值后,所有三个变量foobarbaz都指向同一个对象(堆上的Apple实例)。

向上转型(Fruit bar = foo)将仅限制变量可用访问的Fruit方法和属性,如果(Apple)bar向下转型成功,则所有向下转型类型的方法、属性和事件都将对变量可用。如果向下转型失败,则会抛出InvalidCastException,因为类型系统将在运行时检查堆对象的类型与变量类型的兼容性。

转换运算符

根据tolanj的评论,如果显式转换运算符替换引用类型的默认转换,则关于堆的所有赌注都无效。

例如,如果我们添加一个不相关的类:

public class WaxApple // Not inherited from Fruit or Apple
{
    public static explicit operator Apple(WaxApple wax)
    {
        return new Apple
        {
            Edible = false,
            Colour = Color.Green,
            KeepsDoctorAtBay = false
        };
    }
}

正如你所想象的那样,WaxApple的explicit operator Apple可以随心所欲地进行操作,包括在堆上分配新对象。
var wax = new WaxApple();
var fakeApple = (Apple)wax;
// Explicit cast operator called, new heap allocation as per the conversion code. 

4
(down-)cast只是通过“父类的眼睛”查看类实例的视图。因此,通过转换,你既不会丢失也不会添加任何信息或内存,你只是引用了已经为原始实例分配的相同内存。这就是为什么你仍然可以访问(例如通过反射)ChildA的成员变量,在Parent类型的变量中。信息仍然存在,只是不可见。
因此,与其有两个内存分配,不如有两个内存引用
但请注意,如果你提供自己的转换,例如从ChildAChildB,则此规则不适用。这样做通常看起来会更加相似,如下所示:
public static explicit operator ChildA(ChildB b)
{
    var a = new ChildA((Parent)b);
    /* set further properties defined in ChildA but not in ChildB*/
}

这里有两个完全不同的实例,一个是类型为ChildA的实例,另一个是类型为ChildB的实例,它们都使用自己的内存。


解释一下,我对类型转换的工作原理有不同的想法。我以为你的例子是不可能的,谢谢你的解释。 - Honey

1
我认为这意味着父实例会为子类分配内存。
不,因为父类不知道它的子类。
var a = new ClassA();

.NETClassA 的所有成员分配内存。

var b = (Parent)a;

.NET 不处理内存。ab 指向同一块内存块(为 ClassA 分配)。


谢谢你的回答,Backs。我之前也是这么想的,因为父变量持有子变量,仍然可以将其向下转换并访问其成员。但现在我明白了,感谢你的解释。 - Honey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接