Явные таблицы методов в C # вместо OO - хорошо? плохой? - PullRequest
3 голосов
/ 10 июня 2010

Надеюсь, название не звучит слишком субъективно;Я абсолютно не хочу начинать дебаты по ОО в целом.Я просто хотел бы обсудить основные плюсы и минусы для различных способов решения проблемы следующего типа.

Давайте возьмем этот минимальный пример: вы хотите выразить абстрактный тип данных T сфункции, которые могут принимать T в качестве входа, выхода или оба:

  • f1 : принимает T , возвращает int
  • f2 : принимает строку , возвращает T
  • f3 : Принимает T и double , возвращает другое T

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

1: попытка на основе абстрактного класса

abstract class T {
  abstract int f1();
  // We can't have abstract constructors, so the best we can do, as I see it, is:
  abstract void f2(string s);
  // The convention would be that you'd replace calls to the original f2 by invocation of the nullary constructor of the implementing type, followed by invocation of f2. f2 would need to have side-effects to be of any use.

  // f3 is a problem too:
  abstract T f3(double d);
  // This doesn't express that the return value is of the *same* type as the object whose method is invoked; it just expresses that the return value is *some* T.
}

2: параметрический полиморфизм и вспомогательный класс

(всеклассы реализации TImpl будут одноэлементными классами):

abstract class TImpl<T> {

  abstract int f1(T t);

  abstract T f2(string s);

  abstract T f3(T t, double d);

}

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

// Say we want to return a Bar given an arbitrary implementation of our abstract type
Bar bar<T>(TImpl<T> ti, T t);

На этом этапе можно также вообще пропустить наследование и синглтоны ииспользуйте

3 Первоклассную таблицу функций

class /* or struct, even */ TDict<T> {
  readonly Func<T,int> f1;
  readonly Func<string,T> f2;
  readonly Func<T,double,T> f3;

  TDict( ... ) {
    this.f1 = f1;
    this.f2 = f2;
    this.f3 = f3;
  }
}

Bar bar<T>(TDict<T> td; T t);

Хотя я не вижу большого практического различия между # 2 и # 3.

Пример реализации

class MyT { 
  /* raw data structure goes here; this class needn't have any methods */
}

// It doesn't matter where we put the following; could be a static method of MyT, or some static class collecting dictionaries
static readonly TDict<MyT> MyTDict 
  = new TDict<MyT>(
      (t) => /* body of f1 goes here */ ,
      // f2
      (s) => /* body of f2 goes here */,
      // f3
      (t,d) => /* body of f3 goes here */
    );

Мысли?№ 3 однотипен, но кажется довольно безопасным и чистым.Один вопрос заключается в том, есть ли какие-либо проблемы с производительностью.Обычно мне не нужна динамическая диспетчеризация, и я бы предпочел, чтобы эти тела функций были статически встроены в местах, где конкретный тип реализации известен статически.# 2 лучше в этом отношении?

Ответы [ 2 ]

10 голосов
/ 11 июня 2010

Hurm.Итак, позвольте мне понять, понимаю ли я это: в языке ООП с неявным специальным полиморфизмом начальных параметров вы хотели бы использовать параметрический полиморфизм для того, чтобы свернуть вашу собственную квази-ОО систему с явный специальный полиморфизм с помощью словарей метода reified, все ради того, чтобы отправляемый тип мог появляться в другом месте в сигнатуре функции, чем неявный параметр this.И вы хотите знать, если это хорошая идея.

От вашего имени пользователя, я уверен, вы знаете отлично , что вам действительно нужны классы типов а-ля Wadler и др. Если Microsoft подписывает зарплату SPJ, это не значит, что писать Haskell на C # - хорошая идея.

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

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

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

1 голос
/ 12 июня 2010

Вы никогда не добьетесь того, чтобы C # работал как на Haskell, поскольку он сильно отклоняется от функций, не относящихся к методам, но вот как я попытаюсь перевести ваше намерение в идиоматический C #. f1 и f3 обрабатываются тривиально с использованием регулярного специального полиморфизма.Например, рассмотрим:

interface INumber
{
    int Sign { get; }
    INumber Scale(double amount);
}

Теперь у нас есть абстрактный тип данных с двумя функциями, которые вам нужны: Sign - f1 и Scale() - f3 . f2 , абстрактный конструктор, сложнее.Типичное решение ООП заключается в использовании фабрики, абстрактного класса, который инкапсулирует конструктор:

interface INumberFactory
{
    INumber Parse(string text);
}

Parse(), конечно, f2 .С этими двумя интерфейсами, я думаю, мы охватили все ваши функции, не прибегая к каким-либо конкретным типам.Это то, что вы искали?

Редактировать: В ответ на комментарий FunctorSalad:

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

interface INumber<T> where T : INumber<T>
{
    int Sign { get; }
    T Scale(double amount);
}

interface INumberFactory<T> where T : INumber<T>
{
    T Parse(string text);
}

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

class Rational : INumber<Rational>
{
    public int Sign { get { /* ... */ } }
    public Rational Scale(double amount) { /* ... */ }
}

class RationalFactory : INumberFactory<Rational>
{
    Rational Parse(string text);
}

Недостатком этого, конечно, является то, что вашему абстрактному типу теперь требуется аргумент типа, так что вы больше не можете передавать «сырые» INumber или INumberFactory.Если вы действительно хотите пойти по этому пути, C # поможет вам с явной реализацией интерфейса.При этом вы можете определить класс Rational, который реализует оба INumber<Rational> для мест, где вы хотите строго типизированный возврат из Scale() и INumber, где вы в порядке с Scale(), возвращающим INumber.Это обычно не используется, но оно там.Система типов в C # намного интереснее, чем думают многие.

...