不能转换类型:为什么需要两次强制转换?

25

考虑这个高度简化的例子:

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
    }
  }
}

为什么编译器会对单个类型转换 (Dog)this._animal 报错?我不明白为什么编译器需要两次转换。 _animal 不可能是除了 Animal 以外的任何东西,对吗?
当然,这个问题源于一个实际例子,我需要修改现有代码,而类似的转换是最方便的方法,而不必重构整个代码(是的,使用组合而不是继承 ;))。

1
为什么不将 SoundRecorder 方法移动到接口中,然后让两个类都实现它呢?这样你就可以轻松地调用它了! - Ahmed Magdy
10
@AhmedMagdy:我能做很多事情,但这不是问题的关键。 - Gert Arnold
1
请注意,即使您有两个强制转换,编译器也只会生成一个“castclass Dog”的IL指令。 - Random832
@Random832: 你说得对!我必须承认,我几乎从不检查IL代码,但这种观察有点有趣。我认为,在编译过程通过所有验证之后,可以优化编写IL代码的过程。 - Gert Arnold
@GertArnold:我已经编辑了我的回答 - 你能看懂它吗? - Jon Skeet
5个回答

34

编辑:这是对Polit的回答的一次尝试性重述——我认为我知道他想说什么,但我可能错了。

我的原始答案(在下面)某种程度上仍然是标准答案:编译器拒绝它是因为语言规范要求如此 :) 但是,为了猜测语言设计者的观点(我从未参加过C#设计委员会,我也不认为我向他们询问过这个问题,所以这确实是猜测......),以下是......

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

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

如果某些事情不可能出错,那么它是隐式的。如果某些事情可能出错,语言的设计就要求你告诉编译器:“相信我,在执行时它会工作,尽管现在你不能保证。”显然,在执行时仍然有检查,但你基本上是告诉编译器你知道自己在做什么:

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

现在编译器已经可以防止您尝试转换,因为它认为这种转换在实际中无法起到有用的作用1

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

编译器知道从字符串到 GUID 没有转换,因此编译器不相信你执意认为自己知道在做什么:显然你不知道。这是“编译时”和“运行时”检查的简单情况。但泛型呢?考虑下面的方法:
public Stream ConvertToStream<T>(T value)
{
    return (Stream) value;
}

编译器知道什么?我们有两个可能变化的东西:

  1. 值(当然是在执行时变化)
  2. 类型参数T,它在可能不同的编译时间指定。(这里忽略反射,即使T也只有在执行时才知道。)我们可以稍后编译调用代码,就像这样:
ConvertToStream<string>(value);

如果您将类型参数T替换为string,那么此时该方法就没有意义了,因为最终生成的代码是无法编译的:

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

泛型并不是通过进行这种类型替换和重新编译来工作的,这会影响到重载等 - 但有时这种想法是有帮助的。

编译器无法在编译调用时报告这个问题 - 调用不违反对T的任何约束,并且方法体应视为实现细节。因此,如果编译器想要防止以引入非意义转换的方式调用该方法,它必须在方法本身被编译时这样做。

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

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

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

这段代码在通用形式下是可以编译的,尽管类型替换后的版本不行。所以可能有更深层次的原因。也许在某些情况下,根本没有合适的IL来表示转换 - 而更容易的情况并不值得让语言变得更加复杂...
有时它会出现"错误",因为有时候CLR中的转换是有效的,但在C#中却无效,例如int[]到uint[]。此处暂且忽略这些边缘情况。
对于那些不喜欢在这个答案中拟人化编译器的人表示歉意。显然,编译器实际上没有任何关于开发者的情感观点,但我相信这有助于传达观点。
简单地说,编译器抱怨是因为语言规范要求它这样做。规则在C#4规范的第6.2.7节中给出。
以下显式转换存在于给定类型参数T中:
从类型参数U到T,前提是T依赖于U。(见10.1.5节。)
这里Dog不依赖于T,因此不允许进行转换。
我怀疑这个规则的存在是为了避免一些晦涩的角落情况 - 在这种情况下,当你可以逻辑上看到它应该是一个有效的转换尝试时,这有点麻烦,但我认为将这种逻辑编码化会使语言更加复杂。
请注意,另一种选择可能是使用as而不是is-then-cast:
Dog dog = this._animal as Dog;
if (dog != null)
{
    dog.Bark();
}

我认为这样更清晰,因为只需要进行一次转换。


可能只能接受这种情况了,还有更糟糕的事情... 你关于 as 关键字的观点是正确的,但我尽可能使用显式转换。 - Gert Arnold
@GertArnold:当这是一个无条件转换时,这很好-但我更喜欢使用as而不是is后跟转换。 - Jon Skeet
1
Jon,太棒了!现在你已经真正探索了你之前提到的晦涩的边缘案例。我感到很荣幸能够得到如此详尽的答案,谢谢! - Gert Arnold

14
问题在于编译器无法保证_animal能够被转换为Dog,因为SoundRecorded的类型参数只要求类型为Animal或继承自Animal。因此,编译器实际上在考虑:如果您构造了一个SoundRecorder<Cat>,那么强制转换操作就是无效的。
不幸的是(或者说不是不幸的是),编译器并不聪明到可以看到您通过先进行“is”检查安全地防止代码到达那里的情况。
如果将给定的动物存储为实际动物,则不会出现这个问题,因为编译器始终允许从基类型向派生类型进行任何转换。但编译器不允许从Dog到Cat进行转换。
编辑 请参见Jon Skeet的答案以获得更具体的解释。

2
你可以争辩说(Animal)this._animal仍然可能是一只猫,而将其强制转换为狗是无效的。不幸的是,我怀疑答案更接近于“因为规范这样规定”,实际的根本原因可能会更加复杂。 :) - Chris
位或继承自动物是我忽略的一点,但实际上,这不应该是一个问题。 - Gert Arnold
1
我不同意这个答案。编译器无法保证转换在执行时能够正常工作,这是它不能成为隐式转换的一个很好的理由 - 但显式转换在许多地方都存在这种风险。我可以写:object x = "hello"; string y = (string) x;,同样,编译器无法保证x可以强制转换为string。那里面有什么不同呢?我怀疑你所写的某个类型参数可能会使转换总是失败,但我认为需要更仔细地表达。 - Jon Skeet
@GertArnold:是的,我认为这个答案中有一些价值,但在我看来它没有被表达得那么清楚。 - Jon Skeet
@Polity:比预期晚了一些,但我已经完成了。你能看一下这是否有意义吗? - Jon Skeet
显示剩余7条评论

2

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

var dog = _animal as Dog;

if(dog != null)
{
    dog.Bark();
}

本文涉及到关于转换泛型参数的主题。


1

在类型约束为Animal的情况下,不存在从AnimalDog的显式类型转换。尽管Dog是一种Animal,但编译器不知道T是Dog,因此不允许进行强制类型转换。

您可以通过隐式转换来解决这个问题。

implicit operator Animal(Dog myClass) 

或者可以使用以下类似的东西

Dog d = _animal as Dog;

0
从 C# 7.0 开始,您现在可以使用 声明模式 来检查表达式的运行时类型,如果匹配成功,则将表达式结果分配给已声明的变量。
public void RecordSound()
{
    if (_animal is Dog dog)
    {
        dog.Bark();
    }
}

我只是想提一下这个,尽管它并没有回答你问题中的“为什么”部分。对于这部分已经有一些很好的答案了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接