【问题标题】:Upcasting and its effect on the heap向上转型及其对堆的影响
【发布时间】:2017-11-20 10:47:49
【问题描述】:

对于以下课程:

public class Parent {
//Parent members
}

public class ChildA : Parent {
//ChildA members
}

public class ChildB : Parent {
//ChildB members
}

如果我将 ChildA 或 ChildB 实例向上转换为父实例,那么我无法访问他们的成员,但他们的成员仍然存在,因为如果我向下转换并尝试再次访问他们的成员,我会发现他们仍然拥有他们的成员数据。

我认为这意味着父实例不断为子类分配内存。

这是否意味着当我实例化一个父类时,它为子类成员分配内存,或者这只是在我投射时发生?

如果我们前后进行强制转换,父母是否有可能为多个孩子分配内存?

【问题讨论】:

  • 区分对象和变量。当你创建一个实例时,你就是在创建一个对象。转换时,您是在转换变量,而不是对象。

标签: c# oop inheritance upcasting


【解决方案1】:

在您上面描述的情况下,转换不会影响从基类转换到子类时分配的内存,反之亦然。

如果您实例化一个 Parent,您将在内存中拥有一个 Parent 对象。如果您将其转换为任何一个子类,它将失败并显示InvalidCastException

如果您实例化任何一个子对象,您将在内存中拥有一个子对象。您可以将其投射到父级,然后再返回。在这两种情况下,内存分配都不会改变。

此外,如果您实例化 ChildA,转换为 Parent,然后尝试转换为 ChildB,您将获得 InvalidCastException

【讨论】:

  • @Honey - 对于引用类型 (class) 变量仅包含指向已分配内存的指针。来回转换不会改变指针,也不会改变分配的内存。它只是改变了该指针的值是如何被解释的。对于值类型(struct,以及intfloatbyte 等基本类型),值存储在变量本身中。由于值类型不能被继承,大多数类型转换(如从intfloat)将简单地执行一些转换并将结果存储在目标变量中。
  • 强制转换不会影响在任何情况下分配的内存。-> 对于 这种类型 的强制转换(子类 -> 父类,类 -> 实现的接口等)为 true ,可能值得注意的是,这仅在任何情况下都是正确的,除非存在、使用和分配内存的隐式/显式转换运算符'
  • @Honey - 大多数时候没有额外的内存分配,除非你在你的演员表中做了一些花哨的事情。实际上,classes 也是如此 - 如果您定义了显式/隐式转换运算符,则所有赌注都将关闭。最后,可以将值类型转换为object(这是一个引用类型),这涉及“装箱”——这实际上在堆上分配了对象的另一个副本并将指向该副本的指针存储在变量中。在引用类型和值类型之间来回转换将涉及内存分配。
  • 感谢 cmets,我已经更改了答案,但不想陷入细节中,所以不要提及明确的演员和拳击,因为他们只会混淆答案。
【解决方案2】:

引用类型的“正常”向上转换和向下转换

对于引用类型,强制转换变量不会改变已在堆上分配的对象的类型,它只会影响引用该对象的变量的类型。

所以不,转换引用类型(即来自类的对象实例)没有任何额外的堆开销只要不涉及自定义转换运算符(见下文,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;}
}

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

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

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

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

转换运算符

根据 tolanj 的评论,如果 explicit conversion operator 替换了引用类型的默认转换,所有关于堆的赌注都将被取消。

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

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. 

【讨论】:

    【解决方案3】:

    (向下)转换只不过是“父类的眼睛”对类实例的视图。因此,您既不会通过强制转换丢失也不会添加任何信息或内存,您只需引用已为原始实例分配的相同内存。这就是为什么您仍然可以访问(例如通过反射)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 类型,它们都消耗自己的内存。

    【讨论】:

    • 解释一下,我对铸造的工作方式有不同的想法,我认为你的例子是可能的,谢谢你的解释
    【解决方案4】:

    我认为这意味着父实例不断为子类分配内存。

    不,因为Parent 类不知道它的孩子。

    var a = new ClassA();
    

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

    var b = (Parent)a;
    

    .NET 不会对内存做任何事情。 ab 指向同一个内存块(分配给ClassA)。

    【讨论】:

    • 感谢 Backs 的回答,我是这么想的,因为父 var 抱着一个孩子,仍然可以将其放下并访问其成员,但知道我明白了,感谢您的解释
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-08
    • 2013-03-30
    • 2016-12-07
    • 2015-01-10
    • 2014-02-22
    相关资源
    最近更新 更多