泛型和类型约束规则

6

只是出于好奇,为什么编译器会将一个无约束的泛型类型与typeof(object)有所不同呢?

class Bar { }

class Foo
{
    void foo(object thing)
    {
        ((Bar)thing).ToString();
    }
}

class Foo<T>
{
    void foo(T thing)
    {
        ((Bar)thing).ToString();
    }
}

在上述代码中,将“T类型的thing”转换为Bar类型会导致编译错误。而将“object类型的thing”转换为Bar类型是可以通过编译的,当然这样做存在风险。
我不明白的是原因所在。毕竟,在.NET中,object类型是一个万能类型,运行时类型可以是任何类型的装箱值或对象。因此,我不明白为什么编译器要区分这两种情况。我只能说,“程序员希望编译器对泛型类型进行类型检查,但不希望对object类型进行检查”。 :) 这就是全部原因吗?
顺便说一下,我知道在Foo的情况下,我仍然可以通过编写以下内容来完成类型转换:
((Bar)(object)thing).ToString();

我只是想了解编译器为什么会这样做...


3
int强制转换为Bar在编译时是否合法?当使用int填充类型参数时,是否应该开始出现编译器错误?如果程序集不是您的,则无法看到问题。T不是对象,而是非常具体的东西。 - Anthony Pegram
1
你是否意识到,你可以说class Foo<T> where T : Bar来_确保_T始终可以转换为Bar - Rawling
1
我相信Eric Lippert在某个地方有一篇关于这个的博客文章,但我找不到它... - Jon Skeet
3
你遇到的问题是你认为T代表任何东西,实际上它代表一些非常具体的东西,但尚未指定。 - siride
由于将装箱的值类型强制转换为任何引用类型都会导致错误,因此真正的问题是编译器无法区分装箱的值类型和引用类型;因为如果它能够区分,即使在没有泛型的情况下,也会在从object进行此类转换时引发编译时错误。 - Damien_The_Unbeliever
2个回答

4
这里的重点在于“object”这个词。如果第一个例子不是“object”,它的行为就会与“object”相同。基本上,你现在所说的是:
(Bar)thing

需要转换一个T类型的对象到Bar类型,但通常情况下这是不合法的。通过添加object关键字,可以将其改成:

(Bar)(object)thing

需要进行的操作是“将T转换为object” - 这总是合法的,因为object是所有托管类型的根; 请注意,这可能涉及一个装箱(box)- "...然后将object强制转换为Bar" - 再次说明; 在编译时这总是合法的,并在运行时涉及类型检查(“unbox-any”)。

例如:假设TDateTime...

DateTime thing = ...
Bar bar = (Bar)(object)thing;

这是完全有效的;当然它在运行时会失败,但是:这是你需要记住的情景。


虽然正确,但我认为OP是在询问在后一种情况下将错误暴露在编译时间,而在前一种情况下暴露在运行时间的选择背后的设计决策(假设thing无法转换为“Bar”)。 - Ron Warholic
你们两个(@Marc,@Ron)都说得很对。我认为这是一个很好的解释。将对象转换为某些东西始终是向下转换,转换为更具体的东西,但一般情况可以是任何一种,因此构造类型不会为任何T编译 - 这是给出编译时错误的一个很好的理由! - The Dag

4

这涉及到创建泛型的语义和目的。如果有一个通用类型T,编译器不会让你随意将其直接转换为任何其他对象。这是有道理的,因为T的目的是强制程序员指定T实际上是什么类型的对象。它不会是"object",它将是特定类型的对象。在编译时,编译器无法知道T中会有什么,并且因此无法进行强制类型转换。

从object进行转换可行,因为它是一个匿名对象 - 而不是在其使用中定义的已知对象类型。

可以通过“where”子句扩展此功能。例如,您可以指定T必须是IBar类型;

interface IBar { }

class Bar : IBar { }

class Foo<T>
    where T : IBar
{
    void foo(T thing)
    {
        ((IBar)thing).ToString();
    }
}

继承也可以与where子句一起使用;
class Bar { }

class Foo<T>
    where T : Bar
{
    void foo(T thing)
    {
        // now you don't need to cast at all as the compiler knows
        // exactly what type T is (at a parent level at least)
        thing.ToString();
    }
}

我很高兴将其标记为答案,尽管我发现你的推理只是比我的原始建议多了一点点;它确实归结为“用户”的期望(我的通用类别的用户,或者说已定义T的构造类别)。说“T不仅仅是一个对象,它是某些具体的东西”是多余的,在我看来。任何对象都可以是,并且通常都是非常具体的东西。并且有不同的方式来查看同一个对象。例如,我可能想支持任何类型,但如果我知道该类型,则以特定方式行事-无论是通用类还是其他。 - The Dag
@TheDag 改变基于类型 T 的通用行为通常不是一个好主意,尽管处理特定类型(例如装箱或未装箱类型)的情况是有效的。在您的示例中,您尝试直接从默认情况下几乎肯定是特定类型的 T 转换为另一个特定类型“Bar”。这就像在没有关系或公共接口的情况下尝试执行 (Foo)Bar。我仍然认为编译器警告是有效的。 - DiskJunky

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