为什么 C# 编译器不调用隐式运算符编译错误?

问题描述

考虑以下结构:

struct SomeWrapper
{
    public Guid guid;

    public static implicit operator SomeWrapper(Guid guid) => new SomeWrapper {guid = guid};
}

这个结构定义了一个隐式运算符,用于将 Guid 视为 SomeWrapper,非常简单。 除了第一个 PleaseDoNotCompile 之外,以下所有方法都会编译:

static Task<SomeWrapper> PleaseDoNotCompile() => Task.Run(() => Guid.NewGuid());

static Task<SomeWrapper> WhyDoYouCompile() => Task.Run(() =>
{
    return Guid.NewGuid();

    return new SomeWrapper();
});

static SomeWrapper IUnderstandWhyYouCompile() => Guid.NewGuid();

static async Task<SomeWrapper> IUnderstandWhyYouCompiletoo() => await Task.Run(() => Guid.NewGuid());

特别是,WhyDoYouCompile 只是第一个带有返回 SomeWrapper 值的附加 return 语句的方法。很明显,返回是无法访问的代码。它仍然可以编译,而第一个没有。

因此,除了附加的 return 语句之外,这两种方法之间实际上还有另一个区别:PleaseDoNotCompile 使用 Task.Run<Guid>(Func<Guid> function)WhyDoYouCompile 使用 Task.Run<SomeWrapper>(Func<SomeWrapper> function)。所以,额外的回报实际上改变了使用的过载。

尽管如此,IUnderstandWhyYouCompiletoo 只是带有 async 和 await 关键字的 PleaseDoNotCompile 也使用 Task.Run<Guid>(Func<Guid> function) 并且它确实可以编译。

那么,问题很简单,为什么 PleaseDoNotCompile 不能编译而其他方法可以编译?我错过了什么?

解决方法

这在语言规范的 Inferred Return Type 部分进行了解释。

在类型推断期间,编译器必须弄清楚您传递给 Task.Run 的 lambda 表达式的返回类型是什么,以便推断 Task.Run 的泛型参数。规则是(对于 lambda F):

  • 如果 F 的主体是具有类型的表达式,则 F 的推断结果类型就是该表达式的类型。
  • 如果 F 的主体是一个块,并且该块的 return 语句中的表达式集具有最佳公共类型 T,则推断的结果类型 FT
  • 否则,无法为 F 推断结果类型。

对于 PleaseDoNotCompile,第 1 点适用,返回类型推断为 Guid,因此 Task<Guid> 返回 Task.Run。请注意,在类型推断中通常不会考虑您分配给 Task<SomeWrapper> 的事实。例如:

static void Main(string[] args)
{
    string t = F(); // cannot infer type!
}

public static T F<T>()
{
    return default(T);
}

WhyDoYouCompile 中,第二点适用,编译器会在 Guid.NewGuid()new SomeWrapper() 的类型之间找到“最佳公共类型”。即使第二次返回不可达,编译器在这个过程中仍然会考虑它。我知道这听起来很傻,但这就是规范!

找到最佳公共类型的规则指定为 here。它涉及到相当多的类型推断算法,这里就不详细解释了。我希望您会同意直观GuidSomeWrapper 之间最常见的类型是 SomeWrapper,因为 Guid 可以转换为SomeWrapper

因此,Task.Run 的泛型参数被推断为 SomeWrapper,并且您会按预期获得 Task<SomeWrapper>

要使表达式主体的 lambda 起作用,您只需为 Task.Run 指定类型参数:

Task.Run<SomeWrapper>(() => Guid.NewGuid())