3

我有一种情况,我想让某种类型的对象能够用作两种不同的类型。如果“基本”类型之一是接口,这将不是问题,但在我的情况下,最好它们都是具体类型。

我正在考虑将一种基类型的方法和属性的副本添加到派生类型,并添加从派生类型到该基类型的隐式转换。然后,用户将能够通过直接使用重复的方法、将派生类型分配给基类型的变量或将其传递给采用基类型的方法来将派生类型视为基类型。

看起来这个解决方案很适合我的需求,但我有什么遗漏吗?是否存在这种情况不起作用,或者在使用 API 时可能会增加混乱而不是简单?

编辑:有关我的具体情况的更多详细信息:

这是为了将来重新设计指标在RightEdge中编写的方式,这是一个自动交易系统开发环境。价格数据表示为一系列条形图,这些条形图具有给定时间段(1 分钟、1 天等)的开盘价、最低价、最高价和收盘价。指标对一系列数据进行计算。一个简单指标的示例是移动平均指标,它给出了其输入的最近n 个值的移动平均值,其中n是用户指定的。移动平均线可以应用于收盘价,也可以应用于另一个指标的输出以使其平滑。

每次出现新柱时,指标都会为该柱计算其输出的新值。

大多数指标只有一个输出系列,但有时有多个输出(参见MACD)很方便,我想支持这一点。

因此,指标需要从“组件”类派生,该类具有在新数据进入时调用的方法。但是,对于只有一个输出系列(这是其中的大多数)的指标,这对他们来说会很好自己作为一个系列。这样,用户可以使用SMA.CurrentSMA 的当前值,而不必使用SMA.Output.Current. 同样,Indicator2.Input = Indicator1;Indicator2.Input = Indicator1.Output;. 这可能看起来差别不大,但我们的许多目标客户都不是专业的 .NET 开发人员,所以我想让这变得尽可能简单。

我的想法是对只有一个输出系列的指标进行从指标到其输出系列的隐式转换。

4

6 回答 6

4

您没有提供太多详细信息,因此这里尝试从您提供的内容中进行回答。

看看基本的区别:
当你有一个基类型B和一个派生类型D时,这样的赋值:

B my_B_object = my_D_object;

分配对同一对象的引用。另一方面,当BD是独立类型并且它们之间存在隐式转换时,上述赋值将创建一个副本并将其存储(如果是类my_D_object,则为对它的引用) 。Bmy_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. 如果你找到匹配的方法,调用它并返回(所以 Helper 的其余方法不会运行)。
  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;的文件中有声明,您可以缩短它。而且,顺便说一句,请随意将异常的类型和消息更改为更具描述性的内容;)。

于 2010-04-18T18:57:50.437 回答
1

我看到两个问题:

  • 用户定义的类型转换运算符通常不太容易被发现——它们不会出现在 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());
于 2010-04-18T18:43:05.210 回答
1

听起来好像您的方法不支持交叉转换。真正的多重继承会。

一个来自 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
于 2010-04-18T18:44:40.043 回答
0

然后,用户将能够通过直接使用重复的方法、将派生类型分配给基类型的变量或将其传递给采用基类型的方法来将派生类型视为基类型。

但是,这将表现不同。在继承的情况下,你只是传递你的对象。但是,通过实现隐式转换器,您将始终在转换发生时构造一个新对象。这可能是非常出乎意料的,因为在这两种情况下它的行为会完全不同。

就个人而言,我会将其设为返回新类型的方法,因为它会使最终用户的实际实现显而易见。

于 2010-04-18T18:41:26.853 回答
0

也许我在这方面走得太远了,但是您的用例听起来很可疑,好像它可以从构建RxRx in 15 Minutes)中受益匪浅。

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。

于 2010-04-18T19:57:34.200 回答
0

这可能是一个愚蠢的想法,但是:如果您的设计需要多重继承,那么您为什么不简单地使用 MI 语言呢?有几种支持多重继承的 .NET 语言。我的脑海中浮现:埃菲尔,Python,Ioke。可能还有更多。

于 2010-04-19T00:24:14.027 回答