【问题标题】:Explicit method tables in C# instead of OO - good? bad?C# 中的显式方法表而不是 OO - 好吗?坏的?
【发布时间】:2011-03-02 06:43:51
【问题描述】:

我希望标题听起来不要太主观;我绝对不是要开始对 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 在这方面更好吗?

【问题讨论】:

  • 这段代码的下一个维护者将是一个知道你住在哪里的杀人狂。他讨厌沙拉,只吃牛排。当然很少见。
  • 如果您反对,我实际上不会调用函数 f1、f2 和 f3 ;)
  • 这是一个学术练习,还是有实际案例证明这种实施是有益的?
  • 您在这里所做的是创建一个完全由函数而不是对象定义的完全人为的示例,并询问我们方法表是否适合它的设计。嗯,是的,确实是这样,但这就像向我们展示一个马蜂窝的流量控制路径并询问goto 是否是一个很好的解决方案。您为什么不给我们一些领域要求的想法,以便我们评估其中任何一项对于它旨在解决的实际问题是否必要或有用?
  • @Aaronaught:他试图模拟一种比接口和继承支持的更具表现力、更通用的多态风格。考虑一个类Collection 表示一个单一类型的对象的排序列表;定义一个类型安全的接口Sortable,这样可以比较同一类的两个对象,但比较不同的类是类型错误。 .NET 框架通过 List.Sort() 采用比较 参数来实现这一点——换句话说,由函数定义。当一个功能不足时,此问题中的代码寻求更通用的解决方案。

标签: c# generics oop functional-programming polymorphism


【解决方案1】:

嗯。所以,让我看看我是否理解这一点:在具有隐式初始参数即席多态性的 OOP 语言中,您想使用 parametric 多态性来推出自己的准 OO 系统explicit 通过具体化的方法字典使用 ad-hoc 多态性,所有这些都是为了允许被分派的类型出现在函数签名中的其他位置,而不是隐式的 this 参数。而且您想知道这是否是个好主意。

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

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

另一方面,找到表达外语习语的简洁方式并不是天生的坏事——例如,将代数数据类型的使用与访问者模式进行比较——因此,如果您需要具有这些属性的代码,那是最好的表达它的方式,在代码背后的意图上留下一些注释,然后去做。

简而言之:确保它解决了您的问题、测试和分析以确保它不会导致其他问题,然后为其他程序员记录并解释解决方案。

【讨论】:

  • 第二段的快速回答:你说得好像我在试图隐藏一些不可告人的不可告人的动机 =) 我只是不想因为不必要地放弃概念而显得沾沾自喜;我认为我的问题对于没有遇到过类型类的人来说是有意义的。这不是类型类宣传;发帖的原因是我真的错过了(用 Haskell 术语)具有除 a -> SomeConstant 之外的其他签名的方法(以 a 为参数)的能力类型类)。
  • @FunctorSalad:是的,如果这显得粗鲁,我深表歉意——善意的抱怨是本意。但无论如何,借用 Haskell 中常见的概念是最近 C# 中的热门新事物,你知道,泛型、LINQ、可空值类型、lambdas 等等。想多借几条当然没有什么丢人的!类型类是如此的血腥有用我认为他们无论如何都需要宣传......
【解决方案2】:

你永远不会让 C# 像 Haskell 那样工作,因为它强烈地远离非方法函数,但我会尝试将你的意图转换为惯用的 C#。 f1f3 使用常规的临时多态性可以轻松处理。例如,考虑:

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

我们现在有了一个抽象数据类型,其中包含您想要的两个特性:Signf1Scale()f3f2 是一个抽象构造函数,比较棘手。典型的 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&lt;Rational&gt; 用于您希望从 Scale()INumber 获得强类型返回的地方,您可以使用 Scale() 返回 INumber。这不常用,但它在那里。 C# 的类型系统比许多人意识到的要有趣得多。

【讨论】:

  • 有趣的是,你发现了一些与我的方法签名相匹配的东西(数字);我随意选择了它们:)。以这种方式进行 Scale 的问题在于,您在表达“缩放一个数字将产生另一个数字”,而不是“缩放一个类型的数字 T 将产生另一个数字键入 T ”。示例结果:如果我们有 Rational : INumber 和属性 MinimalDenominator,我们不能首先缩放有理数然后取结果的 MinimalDenominator(不向下转换)。
猜你喜欢
  • 2012-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-20
  • 1970-01-01
  • 2019-08-31
  • 1970-01-01
相关资源
最近更新 更多