考虑:
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#中,数组有各种特殊的大小写,但是老实说,我不确定。请有人可以向我解释这是怎么回事,为什么?
答案 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[]
)进行隐式强制转换,以及从Array
或IEnumerable
到任何类型的数组的显式强制转换,也可以强制转换(这是粘性部分)对基本类型或枚举数组的Array
和IEnumerable
引用到具有相同大小的基本类型的枚举数组(基于int
,uint
和基于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的规则不匹配。
enumValues as IEnumerable<int>
可以用作Foo[]
,而int[]
和...可以用作”。 uint[]
用作string[]
,并允许object[]
用作IEnumerable<string>
,但不允许IEnumerable<object>
被用作用作Foo[]
或int[]
,依此类推。 C#仅在不同类型都是引用类型时才允许协方差。当各种类型是引用类型或IEnumerable<int>
,int
或uint
大小的枚举时,CLR允许协方差。 C#编译器“知道”从int
到Foo[]
的转换在C#类型系统中无法成功完成 ,因此会产生编译器错误; C#中的转换必须可能才能合法。编译器没有考虑到在更宽松的CLR类型系统中可能做到这一点。
通过将类型转换插入IEnumerable<int>
或object
或其他任何内容,您是在告诉C#编译器停止使用C#规则,并开始让运行时解决它。
因此,现在我们遇到了语言设计问题;显然,我们在这里存在矛盾。有几种方法可以解决这种矛盾。
IEnumerable
运算符,以便在运行时实现C#规则;基本上,它必须检测合法的CLR但非法的C#转换并禁止它们,从而使所有此类转换变慢。而且,这将需要您的方案转到as
的内存分配慢路径,而不是保留引用的快速路径。
第二种选择显然是不可行的。它只会增加成本,而且除了一致性之外没有其他好处。
接着是第一和第三选择,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年关于该主题的文章
和这个相关的问题:
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不会更改对象的类型