有问题的结构/类:
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<string>(IQueryable<string>, string)' requires a receiver of type 'IQueryable<string>'
我的问题是为什么?我可以重新设计代码以使其工作,但我想知道为什么从struct更改为sealed类会产生这种奇怪的错误。
编辑:添加一组简化的示例代码(可在此处获取:https://dotnetfiddle.net/IZ9OXg)。请注意,在第二个类上注释 out 隐式运算符到字符串允许代码编译:
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 "";
}
}
答案 0 :(得分:10)
我知道的事情:
HttpMethod1[]
到IEnumerable<string>
的转换,因为协方差仅适用于引用类型。HttpMethod2[]
转换为IEnumerable<string>
,因为协方差仅适用于参考转化,这是用户定义的转化。我怀疑但需要确认的事情:
更新:
IEnumerable<char>
偶尔会混淆类型推断。)这是一个显示问题的程序片段;更新您的转化以转换为C而不是字符串:
public interface IFoo<out T> {}
public class C {}
public class Program
{
public static bool Contains<T>(IFoo<T> items, T item)
{
System.Console.WriteLine(typeof(T));
return true;
}
public static void Main()
{
IFoo<HttpMethod1> m1 = null;
IFoo<HttpMethod2> m2 = null;
var res1 = Contains(m1, new C()); //works
var res2 = Contains(m2, new C()); //doesn't work
}
}
这看起来像是类型推断中的一个可能的错误,如果是,那是我的错;如果是这样的话会有很多道歉。可悲的是,我今天没有时间进一步研究它。你可能想在github上打开一个问题,让一个人仍然以此为生。我会着迷于了解结果是什么,以及它是否是设计或推理算法实现中的错误。
答案 1 :(得分:1)
首先,这是结构和类之间观察到的行为差异。您已经“密封”了您的课程这一事实并不影响这种情况下的结果。
此外,我们知道以下语句将按照预期的方式编译为声明为struct和class的HttpMethod类型,这要归功于隐式运算符。
string method = HttpMethods[0];
处理数组引入了一些不太了解的编译器细微差别。
当HttpMethod是一个类(引用类型)时,使用一个数组,如HttpRoute.HttpMethods 数组协方差(12.5 C# 5.0 Language Spec)开始发挥作用,允许HttpMethod [x]被处理作为对象。协方差将尊重内置的隐式引用转换(例如类型继承或转换为对象),它将尊重显式运算符,但它不会尊重或寻找用户定义的隐式运算符。 (虽然有点模糊,实际的spec文档特别列出默认的隐式运算符和显式运算符,但它没有提到用户定义的运算符,但是看到其他所有内容都是如此高度指定,你可以推断出不支持用户定义的运算符。)< / em>的
基本上协方差优先于许多通用类型评估。稍等一下。
数组协方差特别不会扩展到值类型的数组。例如,不存在允许将int []视为对象[]的转换。
因此,当HttpMethod是一个struct(值类型)时,协方差不再是问题,并且System.Linq命名空间中的以下通用扩展将适用:
public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value);
因为您传入了字符串比较器,所以Contains语句的计算方法如下:
public static bool Contains<string>(this IEnumerable<string> source, string value);
当HttpMethod是一个类(引用类型)时,由于协方差,它的当前形式的HttpMethod []仅与Object []相当,因此与IEnumerable相当,但不是IEnumerable&lt; T&gt ;,为什么不呢?因为编译器需要能够确定生成IEnumerable&lt;的通用实现的类型。 T>并确定它是否可以执行从对象到T的显式转换。 换句话说,编译器无法确定T是否可以绝对是一个字符串,因此它找不到我们期望的Linq扩展方法中的匹配。
那么你能做些什么呢? (!不是这个!) 第一个常见的尝试可能是尝试使用.Cast&lt; string&gt;()将HttpMethod实例强制转换为字符串以进行比较:
return HttpMethods.Cast<string>().Contains(request.Method) && request.Url.AbsolutePath.StartsWith(Prefix);
你会发现这不起作用。即使Cast的参数&lt; T>是IEnumerable类型,而不是IEnumerable&lt; T>。它允许您使用未使用LINQ实现IEnumerable的通用版本的旧集合。投&LT; T>仅用于通过评估引用类型的常见原点或值类型的Un-Boxing过程将非泛型对象转换为其“真实”类型。如果Boxing and Unboxing (C# Programming Guide)仅适用于值类型(结构),并且由于我们的HttpMethod类型是引用类型(类),HttpMethod和String之间唯一的共同原点是Object。在HttpMethod上没有接受Object的隐式或甚至显式运算符,因为它不是值类型,编译器可以使用的内置非框运算符中没有。
请注意,此Cast&lt;&gt;在HttpMethod是一个值类型(类)时,在这种情况下运行时将失败,编译器将很乐意让它构建。
而不是演员&lt; T>或者依赖隐式转换,我们需要强制HttpMethods数组中的元素显式地强制转换为字符串(这仍将使用隐式运算符!)但是Linq再次制作这是一项微不足道但又必要的任务:
return HttpMethods.Select(c => (string)c).Contains(request.Method) && request.Url.AbsolutePath.StartsWith(Prefix);