Почему установка базового класса, равного производному типу, работает только в той области, где он установлен? - PullRequest
0 голосов
/ 14 сентября 2018

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

Рассмотрим ковариацию, когда вы можете заменить производный тип для базового класса.

class Base
{

}

class Derived : Base
{

}

Передача typeof(Base) этому методу и установка этой переменной в производный тип возможна.

private void TryChangeType(Base instance)
{
  var d = new Derived();
  instance = d;
  Console.WriteLine(instance.GetType().ToString());
}

Однако при проверке типа из вызывающей стороны вышеупомянутой функции экземпляр все равно будет иметь тип Base

private void CallChangeType()
{
  var b = new Base();
  TryChangeType(b);
  Console.WriteLine(b.GetType().ToString());
}  

Я бы предположил, поскольку объекты по своей природе являются ссылочными, что переменная вызывающего теперь будет иметь тип Derived. Единственный способ сделать так, чтобы вызывающая сторона была типа Derived, - передать эталонный объект по ref, например,

private void CallChangeTypeByReference()
{
  var b = new Base();
  TryChangeTypeByReference(ref b);
  Console.WriteLine(b.GetType().ToString());
}  
private void TryChangeTypeByReference(ref Base instance)
{
  var d = new Derived();
  instance = d;
}

Более того, я чувствую, что общеизвестно, что передача объекта в метод, редактирование объектов и передача этого объекта в стек сохранит изменения, внесенные в стек. Это имеет смысл, поскольку объект является эталонным объектом.

Что заставляет объект постоянно менять тип в стеке, только если он передается по ссылке?

Ответы [ 5 ]

0 голосов
/ 14 сентября 2018

Когда вы вызываете TryChangeType (), вы передаете копию ссылки в «b» в «instance». Любые изменения членов"instance" вносятся в то же пространство памяти, на которое все еще ссылается "b" в вызывающем методе. Однако команда «instance = d» переназначает значение памяти , адресуемой «instance». «b» и «instance» больше не указывают на одну и ту же память. Когда вы возвращаетесь в CallChangeType, «b» по-прежнему ссылается на исходное пространство и, следовательно, на Type. TryChangeTypeByReference передает ссылку, где значение указателя «b» фактически хранится. Переназначение «instance» теперь меняет адрес, на который фактически указывает «b».

0 голосов
/ 14 сентября 2018

У вас очень много запутанных и ложных убеждений. Давайте исправим это.

Рассмотрим ковариацию, где вы можете заменить производный тип для базового класса.

Это не ковариация . Это совместимость присвоения . Apple совместима с присвоением с переменной типа Fruit, потому что вы можете назначить Apple такой переменной. Опять же, это не ковариация . Ковариантность - это тот факт, что преобразование для типа сохраняет отношение совместимости назначения. последовательность яблок может использоваться где-то, что последовательность фруктов необходима , потому что яблоки - это разновидность фруктов . Это ковариация . Отображение «яблоко -> последовательность яблок, фрукты -> последовательность фруктов» представляет собой ковариантное отображение .

Двигаемся дальше.

Передача typeof(Base) этому методу и установка этой переменной в производный тип возможны.

Вы путаете типы с экземплярами. Вы не передаете typeof(Base) этому методу; Вы передаете ссылку на Base этому экземпляру. typeof(Base) имеет тип System.Type.

Как вы правильно заметили, формальные параметры являются переменными . Формальный параметр - это новая переменная, которая инициализируется фактическим параметром 1047 * aka аргумент .

Однако при проверке типа из вызывающей стороны вышеупомянутой функции экземпляр все равно будет иметь тип Base

Правильно. Аргумент имеет тип Base. Вы копируете это в переменную , а затем переназначаете переменную. Это ничем не отличается от высказывания:

Base x = new Base();
Base y = x;
y = new Derived();

А теперь x по-прежнему Base, а y - Derived. Вы присвоили одну и ту же переменную дважды; второе назначение выигрывает. Это ничем не отличается от того, что вы сказали бы a = 1; b = a; b = 2; - вы не ожидаете, что a будет 2 впоследствии только потому, что вы сказали b = a в прошлом.

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

Это предположение неверно. Опять же, вы сделали два назначения одной и той же переменной , и у вас есть две переменные , одна в вызывающей и одна в вызываемой. Переменные содержат значения; ссылки на объекты являются значениями .

Единственный способ получить вызывающую функцию типа Derived - это передать опорный объект по ссылке ref, например, так:

Теперь мы подошли к сути проблемы.

Правильный способ думать об этом заключается в том, что ref создает псевдоним переменной . Обычный формальный параметр: , новая переменная . ref формальный параметр делает переменную в формальном параметре псевдонимом переменной на сайте вызова . Итак, теперь у вас есть одна переменная , но она имеет два имени , потому что имя формального параметра - псевдоним для переменной при вызове. Это так же, как:

Base x = new Base();
ref Base y = ref x; // x and y are now two names for the same variable
y = new Derived(); // this assigns both x and y because there is only one variable, with two names

Более того, я чувствую, что общеизвестно, что передача объекта в метод, редактирование объектов и передача этого объекта в стек сохранит изменения, внесенные в стек. Это имеет смысл, поскольку объект является эталонным объектом.

Правильно.

Ошибка, которую вы делаете здесь, очень распространена. Для команды разработчиков C # было плохой идеей называть функцию псевдонима переменной «ref», потому что это вызывает путаницу. Ссылка на переменную создает псевдоним ; это дает другое имя переменной. Ссылка на объект - это токен, который представляет конкретный объект с определенной идентичностью. Когда вы смешиваете два, это сбивает с толку.

Обычно нужно , а не передавать переменные ref, особенно если они содержат ссылки.

Что заставляет объект постоянно менять тип в стеке, только если он передается по ссылке?

Теперь у нас самое фундаментальное замешательство. Вы перепутали объекты с переменными . Объект никогда не меняет своего типа, никогда! Яблоко - это объект, а яблоко - это всегда и навсегда яблоко. Яблоко никогда не становится каким-либо другим видом фруктов.

Хватит думать, что переменные - это объекты, прямо сейчас. Ваша жизнь станет намного лучше. Интернализуйте эти правила:

  • переменные - это места хранения, в которых хранятся значения
  • ссылки на объекты являются значениями
  • объекты имеют тип, который никогда не меняется
  • ref дает новое имя существующей переменной
  • присвоение переменной меняет ее значение

Теперь, если мы зададим ваш вопрос снова, используя правильную терминологию, путаница сразу исчезнет:

Что заставляет значение переменной менять свой тип в стеке, только если оно передано ref?

Ответ теперь очень ясен:

  • Переменная, переданная ref, является псевдонимом другой переменной, поэтому изменение значения параметра аналогично изменению значения переменной на сайте вызова
  • Присвоение ссылки на объект переменной изменяет значение этой переменной
  • Объект имеет определенный тип

Если мы не пройдем мимо ref, а пройдем нормально:

  • Передаваемое значение обычно копируется в новую переменную, формальный параметр
  • Теперь у нас есть две переменные без связи; изменение одного из них не меняет другого.

Если это все еще неясно, начните рисовать прямоугольники, круги и стрелки на доске, где объекты - это круги, переменные - это поля, а ссылки на объекты - это стрелки от переменных к объектам. Создание псевдонима через ref дает новое имя существующему кругу; Вызов без ref делает второй круг и копирует стрелку. Тогда все это будет иметь смысл.

0 голосов
/ 14 сентября 2018

Когда вы не передаете по ссылке, копия объекта базового класса передается внутри функции, и эта копия изменяется внутри функции TryChangeType.Когда вы печатаете тип экземпляра базового класса, он по-прежнему имеет тип «Base», поскольку копия экземпляра была изменена на класс «Derived».

При передаче по ссылке адресэкземпляра, т. е. сам объект будет передан функции.Таким образом, любые изменения, внесенные в экземпляр внутри функции, являются постоянными.

0 голосов
/ 14 сентября 2018

Это не проблема наследования и полиморфизма, вы видите разницу между передачей по значению и передачей по ссылке.

private void TryChangeType(Base instance)

Параметром экземпляра предыдущего метода будет копия базовой ссылки вызывающего абонента.Вы можете изменить объект, на который есть ссылка, и эти изменения будут видны для вызывающей стороны, поскольку как вызывающая сторона, так и вызываемая сторона ссылаются на один и тот же объект.Но любые изменения самой ссылки (например, указание на новый объект) не влияют на ссылку вызывающего абонента .Вот почему он работает, как и ожидалось, когда вы передаете по ссылке.

0 голосов
/ 14 сентября 2018

Мы знаем, что класс являются ссылочными типами, поэтому в общем случае, когда мы передаем тип, мы передаем ссылку, но есть разница между передачей только b и ref b, что можно понимать как:

  1. В первом случае 1 он передает ссылку по значению , что означает создание отдельного указателя внутри ячейки памяти, теперь, когда объект базового класса назначается объекту производного класса, он начинает указывать на другой объект в памяти, и когда этот метод возвращается, остается только исходный указатель, который обеспечивает тот же экземпляр, что и базовый класс, когда новый созданный указатель отключен для сборки мусора
  2. Однако, когда объект передается как ref , он передает ссылку на ссылку в памяти, которая похожа на указатель на указатель, как двойной указатель в C или C ++, что когда изменения действительно изменяют исходное распределение памяти и, таким образом, вы видите разницу

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

Ниже приведена модификация вашей программы для получения ожидаемого результата в случае 1:

private Base TryChangeType(Base instance)
{
    var d = new Derived();
    instance = d;

    Console.WriteLine(instance.GetType().ToString());

    return instance;
}

private void CallChangeType()
{
    var b = new Base();
    b = TryChangeType(b);
    Console.WriteLine(b.GetType().ToString());
}

Ниже приведены графические ссылки обоих случаев:

enter image description here

...