将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
所以你对此能做些什么? (!不是这个!)第一个常见的尝试可能是尝试使用.Cast
return HttpMethods.Cast().Contains(request.Method) && request.Url.AbsolutePath.StartsWith(Prefix);
你会发现这不起作用。 尽管Cast
请注意,当HttpMethod是一个值类型(类)时,此Cast <>将在运行时失败,编译器将很乐意让它构建。
最终解决方法
我们将需要强制将HttpMethods数组中的元素强制显式转换为字符串(这仍将使用隐式运算符!)而不是Cast
return HttpMethods.Select(c => (string)c).Contains(request.Method) && request.Url.AbsolutePath.StartsWith(Prefix);