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

时间:2019-03-05 12:55:04

标签: 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
}

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

所以我偷看了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#中,数组有各种特殊的大小写,但是老实说,我不确定。请有人可以向我解释这是怎么回事,为什么?

3 个答案:

答案 0 :(得分:17)

  

我认为这与以下事实有关: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[])进行隐式强制转换,以及从ArrayIEnumerable到任何类型的数组的显式强制转换,也可以强制转换(这是粘性部分)对基本类型或枚举数组的ArrayIEnumerable引用到具有相同大小的基本类型的枚举数组(基于intuint和基于int的枚举大小都相同)。

这意味着,当人们仅可以直接转换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()都使用了调用{{ 1}}内部,在失败的数组上,这里涉及到的各种变化。

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

答案 1 :(得分:12)

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

  

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

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

现在,我同意Foo[]可以用作IEnumerable<int>的事实很奇怪,但是请记住,Foo只是{{1 }}。 int的大小与Foo的大小相同,int的内容与Foo的内容相同,因此CLR当被问到“这个int可以用作Foo[]吗?”时,其回答是“是”。

那呢?

  

IEnumerable<int>导致编译器错误

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

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

  • CLR表示“ enumValues as IEnumerable<int>可以用作Foo[],而int[]和...可以用作”。
  • C#类型分析器更具限制性。 它没有使用CLR的所有宽松协方差规则。 C#类型分析器将允许uint[]用作string[],并允许object[]用作IEnumerable<string>,但不允许IEnumerable<object>被用作用作Foo[]int[],依此类推。 C#仅在不同类型都是引用类型时才允许协方差。当各种类型是引用类型或IEnumerable<int>intuint大小的枚举时,CLR允许协方差。

C#编译器“知道”从intFoo[]的转换在C#类型系统中无法成功完成 ,因此会产生编译器错误; C#中的转换必须可能才能合法。编译器没有考虑到在更宽松的CLR类型系统中可能做到这一点。

通过将类型转换插入IEnumerable<int>object或其他任何内容,您是在告诉C#编译器停止使用C#规则,并开始让运行时解决它。

通过删除强制类型转换,您是在说要C#编译器呈现其判断,并且确实如此。

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

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

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

接着是第一和第三选择,C#1.0设计团队选择了第三。 (请记住,C#1.0设计团队不知道他们将在C#2.0中添加泛型还是在C#4.0中添加泛型变量。)对于C#1.0设计团队,问题是Cast<T>是否合法,以及他们决定不。然后再次针对C#2.0和C#4.0做出了设计决定。

双方都有很多有原则的论据,但实际上,这种情况在现实世界的代码中几乎不会出现,并且不一致几乎不重要,因此,成本最低的选择就是忍受{{1 }}是合法的,但enumValues as int[]不是合法的。

有关此内容的更多信息,请参阅我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?

答案 2 :(得分:1)

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

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

尽管您将Volvo强制转换为Car,但该对象仍然是Volvo。如果您要求提供其类型,则会得到它是typeof(Volvo)

Enumerable.Cast<TResult>会将输入序列的每个元素强制转换为TResult,但它们仍然是您的原始对象。因此,即使将它们转换为整数之后,您的Foos序列仍然是Foos。

在评论后添加

几个人评论说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

结论:可枚举。Cast不会更改对象的类型