在 .NET 中使用隐式转换替代多重继承
Posted
技术标签:
【中文标题】在 .NET 中使用隐式转换替代多重继承【英文标题】:Using implicit conversion as a substitute for multiple inheritance in .NET 【发布时间】:2011-02-09 10:33:05 【问题描述】:我有一种情况,我希望某种类型的对象能够用作两种不同的类型。如果“基本”类型之一是接口,这将不是问题,但在我的情况下,最好它们都是具体类型。
我正在考虑将其中一种基类型的方法和属性的副本添加到派生类型,并添加从派生类型到该基类型的隐式转换。然后,用户可以通过直接使用重复的方法、将派生类型分配给基类型的变量或将其传递给采用基类型的方法来将派生类型视为基类型。
看起来这个解决方案很适合我的需求,但我有什么遗漏吗?是否存在这种情况不起作用,或者在使用 API 时可能会增加混乱而不是简单?
编辑:关于我的具体场景的更多细节:
这是为了将来重新设计指标在RightEdge 中的编写方式,这是一个自动交易系统开发环境。价格数据表示为一系列条形图,这些条形图具有给定时间段(1 分钟、1 天等)的开盘价、最低价、最高价和收盘价。指标对一系列数据进行计算。一个简单指标的示例是移动平均指标,它给出了其输入的最近 n 个值的移动平均值,其中 n 是用户指定的。移动平均线可以应用于收盘价,也可以应用于另一个指标的输出以使其平滑。
每次出现新柱时,指标都会为该柱计算其输出的新值。
大多数指标只有一个输出系列,但有时有多个输出很方便(见MACD),我想支持这一点。
因此,指标需要从“组件”类派生,该类具有在新数据进入时调用的方法。但是,对于只有一个输出系列(这是其中大部分)的指标,它将是有利于他们自己作为一个系列。这样,用户可以使用SMA.Current
作为 SMA 的当前值,而不必使用SMA.Output.Current
。同样,Indicator2.Input = Indicator1;
优于 Indicator2.Input = Indicator1.Output;
。这可能看起来差别不大,但我们的许多目标客户都不是专业的 .NET 开发人员,所以我想让这尽可能简单。
我的想法是对只有一个输出系列的指标进行从指标到其输出系列的隐式转换。
【问题讨论】:
你能提供更多关于你的实际用例的细节吗?你的基本类型是什么?通常,针对特定情况找到好的解决方案比针对一般问题更容易。 @dtb 根据要求,我在我的场景中添加了一些细节:) 你的问题主要是如何组合系列和指标的问题。如果您想保留具有输入和输出属性的类模式,我可能会保持原样(即Indicator2.Input = Indicator1.Output;
)。它是明确的、容易理解的和容易发现的。隐式转换增加了不必要的复杂性 IMO。但是,由于您谈论的是未来可能的重新设计,您可能会对以完全不同的方式组成系列和指标感兴趣,因此我发布了另一个答案。
【参考方案1】:
然后,用户可以通过直接使用重复的方法、将派生类型分配给基类型的变量或将其传递给采用基类型的方法来将派生类型视为基类型。
但是,这将表现不同。在继承的情况下,你只是传递你的对象。但是,通过实现隐式转换器,您将始终在转换发生时构造一个 new 对象。这可能是非常出乎意料的,因为它在两种情况下的行为会完全不同。
就我个人而言,我会将其设为返回新类型的方法,因为它会使最终用户的实际实现显而易见。
【讨论】:
返回一个成员对象不是比构造一个新对象更有可能吗?但是你仍然失去了平等性和结果作为原始类型的能力,除非所有涉及的类都是从一开始就为此设计的。 @Ben:通过隐式转换,通常,您正在创建一个新对象。然而,即使你返回一个成员,它仍然会表现得“奇怪”,但对用户来说不一定是显而易见的。无论哪种情况,我的建议(使用一种方法使其显而易见)似乎比尝试“模仿”多重继承更好。 查看我对问题的更新以及有关我的方案的更多详细信息。隐式转换将简单地返回类的属性。虽然这在一般情况下可能不太明显,但我认为对于我的用例和目标受众来说并非如此。但是,请随意争论,这就是我首先提出这个问题的原因:) 我主要考虑COM聚合是如何工作的。它还通过在转换后返回不同的对象来模仿 MI,但是委托QueryInterface
的能力是使它一切正常的原因,而 .NET 没有可比性。【参考方案2】:
我看到两个问题:
用户定义的类型转换运算符通常不易被发现——它们不会出现在 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());
【讨论】:
感谢您的回答,但它并不真正符合我的情况。当然,我不太清楚我的情况是什么:) 我已经用更多细节更新了这个问题。【参考方案3】:听起来好像您的方法不支持交叉转换。真正的多重继承。
一个来自 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
【讨论】:
【参考方案4】:你没有提供太多细节,所以这里试图从你提供的内容中回答。
看看基本的区别:
当你有一个基类型 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 类的实现中执行以下操作:
-
用
value.GetType()
检索System.Type
查找该类型是否有与签名匹配的方法
如果找到匹配的方法,调用它并返回(因此 Helper 的其余方法不会运行)。
最后,如果您没有找到具体的实现,则让该方法的其余部分作为“基类实现”运行并工作。
你去吧:C# 中的多重继承,唯一需要注意的是在支持这一点的基类中需要一些丑陋的代码,以及由于反射而产生的一些开销;但除非您的应用程序在巨大的压力下工作,否则这应该可以解决问题。
那么,再一次,为什么您不想使用接口?如果唯一的原因是他们无法提供方法实现,那么上面的技巧可以解决它。如果您对接口有任何其他问题,我可能会尝试解决它们,但我必须先了解它们;)
希望这会有所帮助。
[编辑:基于 cmets 的加法]
我在原始问题中添加了很多细节。我不想使用接口,因为我想防止用户通过错误地实现它们,或者不小心调用一个方法(即 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;
语句,您可以缩短它。而且,顺便说一句,请随意将异常的类型和消息更改为更具描述性的内容;)。
【讨论】:
我在原始问题中添加了很多细节。我不想使用接口,因为我想防止用户通过错误地实现它们,或者不小心调用一个方法(即NewBar
),如果他们想要实现一个指标,他们需要重写该方法,但是他们永远不需要直接调用。【参考方案5】:
也许我在这方面走得太远了,但是您的用例听起来很可疑,好像它可以从 Rx (Rx in 15 Minutes) 的构建中受益匪浅。
Rx 是一个用于处理产生值的对象的框架。它允许以极具表现力的方式组合此类对象,并转换、过滤和聚合此类生成的值流。
你说你有酒吧:
class Bar
double Open get;
double Low get;
double High get;
double Close get;
系列是产生条形的对象:
class Series : IObservable<Bar>
// ...
移动平均线指标是一个对象,它会在生成新柱线时生成最后 count 根柱线的平均值:
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。
【讨论】:
这真的是 Rx 特有的吗?并不是说 Rx 不好,但我认为你可以用IEnumerable<T>
而不是 IObservable<T>
做几乎相同的事情。
@Aaronaught:当然,您可以计算 IEnumerable这可能是一个愚蠢的想法,但是:如果您的设计需要多重继承,那么您为什么不简单地使用 MI 语言呢?有几种支持多重继承的 .NET 语言。我的脑海中浮现:埃菲尔,Python,Ioke。可能还有更多。
【讨论】:
C++/CLI 不支持托管类型的多重继承。它是一种同时支持.NET和多重继承的语言,但它并没有给.NET带来多重继承。 @Ben Voigt:谢谢。我从来没有用过它。不知何故,我的印象是它是 C++ 到 CLI 的 port,但它实际上是 CLI 的 extension。 (我猜旧名称 Managed Extensions for C++ 有点泄露了它。)以上是关于在 .NET 中使用隐式转换替代多重继承的主要内容,如果未能解决你的问题,请参考以下文章
无法将类型 ('string', 'string') 隐式转换为 System.Net.ICredentialsByHost