WedX - журнал о программировании и компьютерных науках

Использование перегруженной функции для перебора List‹Base›

Учитывая следующую структуру класса:

class Base {}
class DerivedA: Base {}
class DerivedB: Base {}
class Container
{
    public List<Base> Items { get; }
}

где список производных объектов со временем растет по мере расширения функциональности моего приложения. Я пытаюсь сделать:

Container container = ReturnsContainer();
foreach (var item in container.Items)
{
    doStuff(item);
}

где doStuff(item) перегружен производным типом.

void doStuff(DerivedA) {}
void doStuff(DerivedB) {}

Это не работает, так как компилятор говорит, что «не может преобразовать из «Base» в «DerivedA» и «Лучший перегруженный метод, соответствующий doStuff (DerivedA), имеет некоторые недопустимые аргументы». Лучший подход, который я могу придумать, это:

Container container = ReturnsContainer();
foreach (var item in container.Items)
{
    if (item is DerivedA)
    {
        doStuff((DerivedA)item);
    }
    else if (item is DerivedB)
    {
        doStuff((DerivedB)item);
    }
}

Любые мысли о том, как я могу сделать это чище? Поскольку мои производные типы со временем будут только расти, мне придется вернуться и добавить к этой длинной структуре if, чтобы продолжать эту работу.

Я думаю, что решение container.Items.OfType‹> приведет к увеличению (и ненужному) снижению производительности по мере увеличения числа производных типов.

Должен ли я использовать общие делегаты для этого? Похоже на слишком сложное решение для того, что чувствуется как должно быть простым полиморфизмом.

Изменить. Для большей ясности предположим, что иерархия базовых и производных типов находится на отдельном уровне API и не может быть изменена (конечные классы). Решение должно работать с существующей структурой наследования и не может расширять базовые и производные объекты. Хотя со временем на уровне API могут появляться новые производные классы.


  • Это далеко от простого полиморфизма. Это связано с ковариантностью и контравариантностью (для которых я всегда путаю значение каждого из них). Попробуйте погуглить эти 2 темы! 28.11.2012
  • @Jamiec, верно. Это похоже на полиморфизм, но это не так. Я не могу найти пример, который точно соответствует моей ситуации. Подход с делегированием кажется наиболее подходящим (попробую сейчас), но я стараюсь не делать свой код трудным для чтения. 28.11.2012
  • @Jeremy Это не полиморфизм, потому что вы его инвертировали. Это похоже на полиморфизм, потому что это то, что вы должны использовать, вы просто не используете. 28.11.2012

Ответы:


1

Здесь есть несколько вариантов, но на самом деле все сводится к тому, как doStuuf узнать, что делать, если в будущем вы собираетесь добавлять новые производные типы?

Итак, один из вариантов: если Container действительно содержит только 1 тип за раз, вы можете сделать его универсальным:

class Container<T> where T: Base
{
    public List<T> Items { get; }
}

Теперь, когда вы выполняете итерацию, вы знаете, что Items — это список из DerivedA или DerivedB.

Container<DerivedA> container = ReturnsContainerOfDerivedA();
foreach (var item in container.Items)
{
    doStuff(item); // item is definately DerivedA
}

Это означало бы, что у вас либо есть несколько методов doStuff

public void doStuff(DerivedA item){...}
public void doStuff(DerivedB item){...}

или, если более уместно, вы можете иметь 1, который берет базу

public void doStuff(Base item){...}

В этом преимущество простого полиморфизма!


Второй вариант — определить doStuff как принимающий dynamic, если вы используете C#3.5.

public void doSomething(dynamic item){..}

Но лично я бы избегал этого варианта!

28.11.2012
  • Попробовал это только сейчас, однако я надеялся сохранить doStuff(DerivedA) и doStuff(DerivedB), каждый из которых обрабатывает каждый производный тип по-разному. Динамический объект означал бы, что мне пришлось бы сегментировать мою реализацию doStuff() через отражение, что также привело бы к такому же количеству условных ветвлений. 28.11.2012
  • @Jeremy - если это неясно, второй вариант (с использованием динамического) совершенно не связан с первой половиной этого ответа. Если вы используете общий контейнер, вам понадобятся разные методы doStuff(DynamicA) и doStuff(DynamicB). Смотрите обновление. 28.11.2012
  • Я понимаю. Похоже, фильтрация по типу — это действительно правильный путь. 28.11.2012
  • @Jeremy, нет, определение этого поведения на Base - правильный путь. Смотрите ответ @Servy. Все, что вы делаете, это просто взлом! 28.11.2012
  • Я бы с удовольствием взломал это... Мои исследования говорят мне, что есть что-то вонючее в реализации API (окончательная базовая и производная иерархия), но пока я мало что могу с этим поделать. 28.11.2012
  • Справедливо, но, по крайней мере, теперь вы понимаете проблему. 28.11.2012

  • 2

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

    Например. реализовать две перегрузки:

    void doStuff(DerivedA a) {
      ...
    }
    
    void doStuff(DerivedB b) {
      ...
    }
    

    И вызовите их из цикла:

    foreach (dynamic item in container.Items)
      doStuff(item);
    
    28.11.2012
  • Проблема в том, что у меня ограниченный доступ к классам Base и Derived. Они находятся на другом уровне, и я ищу решение, которое позволит мне оставить структуру типов без изменений. 28.11.2012
  • Если вы не можете изменить классы, вам в какой-то момент придется разветвить свой код на фактический тип элемента в списке. Есть несколько более элегантные способы сделать это. Например, см. этот вопрос: stackoverflow.com/questions/156467/switch- идея сопоставления шаблонов 28.11.2012

  • 3

    Пользователь объекта Base должен знать, какой это тип объекта Base. В таком случае совершенно ясно, что Base, вероятно, должен быть абстрактным, и у него должен быть абстрактный метод, определяющий любое поведение, которое doStuff требуется от объекта Base. (Если сделать Base abstract не вариант, вы можете определить виртуальный метод без тела, предназначенного для переопределения, или вы можете сделать так, чтобы все производные объекты реализовывали интерфейс.)

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

    После того, как вы сделали это, Container не нужно приводить/преобразовывать объект во что-либо, кроме Base, каждый Base уже будет иметь всю информацию, необходимую для одного метода doStuff.

    28.11.2012
  • ОП уже упоминал, что он не может изменить Base и производные классы. 28.11.2012
  • @DominicKexel На самом деле его точными словами было то, что у меня ограниченный доступ к базовым и производным классам. Дело не в том, что он не может изменить их, просто это потребует больше работы. Я не спорю, что сделать это правильно может быть немного сложнее, но это все же правильный способ решения проблемы. 28.11.2012
  • Это 100% правильный ответ имхо 28.11.2012
  • На самом деле он сказал, что они находятся на другом уровне, что может означать, что они являются своего рода DTO на уровне базы данных, а рассматриваемый код находится, например, на графическом интерфейсе или бизнес-уровне. Хотя кажется естественным изменить Base, как вы описали, это может быть плохой идеей с архитектурной точки зрения. 28.11.2012
  • Извините, отредактировал мой вопрос, чтобы выразить это. Этот ответ - это подход, который я бы использовал, если бы не было ограничений слоя. 28.11.2012
  • @Jeremy Если вы не можете изменить Base или любой из производных классов, создайте для них обертки. Make, Base2 DerivedA2 и т. д., которые обертывают базовые классы и предоставляют любые дополнительные функции, которые вам нужны (в основном с использованием шаблона адаптера). Вместо того, чтобы полностью отказаться от правильной иерархии, измените свою проблему на то, как преобразовать то, что у меня есть, в правильную иерархию? 28.11.2012

  • 4

    Самое простое решение (без изменения Base/DerivedA и т. д.) — привести объекты к dynamic. :

    foreach (dynamic item in container.Items)
    {
        doStuff(item);
    }
    

    поэтому фактический метод для вызова определяется во время выполнения.

    28.11.2012
  • прикрутите правильный дизайн, просто используйте dynamic... 28.11.2012
  • На самом деле dynamic не имеет никакого отношения к дизайну. Есть много случаев, когда использование dynamic делает ваш код проще/легче для чтения. 28.11.2012
  • Дизайн полностью связан с использованием надлежащего инструмента для каждого типа задач. dynamic — это один инструмент. Определение того, является ли dynamic подходящим инструментом для текущей работы, всё связано с дизайном. dynamic действительно имеет место; есть задачи, для которых это подходящий инструмент, это просто не одна из них. dynamic — это одна из тех вещей, которые можно использовать гораздо чаще, чем следуетм, и ее часто используют как костыль для тех, кто не может или не хочет использовать более подходящие инструменты. 28.11.2012
  • Я бы сказал, что это самый простой ответ на то, что требует вопрос, но я понимаю, почему это не самое выгодное решение с точки зрения дизайна. 28.11.2012
  • @Servy Не могли бы вы уточнить, почему этот случай не является парадным примером использования dynamic? Не всегда логично помещать каждый фрагмент кода, так или иначе принадлежащий классу, в сам этот класс (и часто вы просто не можете этого сделать). Вот почему другие языки предоставляют лучшие инструменты: посмотрите на F#, где вы могли бы просто использовать match, или на Clojure, где вы, вероятно, просто использовали бы протоколы. 28.11.2012
  • @DominicKexel Итак, рассмотрим случай, когда создается новый производный тип. Вам нужно не только создать новый тип и реализовать любое поведение, которое он имеет в настоящее время, но вам нужно найти какой-то другой произвольный класс в совершенно отдельном API и создать новый метод doStuff, принимающий вновь созданный тип в качестве параметра. Затем вам нужно скопировать/вставить одну из других реализаций (по всей вероятности, в зависимости от того, что она делает) и внести небольшие изменения, основанные на различиях нового типа (какими бы ни были эти различия). Просто знать, что вам нужно это сделать, — кошмар. 28.11.2012
  • @DominicKexel Другая сторона просто вынуждена реализовать абстрактный метод при первом создании типа. 28.11.2012
  • @Servy Вы можете просто определить void doStuff(Base d) { // nothing or default behaviour}. 28.11.2012
  • @Servy С другой стороны, ОП уже несколько раз говорил, что изменение Base не вариант. 28.11.2012
  • Новые материалы

    Как проанализировать работу вашего классификатора?
    Не всегда просто знать, какие показатели использовать С развитием глубокого обучения все больше и больше людей учатся обучать свой первый классификатор. Но как только вы закончите..

    Работа с цепями Маркова, часть 4 (Машинное обучение)
    Нелинейные цепи Маркова с агрегатором и их приложения (arXiv) Автор : Бар Лайт Аннотация: Изучаются свойства подкласса случайных процессов, называемых дискретными нелинейными цепями Маркова..

    Crazy Laravel Livewire упростил мне создание электронной коммерции (панель администратора и API) [Часть 3]
    Как вы сегодня, ребята? В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что..

    Использование машинного обучения и Python для классификации 1000 сезонов новичков MLB Hitter
    Чему может научиться машина, глядя на сезоны новичков 1000 игроков MLB? Это то, что исследует это приложение. В этом процессе мы будем использовать неконтролируемое обучение, чтобы..

    Учебные заметки: создание моего первого пакета Node.js
    Это мои обучающие заметки, когда я научился создавать свой самый первый пакет Node.js, распространяемый через npm. Оглавление Глоссарий I. Новый пакет 1.1 советы по инициализации..

    Забудьте о Matplotlib: улучшите визуализацию данных с помощью умопомрачительных функций Seaborn!
    Примечание. Эта запись в блоге предполагает базовое знакомство с Python и концепциями анализа данных. Привет, энтузиасты данных! Добро пожаловать в мой блог, где я расскажу о невероятных..

    ИИ в аэрокосмической отрасли
    Каждый полет – это шаг вперед к великой мечте. Чтобы это происходило в их собственном темпе, необходима команда астронавтов для погони за космосом и команда технического обслуживания..


    Для любых предложений по сайту: [email protected]