Вопрос по generics, inheritance, c# – C # & generics - почему метод в базовом классе вызывается вместо нового метода в производном классе?

6

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

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

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

Тестовый код:

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
Что заставляет вас думать, что интерфейс с ограничениями не вызываетIBase.Method? Попробуйте кастингObj вIBase и звонитMethod в теме supercat
Обратите внимание, что если вы напишете вместо этого, это будет иметь значениеpublic class Derived : Base, IBase, то есть повторить интерфейс в объявлении производного класса. Это называется повторная реализация интерфейса. См. Спецификацию. Jeppe Stig Nielsen

Ваш Ответ

4   ответа
7

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

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

Хороший вопрос с типом возврата. (Вы можете продолжить связывание методов, виртуальные вызовы и слоты, но некоторым из нас нужен очевидный пример, чтобы понять это.) Rawling
@ Rawling: причина обсуждения связывания состоит в том, чтобы прояснить, что с точки зрения компилятора универсальный типT ограниченFoo будет во многом вести себя какFoo чем реальный тип, который может быть заменен наT во время выполнения. Обобщения в .net выглядят как шаблоны C ++, но они принципиально отличаются. supercat
@ supercat: Спасибо, я думаю, если бы не было проблем с возвращаемыми типами новых методов, дизайн мог бы быть другим. Это кажется мнене замуже причина этого дизайнерского решения. Все остальное о связывании, а не о перегрузке и т. Д. - это просто следствие Мне также следовало бы более четко сформулировать в вопросе, что я знаю о существовании перегрузки. Roland Pihlakas
@ RolandPihlakas: Я сомневаюсь, что это единственная причина, на самом деле. Даже если бы нужно было сформулировать правила, которые позволили бы компилятору разрешить все потенциальные неоднозначности, возникающие из-за подобных проблем, поведение обобщенных типов, как если бы они были связаны во время выполнения, потребовало бы перекомпиляции каждого универсального метода для каждой комбинации используемых в нем универсальных типов. , Это серьезно повредит производительности, но принесет сравнительно небольшую выгоду. supercat
@ supercat: Да, было бы неизбежно, если бы несколько методов были скомпилированы для разных универсальных типов. Но C ++ тоже это делает, не так ли? Я надеялся, что использование дженериков позволит мне избавиться от виртуальных звонков. Похоже, что это не так, и единственное использование обобщений, связанных с производительностью, состоит в том, чтобы избегать типов значений бокса. Для классов использование общих типов аргументов вместо интерфейсов (или базовых классов с виртуальными методами) не может обеспечить выигрыша в производительности. Roland Pihlakas
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

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

Таким образом, без правильного приведения будет вызван оригинальный метод, если ссылка имеет тип 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

Редактировать обновлено в соответствии с обновленным выводом вопроса.

Похожие вопросы