Как правильно работать с различным поведением на основе полиморфизма

Предположим, у меня есть интерфейс IFoo с классами реализации VideoFoo, AudioFoo и TextFoo. Предположим далее, что я не могу изменить ни один из этих кодов. Предположим, что затем я хотел бы написать функцию, которая действует по-разному в зависимости от типа IFoo во время выполнения, например

Public Class Bar
    Public Shared Sub Fix(ByVal Foo as IFoo)
        If TypeOf Foo Is VideoFoo Then DoBar1()
        If TypeOf Foo Is AudioFoo Then DoBar2()
        If TypeOf Foo Is TextFoo Then DoBar3()

    End Sub
End Class

Я хотел бы реорганизовать это, чтобы использовать перегруженные методы:

Sub DoBar(ByVal foo as VideoFoo)
Sub DoBar(ByVal foo as AudioFoo)
Sub DoBar(ByVal foo as TextFoo)

Но единственный способ сделать что-то подобное я вижу, это написать

Sub DoBar(ByVal foo as IFoo)

Затем мне нужно снова выполнить «If TypeOf ... Is». Как я могу реорганизовать это, чтобы воспользоваться преимуществами полиморфизма реализаций IFoo без ручной проверки типов?

(в VB.NET, хотя мой вопрос относится и к С#)


person Patrick Szalapski    schedule 24.03.2011    source источник
comment
Почему бы не иметь перегрузки для Fix?   -  person Blorgbeard    schedule 24.03.2011
comment
Поскольку у меня есть экземпляр типа IFoo; Если я просто добавлю другие перегрузки в Class Bar, мой код всегда будет вызывать Fix(IFoo), а не конкретное Fix(...), которое мне нужно.   -  person Patrick Szalapski    schedule 24.03.2011


Ответы (3)


Что ж, один из вариантов — просто перегрузить метод Fix(), чтобы у вас была одна перегрузка для каждого типа, реализующего IFoo. Но я подозреваю, что вы хотите принять интерфейс напрямую, а не реализовывать типы.

На самом деле вам нужна множественная отправка. Обычно C# /VB использует типы аргументов для выполнения разрешения перегрузки во время компиляции и динамической отправки вызова на основе типа среды выполнения экземпляра, для которого вызывается метод. Что вам нужно, так это выполнять разрешение перегрузки во время выполнения на основе времени выполнения типов аргументов — функция, которая напрямую не поддерживается ни в VB.NET, ни в C#.

Раньше я обычно решал такие проблемы, используя словарь делегатов, проиндексированный System.Type:

private readonly Dictionary<Type,Action<IFoo>> _dispatchDictionary;

static Bar()
{
    _dispatchDictionary.Add( typeof(TextFoo),  DoBarTextFoo );
    _dispatchDictionary.Add( typeof(AudioFoo), DoBarAudioFoo );
    _dispatchDictionary.Add( typeof(VideoFoo), DoBarVideoFoo );        
}

public void Fix( IFoo foo )
{
   Action<IFoo> barAction;
   if( _dispatchDictionary.TryGetValue( foo.GetType(), out barAction ) )
   {
      barAction( foo );
   }
   throw new NotSupportedException("No Bar exists for type" + foo.GetType());
}

private void DoBarTextFoo( IFoo foo ) { TextFoo textFoo = (TextFoo)foo; ... }
private void DoBarAudioFoo( IFoo foo ) { AudioFoo textFoo = (AudioFoo)foo; ... }
private void DoBarVideoFoo( IFoo foo ) { VideoFoo textFoo = (VideoFoo)foo; ... }

Однако, начиная с C# 4, теперь мы можем использовать ключевое слово dynamic в C#, по существу делая то же самое (VB.NET пока не имеет этой функции):

public void Fix( IFoo foo )
{
    dynamic dynFoo = foo;
    dynamic thisBar = this;

    thisBar.DoBar( dynFoo ); // performs runtime resolution, may throw
}

private void Dobar( TextFoo foo ) { ... /* no casts needed here */ }
private void Dobar( AudioFoo foo ) { ... }
private void Dobar( VideoFoo foo ) { ... }

Обратите внимание, что использование ключевого слова dynamic таким образом имеет свою цену — требуется, чтобы сайт вызова обрабатывался во время выполнения. По сути, он запускает версию компилятора C# во время выполнения, обрабатывает метаданные, захваченные компилятором, выполняет анализ типов во время выполнения и выдает код C#. К счастью, DLR обычно может эффективно кэшировать такие сайты вызовов после их первого использования.

Как правило, я нахожу оба эти шаблона запутанными и излишними в большинстве ситуаций. Если количество подтипов невелико и все они известны заранее, простой блок if/else может оказаться более полезным. проще и понятнее.

person LBushkin    schedule 24.03.2011
comment
Я не думаю, что множественная отправка является ответом на этот вопрос. Похоже, что Патрик переключается только на основе одного типа, а не двух. - person Daniel T.; 25.03.2011
comment
На самом деле VB.Net имел разрешение во время выполнения с самой первой версии, при условии, что вы используете Option Strict Off. Большинство людей рекомендуют ограничить Option Strict Off как можно меньшим количеством исходных файлов, потому что нет прямого эквивалента dynamic — вы не можете ограничить динамическое разрешение и неявное приведение к определенным переменным, это относится ко всему исходному файлу. - person MarkJ; 25.03.2011
comment
... Запросы точного эквивалента ключевого слова dynamic в VB.Net не соответствуют большая поддержка от Microsoft :( - person MarkJ; 25.03.2011
comment
Я даю вам ответ с НАСТОЯЩЕЙ многократной отправкой ДО ответа этого парня (в котором есть имитационная отправка и вопиющая копия моего ответа). - person Martin Doms; 30.03.2011

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

К сожалению, C# и VB.NET являются языками с единой диспетчеризацией, что означает, что перегрузка метода выбирается во время компиляции. Это означает, что перегрузка для объекта IFoo всегда будет выбираться для IFoo, независимо от типа его реализации.

Однако есть способы обойти это. Один из способов — использовать шаблон проектирования «Посетитель» для реализации двойной отправки, что будет работать. В C# вы также можете использовать новое ключевое слово dynamic, чтобы заставить среду выполнения разрешить перегрузку во время выполнения. Я написал блог запись о том, как выполнять обработку столкновений с помощью этой техники, но она, безусловно, применима и к тому, что вы делаете.

Я не очень хорошо знаком с VB.NET, но я считаю, что язык демонстрирует некоторые динамические поведения по умолчанию, если объекты преобразуются в Object. Кто-нибудь, пожалуйста, поправьте меня, если это неправильно.

person Martin Doms    schedule 24.03.2011
comment
Да, я хотел бы избежать динамического поведения, поэтому мой вопрос одинаков для C# и VB.NET. - person Patrick Szalapski; 24.03.2011
comment
VB.Net можно использовать динамически, если вы используете Option Strict Off. Я рекомендую использовать Option Strict On в большей части вашего кода и ограничивать Option Strict Off исходными файлами, где это особенно необходимо для чего-то подобного. - person MarkJ; 25.03.2011

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

Я не знаю VB.net, но не могу не задаться вопросом, почему бы вам просто не создать подкласс каждого из текущих классов (и интерфейса) и не поместить свой новый метод Fix в подклассы. . Весь ваш новый код, который хочет отправить сообщение об исправлении, должен принимать IFixFoo вместо IFoo.

Если вы хотите вызывать Fix для объектов IFoo, которые вы не создавали, вам нужен метод, который может создать правильный IFixFoo. Используя вышеизложенное, у вас есть только одно место, где вы должны выполнить If TypeOf ... Is (когда вы фактически конвертируете IFoo в IFixFoo.

person Daniel T.    schedule 25.03.2011