Как работает метод скрытия в C #? (Часть вторая) - PullRequest
13 голосов
/ 02 апреля 2009

Следующая программа печатает

A:C(A,B)
B:C(A,B)

(как и положено)

public interface I
{
    string A();
}

public class C : I
{
    public string A()
    {
        return "A";
    }

    public string B()
    {
        return "B";
    }
}

public class A
{
    public virtual void Print(C c)
    {
        Console.WriteLine("A:C(" + c.A() + "," + c.B() + ")");
    }
}

public class B : A
{
    public new void Print(C c)
    {
        Console.WriteLine("B:C(" + c.A() + "," + c.B() + ")");
    }

    public void Print(I i)
    {
        Console.WriteLine("B:I(" + i.A() + ")");
    }
}

class Program
{
    public static void Main(string[] args)
    {
        A a = new A();
        B b = new B();
        C c = new C();
        a.Print(c);
        b.Print(c);
    }
}

однако, если я изменю ключевое слово «новый» на «переопределить» в классе B, например:

    public override void Print(C c)

вдруг программа начинает печатать:

A:C(A,B)
B:I(A)

Почему?

Ответы [ 4 ]

8 голосов
/ 02 апреля 2009

Это связано с разрешением перегруженных методов.

Фактически (несколько упрощенно), компилятор сначала просматривает объявленный тип выражения (B) в этом случае и ищет подходящие методы , которые сначала объявлены в этом типе . Если есть какие-либо подходящие методы (то есть, где все аргументы могут быть преобразованы в типы параметров метода), тогда он не смотрит на родительские типы. Это означает, что переопределенные методы, в которых начальное объявление относится к родительскому типу, не проверяются, если в производном типе есть какие-либо «недавно объявленные» соответствующие методы.

Вот немного более простой пример:

using System;

class Base
{
    public virtual void Foo(int x)
    {
        Console.WriteLine("Base.Foo(int)");
    }
}

class Derived : Base
{
    public override void Foo(int x)
    {
        Console.WriteLine("Derived.Foo(int)");
    }

    public void Foo(double d)
    {
        Console.WriteLine("Derived.Foo(double)");
    }
}

class Test
{
    static void Main()
    {
        Derived d = new Derived();
        d.Foo(10);
    }
}

Это печатает Derived.Foo(double) - даже если компилятор знает, что существует метод сопоставления с параметром типа int, и аргумент имеет тип int, и преобразование из int в int равно " лучше, чем преобразование из int в double, тот факт, что только метод Foo(double) изначально объявлен в Derived, означает, что компилятор игнорирует Foo(int).

Это очень удивительно, ИМО. Я понимаю, почему так будет, если Derived не переопределит Foo - иначе введение нового, более конкретного метода в базовый класс может неожиданно изменить поведение - но ясно, что Derived здесь знает о Base.Foo(int), поскольку это переопределяет его. Это один из (относительно немногих) моментов, когда я считаю, что разработчики C # приняли неверное решение.

1 голос
/ 02 апреля 2009

Это был отличный вопрос.
Все ответы можно найти здесь: http://msdn.microsoft.com/en-us/library/6fawty39(VS.80).aspx

Суть этого такова:

... компилятор C # сначала попытается сделать вызов совместим с версиями из [functionName] объявлен изначально [производный класс]. Методы переопределения не считается объявленным в классе, они являются новыми реализациями метод объявлен в базовом классе. Только если компилятор C # не может соответствовать вызов метода оригинального метода на [Производный класс] попытается сопоставить вызов к переопределенному методу с тем же имя и совместимые параметры.

Так как у вас есть новый метод Print (I i) в производном классе, который соответствует аргументу "c" (потому что c реализует I), этот метод имеет приоритет над методом "override".

Когда вы помечаете метод как «новый», они оба считаются реализованными в производном классе, а метод Print (C c) более точно соответствует параметру «c», поэтому он имеет приоритет.

1 голос
/ 02 апреля 2009

Хорошо, так что

    public new void Print(C c)
    {
        Console.WriteLine("B:C(" + c.A() + "," + c.B() + ")");
    }

    public void Print(I i)
    {
        Console.WriteLine("B:I(" + i.A() + ")");
    }

Это объявляет новый метод для печати. Теперь, поскольку B наследуется от A, вы просто вызываете новый метод дважды. Когда вы переопределяете метод, это затем изменяет сигнатуру метода при вызове A, но когда вы вызываете сигнатуру B, тогда у него есть собственная сигнатура метода.

Я не уверен, что объясняю ясный, но хороший вопрос.

с использованием нового:

A и B получают одинаковую реализацию метода Print.

с использованием переопределения:

A имеет сигнатуру метода, отличную от B, поскольку вы не изменили сигнатуру метода в B только в A.

используя новый, он в основном игнорирует это:

    public void Print(I i)
    {
        Console.WriteLine("B:I(" + i.A() + ")");
    }
0 голосов
/ 02 апреля 2009

Это как минимум вопрос о том, как метод перегрузка работает в C #. Я думаю, что вы выделили интересную ситуацию здесь ...

В первом случае (используя ключевое слово new для метода), компилятор решает использовать перегрузку метода Print с параметром типа C, поскольку его тип в точности соответствует типу передаваемого параметра (т.е. неявное преобразование не требуется), тогда как неявное преобразование в интерфейс мне потребовалось бы, если бы компилятор выбрал метод Print, который принимает аргумент типа I - другими словами, он выбирает более «очевидную» перегрузку метода.

Во втором случае (используя ключевое слово override для метода), компилятор решает использовать перегрузку Print с параметром типа I, потому что, хотя вы переопределяете перегрузку метода Print(C c) в классе B , он эффективно определен в родительском классе A, что делает перегрузку метода Print(I i) на самом деле перегрузкой самого высокого уровня и, следовательно, самой прямой, то есть первой, которую компилятор находит.

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

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

...