Использование неявного преобразования в качестве замены множественного наследования в .NET - PullRequest
3 голосов
/ 18 апреля 2010

У меня есть ситуация, когда я хотел бы, чтобы объекты определенного типа могли использоваться как два разных типа. Если бы один из «базовых» типов был интерфейсом, это не было бы проблемой, но в моем случае предпочтительно, чтобы они оба были конкретными типами.

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

Кажется, что это решение хорошо подойдет моим потребностям, но я что-то упустил? Есть ли ситуация, когда это не сработает или когда при использовании API-интерфейса может возникнуть путаница, а не простота?

РЕДАКТИРОВАТЬ: Подробнее о моем конкретном сценарии:

Это для потенциальной будущей редизайн написания индикаторов в RightEdge , который является средой разработки автоматизированной торговой системы. Данные о ценах представляются в виде серии баров, которые имеют значения цен открытия, минимума, максимума и закрытия за определенный период (1 минута, 1 день и т. Д.). Индикаторы выполняют расчеты по серии данных. Примером простого индикатора является индикатор скользящей средней, который выдает скользящую среднюю из самых последних n значений своего входа, где n задается пользователем. Скользящую среднюю можно применить к закрытию бара или к выходу другого индикатора, чтобы сгладить его.

Каждый раз, когда появляется новый бар, индикаторы вычисляют новое значение для их выхода для этого бара.

Большинство индикаторов имеют только одну серию выходов, но иногда удобно иметь более одного выхода (см. MACD ), и я хочу поддержать это.

Таким образом, индикаторы должны быть производными от класса «Компонент», в котором есть методы, вызываемые при поступлении новых данных. Однако для индикаторов, которые имеют только один выходной ряд (а это большинство из них), это будет хорошо для них самим выступать в роли серии. Таким образом, пользователи могут использовать SMA.Current для текущего значения SMA вместо того, чтобы использовать SMA.Output.Current. Аналогично, Indicator2.Input = Indicator1; предпочтительнее, чем Indicator2.Input = Indicator1.Output;. Это может показаться небольшим отличием, но многие наши целевые клиенты не являются профессиональными разработчиками .NET, поэтому я хочу сделать это как можно проще.

Моя идея заключается в неявном преобразовании индикатора в его выходные серии для индикаторов, которые имеют только одну выходную серию.

Ответы [ 6 ]

4 голосов
/ 18 апреля 2010

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

Взгляните на основные отличия:
Если у вас есть базовый тип B и производный тип D, присваивание будет следующим:

B my_B_object = my_D_object;

назначает ссылку на тот же объект. С другой стороны, когда B и D являются независимыми типами с неявным преобразованием между ними, указанное выше присваивание создаст копию из my_D_object и сохранит ее (или ссылку на нее, если B это класс) на my_B_object.

Таким образом, «настоящее» наследование работает по ссылке (изменения в ссылке влияют на объект, общий для многих ссылок), тогда как пользовательские преобразования типов обычно работают по значению (это зависит от того, как вы его реализуете, но реализуете что-то близкое к поведение "по ссылке" для конвертеров было бы почти безумным): каждая ссылка будет указывать на свой собственный объект.

Вы говорите, что не хотите использовать интерфейсы, но почему? Использование комбинированного интерфейса + вспомогательный класс + методы расширения (требуются C # 3.0 и .Net 3.5 или более поздняя версия) могут приблизиться к реальному множественному наследованию. Посмотрите на это:

interface MyType { ... }
static class MyTypeHelper {
    public static void MyMethod(this MyType value) {...}
}

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

Они не будут вести себя как виртуальные методы из коробки; но вы можете использовать отражение, чтобы достичь этого; вам нужно будет сделать следующее в реализации класса Helper:

  1. восстановить System.Type с помощью value.GetType()
  2. найти, если этот тип имеет метод, соответствующий сигнатуре
  3. если вы найдете подходящий метод, вызовите его и вернитесь (так что остальная часть метода Помощника не будет запущена).
  4. Наконец, если вы не нашли конкретной реализации, позвольте остальной части метода работать и работать как "реализация базового класса".

Вот и все: множественное наследование в C # с единственным предупреждением о необходимости некоторого некрасивого кода в базовых классах, который будет поддерживать это, и некоторых накладных расходов из-за отражения; но если ваше приложение не работает под сильным давлением, это должно сработать.

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

Надеюсь, это поможет.


[РЕДАКТИРОВАТЬ: Добавление на основе комментариев]

Я добавил несколько деталей к исходному вопросу. Я не хочу использовать интерфейсы, потому что я хочу, чтобы пользователи не стреляли себе в ноги, неправильно внедряя их, или случайно вызывая метод (например, NewBar), который им нужно переопределить, если они хотят реализовать индикатор, но который они никогда не нужно звонить напрямую.

Я посмотрел на ваш обновленный вопрос, но комментарий довольно резюмирует его. Может быть, я чего-то упускаю, но интерфейсы + расширения + отражение могут решить все, что может сделать множественное наследование, и обходятся намного лучше, чем неявные преобразования при выполнении задачи:

  • Поведение виртуального метода (предоставляется реализация, наследники могут переопределить): включить метод на помощнике (в отражении «виртуализация», описанном выше), не объявлять на интерфейсе. *
  • Поведение абстрактного метода (реализация не предусмотрена, наследники должны реализовать): объявить метод в интерфейсе, не включать его в помощник.
  • Поведение не виртуального метода (предоставляется реализация, наследники могут скрывать , но не может переопределить ): просто реализуйте его как обычно на помощнике.
  • Бонус: странный метод (реализация предусмотрена, но наследники должны реализовать в любом случае; они могут явно вызывать базовую реализацию): это невозможно сделать с обычным или множественным наследованием, но я включаю его для завершенность: это то, что вы получите, если предоставите реализацию на помощнике и также объявите ее на интерфейсе. Я не уверен, как это будет работать (в аспекте виртуального или не виртуального) или как его использовать, но, эй, мое решение уже побило множественное наследование: P

Примечание. В случае невиртуального метода вам необходимо иметь тип интерфейса в качестве «объявленного» типа, чтобы обеспечить использование базовой реализации. Это точно так же, как когда наследник скрывает метод.

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

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

или случайный вызов метода (например, NewBar), который они должны переопределить, если они хотят реализовать индикатор

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

но они никогда не должны звонить напрямую Если метод (или любой другой элемент) открыт клиентскому коду, но его не следует вызывать из клиентского кода, не существует программного решения, которое могло бы обеспечить это (на самом деле, есть со мной). Правильное место для адресации есть в документации. Потому что вы документируете свой API, не так ли? ;) Здесь вам не помогут ни преобразования, ни множественное наследование. Однако может помочь отражение:

if(System.Reflection.Assembly.GetCallingAssembly()!=System.Reflection.Assembly.GetExecutingAssembly())
    throw new Exception("Don't call me. Don't call me!. DON'T CALL ME!!!");

Конечно, вы можете сократить его, если в вашем файле есть заявление using System.Reflection;. И, кстати, не стесняйтесь менять тип и сообщение Исключения на что-то более описательное;).

1 голос
/ 18 апреля 2010

Звучит не так, как если бы ваш метод поддерживал перекрестное приведение. Истинное множественное наследование будет.

Пример из C ++, который имеет множественное наследование:

class A {};
class B {};
class C : public A, public B {};

C o;
B* pB = &o;
A* pA = dynamic_cast<A*>(pB); // with true MI, this succeeds
1 голос
/ 18 апреля 2010

Я вижу два вопроса:

  • Определяемые пользователем операторы преобразования типов, как правило, не очень доступны для обнаружения - они не отображаются в IntelliSense.

  • При неявном пользовательском операторе преобразования типов часто не очевидно, когда применяется оператор.

Это не было тем, что вы вообще не должны определять операторы преобразования типов, но вы должны помнить об этом при разработке своего решения.

Легко обнаруживаемым, легко узнаваемым решением было бы определение явных методов преобразования:

class Person { }

abstract class Student : Person
{
    public abstract decimal Wage { get; }
}

abstract class Musician : Person
{
    public abstract decimal Wage { get; }
}

class StudentMusician : Person
{
    public decimal MusicianWage { get { return 10; } }

    public decimal StudentWage { get { return 8; } }

    public Musician AsMusician() { return new MusicianFacade(this); }

    public Student AsStudent() { return new StudentFacade(this); }
}

Использование:

void PayMusician(Musician musician) { GiveMoney(musician, musician.Wage); }

void PayStudent(Student student) { GiveMoney(student, student.Wage); }

StudentMusician alice;
PayStudent(alice.AsStudent());
0 голосов
/ 19 апреля 2010

Это может быть глупой идеей, но: если ваш дизайн требует множественного наследования, почему бы вам просто не использовать язык с MI? Существует несколько языков .NET, которые поддерживают множественное наследование. С головы до головы: Эйфелева, Питон, Иок. Там, вероятно, больше.

0 голосов
/ 18 апреля 2010

Может быть, я слишком далеко зашёл с этим, но ваш вариант использования звучит подозрительно, как будто он может принести большую пользу от использования Rx ( Rx за 15 минут ).

Rx - это структура для работы с объектами, которые производят значения. Это позволяет составлять такие объекты очень выразительным образом и преобразовывать, фильтровать и объединять такие потоки произведенных значений.

Вы говорите, что у вас есть бар:

class Bar
{
    double Open { get; }
    double Low { get; }
    double High { get; }
    double Close { get; }
}

Серия - это объект, который производит бары:

class Series : IObservable<Bar>
{
    // ...
}

Индикатор скользящего среднего - это объект, который выдает среднее значение за последние кол-во баров при каждом создании нового бара:

static class IndicatorExtensions
{
    public static IObservable<double> MovingAverage(
        this IObservable<Bar> source,
        int count)
    {
        // ...
    }
}

Использование будет следующим:

Series series = GetSeries();

series.MovingAverage(20).Subscribe(average =>
{
    txtCurrentAverage.Text = average.ToString();
});

Индикатор с несколькими выходами аналогичен GroupBy.

0 голосов
/ 18 апреля 2010

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

Однако это будет вести себя по-другому. В случае наследования вы просто передаете свой объект. Однако, реализовав неявный преобразователь, вы всегда будете создавать новый объект, когда происходит преобразование. Это может быть очень неожиданным, поскольку в обоих случаях оно будет вести себя совершенно по-разному.

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

...