2012-05-24 6 views
6

Если аргумент общего типа (как вызывающего класса, так и метода вызова) ограничен where T : Base, новый метод в T == Derived не вызывается, а вызывается метод в Base.C# & generics - почему метод в базовом классе называется вместо нового метода в производном классе?

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

Обновление: НО, когда ограничение использует интерфейс, как where T : IBase метод в базовом классе вызывается (не метод в интерфейсе, который также невозможно).
Таким образом, система действительно способна обнаруживать типы, которые находятся далеко и выходит за пределы ограничения типа! Тогда почему он не выходит за пределы ограничения типа в случае ограничения типа класса?
Означает ли это, что метод в классе Base, реализующий интерфейс, имеет неявное ключевое слово переопределения для метода?

код теста:

public interface IBase 
{ 
    void Method(); 
} 

public class Base : IBase 
{ 
    public void Method() 
    { 

    } 
} 

public class Derived : Base 
{ 
    public int i = 0; 

    public new void Method() 
    { 
     i++; 
    } 
} 

public class Generic<T> 
    where T : Base 
{ 
    public void CallMethod(T obj) 
    { 
     obj.Method(); //calls Base.Method() 
    } 

    public void CallMethod2<T2>(T2 obj) 
     where T2 : T 
    { 
     obj.Method(); //calls Base.Method() 
    } 
} 

public class GenericWithInterfaceConstraint<T> 
    where T : IBase 
{ 
    public void CallMethod(T obj) 
    { 
     obj.Method(); //calls Base.Method() 
    } 

    public void CallMethod2<T2>(T2 obj) 
     where T2 : T 
    { 
     obj.Method(); //calls Base.Method() 
    } 
} 

public class NonGeneric 
{ 
    public void CallMethod(Derived obj) 
    { 
     obj.Method(); //calls Derived.Method() 
    } 

    public void CallMethod2<T>(T obj) 
     where T : Base 
    { 
     obj.Method(); //calls Base.Method() 
    } 

    public void CallMethod3<T>(T obj) 
     where T : IBase 
    { 
     obj.Method(); //calls Base.Method() 
    } 
} 

public class NewMethod 
{ 
    unsafe static void Main(string[] args) 
    { 
     Generic<Derived> genericObj = new Generic<Derived>(); 
     GenericWithInterfaceConstraint<Derived> genericObj2 = new GenericWithInterfaceConstraint<Derived>(); 
     NonGeneric nonGenericObj = new NonGeneric(); 
     Derived obj = new Derived(); 

     genericObj.CallMethod(obj); //calls Base.Method() 
     Console.WriteLine(obj.i); 

     genericObj.CallMethod2(obj); //calls Base.Method() 
     Console.WriteLine(obj.i); 

     genericObj2.CallMethod(obj); //calls Base.Method() 
     Console.WriteLine(obj.i); 

     genericObj2.CallMethod2(obj); //calls Base.Method() 
     Console.WriteLine(obj.i); 

     nonGenericObj.CallMethod(obj); //calls Derived.Method() 
     Console.WriteLine(obj.i); 

     nonGenericObj.CallMethod2(obj); //calls Base.Method() 
     Console.WriteLine(obj.i); 

     nonGenericObj.CallMethod3(obj); //calls Base.Method() 
     Console.WriteLine(obj.i); 

     obj.Method(); //calls Derived.Method() 
     Console.WriteLine(obj.i); 
    } 
} 

Выход:

0 
0 
0 
0 
1 
1 
1 
2 
+0

Что заставляет вас думать, что интерфейс с ограничениями не вызывает 'IBase.Method'? Попробуйте выполнить литье 'Obj' на' IBase' и вызывать 'Method'. – supercat

+1

Обратите внимание, что это имеет значение, если вместо этого вы пишете 'public class Derived: Base, IBase', т. Е. Повторите интерфейс в объявлении производного класса. Это называется повторной реализацией интерфейса *. [См. Спецификацию] (https://msdn.microsoft.com/en-us/library/aa664594.aspx). –

ответ

7

За исключением случаев использования объектов dynamic, C# всегда связывает методы во время компиляции - даже при использовании дженериков. Вызовы виртуальных методов привязаны к слотам виртуальных методов, а не к методам реализации, поэтому, когда они выполняются над объектами производного класса, они будут направлены на реализации производного класса; хотя методы, для которых точка слотов будет определена во время выполнения, привязка к слотам происходит во время компиляции. Если метод производного класса объявлен new, а не override, код, связанный с использованием производного класса, будет использовать метод производного класса, но код, связанный с использованием базового класса, будет использовать метод базового класса.

Чтобы понять, почему это так, представьте, что это не так. Что произойдет, если класс Base объявляет метод int Foo(), а класс Derived:Base объявляет new string Foo(). Если общий класс с ограничением T:Base пытается вызвать метод Foo на объект типа T, каков должен быть тип возврата этого метода?

+2

Хорошая точка с типом возврата. (Вы можете продолжать использовать привязку к методу и виртуальные вызовы и слоты, но для некоторых из нас нужен очевидный пример нарушения, чтобы понять это.) – Rawling

+0

@Rawling: причина для обсуждения привязки - дать понять, что с точки зрения компилятора общий тип 'T', ограниченный' Foo', во многом будет больше напоминать 'Foo', чем тот, который может быть заменен на' T' во время выполнения. Дженерики в .net похожи на шаблоны C++, но они принципиально разные. – supercat

+0

@supercat: Спасибо, я думаю, если бы не были проблемы с возвращаемыми типами новых методов, тогда дизайн мог быть другим. Это кажется мне ** единственной причиной этого дизайнерского решения. Все, что связано с привязкой, а не перегрузкой и т. Д., Просто указывает на последствия. Я также должен был более четко заявить в вопросе о том, что я знаю о перегрузке. –

6

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

Вы не должным образом переопределяете метод, но вместо этого скрываете «новое», если используете ссылку на базовый класс, вы избегаете любого скрытия. Здесь скрывается спад.

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

var derived = new Derived(); 
var baseRef = (Base)derived; 
baseRef.Method(); // calls Base.Method instead of Derived.Method. 

Чтобы правильно переопределить метод и иметь этот код работает, отметьте метод как virtual в базовом классе и override его в производном классе ,

class Base 
{ 
    public virtual void Method() {} 
} 

class Derived : Base 
{ 
    public override void Method() {} 
} 

Вы можете доказать это, изменить общие ограничения, чтобы быть where T : Derived и он должен ударить «новый» элемент.

0

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

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

0

Ключевое слово new просто скрывает метод, а не перегружает его. Причина, по которой ваш не общий CallMethod работает, как и ожидалось, заключается в том, что подпись метода ожидает Derived вместо Base.

Дженерики здесь действительно не преступники. Если изменить метод подпись CallMethod(Base obj), вы увидите то же самое «неожиданное» поведение как родовая реализация и получить следующий вывод:

0 
0 
0 
0 
0 
0 
0 
1 

Если вы Base.Method виртуальными и переопределить его Derived.Method следующим образом:

public class Base 
{ 
    public virtual void Method() 
    { 

    } 
} 

public class Derived : Base 
{ 
    public int i = 0; 

    public override void Method() 
    { 
     i++; 
    } 
} 

Вы получите следующий результат:

1 
2 
3 
4 
5 
6 
7 
8 

Edit: обновлен для соответствия обновленному выпуску вопроса.