接口、继承、隐式运算符和类型转换,为什么是这样的?

22
我正在使用一个名为DDay ICal的类库。 它是一个C#包装器,用于Outlook日历系统和许多其他系统中实施的iCalendar系统。 我的问题源于我与这个系统所做的一些工作。
这里有三个相关对象:
- IRecurrencePattern - 接口 - RecurrencePattern - 实现IRecurrencePattern接口的类 - DbRecurPatt - 具有隐式类型操作符的自定义类
IRecurrencePattern:并非所有代码都被展示。
public interface IRecurrencePattern
{
    string Data { get; set; }
}

重复模式:并非所有代码都显示

public class RecurrencePattern : IRecurrencePattern
{
    public string Data { get; set; }
}

DbRecurPatt:并非所有代码都显示

public class DbRecurPatt
{
    public string Name { get; set; }
    public string Description { get; set; }

    public static implicit operator RecurrencePattern(DbRecurPatt obj)
    {
        return new RecurrencePattern() { Data = $"{Name} - {Description}" };
    }
}

混淆的部分:在DDay.ICal系统中,他们使用IList来包含每个日历事件的重复模式集合,自定义类用于从数据库获取信息,然后通过隐式类型转换运算符将其转换为Recurrence Pattern。
但是在代码中,我注意到当将List<DbRecurPatt>转换为List<IRecurrencePattern>时,它经常崩溃。我意识到需要先转换为RecurrencePattern,然后再转换为IRecurrencePattern(因为集合中还包括其他实现IRecurrencePattern的类,它们的实现方式不同)。
var unsorted = new List<DbRecurPatt>{ new DbRecurPatt(), new DbRecurPatt() };
var sorted = unsorted.Select(t => (IRecurrencePattern)t);

上述代码无法运行,会在 IRecurrencePattern 处抛出错误。
var sorted = unsorted.Select(t => (IRecurrencePattern)(RecurrencePattern)t);

这个确实可以工作,所以我的问题是:为什么第一个方法不能工作?(有没有改进这种方法的方式?)

我相信这可能是因为隐式操作符在 RecurrencePattern 对象上而不是接口上,这是正确的吗?(我对接口和隐式操作符都很新手)


1
List<DbRecurPatt>是一个实际对象。您不能将其强制转换为List<IRecurrencePattern>,因为某些人可能会试图将非DbRecurPatt对象存入其中。 - Damien_The_Unbeliever
需要注意的是,从RecurrencePatternIRecurrencePattern的转换将引用相同的对象,但是从DbRecurPattRecurrencePattern的转换将创建一个全新的对象。因此,在将其作为接口引用之前,您需要告诉它创建新对象。 - juharr
5个回答

16

你基本上要求编译器执行以下操作:

  1. 我有这个:DbRecurPatt
  2. 我想要这个:IRecurrencePattern
  3. 请找出一种方法从第1步到第2步。

即使编译器可能只有一种选择,但它也不允许你这样做。类型转换运算符明确指出DbRecurPatt可以被转换为RecurrencePattern,而不是IRecurrencePattern

编译器仅检查两种类型中是否有一种规则来指定如何将一种类型转换为另一种类型,并且不允许中间步骤。

由于没有定义任何运算符允许将DbRecurPatt直接转换为IRecurrencePattern,因此编译器将其编译为硬转换,重新解释引用作为通过接口的引用,在运行时会失败。

那么下一个问题就是:我该怎么做呢?答案是你不能。

编译器不允许你定义一个用户定义的从接口到接口的转换运算符。Stack Overflow上的另一个问题提供了更多信息

如果你尝试定义这样的运算符:

public static implicit operator IRecurrencePattern(DbRecurPatt obj)
{
    return new RecurrencePattern() { Data = $"{obj.Name} - {obj.Description}" };
}

编译器会报告如下错误:

CS0552
'DbRecurPatt.implicit operator IRecurrencePattern(DbRecurPatt)': 不允许使用用户定义的类型转换来自或到一个接口。


2
我认为您在隐式操作符代码示例中遗漏了接口名称中的 I - juharr
那个语句的最后一部分;你是不是想说 return new IRecurrencePattern() 而不是 return new RecurrencePattern 因为在尝试了那段代码之后,它可以工作? - Alec Scratch
@AlecScratch 不,我认为他的意思是将签名设置为 public static implicit operator IRecurrencePattern(DbRecurPatt obj) - juharr
啊,好的,我现在明白了。我的想法本来也没有意义,因为你不能实例化一个接口。傻瓜。 - Alec Scratch
是的,我的签名中缺少了一个 I,我从我的示例 LINQPad 程序中复制了错误的方法,但我确实想在方法内部使用该类。 - Lasse V. Karlsen

6
为什么第一个不起作用?
因为您要求运行时进行两个隐式转换 - 一个转换为RecurrencePattern,另一个转换为IRecurrencePattern。运行时只会查找直接的隐式关系 - 它不会扫描所有可能的路线来获取您要求它去的结果。假设到实现IRecurrencePattern的不同类型的类存在多个隐式转换。运行时会选择哪一个呢?相反,它强制您指定单个强制转换。
这在C#语言规范的第6.4.3节中有记录:
用户定义的转换的评估永远不会涉及超过一个用户定义的或提升的转换运算符。换句话说,从类型S到类型T的转换永远不会首先执行从S到X的用户定义的转换,然后再执行从X到T的用户定义的转换。

4

正如其他人已经指出的,你无法直接从 DbRecurPatt 跳转到 IRecurrencePattern。这就是为什么你最终会得到这个非常丑陋的双重转换:

var sorted = unsorted.Select(t => (IRecurrencePattern)(RecurrencePattern)t);

然而,为了完整起见,应该提到,在您当前的设计中,可以从 DbRecurPatt 转换为 IRecurrencePattern,而不需要任何强制类型转换。只是这样做,您需要将表达式拆分成多个语句,这样做会使代码变得相当丑陋。

尽管如此,知道您可以在没有任何强制类型转换的情况下执行此操作仍然很好:

var sorted = unsorted.Select( t => {
    RecurrencePattern recurrencePattern = t; // no cast
    IRecurrencePattern recurrencePatternInterface = recurrencePattern; // no cast here either
    return recurrencePatternInterface;
});

编辑

感谢Bill Nadeau的答案提供了思路。您还可以通过以下方法编写代码,以保持其优雅美观,并从隐式转换及其编译时的保证中受益:

var sorted = unsorted
    .Select<DbRecurPatt, RecurrencePattern>(t => t) // implicit conversion - no cast
    .Select<RecurrencePattern, IRecurrencePattern>(t => t); // implicit conversion - no cast

谢谢回复,不过我有一个问题,你知道这个方法是否比强制转换更好吗?还是纯粹是“好知道”的呢? - Alec Scratch
有一个优点(我个人认为非常重要):如果你能够编译不需要任何转换(casts)的代码,那么你可以放心地确信转换不会在运行时突然失败。与此相对的是你的语句(IRecurrencePattern)t,它虽然能编译通过,但在运行时失败了。但是不可否认的是,这种写法更加丑陋和冗长。 - sstan
那里仍然有两个转换,只是它们是隐式转换,因为你正在从一个类型的变量复制引用到另一个类型的变量。因此说“没有转换”是不正确的。 - D Stanley
@D Stanley:我明白你的意思。但是我认为强制类型转换隐式类型转换之间有所不同。如果我说不需要进行类型转换,那么我同意你的观点是错误的。在标准文档中使用这种术语的示例可以在这里(“声明为显式的转换需要进行强制类型转换才能调用。”),这里(“隐式转换——不需要强制类型转换”)或这里找到。 - sstan
2
万岁! 人们喜欢我的想法! 我认为对于 OP 的情况,两种技术的结合最终会变得最好和最优雅。老实说,只要避免硬转换,OP 就应该很好。 - Bill Nadeau

2

有另一种方法可以实现您想要的功能。具体地,在方法调用中标记您的通用参数,而不是让编译器推断您的通用参数。您仍将避免转换,并且可能比其他选项更少冗长。唯一的注意事项是,如果需要,您必须包含一个额外的Linq语句来解析您的列表。

var sorted = unsorted
   .Select<DbRecurPatt, RecurrencePattern>(t => t)
   .ToList<IRecurrencePattern>();

你也可以将这个答案与sstan的答案结合起来,避免额外的Linq语句。

1
我非常喜欢你的想法。我借鉴了你的想法,并对我的答案进行了调整。基本上,我会链接两个 Select 方法调用,以避免强制转换,同时保持代码等效于 OP 的原始代码片段。结果非常简洁。 - sstan

1

关于隐式操作符的最后一个问题的回答是 - 不可以在接口上定义隐式操作符。有关该主题的更多详细信息,请参阅以下问题:

使用接口定义隐式操作符


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接