为啥铸造给CS0030,而“as”有效?

Posted

技术标签:

【中文标题】为啥铸造给CS0030,而“as”有效?【英文标题】:Why does casting give CS0030, while "as" works?为什么铸造给CS0030,而“as”有效? 【发布时间】:2011-09-21 18:01:25 【问题描述】:

假设我有一个通用方法:

T Foo(T x) 
    return x;

到目前为止一切顺利。但如果它是一个 Hashtable,我想做一些特别的事情。 (我知道这是一个完全人为的例子。Foo() 也不是一个非常令人兴奋的方法。一起玩。)

if (typeof(T) == typeof(Hashtable)) 
    var h = ((Hashtable)x);  // CS0030: Cannot convert type 'T' to 'System.Collections.Hashtable'

该死。不过,公平地说,我实际上无法判断这是否应该是合法的 C#。那么,如果我尝试用不同的方式来做呢?

if (typeof(T) == typeof(Hashtable)) 
    var h = x as Hashtable;  // works (and no, h isn't null)

这有点奇怪。根据 MSDN,expression as Typeexpression is type ? (type)expression : (type)null 相同(除了计算表达式两次)。

如果我尝试使用文档中的等效表达式会怎样?

if (typeof(T) == typeof(Hashtable)) 
    var h = (x is Hashtable ? (Hashtable)x : (Hashtable)null);  // CS0030: Cannot convert type 'T' to 'System.Collections.Hashtable'

我看到的强制转换和as 之间唯一记录的区别是“as 运算符仅执行引用转换和装箱转换”。也许我需要告诉它我正在使用引用类型?

T Foo(T x) where T : class 
    var h = ((Hashtable)x);  // CS0030: Cannot convert type 'T' to 'System.Collections.Hashtable'
    return x;

发生了什么事?为什么as 工作正常,而强制转换甚至无法编译?演员表应该起作用,还是 as 不起作用,或者演员表和 as 之间是否存在其他语言差异,而这些差异不在我发现的这些 MSDN 文档中?

【问题讨论】:

我很确定你从 MSDN 引用的那句话在泛型之前就已经存在了。 不过,如果仅此而已...肯定是个有趣的问题! 这是一个骗子,还是只是密切相关:***.com/questions/884315/…? 我会说它是相关的——我是从另一个方向来的。如果是这样的话,这不是我第一次尝试遵循过时的 MSDN 文档! @Ken:我认为另一个问题的标题是倒退的,在我看来实际上是一样的情况。 【参考方案1】:

Ben 的回答基本上是一针见血,但要稍微扩展一下:

这里的问题是,如果在编译时给定类型,人们自然期望泛型方法将执行与等效的非泛型方法相同的操作。在您的特定情况下,人们会期望如果 T 很短,那么 (int)t 应该做正确的事情 - 将 short 转换为 int。并且(double)t 应该把short 变成double。如果 T 是字节,那么 (int)t 应该将字节转换为 int,(double)t 应该将字节转换为双精度......现在也许你开始看到问题了。我们必须生成的通用代码基本上必须在运行时再次启动编译器并进行完整的类型分析,然后动态生成代码以按预期进行转换

这可能很昂贵;我们在 C# 4 中添加了该功能,如果这是您真正想要的,您可以将对象标记为“动态”类型,并且编译器的精简版本将在运行时再次启动并为您执行转换逻辑.

但那种昂贵的东西通常不是人们想要的。

“as”逻辑没有强制转换逻辑复杂,因为它不需要处理除装箱、拆箱和引用转换之外的任何转换。它不必处理用户定义的转换,也不必处理花哨的表示变化转换,例如将一字节数据结构转换为八字节数据结构的“字节到双精度”等。

这就是为什么泛型代码中允许使用“as”但不允许强制转换的原因。

说了这么多:你几乎肯定做错了。如果您必须在通用代码中进行类型测试您的代码不是通用的。这是一种非常糟糕的代码气味。

【讨论】:

要添加一个可能不是代码异味的示例,Enumerable.Count<TSource> 方法是通用的,但通过调用 .Count 而不是检查 TSource 是否是 ICollection 以获得巨大的性能优势迭代。是的,一个边缘案例,我同意这不是代码异味,但可能有充分的理由。 @MichaelStum:我同意你的观点。然而,问一个对象“你支持这个接口吗?”仍然是一件相当“通用”的事情。非泛型代码也会做这种事情。但是当你说“如果 T 恰好是 HashTable 类型,那么......”那么 T 根本没有被以通用方式使用。 啊,我想我明白了。从哲学上讲,您说“我关心这实现了一个 Count 方法,该方法具有我期望由 ICollection 定义的行为(又名。它不应该做我不希望的时髦的东西,比如通过扬声器从 1 计数到 X)” ? (只是试图理解这背后的设计理念,而不是“它加快了很多事情”) 我同意这是一种明确的代码气味;话虽如此,我认为它非常有用的地方之一是在您将在 C++ 中使用部分专业化的情况下。也许部分专业化也是一种代码味道,但这是我在模板和泛型之间错过的一个特性......【参考方案2】:

C# 中的强制转换运算符可以:

装箱/拆箱 向上/向下 调用用户定义的转换运算符

as Hashtable 总是表示第二个。

通过使用约束消除值类型,您已经淘汰了选项 1,但它仍然模棱两可。


以下是两种“最好”的方法,它们都有效:

Hashtable h = x as Hashtable;
if (h != null) 
    ...

if (x is Hashtable) 
    Hashtable h = (Hashtable)(object)x;
    ...

第一个只需要一次类型测试,因此非常有效。 JIT 优化器识别出第二个,并将其视为第一个(至少在处理非泛型类型时,我不确定这种特殊情况。)

【讨论】:

所以你说转换失败是因为类型中可能有转换运算符?不是说你错了,但是.. 编译器已经知道泛型类型至少是 object (OP 中的 T : class 部分),所以如果你可以盲目地投射 objects,你不应该也能够为泛型类型做到这一点吗? 这个解释对我来说很有意义。这也意味着 MSDN 文档没有错,只是令人困惑地写道:“A 与 B 相同,除了案例 C。请注意,方式 D 也不同。” @Blindy:当两者都匹配时,向下转换优先于用户定义的转换。由于每个对象都派生自object,因此来自object 的转换总是匹配向下转换。例如,如果这段代码写成(Hashtable)(object)x;,它就可以工作。 "as" 也可以装箱/拆箱。 “someInt as object”将框,“someObject as int?”如果它是一个装箱的 int,将取消装箱,如果不是,则给出一个可以为 null 的 int。 @EricLippert:正在编辑以处理这个问题。但它不会引入歧义,因为isas 不是动态的,编译器知道目标类型是引用类型还是值类型。 (显然你已经知道了)【参考方案3】:

“C# 编译器只允许您将泛型类型参数隐式转换为 Object 或约束指定的类型,如代码块 5 所示。这种隐式转换是类型安全的,因为在编译时会发现任何不兼容性。”

请参阅泛型和强制转换部分: http://msdn.microsoft.com/en-us/library/ms379564(v=vs.80).aspx#csharp_generics_topic5

【讨论】:

以上是关于为啥铸造给CS0030,而“as”有效?的主要内容,如果未能解决你的问题,请参考以下文章

为啥sql语句中float数据保留有效数据后小数点前的0不显示

为啥在 Swift 中的铸造成本如此之高,而不是……将其保留为 AnyObject?

为啥 (1)JDBC 代码有效而 (2) 无效?

Vigenere Cipher 只能在处理 C 中的空格(“”)之前有效 - 为啥?

为啥“引发错误”有效,而“断言”无效?

Angular DOM 操作:为啥 ViewChildren 有效而 ViewChild 无效