在 .NET 中使用隐式转换替代多重继承

发布于 2024-08-29 20:03:04 字数 1153 浏览 6 评论 0原文

我有一种情况,我希望某种类型的对象能够用作两种不同类型。如果“基本”类型之一是接口,这不会成为问题,但就我而言,最好它们都是具体类型。

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

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

编辑:有关我的具体场景的更多详细信息:

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

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

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

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

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

I have a situation where I would like to have objects of a certain type be able to be used as two different types. If one of the "base" types was an interface this wouldn't be an issue, but in my case it is preferable that they both be concrete types.

I am considering adding copies of the methods and properties of one of the base types to the derived type, and adding an implicit conversion from the derived type to that base type. Then users will be able treat the derived type as the base type by using the duplicated methods directly, by assigning it to a variable of the base type, or by passing it to a method that takes the base type.

It seems like this solution will fit my needs well, but am I missing anything? Is there a situation where this won't work, or where it is likely to add confusion instead of simplicity when using the API?

EDIT: More details about my specific scenario:

This is for a potential future redesign of the way indicators are written in RightEdge, which is an automated trading system development environment. Price data is represented as a series of bars, which have values for the open, low, high, and close prices for a given period (1 minute, 1 day, etc). Indicators perform calculations on series of data. An example of a simple indicator is the moving average indicator, which gives the moving average of the most recent n values of its input, where n is user-specified. The moving average might be applied to the bar close, or it could be applied to the output of another indicator to smooth it out.

Each time a new bar comes in, the indicators compute the new value for their output for that bar.

Most indicators have only one output series, but sometimes it is convenient to have more than one output (see MACD), and I want to support this.

So, indicators need to derive from a "Component" class which has the methods that are called when new data comes in. However, for indicators which have only one output series (and this is most of them), it would be good for them to act as a series themselves. That way, users can use SMA.Current for the current value of an SMA, instead of having to use SMA.Output.Current. Likewise, Indicator2.Input = Indicator1; is preferable to Indicator2.Input = Indicator1.Output;. This may not seem like much of a difference, but a lot of our target customers are not professional .NET developers so I want to make this as easy as possible.

My idea is to have an implicit conversion from the indicator to its output series for indicators that have only one output series.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

甩你一脸翔 2024-09-05 20:03:04

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

看一下基本差异:
当您有基类型 B 和派生类型 D 时,如下所示的赋值:

B my_B_object = my_D_object;

将引用分配给同一对象。另一方面,当 BD 是独立类型并且它们之间存在隐式转换时,上述赋值将创建 的副本>my_D_object 并将其存储在 my_B_object 上(如果 B 是一个类,则存储对其的引用)。

总之,“真正的”继承通过引用工作(对引用的更改会影响许多引用共享的对象),而自定义类型转换通常按值工作(这取决于您如何实现它,但实现接近“通过引用”的东西) “转换器的行为几乎是疯狂的):每个引用都将指向它自己的对象。

你说你不想使用接口,但为什么呢?使用组合接口 + 辅助类 + 扩展方法(需要 C# 3.0 和 .Net 3.5 或更高版本)可以非常接近真正的多重继承。看看这个:

interface MyType { ... }
static class MyTypeHelper {
    public static void MyMethod(this MyType value) {...}
}

对每个“基本”类型执行此操作将允许您为所需的方法提供默认实现。

这些方法不会像开箱即用的虚拟方法一样运行;但你可以使用反射来实现这一点;您需要在 Helper 类的实现中执行以下操作:

  1. 使用 value.GetType() 检索 System.Type
  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; 语句,则可以缩短该时间。而且,顺便说一句,请随意将异常的类型和消息更改为更具描述性的内容;)。

You don't provide too many details, so here is an attempt to answering from what you provide.

Take a look at the basic differences:
When you have a base type B and a derived type D, an assignment like this:

B my_B_object = my_D_object;

assigns a reference to the same object. On the other hand, when B and D are independent types with an implicit conversion between them, the above assignment would create a copy of my_D_object and store it (or a reference to it if B is a class) on my_B_object.

In summary, with "real" inheritance works by reference (changes to a reference affect the object shared by many references), while custom type conversions generally work by value (that depends on how you implement it, but implementing something close to "by reference" behavior for converters would be nearly insane): each reference will point to its own object.

You say you don't want to use interfaces, but why? Using the combo interface + helper class + extension methods (C# 3.0 and .Net 3.5 or newer required) can get quite close to real multiple inheritance. Look at this:

interface MyType { ... }
static class MyTypeHelper {
    public static void MyMethod(this MyType value) {...}
}

Doing that for each "base" type would allow you to provide default implementations for the methods you want to.

These won't behave as virtual methods out-of-the-box; but you may use reflection to achieve that; you would need to do the following from within the implementation on the Helper class:

  1. retrieve a System.Type with value.GetType()
  2. find if that type has a method matching the signature
  3. if you find a matching method, invoke it and return (so the rest of the Helper's method is not run).
  4. Finally, if you found no specific implementation, let the rest of the method run and work as a "base class implementation".

There you go: multiple inheritance in C#, with the only caveat of requiring some ugly code in the base classes that will support this, and some overhead due to reflection; but unless your application is working under heavy pressure this should do the trick.

So, once again, why you don't want to use interfaces? If the only reason is their inability to provide method implementations, the trick above solves it. If you have any other issue with interfaces, I might try to sort them out, but I'd have to know about them first ;)

Hope this helps.


[EDIT: Addition based on the comments]

I've added a bunch of details to the original question. I don't want to use interfaces because I want to prevent users from shooting themselves in the foot by implementing them incorrectly, or accidentally calling a method (ie NewBar) which they need to override if they want to implement an indicator, but which they should never need to call directly.

I've looked at your updated question, but the comment quite summarizes it. Maybe I'm missing something, but interfaces + extensions + reflection can solve everything multiple inheritance could, and fares far better than implicit conversions at the task:

  • Virtual method behavior (an implementation is provided, inheritors can override): include method on the helper (wrapped in the reflection "virtualization" described above), don't declare on the interface.
  • Abstract method behavior (no implementation provided, inheritors must implement): declare method on the interface, don't include it on the helper.
  • Non-virtual method behavior (an implementation is provided, inheritors may hide but can't override): Just implement it as normal on the helper.
  • Bonus: weird method (an implementation is provided, but inheritors must implement anyway; they may explicitly invoke the base implementation): that's not doable with normal or multiple inheritance, but I'm including it for completeness: that's what you'd get if you provide an implementation on the helper and also declare it on the interface. I'm not sure of how would that work (on the aspect of virtual vs. non-virtual) or what use it'd have, but hey, my solution has already beaten multiple inheritance :P

Note: On the case of the non-virtual method, you'd need to have the interface type as the "declared" type to ensure that the base implementation is used. That's exactly the same as when an inheritor hides a method.

I want to prevent users from shooting themselves in the foot by implementing them incorrectly

Seems that non-virtual (implemented only on the helper) will work best here.

or accidentally calling a method (ie NewBar) which they need to override if they want to implement an indicator

That's where abstract methods (or interfaces, which are a kind of super-abstract thing) shine most. The inheritor must implement the method, or the code won't even compile. On some cases virtual methods may do (if you have a generic base implementation but more specific implementations are reasonable).

but which they should never need to call directly

If a method (or any other member) is exposed to client code but shouldn't be called from client code, there is no programmatic solution to enforce that (actually, there is, bear with me). The right place to address that is on the documentation. Because you are documenting you API, aren't you? ;) Neither conversions nor multiple inheritance could help you here. However, reflection may help:

if(System.Reflection.Assembly.GetCallingAssembly()!=System.Reflection.Assembly.GetExecutingAssembly())
    throw new Exception("Don't call me. Don't call me!. DON'T CALL ME!!!");

Of course, you may shorten that if you have a using System.Reflection; statement on your file. And, BTW, feel free to change the Exception's type and message to something more descriptive ;).

初见 2024-09-05 20:03:04

我发现两个问题:

  • 用户定义的类型转换运算符通常不太容易被发现——它们不会出现在 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());

I see two issues:

  • User-defined type conversion operators are generally not very discoverable -- they don't show up in IntelliSense.

  • With an implicit user-defined type conversion operator, it's often not obvious when the operator is applied.

This doesn't been you shouldn't be defining type conversion operators at all, but you have to keep this in mind when designing your solution.

An easily discoverable, easily recognizable solution would be to define explicit conversion methods:

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

Usage:

void PayMusician(Musician musician) { GiveMoney(musician, musician.Wage); }

void PayStudent(Student student) { GiveMoney(student, student.Wage); }

StudentMusician alice;
PayStudent(alice.AsStudent());
注定孤独终老 2024-09-05 20:03:04

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

来自 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

It doesn't sound as if your method would support a cross-cast. True multiple inheritance would.

An example from C++, which has multiple inheritance:

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
半窗疏影 2024-09-05 20:03:04

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

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

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

Then users will be able treat the derived type as the base type by using the duplicated methods directly, by assigning it to a variable of the base type, or by passing it to a method that takes the base type.

This will behave differently, however. In the case of inheritance, you're just passing your object. However, by implementing an implicit converter, you'll always be constructing a new object when the conversion takes place. This could be very unexpected, since it will behave quite differently in the two cases.

Personally, I'd make this a method that returns the new type, since it would make the actual implementation obvious to the end user.

纵山崖 2024-09-05 20:03:04

也许我对此太过分了,但您的用例听起来很可疑,好像它可以从 Rx (15 分钟内接收)。

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。

Maybe I'm going too far off with this, but your use case sounds suspiciously as if it could heavily benefit from building on Rx (Rx in 15 Minutes).

Rx is a framework for working with objects that produce values. It allows such objects to be composed in a very expressive way and to transform, filter and aggregate such streams of produced values.

You say you have a bar:

class Bar
{
    double Open { get; }
    double Low { get; }
    double High { get; }
    double Close { get; }
}

A series is an object that produces bars:

class Series : IObservable<Bar>
{
    // ...
}

A moving average indicator is an object that produces the average of the last count bars whenever a new bar is produced:

static class IndicatorExtensions
{
    public static IObservable<double> MovingAverage(
        this IObservable<Bar> source,
        int count)
    {
        // ...
    }
}

The usage would be as follows:

Series series = GetSeries();

series.MovingAverage(20).Subscribe(average =>
{
    txtCurrentAverage.Text = average.ToString();
});

An indicator with multiple outputs is similar to GroupBy.

夜声 2024-09-05 20:03:04

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

This might be a stupid idea, but: if your design requires multiple inheritance, then why don't you simply use a language with MI? There are several .NET languages which support multiple inheritance. Off the top of my head: Eiffel, Python, Ioke. There's probable more.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文