【问题标题】:Type system oddity: Enumerable.Cast<int>()类型系统怪异:Enumerable.Cast<int>()
【发布时间】:2019-07-26 22:49:51
【问题描述】:

考虑:

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
}

注意第三个Console.WriteLine - 我希望它打印数组被转换为的类型(Int32[]),但它打印的是原始类型(Foo[])!并且ReferenceEquals 确认确实,第一个Cast&lt;int&gt; 调用实际上是空操作。

于是我偷看了source of 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);
}

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

var intValues = enumValues.Cast<int>();

被有效地翻译成:

var intValues = ((IEnumerable)enumValues) as IEnumerable<int>;

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

var intValues = enumValues as IEnumerable<int>; // error

我一直在摸不着头脑,为什么会这样,我认为这与 Array 实现了非泛型 IEnumerable 并且所有C# 中数组的各种特殊大小写,但老实说我不确定。请有人向我解释这里发生了什么以及为什么?

【问题讨论】:

  • 好吧,ReSharper 甚至抱怨((IEnumerable)enumValues) as IEnumerable&lt;int&gt; 永远是null,这显然是错误的。
  • 你所说的特殊类型是协方差,IEnumerable&lt;out t&gt;。实际上是不允许这样做的:var intValues = enumValues as IEnumerable&lt;int&gt;; 这是众所周知的。它打印 Foo[] 而不是 int 的原因是 int 默认情况下是枚举的基础类型...

标签: c# linq


【解决方案1】:

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

是的,你是对的。更准确地说,它与数组方差有关。数组变体是 .NET1.0 中发生的类型系统的松散,这是有问题的,但允许解决一些棘手的情况。这是一个例子:

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

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

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

还有更糟糕的情况。

现在我们有了泛型(从 .NET2.0 和 C#2 开始),它的用处比以前少了(如果它们被证明是合理的,有些人会争论),因为它允许我们克服一些没有强加泛型的限制在我们身上。

规则允许我们对引用类型的基进行隐式转换(例如,string[]object[])显式转换到派生引用类型(例如,object[]string[])以及从 Array 或 @ 显式转换987654328@ 到任何类型的数组,并且(这是粘性部分)ArrayIEnumerable 对原始类型数组或枚举的引用可以转换为相同大小的原始类型枚举的数组(int,基于uintint 的枚举大小都相同。

这意味着,当您可以直接转换 source 时,尝试优化不不必要地转换单个值可能会产生您注意到的令人惊讶的效果。

如果您要尝试enumValues.Cast&lt;StringComparison&gt;().ToArray()enumValues.Cast&lt;StringComparison&gt;().ToList(),过去让我感到困惑的实际效果是。即使enumValues.Cast&lt;StringComparison&gt;().Skip(0).ToArray() 会成功,这些也会因ArrayTypeMismatchException 而失败,因为Cast&lt;TResult&gt;() 使用注意到的优化,ToArray&lt;TSource&gt;()ToList&lt;TSource&gt;() 使用内部调用ICollection&lt;T&gt;.CopyTo() 的优化,以及在因这里涉及到某种差异。

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

【讨论】:

    【解决方案2】:

    假设您有一个类 Car 和一个派生汽车:Volvo

    Car car = new Volvo();
    var type = car.GetType();
    

    虽然您将Volvo 转换为Car,但对象仍然是Volvo。如果你询问它的类型,你会得到它是typeof(Volvo)

    Enumerable.Cast&lt;TResult&gt; 会将输入序列的每个元素转换为TResult,但它们仍然是您的原始对象。因此,您的 Foos 序列仍然是 Foos,即使您将它们转换为整数。

    在 cmets 之后添加

    一些人评论说,IEnumerable.Cast 并非如此。我认为这很奇怪,但让我们试一试:

    class Car
    {
        public string LicensePlate {get; set;}
        public Color Color {get; set;}
        ...
    }
    class Volvo : Car
    {
        public string OriginalCountry => "Sverige";
    }
    

    用法:

    var volvos = new List<Volvo>() {new Volvo(), new Volvo(), new Volvo()};
    var cars = volvos.Cast<Car>();
    foreach (var car in cars) Console.WriteLine(car.GetType());
    

    结果:

    CarTest.Volvo
    CarTest.Volvo
    CarTest.Volvo
    

    结论: Enumerable.Cast 不会改变对象的类型

    【讨论】:

    • 这不适用于枚举类型的数组被强制转换为int数组的情况。
    • 也没有优化Cast&lt;TResult&gt;
    【解决方案3】:

    Jon Hanna 的回答非常正确,但我可以补充一些小细节。

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

    你应该期待什么? Cast&lt;int&gt; 的约定是返回的对象可以在任何需要 IEnumerable&lt;int&gt; 的上下文中使用,你明白了。这就是你应该预料到的。剩下的就是实现细节了。

    现在,我承认Foo[] 可以用作IEnumerable&lt;int&gt; 的事实很奇怪,但请记住,Foo 只是int 的一个非常薄的包装。 Foo 的大小与int 的大小相同,Foo 的内容与int 的内容相同,因此 CLR 在其智慧中回答“是”时问“这是Foo[] 可用作IEnumerable&lt;int&gt; 吗?”

    但是这个呢?

    enumValues as IEnumerable&lt;int&gt; 导致编译器错误

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

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

    • CLR 说“Foo[] 可以用作int[]uint[] 和...”。
    • C# 类型分析器的限制性更强。 它不使用 CLR 的所有宽松协方差规则。 C# 类型分析器将允许将string[] 用作object[],并允许将IEnumerable&lt;string&gt; 用作IEnumerable&lt;object&gt;,但不允许将Foo[] 用作int[]IEnumerable&lt;int&gt; 和很快。 C# 仅在不同类型都是引用类型时才允许协变。当可变类型是引用类型、intuintint 大小的枚举时,CLR 允许协方差。

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

    通过向objectIEnumerable 或其他任何内容插入强制转换,您是在告诉C# 编译器停止使用C# 的规则,并开始让运行时解决它。 通过删除演员,你是说你希望 C# 编译器做出判断,它确实做到了。

    所以现在我们遇到了语言设计问题;显然,我们在这里有不一致之处。有几种方法可以解决这种不一致。

    • C# 可以匹配 CLR 的规则,并允许在整数类型之间进行协变转换。
    • C#可以生成as操作符,以便在运行时实现C#的规则;基本上,它必须检测在 CLR 中合法但在 C# 中非法的转换并禁止它们,从而使所有此类转换变慢。此外,它会要求您的方案转到 Cast&lt;T&gt; 的内存分配慢速路径,而不是保留引用的快速路径。
    • 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&lt;int&gt;)(object)enumValues 的奇怪事实是合法的,但(IEnumerable&lt;int&gt;)enumValues 不合法。

    有关这方面的更多信息,请参阅我 2009 年有关该主题的文章

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

    还有这个相关的问题:

    Why does my C# array lose type sign information when cast to object?

    【讨论】:

    • 优化失败的一种情况是enumValues.Cast&lt;StringComparison&gt;().ToArray(),它会因ArrayTypeMismatchException 而失败,而enumValues.Cast&lt;StringComparison&gt;().Skip(0).ToArray() 会成功,因为Cast&lt;TResult&gt; 中的优化意味着ToArray() 中的优化使用Array.CopyTo() 抛出,因为它需要比产生的 Cast() 更紧密的匹配。 (除了在 .NET Core 中放宽了对 Array.CopyTo() 的限制以允许这样做)。
    猜你喜欢
    • 2023-03-24
    • 1970-01-01
    • 2011-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-16
    • 1970-01-01
    相关资源
    最近更新 更多