类型系统的奇数:Enumerable.Cast <int>()

Ian*_*emp 22 c# linq

考虑:

enum Foo
{
    Bar,
    Quux,
}

void Main()
{
    var enumValues = new[] { Foo.Bar, Foo.Quux, };
    Console.WriteLine(enumValues.GetType());         // output: Foo[]
    Console.WriteLine(enumValues.First().GetType()); // output: Foo

    var intValues = enumValues.Cast<int>();
    Console.WriteLine(intValues.GetType());         // output: Foo[] ???
    Console.WriteLine(intValues.First().GetType()); // output: Int32

    Console.WriteLine(ReferenceEquals(enumValues, intValues)); // true

    var intValuesArray = intValues.ToArray();
    Console.WriteLine(intValuesArray.GetType());         // output: Int32[]
    Console.WriteLine(intValuesArray.First().GetType()); // output: Int32

    Console.WriteLine(ReferenceEquals(intValues, intValuesArray)); // false
}
Run Code Online (Sandbox Code Playgroud)

注意第三个Console.WriteLine-我期望它打印出将要转换为数组的类型(Int32[]),但是它将打印原始类型(Foo[])!并ReferenceEquals确认确实,第一个Cast<int>电话实际上是无人接听。

因此,我查看了以下内容的来源,Enumerable.Cast并发现了以下内容:

public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) 
{
    IEnumerable<TResult> typedSource = source as IEnumerable<TResult>;
    if (typedSource != null) return typedSource;
    if (source == null) throw Error.ArgumentNull("source");
    return CastIterator<TResult>(source);
}
Run Code Online (Sandbox Code Playgroud)

就我们的意图和目的而言,唯一重要的是前两行,因为它们是唯一被调用的行。这意味着该行:

var intValues = enumValues.Cast<int>();
Run Code Online (Sandbox Code Playgroud)

有效地翻译为:

var intValues = ((IEnumerable)enumValues) as IEnumerable<int>;
Run Code Online (Sandbox Code Playgroud)

但是,删除对非泛型的强制转换IEnumerable会导致编译器错误:

var intValues = enumValues as IEnumerable<int>; // error
Run Code Online (Sandbox Code Playgroud)

我一直在摸索为什么这样做,我认为这与以下事实有关:Array实现非泛型,IEnumerable并且在C#中存在各种特殊的数组大小写框,但是我确实不确定。请有人可以向我解释这是怎么回事,为什么?

Jon*_*nna 17

我认为这与Array实现非泛型IEnumerable以及C#中的数组有各种特殊大小写有关

是的,你是对的。更准确地说,它与数组variance有关。数组差异是.NET1.0中发生的类型系统的松动,虽然存在问题,但可以解决一些棘手的情况。这是一个例子:

string[] first = {"a", "b", "c"};
object[] second = first;
string[] third = (string[])second;
Console.WriteLine(third[0]); // Prints "a"
Run Code Online (Sandbox Code Playgroud)

这是很薄弱的,因为它不会阻止我们这样做:

string[] first = {"a", "b", "c"};
object[] second = first;
Uri[] third = (Uri[])second; // InvalidCastException
Run Code Online (Sandbox Code Playgroud)

还有更糟的情况。

现在我们有了泛型(从.NET2.0和C#2起),它比以前没有用处(如果有人说服了,有些人会争论),以前它允许我们克服一些没有强加给我们的限制。

规则允许我们做隐式转换到引用类型的碱(例如string[]object[])显式转换到导出的参考类型(例如object[],以string[]从)和显式转换ArrayIEnumerable任何类型的阵列,并且还(这是发粘的部分)ArrayIEnumerable引用的数组原始类型或枚举可以转换为原始类型相同大小的枚举的阵列(intuintint基于枚举都是相同的大小)。

这意味着,当人们可以source直接转换直接值时,尝试不对不必要的值进行不必要的优化可能会产生令人惊讶的效果。

过去使我震惊的实际效果是,如果您尝试使用enumValues.Cast<StringComparison>().ToArray()enumValues.Cast<StringComparison>().ToList()ArrayTypeMismatchException即使enumValues.Cast<StringComparison>().Skip(0).ToArray()成功了,这些方法也会失败,因为除了Cast<TResult>()使用所指出的优化方法之外,ToArray<TSource>()ToList<TSource>()使用ICollection<T>.CopyTo()内部调用的优化方法,以及在失败的数组上使用此处涉及的各种变化。

在.NET Core中,放宽了对CopyTo()数组的限制,这意味着此代码成功而不是抛出,但是我忘记了引入哪个版本的更改。


Eri*_*ert 12

乔恩·汉纳(Jon Hanna)的答案非常正确,但是我可以添加一些小细节。

我希望它能打印出要转换为数组的类型Int32[],但它会打印出原始类型Foo[]

您应该期待什么?的约定Cast<int>是,返回的对象可以在需要使用的任何上下文中使用IEnumerable<int>,而您知道了。这就是您应该期望的一切;其余的是实现细节。

现在,我同意您Foo[]可以使用a的事实IEnumerable<int>很奇怪,但是请记住,a Foo只是a 周围的一个非常薄的包装int。a Foo的大小与an的大小相同int,a Foo的内容与an的内容相同int,因此当被问到“这Foo[]可用作a IEnumerable<int>吗?” 时,CLR明智地回答“是” 。

但是呢?

enumValues as IEnumerable<int> 导致编译器错误

这听起来像是矛盾,不是吗?

问题是在这种情况下C#规则和CLR规则不匹配。

  • CLR表示“ a Foo[]可以用作int[]and,而a uint[]和...”。
  • C#类型分析器更具限制性。它没有使用CLR的所有松散协方差规则。的C#类型分析器将允许string[]将被用作object[],并且将允许IEnumerable<string>将被用作IEnumerable<object>但它不会让Foo[]被用作int[]IEnumerable<int>等等。 当变化类型都是引用类型时,C#仅允许协方差。在CLR允许协方差当变化类型是引用类型,或intuintint尺度的枚举。

C#编译器“知道”从转换Foo[]IEnumerable<int>不能成功在C#的类型系统,因此它产生一个编译器错误; 在C#中的转换必须是可能是合法的。编译器未考虑在更宽松的CLR类型系统中可能做到这一点的事实。

通过将强制转换插入objectIEnumerable或其他命令,您将告诉C#编译器停止使用C#规则,并开始让运行时解决它。 通过删除强制类型转换,您是在说您希望C#编译器呈现其判断,并且确实如此。

因此,现在我们遇到了语言设计问题;显然,我们在这里存在矛盾。有几种方法可以消除这种不一致。

  • C#可以匹配CLR的规则,并允许在整数类型之间进行协变转换。
  • C#可以生成as运算符,以便在运行时实现C#规则;基本上,它必须检测合法的CLR转换但不允许C#的非法转换,并禁止它们进行转换,从而使所有此类转换速度变慢。而且,这将需要您的方案转到分配内存的慢路径,Cast<T>而不是保留参考的快速路径。
  • C#可能是不一致的,并且可能与不一致保持一致。

第二种选择显然是不可行的。它只会增加成本,除了保持一致性之外没有其他好处。

然后归结为第一和第三选择,C#1.0设计团队选择了第三。(请记住,C#1.0设计团队不知道他们会在C#2.0中添加泛型还是在C#4.0中添加泛型变量。)对于C#1.0设计团队来说,问题是是否enumValues as int[]合法,他们决定不这样做。然后,再次针对C#2.0和C#4.0做出该设计决策。

双方都有很多有原则的论据,但在实践中,这种情况在现实世界代码中几乎不会发生,并且不一致几乎不会发生,因此,成本最低的选择是忍受(IEnumerable<int>)(object)enumValues合法但(IEnumerable<int>)enumValues不合法的奇怪事实。

有关更多信息,请参阅我在2009年发表的有关该主题的文章

https://blogs.msdn.microsoft.com/ericlippert/2009/09/24/why-is-covariance-of-value-typed-arrays-inconsistent/

和这个相关的问题:

为什么我的C#数组在投射到对象时会丢失类型标志信息?

  • 优化失败的一种情况是“enumValues.Cast&lt;StringComparison&gt;().ToArray()”,它会因“ArrayTypeMismatchException”而失败,而“enumValues.Cast&lt;StringComparison&gt;().Skip(0).ToArray()”则会失败成功是因为 `Cast&lt;TResult&gt;` 中的优化意味着使用 `Array.CopyTo()` 的 `ToArray()` 中的优化会抛出异常,因为它需要比生成的 `Cast()` 更紧密的匹配。(除了在 .NET Core 中,放松了对 Array.CopyTo() 的限制以允许这样做)。 (2认同)