Обновление и его влияние на кучу

Для следующих классов:

public class Parent {
//Parent members
}

public class ChildA : Parent {
//ChildA members
}

public class ChildB : Parent {
//ChildB members
}

Если я приведу экземпляр ChildA или ChildB к экземпляру Parent, то я не смогу получить доступ к их членам, но их члены все еще будут там, потому что, если я приведу вниз и попытаюсь снова получить доступ к их членам, я обнаружу, что они все еще имеют свои данные.

Я думаю, это означает, что родительский экземпляр продолжает выделять память для дочерних классов.

Значит ли это, что когда я создаю экземпляр родительского класса, он выделяет память для членов дочерних классов, или это происходит только при приведении?

И возможно ли, чтобы родитель выделил память более чем для одного ребенка, если мы идем вперед и назад с приведением?


person Honey    schedule 20.11.2017    source источник
comment
Отличие объекта от переменной. Когда вы создаете экземпляр, вы создаете объект. Когда вы приводите, вы приводите переменную, а не объект.   -  person Rotem    schedule 20.11.2017


Ответы (4)


В случае, который вы описали выше, приведение не влияет на память, которая выделяется при приведении от базового к подклассу и наоборот.

Если вы создаете экземпляр Parent, у вас будет родительский объект в памяти. Если вы примените это к любому из дочерних классов, это приведет к ошибке InvalidCastException.

Если вы создадите экземпляр любого дочернего элемента, у вас будет дочерний объект в памяти. Вы можете привести это к родителю, а затем обратно. Распределение памяти не меняется ни в том, ни в другом случае.

Кроме того, если вы создаете экземпляр ChildA, приводите его к Parent, а затем пытаетесь привести к ChildB, вы получите InvalidCastException

person Tim Rutter    schedule 20.11.2017
comment
@Honey - для ссылочного типа (class) переменная просто содержит указатель на выделенную память. Приведение вперед и назад не изменяет ни указатель, ни выделенную память. Это просто меняет способ интерпретации значения этого указателя. Для типа значения (struct, а также базовых типов, таких как int, float, byte и т. д.) значение хранится в самой переменной. Поскольку типы значений не могут быть унаследованы, большинство приведений (например, от int к float) просто выполняют некоторое преобразование и сохраняют результаты в целевой переменной. - person Vilx-; 20.11.2017
comment
Приведение не влияет на память, которая выделяется ни при каких обстоятельствах.-› верно для этого типа приведения (подкласс -> родительский класс, класс -> реализованный интерфейс и т. д.), возможно, стоит отметить, что это только истина «при любых обстоятельствах, за исключением случаев, когда неявный/явный оператор приведения существует, используется и выделяет память» - person tolanj; 20.11.2017
comment
@Honey - В большинстве случаев дополнительное выделение памяти не выделяется, за исключением случаев, когда вы делаете что-то необычное в своем операторе приведения. На самом деле, это верно и для classes - если у вас есть явный/неявный оператор приведения, все ставки отключены. Наконец, тип значения может быть приведен к object (который является ссылочным типом), и это включает в себя упаковку — это фактически выделяет другую копию объекта в куче и сохраняет указатель на эту копию в переменной. Приведение туда и обратно между ссылочными типами и типами значений будет включать выделение памяти. - person Vilx-; 20.11.2017
comment
Спасибо за комментарии, я изменил свой ответ, однако не хочу увязнуть в деталях, поэтому не упоминая явные операторы приведения и бокс, поскольку они просто запутают ответ. - person Tim Rutter; 20.11.2017

"Нормальное" преобразование типов ссылок вверх и вниз

Для ссылочных типов приведение переменных не изменяет тип объекта, уже размещенного в куче, а просто влияет на тип переменной, которая ссылается на объект.

Так что нет, нет никаких дополнительных накладных расходов на кучу при приведении ссылочных типов (т.е. экземпляров объектов из классов).

Рассмотрим следующую иерархию классов:

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().

После различных назначений переменных все три переменные, foo, bar и baz, указывают на один и тот же объект (экземпляр 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
        };
    }
}

Как вы понимаете, explicit operator Apple от WaxApple может делать все, что захочет, в том числе размещать новые объекты в куче.

var wax = new WaxApple();
var fakeApple = (Apple)wax;
// Explicit cast operator called, new heap allocation as per the conversion code. 
person StuartLC    schedule 20.11.2017

Приведение (вниз) — это не что иное, как представление экземпляра класса "глазами родительского класса". Таким образом, вы не теряете и не добавляете какую-либо информацию или память при приведении, вы просто ссылаетесь на ту же память, которая уже выделена для исходного экземпляра. По этой причине вы все еще можете получить доступ (например, путем отражения) к членам ChildA в переменной типа Parent. Информация все равно есть, ее просто не видно.

Таким образом, вместо двух выделений памяти у вас есть две ссылки на память.

Однако имейте в виду, что это не применяется, если вы предоставляете свой собственный состав, например. с ChildA по ChildB. Это обычно будет выглядеть более или менее похоже на это:

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, которые оба потребляют свою собственную память.

person HimBromBeere    schedule 20.11.2017
comment
это объяснить, у меня было другое представление о том, как работает кастинг, я думал, что ваш пример возможен, спасибо за объяснение - person Honey; 20.11.2017

Я думаю, это означает, что родительский экземпляр продолжает выделять память для дочерних классов.

Нет, потому что класс Parent не знает о своих потомках.

var a = new ClassA();

.NET выделяет память для всех членов ClassA.

var b = (Parent)a;

.NET ничего не делает с памятью. a и b указывают на один и тот же блок памяти (выделенный для ClassA).

person Backs    schedule 20.11.2017
comment
спасибо за ответ Backs, я так думал, потому что родительский var держит дочерний элемент и все еще может бросить его и получить доступ к его членам, но знаю, что я понял, спасибо за объяснение - person Honey; 20.11.2017