Перегрузки методов позднего связывания C# не работают, если перегрузка определена в производном классе

Мне нужно вызывать перегрузки методов в соответствии с типом объекта во время выполнения, используя функции позднего связывания С#. Он отлично работает, когда все перегрузки определены в том же классе, что и вызов. Но когда перегрузка определена в производном классе, она не будет привязана во время выполнения.

class BaseT
{}

class DerivedA : BaseT
{}

class DerivedB : BaseT
{}

class Generator
{
    public void Generate(IEnumerable<BaseT> objects)
    {
        string str = "";
        foreach (dynamic item in objects)
        {
            str = str + this.Generate(item); //throws an exception on second item
        }
    }

    protected virtual string Generate(DerivedA a)
    {
        return " A ";
    }        
}

class DerivedGenertor : Generator
{
    protected virtual string Generate(DerivedB b)
    {
        return " B ";
    }
}



class Program
{
    static void Main(string[] args)
    {
        List<BaseT> items = new List<BaseT>() {new DerivedA(), new DerivedB()};
        var generator = new DerivedGenertor();
        generator.Generate(items);
    }
}

Вот еще один более наглядный пример:

class BaseT
{}

class DerivedA : BaseT
{}

class DerivedB : BaseT
{}

class DerivedC : BaseT
{ }

class Generator
{
    public void Generate(IEnumerable<BaseT> objects)
    {
        string str = "";
        foreach (dynamic item in objects)
        {
            str = str + this.Generate(item); //throws an exception on third item
        }
    }

    public virtual string Generate(DerivedA a)
    {
        return " A ";
    }

    public virtual string Generate(DerivedC c)
    {
        return " C ";
    }
}

class DerivedGenertor : Generator
{
    public virtual string Generate(DerivedB b)
    {
        return " B ";
    }
}



class Program
{
    static void Main(string[] args)
    {
        List<BaseT> items = new List<BaseT>() {new DerivedA(), new DerivedC(), new DerivedB()};
        dynamic generator = new DerivedGenertor();
        generator.Generate(items);
    }
}

person Siamak S.    schedule 08.09.2015    source источник
comment
DerivedGenertor.Generate — это protected, поэтому он недоступен из класса Generator.   -  person user4003407    schedule 09.09.2015
comment
Что именно является вторым пунктом?   -  person MikeG    schedule 09.09.2015


Ответы (2)


Вам также нужно будет объявить Generator динамическим, чтобы у вас было динамическое разрешение для входного объекта и вызываемого метода. Но для этого вам придется изменить модификаторы доступа на public или protected internal, потому что теперь у вас есть метод с внешним разрешением.

class BaseT
{ }

class DerivedA : BaseT
{ }

class DerivedB : BaseT
{ }

class Generator
{
    public string Generate(IEnumerable<BaseT> objects)
    {
        string str = "";
        dynamic generator = this;
        foreach (dynamic item in objects)
        {
            str = str + generator.Generate(item); 
        }
        return str;
    }

    protected internal virtual string Generate(DerivedA a)
    {
        return " A ";
    }
}

class DerivedGenertor : Generator
{
    protected internal virtual string Generate(DerivedB b)
    {
        return " B ";
    }
}



class Program
{
    static void Main(string[] args)
    {
        List<BaseT> items = new List<BaseT>() { new DerivedA(), new DerivedB() };
        var generator = new DerivedGenertor();
        string ret = generator.Generate(items);
    }
}
person Brian Rudolph    schedule 08.09.2015
comment
Я не понимаю, почему CLR не может найти другую перегрузку, поскольку тип генератора времени выполнения действительно имеет правильную перегрузку. Разве CLR не просматривает все методы, чтобы найти правильный? Это по дизайну? - person Siamak S.; 09.09.2015
comment
@СиамакС. Нет. Это определение разницы между поздним связыванием и ранним связыванием. CLR НИКОГДА не выполняет поиск какими-либо методами. ДЛР делает. Вам нужно явно сказать, что вы хотите позднее связывание, потому что это намного медленнее и более подвержено ошибкам. В момент компиляции (раннее связывание) компилятор знает только о Generate(DerivedA a), поэтому он использует его. - person Aron; 09.09.2015
comment
@Aron Компилятор ничего не связывает заранее, потому что аргумент равен dynamic. Если бы он попытался выполнить привязку во время компиляции, это дало бы ошибку компиляции, поскольку нет подходящего кандидата; не каждый BaseT является DerivedA. Измените dynamic item на var item, и компилятор выдаст вам очевидную ошибку. Тот факт, что ошибка возникает во время выполнения, указывает на позднее связывание. - person InBetween; 09.09.2015
comment
@inBetween Насколько я знаю, объявление его динамическим только изменяет вызовы динамического объекта для поздней привязки. Передача его другому нединамическому методу по-прежнему будет решением во время компиляции. - person Brian Rudolph; 09.09.2015
comment
@BrianRudolph Ты уверен? в общем случае это неверно. Предположим, что Generate имеет две допустимые перегрузки, определенные в базовом классе. Как компилятор решит, какой из двух во время компиляции, если он не знает тип аргумента? В этом случае есть только один допустимый кандидат, поэтому я не совсем уверен, действительно ли компилятор связывает его на ранней стадии и просто выполняет проверку типа во время выполнения, хотя я бы предпочел, чтобы это было не так. - person InBetween; 09.09.2015
comment
@InBetween Согласен. В этом случае я не уверен, почему такое поведение присутствует. Я собираюсь проверить IL, чтобы увидеть, что на самом деле делает компилятор. - person Brian Rudolph; 09.09.2015

Как вы ожидаете, что это будет связано? Компилятор связывает вызов this.Generate(item) с единственным возможным кандидатом: Generator.Generate(DerivedA a). Это не имеет ничего общего с тем, когда происходит связывание; DerivedGenerator.Generate(DerivedB b) не считается допустимым кандидатом, потому что Generator абсолютно не знает о его существовании, и вы вызываете метод через статически типизированный Generator экземпляр this (замечу, что метод protected не является проблемой, даже если это был public второй вызов потерпит неудачу). Зачем базовому классу знать что-либо о новом виртуальном методе, определенном в производном классе?

Чтобы это работало, вы либо определяете virtual Generate(DerivedB) в базовом классе Generator и переопределяете его в DerivedGenerator, либо, если это невозможно, разрешаете все во время выполнения.

В вашем случае, как правильно указывает Брайан, вам нужно будет также сделать экземпляр Generator dynamic, чтобы разрешить привязку вызова Generate к DerivedGenerator.Generate, когда это уместно. В противном случае набор кандидатов будет ограничен только теми, о которых знает Generator.

Это обяжет вас значительно изменить структуру вашего кода, так как вам также нужно будет сделать Generate как минимум internal или internal protected.

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

Я также рекомендую вам прочитать фантастическую серию Эрика Липперта: Волшебники и воины объясняя, почему некоторые иерархии объектов не могут быть хорошо выражены с помощью системы типов C#, и как вы можете (но обычно не должны) использовать dynamic для достижения двойной диспетчеризации в C#, чтобы обойти некоторые из его ограничений.

person InBetween    schedule 08.09.2015
comment
это пример позднего связывания, которое происходит во время выполнения, потому что элемент объявлен в динамическом режиме. Но перегрузки находятся только в том же классе, в котором происходит вызов. Но убедитесь, что связывание происходит во время выполнения, иначе компилятор будет жаловаться, потому что ни одна перегрузка не принимает BaseT. - person Siamak S.; 09.09.2015
comment
@СиамакС. Я отредактировал свой ответ, чтобы прояснить некоторые важные моменты. - person InBetween; 09.09.2015