将struct更改为sealed类时,隐式转换失败

有问题的结构/类:

public struct HttpMethod { public static readonly HttpMethod Get = new HttpMethod("GET"); public static readonly HttpMethod Post = new HttpMethod("POST"); public static readonly HttpMethod Put = new HttpMethod("PUT"); public static readonly HttpMethod Patch = new HttpMethod("PATCH"); public static readonly HttpMethod Delete = new HttpMethod("DELETE"); private string _name; public HttpMethod(string name) { // validation of name _name = name.ToUpper(); } public static implicit operator string(HttpMethod method) { return method._name; } public static implicit operator HttpMethod(string method) { return new HttpMethod(method); } public static bool IsValidHttpMethod(string method) { // ... } public override bool Equals(object obj) { // ... } public override int GetHashCode() { return _name.GetHashCode(); } public override string ToString() { return _name; } } 

以下代码触发了此问题:

 public class HttpRoute { public string Prefix { get; } public HttpMethod[] Methods { get; } public HttpRoute(string pattern, params HttpMethod[] methods) { if (pattern == null) throw new ArgumentNullException(nameof(pattern)); Prefix = pattern; Methods = methods ?? new HttpMethod[0]; } public bool CanAccept(HttpListenerRequest request) { return Methods.Contains(request.HttpMethod) && request.Url.AbsolutePath.StartsWith(Prefix); } } 

通过将HttpMethod结构更改为密封类来创建编译器错误。 return Methods.Contains(request.HttpMethod)报告错误,注意: request.HttpMethod在这种情况下是一个string 。 其中产生以下内容:

 Error CS1929 'HttpMethod[]' does not contain a definition for 'Contains' and the best extension method overload 'Queryable.Contains(IQueryable, string)' requires a receiver of type 'IQueryable' 

我的问题是为什么? 我可以重新设计代码以使其工作,但我想知道为什么从struct更改为sealed类会产生这种奇怪的错误。

编辑 :添加一组简化的示例代码(可在此处获取: https : //dotnetfiddle.net/IZ9OXg )。 请注意,在第二个类上注释掉隐式运算符到字符串允许代码编译:

 public static void Main() { HttpMethod1[] Methods1 = new HttpMethod1[10]; HttpMethod2[] Methods2 = new HttpMethod2[10]; var res1 = Methods1.Contains("blah"); //works var res2 = Methods2.Contains("blah"); //doesn't work } public struct HttpMethod1 { public static implicit operator HttpMethod1(string method) { return new HttpMethod1(); } public static implicit operator string (HttpMethod1 method) { return ""; } } public class HttpMethod2 { public static implicit operator HttpMethod2(string method) { return new HttpMethod2(); } //Comment out this method and it works fine public static implicit operator string (HttpMethod2 method) { return ""; } } 

我知道的事情:

  • 显然问题在于类型推断。
  • 在第一种情况下,T被推断为HttpMethod1。
  • 在结构案例中,没有从HttpMethod1[]IEnumerable转换,因为协方差仅适用于引用类型。
  • 在类的情况下,没有从HttpMethod2[]IEnumerable转换,因为协方差仅适用于引用转换,这是用户定义的转换。

我怀疑但需要确认的事情:

  • 关于我的最后两点之间的细微差别的一点是混淆了类型推断算法。

更新:

  • 它与协变arrays转换无关。 即使没有数组转换,问题也会重现。
  • 然而,它确实与协变接口转换有关。
  • 它与字符串无关。 (字符串通常有点奇怪,因为它们难以记住转换为IEnumerable偶尔会混淆类型推断。)

这是一个显示问题的程序片段; 更新您的转化以转换为C而不是字符串:

 public interface IFoo {} public class C {} public class Program { public static bool Contains(IFoo items, T item) { System.Console.WriteLine(typeof(T)); return true; } public static void Main() { IFoo m1 = null; IFoo m2 = null; var res1 = Contains(m1, new C()); //works var res2 = Contains(m2, new C()); //doesn't work } } 

这看起来像是类型推断中的一个可能的错误,如果是,那就是我的错; 如果是这样的话会有很多道歉。 可悲的是,我今天没有时间进一步研究它。 您可能想在github上打开一个问题并让某人仍以此为生。 我会着迷于了解结果是什么,以及它是否是设计或推理算法实现中的错误。

首先,这是结构和类之间观察到的行为差异。 您已经“密封”了您的课程这一事实并不影响这种情况下的结果。

此外,我们知道以下语句将按照预期的方式编译为声明为struct和class的HttpMethod类型,这要归功于隐式运算符。

 string method = HttpMethods[0]; 

处理数组引入了一些较少理解的编译器细微差别。

协方差

当HttpMethod是一个类(引用类型)时,使用一个数组,如HttpRoute.HttpMethods 数组协方差 (12.5 C#5.0语言规范 )开始发挥作用,允许将HttpMethod [x]视为一个对象 。 协方差将尊重内置的隐式引用转换(例如类型inheritance或转换为对象),它将尊重显式运算符,但它不会尊重或寻找用户定义的隐式运算符。 (虽然有点模糊,实际的spec文档列出了默认的隐式运算符和显式运算符,但它没有提到用户定义的运算符,但是看到其他所有内容都是如此高度指定,你可以推断出不支持用户定义的运算符。)

基本上协方差优先于许多generics类型评估。 稍后详细介绍。

数组协方差特别不扩展到值类型的数组。 例如,不存在允许将int []视为对象[]的转换。

因此,当HttpMethod是一个struct(值类型)时,协方差不再是问题,并且System.Linq命名空间中的以下通用扩展将适用:

 public static bool Contains(this IEnumerable source, TSource value); 

因为您已传入字符串比较器,所以Contains语句将按如下方式计算:

 public static bool Contains(this IEnumerable source, string value); 

当HttpMethod是一个类(引用类型)时,由于协方差 ,它的当前forms的HttpMethod []仅与Object []相当,因此IEnumerable,但不是IEnumerable ,为什么不呢? 因为编译器需要能够确定生成IEnumerable 的generics实现的类型,并确定它是否可以执行从对象到T的显式强制转换。换句话说,编译器无法确定T是否可以绝对是一个字符串或不,所以它没有找到我们期待的Linq扩展方法中的匹配。

所以你对此能做些什么? (!不是这个!)第一个常见的尝试可能是尝试使用.Cast ()将HttpMethod实例强制转换为字符串以进行比较:

 return HttpMethods.Cast().Contains(request.Method) && request.Url.AbsolutePath.StartsWith(Prefix); 

你会发现这不起作用。 尽管Cast 的参数是IEnumerable类型,但不是IEnumerable 。 它允许您使用未使用LINQ实现IEnumerable的通用版本的旧集合。 Cast 仅用于通过评估引用类型的常见原点或值类型的Un-Boxing过程将非generics对象转换为其“true”类型。 如果Boxing和Unboxing(C#编程指南)仅适用于值类型(结构),并且由于我们的HttpMethod类型是引用类型(类),HttpMethod和String之间唯一的共同原点是Object。 在HttpMethod上没有接受Object的隐式或甚至显式运算符,因为它不是值类型,所以编译器可以使用的内置un-box运算符中没有。

请注意,当HttpMethod是一个值类型(类)时,此Cast <>将在运行时失败,编译器将很乐意让它构建。

最终解决方法

我们将需要强制将HttpMethods数组中的元素强制式转换为字符串(这仍将使用隐式运算符!)而不是Cast 或依赖于隐式转换,但Linq再次使这成为一项微不足道但却是必要的任务:

 return HttpMethods.Select(c => (string)c).Contains(request.Method) && request.Url.AbsolutePath.StartsWith(Prefix);