Какая функция будет вызвана? - PullRequest
0 голосов
/ 06 сентября 2018

Ранее я задавал вопрос, на который ответили не полностью, поэтому я решил переформулировать свой вопрос, чтобы понять, что происходит:

Вот моя иерархия классов:

interface I
{
    void f();
}

class A : I
{
    // non virtual method
    public void f()
    {
        Debug.Log("---->> A ");
    }
}

class B : A
{
    // non overriding but hiding class A method
    public void f()
    {
        Debug.Log("---->> B ");
    }
}

class C : I
{
    // non virtual method
    public void f()
    {
        Debug.Log("---->> C ");
    }
}

Вот код выполнения:

Random rnd = new Random();
var randomI = rnd.Next(0, 2);

I i = null;
if (randomI == 0)
{
     i = new B(); 
}
else
{
    i = new C();
}
i.f(); 

Как сейчас, он будет либо выводить A, либо C. Он не будет выводить B.

Вот вопрос: не могли бы вы объяснить, как принимается решение, какую функцию вызывать, покрывая эти шаги?

  1. Когда принимается решение, какую функцию вызывать - время выполнения или время компиляции?
  2. Какой механизм, как решить, какую функцию вызывать?

Ответы [ 4 ]

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

Я думаю, что это особенно вероятно, чтобы сбить с толку людей, которые (как и я) узнали о виртуальных и не виртуальных методах в C ++ и об интерфейсах в Java, а позже должны были выяснить, как эти два могут взаимодействовать в C #.

Диспетчеризация методов через ссылку на интерфейс в Java (сравнительно) проста, поскольку все методы в Java являются виртуальными. Поэтому, конечно, тип среды выполнения экземпляра решает, что вызывается, и это все ... в Java .

Это означает, что некоторые более сложные отношения между экземпляром, именем метода и реализацией недоступны в Java, что в зависимости от вашей точки зрения может рассматриваться как «хорошая вещь» или «плохая вещь».

Несмотря на это, это одно из больших различий между Java и C #, потому что методы не обязательно должны быть виртуальными в C #, они не являются по умолчанию. И не вдаваясь в детали реализации, это приводит к ключевому пониманию:

Если метод не помечен virtual в родительском классе, вы не можете пометить соответствующий (тот же имя и подпись) метод в дочернем классе как override. И отдельно, если метод в дочернем классе не помечен как override, то он ведет себя как new независимо от того, был ли он виртуальным в базовом классе .

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

НО

Это не имеет значения. Даже если это так, в C # virtual не означает, что реализация дочернего класса должна переопределить метод базового класса; это просто означает, что может, если объявлено об этом. А если явно не объявить метод базового класса virtual, вы не сможете скомпилировать дочерний класс с методом, который объявляет себя переопределением этого метода.


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

public void myMethod(I i) {
    i.f();
}

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

myMethod(new A());

компилятор не знает, что f() определенно нужно будет разрешить. Таким образом, компилятор, обрабатывающий этот второй блок, может настроить информацию, которая будет использоваться выходными данными компиляции первого блока, при принятии решения о том, как отправлять любой данный метод.

Но, в конечном счете, решение о том, чтобы фактически выполнить данную реализацию метода, не может быть принято до времени выполнения; принимает ли это форму виртуальной отправки, или какое-то чудовище, основанное на отражении, или что-то в этом роде.

На уровне языка это всего лишь детали реализации. Указанное поведение является тем, что важно, и именно здесь отношение между ключевыми словами virtual и override становится ключевым.

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

Во время компиляции он связывает вызов с интерфейсом I, а затем во время выполнения вызывает метод верхнего уровня в цепочке наследования, который реализует I.f().

Итак, в вашем коде это

A a = new A();
a.f();

B b = new B();
b.f();

заставит компилятор выполнить следующие инструкции:

  • создать экземпляр класса A и присвоить "a"
  • возьмите объект, присвоенный "a", и вызовите метод, который находится на вершине цепочки наследования и который реализует A.f () . в данном случае это само A.f ()
  • создать экземпляр класса B и назначить
  • взять объект, присвоенный "a", и вызвать метод, который находится на вершине цепочки наследования и который реализует B.f () . в данном случае это само B.f ()

, что приводит к "A" и "B".

Однако, когда вы делаете это:

I i;
i = new B();
i.f();

вы заставите его скомпилировать следующие инструкции:

  • объявить переменную "i"
  • создать новый объект B и присвоить его «i»
  • возьмите объект, присвоенный "i", и вызовите метод, который находится на вершине цепочки наследования и который реализует I.f () . Это A.f (), потому что класс B не реализует интерфейс I

В строке i.f() он не знает, что new B() был присвоен i, его можно было бы передать откуда-то еще. Он просто знает, что существует некоторый экземпляр абстрактного объекта, который реализует I, и ему нужно вызвать его метод f().

Вы можете думать о new методах, как методы с другим именем :

public class B : A
{
    // non overriding but hiding class A method
    public void anotherName()
    {
       Debug.Log("---->> B ");
    }
} 

A a = new A();  
a.f();
B b = new B();
b.anotherName();

I i = new B();
i.f(); // this will obviously call the `A.f()` because there is no such method in B

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

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

Когда принимается решение, какую функцию вызывать - во время выполнения или компиляции время

В время компиляции компилятор определяет, что A.f - это метод для вызова, если кто-то приводит B к I и вызывает f. В runtime он вызывает этот метод, если задействован экземпляр B (например, экземпляр C). Другими словами, ключевое решение принимается во время компиляции.

Обратите внимание, что если метод был virtual, то посмотрите ответ @ YeldarKurmangaliyev о том, как он вызывает «метод top в цепочке наследования» (но это не тот сценарий).

Какой механизм, как решить, какую функцию вызывать?

Соответствующей частью спецификации является 13.4.5 Наследование реализации интерфейса :

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

Вот почему class B : A показывает A , а class B : A, I показывает B . Поскольку с последним вы явно переопределяете интерфейс.

Пример из спецификации (в основном это ваш сценарий):

Класс наследует все реализации интерфейса, предоставляемые его базой классы. Без явной повторной реализации интерфейса, производный класс никак не может изменить отображения интерфейса, которые он наследует от его базовые классы. Например, в объявлениях

interface IControl
{
    void Paint();
}
class Control: IControl
{
    public void Paint() {...}
}
class TextBox: Control
{
    new public void Paint() {...}
}

метод Paint в TextBox скрывает метод Paint в Control, но не изменяет отображение Control.Paint на IControl.Paint и вызовы Paint через экземпляры класса и экземпляры интерфейса имеют следующие эффекты

Control c = new Control();
TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint();          // invokes Control.Paint();
t.Paint();          // invokes TextBox.Paint();
ic.Paint();         // invokes Control.Paint();
it.Paint();         // invokes Control.Paint();

В спецификации также говорится об использовании virtual (что является более распространенным решением, чем явное указание, что B реализует I):

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

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

ИМХО, интересная часть почему: ((I)new B()).f() печатает

---->> A

Приведение B к I, будет использоваться метод базового класса. Если вы хотите напечатать ---->> B вместо этого и сохранить ветвление if / else, вам придется привести i к B, что вызовет явную реализацию B:

if (i is B)
   ((B)i).f();
else
   i.f();

При приведении к I вот что происходит с вашими объявлениями классов:

I -> A -> B
|_ f() is implemented in subclasses, let's go one step down;

I -> A -> B
     |_ f() is found, let's call A's f();

Если вы хотите, чтобы приведение вызывало реализацию B, сделайте так, чтобы B реализовал I напрямую:

class B : A, I

Таким образом, при наложении на I это произойдет:

// Paths from I to B
I -> A -> B 
I -> B // Shorter path, let's go via this one.

I -> B
|_ f() is implemented in subclasses, let's go one step down;

I -> B
     |_ f() is found, let's call B's f();

Конечно, это более простая версия того, что действительно происходит, но это помогает понять концепцию

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