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

Ger*_*old 23 c# generics inheritance

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

abstract class Animal { }

class Dog : Animal
{
  public void Bark() { }
}
class Cat : Animal
{
  public void Mew() { }
}

class SoundRecorder<T> 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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Jon*_*eet 32

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

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

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

string x = "foo";
object y = x;
Run Code Online (Sandbox Code Playgroud)

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

object x = "foo";
string y = (string) x;
Run Code Online (Sandbox Code Playgroud)

现在,编译器已经阻止您尝试它认为不能转换工作1在一个有用的方法:

string x = "foo";
Guid y = (Guid) x;
Run Code Online (Sandbox Code Playgroud)

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

所以这些是"编译时间"与"执行时间"检查的简单情况.但是仿制药呢?考虑这种方法:

public Stream ConvertToStream<T>(T value)
{
    return (Stream) value;
}
Run Code Online (Sandbox Code Playgroud)

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

ConvertToStream<string>(value);
Run Code Online (Sandbox Code Playgroud)

在这一点上,如果更换类型参数的方法没有任何意义Tstring,你最终与代码不会 编译:

// After type substitution
public Stream ConvertToStream(string value)
{
    // Invalid
    return (Stream) value;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

// Valid
public Stream ConvertToStream<T>(T value)
{
    return value as Stream;
}

// Invalid
public Stream ConvertToStream(string value)
{
    return value as Stream;
}
Run Code Online (Sandbox Code Playgroud)

此代码不会编译通用的形式,即使取代后的版本没有.所以也许有更深层次的原因.也许在某些情况下,根本就没有合适的IL代表转换 - 而更容易的案例不值得使语言更复杂......

1它有时会这个"错误"的,在有些时候,转换为在CLR有效,但不是在C#中,如int[]uint[].我暂时会忽略这些边缘情况.

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


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

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

...

  • 从类型参数UT,提供T依赖于U.(见10.1.5节)

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

我怀疑这条规则是为了避免一些不起眼的角落情况下 - 在这种情况下,它是一个有点痛的时候可以逻辑地看到,它应该是一个有效的尝试转换,但我怀疑编纂这一逻辑会使语言更复杂.

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

Dog dog = this._animal as Dog;
if (dog != null)
{
    dog.Bark();
}
Run Code Online (Sandbox Code Playgroud)

无论如何,我认为这只是清洁工,只进行一次转换.


Pol*_*ity 13

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

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

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

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

  • 你可能会争辩说`(动物)this._animal`仍然可能是一只猫而且施放给狗是无效的.可悲的是,我怀疑答案更像是"因为规范如此说明",实际的潜在原因有点复杂.:) (2认同)