Подробнее о виртуальных / новых ... плюс интерфейсах! - PullRequest
4 голосов
/ 15 января 2010

Вчера я опубликовал вопрос о новых / виртуальных / переопределенных ключевых словах и многому научился из ваших ответов. Но я все еще сомневаюсь.

Между всеми "ящиками" я потерял связь с тем, что действительно происходит с таблицами методов типа. Например:

interface I1 { void Draw(); }
interface I2 { void Draw(); }

class A : I1, I2
{
    public void Minstance() { Console.WriteLine("A::MInstance"); }
    public virtual void Draw() { Console.WriteLine("A::Draw"); }
    void I2.Draw() { Console.WriteLine("A::I2.Draw"); }
}
class B : A, I1, I2
{
    public new virtual void Draw() { Console.WriteLine("B::Draw"); }
    void I1.Draw() { Console.WriteLine("B::I1.Draw"); }
}

class Test
{

    public static void Main()
    {
        A a = new B();
        a.Draw();
        I1 i1 = new A();
        i1.Draw();
        I2 i2 = new B();
        i2.Draw();
        B b = (B)a;
        b.Draw();
    }

}
}

Вопрос, который задают в этом упражнении: Заполните таблицы методов типов в соответствии с кодом и объясните результат, полученный при выполнении Main ().

Мой ответ был: В типе A у нас есть 3 метода: MInstance (), Draw () - версия A :: Draw - и I2 :: Draw В типе B у нас есть 4 метода: MInstance из A, B :: Draw, I1 :: Draw и I2 :: Draw

Я не очень уверен в своем ответе, и именно поэтому я публикую этот вопрос. Когда мы реализуем интерфейсы, в таблице методов создается новый слот для методов указанного интерфейса? разве мы не должны реализовывать I2 :: Draw в классе A?

Точно так же, когда мы вызываем метод с использованием интерфейсной переменной (например, i1.Draw ()), я понимаю, что мы находимся в динамической диспетчеризации, и поэтому мы должны смотреть на тип объекта, удерживаемого переменной (тип A). в этом случае) и найдите в таблице методов А метод, называемый конкретно I1.Draw. Но что, если мы не найдем это? Как мне поступить в этих случаях? Есть ли какое-то практическое правило, о котором я должен знать, чтобы успешно решать эти проблемы?

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

ура!

Ответы [ 6 ]

5 голосов
/ 15 января 2010

Хороший вопрос.

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

  • Интерфейс I1 имеет слот, который мы назовем I1SLOT.
  • Интерфейс I1 имеет слот, который мы назовем I2SLOT.
  • Класс A имеет два собственных слота, AMinSLOT и ADrawSLOT.
  • В классе A есть три метода, которые мы будем называть AMinMethod, ADrawMethod и AI2DrawMethod.
  • Когда вы говорите «новый A», среда выполнения имеет четыре слота для заполнения.
  • I1SLOT заполнен ADrawMethod.
  • I2SLOT заполнен AI2DrawMethod.
  • AMinSLOT заполняется с помощью AMinMethod.
  • ADrawSLOT заполняется с помощью ADrawMethod.
  • Класс B имеет три слота. Он наследует AMinSLOT и ADrawSLOT и определяет новый слот, BDrawSLOT.
  • В классе B есть два метода, BDrawMethod и BI1DrawMethod.
  • Когда вы говорите «new B», среда выполнения имеет пять слотов для заполнения.
  • I1SLOT заполняется BI1DrawMethod.
  • I2SLOT заполняется BDrawMethod.
  • AMinSLOT заполняется с помощью AMinMethod.
  • ADrawSLOT заполнен ADrawMethod.
  • BDrawSLOT заполняется методом BDrawMethod.

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

  • Когда вы вызываете Draw для объекта типа A времени компиляции, наилучшим совпадением является ADrawSLOT.
  • Когда вы вызываете Draw для объекта типа B во время компиляции, наилучшим совпадением является BDrawSLOT.
  • Когда вы вызываете Draw для объекта типа I1 времени компиляции, наилучшим совпадением будет I1SLOT.
  • Когда вы вызываете Draw для объекта типа I2 времени компиляции, наилучшим совпадением будет I2SLOT.

И компилятор генерирует код, который говорит: «вызовите любой метод, который находится в выбранном слоте во время выполнения».

Подводя итог:

A a1 = new A();
A a2 = new B();
B b = new B();
(a1 as A).Draw();  // ADrawSLOT contains A::Draw
(a1 as I1).Draw(); // I1SLOT    contains A::Draw
(a1 as I2).Draw(); // I2SLOT    contains A::I2.Draw
(a2 as A).Draw();  // ADrawSLOT contains A::Draw
(a2 as B).Draw();  // BDrawSLOT contains B::Draw
(a2 as I1).Draw(); // I1SLOT    contains B::I1.Draw
(a2 as I2).Draw(); // I2SLOT    contains B::Draw
(b as A).Draw();   // ADrawSLOT contains A::Draw
(b as B).Draw();   // BDrawSLOT contains B::Draw
(b as I1).Draw();  // I1SLOT    contains B::I1Draw
(b as I2).Draw();  // I2SLOT    contains B::Draw

Если вам интересно, как это реализовано, используйте ILDASM для дизассемблирования вашей программы, а затем посмотрите на таблицу метаданных 25 (0x19), таблицу MethodImpl. MethodImplTable этой программы:

1 == 0:TypeDef[2000004], 1:MethodDefOrRef[06000005], 2:MethodDefOrRef[06000002]
2 == 0:TypeDef[2000005], 1:MethodDefOrRef[06000008], 2:MethodDefOrRef[06000001]

Затем вы можете посмотреть в таблицах typedef и methoddef, и вы увидите, что это декодируется как:

in type A the method A::I2.Draw implements the method I2::Draw
in type B the method B::I1.Draw implements the method I1::Draw

Таблица MethodImpl - это то, как CLI представляет понятие «мне нужно вставить в этот слот что-то, что отличается от того, что выбирают обычные правила сопоставления имен». Обычно правила сопоставления имен выбирают метод «Draw» для добавления в этот слот, а не «I1.Draw» или «I2.Draw».

Возможно, вы также захотите прочитать раздел 22.27 раздела II спецификации CLI.

4 голосов
/ 15 января 2010

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

В принципе, есть 3 варианта, которые я могу придумать:

  1. Если SubClass не определяет метод в BaseClass, метод BaseClass будет вызываться
  2. Если SubClass ПЕРЕИГРЫВАЕТ метод на BaseClass, то метод Подкласса будет вызываться
  3. Если SubClass определяет метод NEW, который ТАКЖЕ существует в BaseClass, то вызываемый метод будет зависеть от ОТНОШЕНИЯ к объекту (указана ли ваша переменная как BaseClass или SubClass)

Надеюсь, я понял ваш вопрос

edit: краткое примечание об интерфейсах: если BaseClass реализует IInterface, то SubClass, производный от BaseClass, уже имеет реализацию IInterface и не нуждается в его повторной реализации. Например. если есть IVehicle со свойством MPH, и есть BaseVehicle, который реализует это, поскольку Car наследуется от BaseVehicle, Car уже имеет свойство MPH

1 голос
/ 15 января 2010
interface I1 { void Draw(); }
interface I2 { void Draw(); }

class A : I1, I2
{
    // this is just a method in A
    public void Minstance() { Console.WriteLine("A::MInstance"); }

    // method in A, also implements I1.Draw. May be overridden in
    // derived types.
    public virtual void Draw() { Console.WriteLine("A::Draw"); }

    // implements I2.Draw, accessible on object a of type A via ((I2)a).Draw()
    void I2.Draw() { Console.WriteLine("A::I2.Draw"); }
}
class B : A, I1, I2
{
    // new method in B, does not override A.Draw, so A.Draw is only
    // callable on an object b of type B via ((A)b).Draw(). Types
    // derived from B may override this method, but can't override
    // A.Draw because it's hidden. Also implements I2.Draw (see notes).
    public new virtual void Draw() { Console.WriteLine("B::Draw"); }

    // implements I1.Draw, accessible on object b of type B via ((I1)b).Draw()
    void I1.Draw() { Console.WriteLine("B::I2.Draw"); }
}

Примечания и справка: Мне пришлось вернуться к стандарту (ECMA-334) для этого, и он найден в §20.4.4 Повторная реализация интерфейса:

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

Повторная реализация интерфейса следует точно таким же правилам отображения интерфейса, что и первоначальная реализация интерфейса. Таким образом, унаследованное отображение интерфейса никак не влияет на отображение интерфейса, установленное для повторной реализации интерфейса. [ Пример : в объявлениях

interface IControl
{
    void Paint();
}
class Control: IControl
{
    void IControl.Paint() {…}
}
class MyControl: Control, IControl
{
    public void Paint() {}
}

тот факт, что Control отображает IControl.Paint на Control.IControl.Paint, не влияет на повторную реализацию в MyControl, которая отображает IControl.Paint на MyControl.Paint. конец примера ]

Унаследованные публичные объявления членов и унаследованные явные объявления членов интерфейса участвуют в процессе отображения интерфейса для повторно реализованных интерфейсов. [ Пример * * тысяча двадцать-пять: * 1 026 *

interface IMethods
{
    void F();
    void G();
    void H();
    void I();
}
class Base: IMethods
{
    void IMethods.F() {}
    void IMethods.G() {}
    public void H() {}
    public void I() {}
}
class Derived: Base, IMethods
{
    public void F() {}
    void IMethods.H() {}
}

Здесь реализация IMethods в Derived отображает методы интерфейса на Derived.F, Base.IMethods.G, Derived.IMethods.H и Base.I. конец примера ]

Когда класс реализует интерфейс, он неявно также реализует все базовые интерфейсы этого интерфейса. Аналогично, повторная реализация интерфейса также неявно представляет собой повторную реализацию всех базовых интерфейсов интерфейса. [* +1041 * Пример * +1042 *:

interface IBase
{
    void F();
}
interface IDerived: IBase
{
    void G();
}
class C: IDerived
{
    void IBase.F() {…}
    void IDerived.G() {…}
}
class D: C, IDerived
{
    public void F() {…}
    public void G() {…}
}

Здесь повторная реализация IDerived также повторно реализует IBase, отображая IBase.F на D.F. конец примера ]

1 голос
/ 15 января 2010

«Таблица методов»? Нет, это просто контракт, который должен быть выполнен. Контракты I1 и I2 могут быть удовлетворены одним и тем же методом Draw, и будут, если вы не отделите один с неявной реализацией, как в данном случае. Без этого подойдет один метод Draw.

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

0 голосов
/ 06 августа 2010

Сначала объяснение нового и виртуального

New

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

, например

A a2= new B() (creates a new object using constructor of B, because b is of type A, therefore it works )
and a2.draw() will result in execution of A class draw because the type is A.

Виртуальный

  1. Это устанавливает, что метод является виртуальным (то есть не постоянным или физическим) и должен быть переопределен, то есть его определение должно быть предоставлено в базовом классе.

1020 * переопределение *

  1. Ключевое слово, используемое для обозначения того, что вы хотите переопределить этот метод и т. Д.
0 голосов
/ 15 января 2010

В дополнение к другим ответам я публикую правильный ответ:

            A a = new B();
            a.Draw(); //A::Draw

            I1 i1 = new A();
            i1.Draw(); //A::Draw

            I2 i2 = new B();
            i2.Draw();// B::Draw

            B b = (B) a;
            b.Draw();// B::Draw
...