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

Interfaces, Inheritance, Implicit operators and type conversions, why is it this way?

我正在使用一个名为 DDay ICal 的 class 库。 它是在 Outlook 日历中实现的 iCalendar 系统的 C# 包装器,以及更多的系统。 我的问题来自于我在这个系统上所做的一些工作。

这里有 3 个对象

IRecurrencePattern:未显示所有代码

public interface IRecurrencePattern
{
    string Data { get; set; }
}

RecurrencePattern:未显示所有代码

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 来包含日历中每个事件的重复模式集合,自定义 class 用于从数据库中获取信息,然后通过隐式类型转换运算符将其转换为循环模式。

但是在代码中,我注意到它在从 List<DbRecurPatt> 转换为 List<IRecurrencePattern> 时一直崩溃 我意识到我需要转换为 RecurrencePattern,然后转换为 IRecurrencePattern(因为还有其他 class 以不同方式实现 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 对象而不是接口上,这是正确的吗? (我是接口和隐式运算符的新手)

Why does the first one not work?

因为您要求运行时进行 两次 隐式转换 - 一次到 RecurrencePattern 和一次到 IRecurrencePattern。运行时只会寻找 direct 隐式关系——它不会扫描所有可能的路线来让你让它去。假设有 多个 到实现 IRecurrencePattern 的不同类型 类 的隐式转换。运行时会选择哪一个?相反,它会强制您指定单个演员表。

这在 C# 语言规范的第 6.4.3 节中有记录:

Evaluation of a user-defined conversion never involves more than one user-defined or lifted conversion operator. In other words, a conversion from type S to type T will never first execute a user-defined conversion from S to X and then execute a user-defined conversion from X to T.

你基本上已经要求编译器这样做了:

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

编译器,即使它可能只有一个选择,也不允许您这样做。转换运算符特别指出 DbRecurPatt 可以转换为 RecurrencePattern,而不是 IRecurrencePattern.

编译器只检查所涉及的两种类型之一是否指定了如何从一种类型转换为另一种类型的规则,它不允许中间步骤。

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

所以,下一个问题是:我该怎么做?答案是你不能。

编译器不允许您定义用户定义的与接口之间的转换运算符。 A different question here on Stack Overflow has more information.

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

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

编译器会这样说:

CS0552
'DbRecurPatt.implicit operator IRecurrencePattern(DbRecurPatt)': user-defined conversions to or from an interface are not allowed

... 并回答关于隐式运算符的最后一个问题 - 不,您不能在接口上定义隐式运算符。这个问题更详细地介绍了该主题:

implicit operator using interfaces

正如其他人已经指出的那样,您无法从 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

还有另一条路可以完成你想要的。在您的方法调用上专门标记您的通用参数,而不是让编译器推断您的通用参数。您仍然会避免转换,并且它可能比其他一些选项更简洁。唯一需要注意的是,您必须包含一个额外的 Linq 语句,如果这很重要,它将解析您的列表。

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

您也可以将此答案与 sstan 的答案结合起来以避免额外的 Linq 语句。