无法转换类型:为什么必须投两次?

鉴于这个高度简化的例子:

abstract class Animal { } class Dog : Animal { public void Bark() { } } class Cat : Animal { public void Mew() { } } class SoundRecorder where T : Animal { private readonly T _animal; public SoundRecorder(T animal) { _animal = animal; } public void RecordSound(string fact) { if (this._animal is Dog) { ((Dog)this._animal).Bark(); // Compiler: Cannot convert type 'T' to 'Dog'. ((Dog)(Animal)this._animal).Bark(); // Compiles OK } } } 

为什么编译器会抱怨单一类型转换(Dog)this._animal ? 我只是不明白为什么编译器似乎需要做两个演员的帮助。 _animal不能是Animal ,不是吗?

当然,这个问题是由一个现实生活中的例子推动的,在这个例子中,我必须修改现有代码,使得类似的强制转换是最方便的方法,而无需重构整个批次。 (是的,使用组合而不是inheritance;))。

问题是编译器不能保证_animal可以转换为Dog,因为你给SoundRecorded类型参数的唯一限制是类型应该是Animal或者inheritance自Animal。 所以编译器实际上在想:如果你构造一个SoundRecorder ,那么转换操作就是无效的。

不幸的是,编译器不够聪明,无法通过事先进行’is’检查来确保您安全地保护您的代码不会到达那里。

如果您要将给定的动物存储为实际动物,这不会成为问题,因为编译器始终允许从基本类型到派生类型的任何转换。 编译器不允许从Dog到Cat的转换

编辑请参阅Jon Skeets的答案以获得更具体的解释。

编辑:这是对Polity的回答的尝试重述 – 我我知道他想说的是什么,但我可能是错的。

我的原始答案(在线下)仍然在某些方面是规范的:编译器拒绝它,因为语言规范说它必须:)但是,试图猜测语言设计者的观点(我从来没有C#设计委员会的一部分,我不认为我已经问过这个问题了,所以这真的是猜测……)这里……

我们习惯于在“编译时”或“执行时”考虑转换的有效性。 通常,隐式转换是编译时保证有效的转换:

 string x = "foo"; object y = x; 

这不会出错,所以这是隐含的。 如果出现问题,那么语言的设计是为了让你必须告诉编译器,“相信我,我相信它会在执行时工作,即使你现在不能保证它。” 显然,无论如何都会检查执行时间,但是你基本上告诉编译器你知道你在做什么:

 object x = "foo"; string y = (string) x; 

现在,编译器已经阻止您以有用的方式尝试它认为永远无法工作的转换:

 string x = "foo"; Guid y = (Guid) x; 

编译器知道没有从stringGuid转换,所以编译器不相信你的抗议,你知道你在做什么:你显然不是2

所以这些是“编译时间”与“执行时间”检查的简单情况。 但是仿制药怎么样? 考虑这种方法:

 public Stream ConvertToStream(T value) { return (Stream) value; } 

编译器知道什么? 这里我们有两件事可以变化:值(当然在执行时变化)和类型参数T ,它在可能不同的编译时指定。 (我忽略了这里的reflection,甚至T只在执行时才知道。)我们可以稍后编译调用代码,如下所示:

 ConvertToStream(value); 

此时,如果将类型参数T替换为string ,则该方法没有意义,最终会得到无法编译的代码:

 // After type substitution public Stream ConvertToStream(string value) { // Invalid return (Stream) value; } 

(generics并不真正通过这种类型的替换和重新编译来工作,这会影响重载等 – 但它有时可能是一种有用的思考方式。)

编译器在编译调用时无法报告 – 调用不违反对T任何约束,并且方法的主体应被视为实现细节。 因此,如果编译器想要阻止以引入非感知转换的方式调用该方法,则必须在编译方法本身时执行此操作。

现在编译器/语言在这种方法中并不总是一致的。 例如,考虑对generics方法的这种更改,以及“使用T=string ”调用时的“以下类型替换”版本:

 // Valid public Stream ConvertToStream(T value) { return value as Stream; } // Invalid public Stream ConvertToStream(string value) { return value as Stream; } 

此代码以通用forms进行编译,即使类型替换后的版本没有。 所以也许有更深层次的原因。 也许在某些情况下,根本就没有合适的IL代表转换 – 更容易的情况是不值得使语言更复杂…

1它有时会出现“错误”,因为有时转换在CLR中有效但在C#中无效,例如int[]uint[] 。 我暂时会忽略这些边缘情况。

2向在这个答案中不喜欢编译器的拟人化的人道歉。 显然,编译器并没有真正对开发人员有任何情绪化的看法,但我相信它有助于理解这一点。


简单的答案是编译器抱怨,因为语言规范说它必须。 规则在C#4规范的6.2.7节中给出。

对于给定的类型参数T存在以下显式转换:

  • 从类型参数UT ,假设T取决于U (见10.1.5节)

这里Dog不依赖于T ,因此不允许转换。

我怀疑这个规则是为了避免一些模糊的角落情况 – 在这种情况下,当你可以逻辑地看到它应该是一个有效的尝试转换时有点痛苦,但我怀疑编写那个逻辑会使语言更加复杂。

请注意,替代方法可能是使用而不是is -then-cast:

 Dog dog = this._animal as Dog; if (dog != null) { dog.Bark(); } 

无论如何,我只是认为它更干净,只进行一次转换。

这可能是因为您指定generics类型扩展了Animal ,因此SoundRecorder可以使用Cat作为generics类型进行实例化。 因此,编译器不允许您将Animal的任意子类强制转换为Animal其他子类。 如果您想避免双重演绎,请尝试执行以下操作:

 var dog = _animal as Dog; if(dog != null) { dog.Bark(); } 

本文讨论了转换generics参数的主题

AnimalDog之间不存在明确的类型转换,因为您的约束表示T必须是Animal类型。 虽然Dog ‘是’ Animal ‘,但编译器并不知道T是Dog 。 因此,它不会让你施展。

您可以通过隐式转换来解决此问题

 implicit operator Animal(Dog myClass) 

或者可以使用下面的内容

 Dog d = _animal as Dog;