C#中的显式方法表而不是OO - 好吗?坏?

时间:2010-06-10 16:38:25

标签: c# generics oop functional-programming polymorphism

我希望标题听起来不太主观;我绝对不是故意开始就OO进行辩论。我只想讨论解决以下问题的不同方法的基本利弊。

让我们来看一个这个最小的例子:你想表达一个抽象数据类型 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 个答案:

答案 0 :(得分:10)

Hurm。所以,让我看看我是否理解这一点:在具有隐式初始参数ad-hoc多态的OOP语言中,您希望使用参数多态来推动自己的准OO系统使用显式 ad-hoc多态通过已知的方法词典,所有这些都是为了允许调度的类型出现在函数签名的其他地方而不是隐式this参数。你想知道这是不是一个好主意。

从您的用户名,我很确定您完全了解 您真正想要的是类型类 a la Wadler 等。仅仅因为微软签署SPJ的薪水并不意味着用C#编写Haskell是一个好主意。

你的代码对于理解所表达成语的人来说非常清楚,但是它远远超出了OOP风格的主流,你要确保简洁和正确的收益值得使用外国成语的缺点, 例如让其他C#程序员感到困惑。我还建议通过分析一些概念验证代码来处理性能问题,因为这种风格与大多数C#程序中的风格相差甚远。

另一方面,找到表达外国习语的简洁方法本质上并不是邪恶的 - 比较代数数据类型与访问者模式的比较 - 所以如果你需要具有这些属性的代码,那就是最好的表达它的方式,留下关于代码背后的意图的一些注释并去寻找它。

简而言之:确保它正在解决您的问题,测试和配置文件,以确保它不会导致其他问题,然后为其他程序员记录并解释解决方案。

答案 1 :(得分:1)

你永远不会让C#像Haskell一样工作,因为它强烈倾向于非方法函数,但是这就是我试图将你的意图转换为惯用的C#。使用常规ad-hoc多态性可以轻松处理 f1 f3 。例如,考虑:

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

我们现在有一个抽象数据类型,其中包含您需要的两个功能:Sign f1 Scale() f3 f2 是一个抽象的构造函数,比较棘手。典型的OOP解决方案是使用工厂,一个封装构造函数的抽象类:

interface INumberFactory
{
    INumber Parse(string text);
}

Parse()当然是 f2 。将这两个接口放在一起,我认为我们已经完成了所有功能而无需使用任何具体类型。那是你在找什么?

编辑:回应FunctorSalad的评论:

如果您希望对函数返回的类型进行更精确的控制,那么典型的(但不是那么常见的)OOP解决方案是curiously recurring template pattern。除了上述内容,您可以像这样定义接口:

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);
}

它看起来有点奇怪,但它允许您实例化类型,以保证它们返回自己的类型,而不一定是基类型。例如:

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

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

当然,缺点是你的抽象类型现在需要一个类型参数,因此你不能再传递“原始”INumberINumberFactory。如果你真的想沿着那条路走下去,那么C#会通过明确的接口实现来帮助你。有了这个,您可以定义一个Rational类,为您希望从INumber<Rational>Scale()获得强类型返回的地方INumber实现Scale() {1}}返回INumber。这不常用,但它就在那里。 C#的类型系统比许多人意识到的要有趣得多。