泛型:强制转换和值类型,为啥这是非法的?

Posted

技术标签:

【中文标题】泛型:强制转换和值类型,为啥这是非法的?【英文标题】:Generics: casting and value types, why is this illegal?泛型:强制转换和值类型,为什么这是非法的? 【发布时间】:2021-09-11 06:10:34 【问题描述】:

为什么这是编译时错误?

public TCastTo CastMe<TSource, TCastTo>(TSource i)

     return (TCastTo)i;

错误:

无法将类型“TSource”转换为“TCastTo”

为什么这是一个运行时错误?

public TCastTo CastMe<TSource, TCastTo>(TSource i)

     return (TCastTo)(object)i;


int a = 4;
long b = CastMe<int, long>(a); // InvalidCastException

// this contrived example works
int aa = 4;
int bb = CastMe<int, int>(aa);

// this also works, the problem is limited to value types
string s = "foo";
object o = CastMe<string, object>(s);

我在 SO 和互联网上搜索了这个问题的答案,并找到了很多关于类似通用相关铸造问题的解释,但我在这个特别简单的案例中找不到任何东西。

【问题讨论】:

【参考方案1】:

为什么这是编译时错误?

问题是每个可能的值类型组合都有不同的规则来说明强制转换的含义。将 64 位 double 转换为 16 位 int 与将小数转换为 float 等代码完全不同。可能性的数量是巨大的。所以像编译器一样思考。 编译器应该为您的程序生成什么代码?

编译器必须生成代码,在运行时再次启动编译器,重新分析类型,并动态发出适当的代码

这看起来可能比你预期的泛型更多的工作和更少的性能,所以我们简单地取缔它。如果您真正想要的是让编译器重新启动并分析类型,请在 C# 4 中使用“动态”;这就是它的作用。

为什么这是一个运行时错误?

同样的原因。

出于与上述相同的原因,装箱的 int 只能拆箱为 int(或 int?);如果 CLR 尝试从装箱值类型到所有其他可能的值类型进行所有可能的转换,那么本质上它必须在运行时再次运行编译器。那会出乎意料的慢。

那么为什么引用类型不是错误呢?

因为每个引用类型转换都与其他所有引用类型转换相同:您询问对象以查看它是否派生自所需类型或与所需类型相同。如果不是,则抛出异常(如果进行强制转换)或导致 null/false(如果使用“as/is”运算符)。这些规则对于引用类型是一致的,但对于值类型却不是这样。请记住引用类型知道它们自己的类型。值类型没有;对于值类型,进行存储的变量是唯一知道适用于这些位的类型语义的东西。值类型包含它们的值并且没有附加信息。引用类型包含它们的值以及大量额外数据。

有关更多信息,请参阅我关于该主题的文章:

http://ericlippert.com/2009/03/03/representation-and-identity/

【讨论】:

dynamic 的性能与创建和编译表达式树(并缓存生成的委托)相比如何? @Ben:这正是 dynamic 所做的; DLR 执行类型分析、构造表达式树、编译它并缓存结果。下次遇到代码时,DLR 会查询其缓存,确定类型匹配是否足够接近以重用缓存状态,并在可能的情况下重用它。 @Eric:好的,那么 DLR 缓存查找或获取泛型类的静态成员哪个更有效?似乎当类型被称为泛型参数时,显式执行表达式树的内容可能比将其委托给dynamic 稍快。 @Ben:当然,也许。有关系吗?问哪种表达式树编译“更高效”,就像问哪种 1970 年代的凯迪拉克更省油。如果您关心燃油效率,请不要一开始就驾驶 1970 年代的 Caddy。任何涉及表达式树的解决方案都将比进行静态确定的强制转换贵几个数量级。表达式树是重量级的。 @Eric,你不是说使用“as”运算符吗? (如果使用“as”运算符)【参考方案2】:

C# 对多个不同的底层操作使用一种强制转换语法:

上调 沮丧 拳击 拆箱 数值转换 用户自定义转换

在通用上下文中,编译器无法知道哪些是正确的,并且它们都会生成不同的 MSIL,因此它会退出。

改为写return (TCastTo)(object)i;,强制编译器向上转换为object,然后向下转换为TCastTo。编译器会生成代码,但如果这不是转换相关类型的正确方法,则会出现运行时错误。


代码示例:

public static class DefaultConverter<TInput, TOutput>

    private static Converter<TInput, TOutput> cached;

    static DefaultConverter()
    
        ParameterExpression p = Expression.Parameter(typeof(TSource));
        cached = Expression.Lambda<Converter<TSource, TCastTo>(Expression.Convert(p, typeof(TCastTo), p).Compile();
    

    public static Converter<TInput, TOutput> Instance  return cached; 


public static class DefaultConverter<TOutput>

     public static TOutput ConvertBen<TInput>(TInput from)  return DefaultConverter<TInput, TOutput>.Instance.Invoke(from); 
     public static TOutput ConvertEric(dynamic from)  return from; 

Eric 的方式肯定更短,但我认为我的方式应该更快。

【讨论】:

我有点后悔现在问这个问题,因为进一步的探索表明这里并没有真正涉及泛型。与 C# 一样,即使 string s = "s"; Type t = (Type)s; 也是编译时错误。这是一个运行时错误:int a = 4; long b = (long)(object)a;,所以我可以在不使用泛型的情况下得到这两个错误。可能泛型确实会在其中抛出某种看不见的扳手? @Matt:(不受约束的)泛型阻止编译器尝试找出您需要的转换。但是无论是否涉及泛型,进行错误的样式转换都会导致失败。【参考方案3】:

编译错误是因为 TSource 不能隐式转换为 TCastTo。这两种类型可能在它们的继承树上共享一个分支,但不能保证。如果您只想调用共享祖先的类型,您应该修改 CastMe() 签名以使用祖先类型而不是泛型。

运行时错误示例通过首先将 TSource i 转换为一个对象来避免第一个示例中的错误,C# 中的所有对象都派生自该对象。虽然编译器没有抱怨(因为 object -> 从它派生的东西可能是有效的),但如果强制转换无效,则通过 (Type)variable 语法进行强制转换的行为将抛出。 (编译器在示例 1 中阻止发生的相同问题)。

另一种解决方案,其功能类似于您正在寻找的...

    public static T2 CastTo<T, T2>(T input, Func<T, T2> convert)
    
        return convert(input);
    

你可以这样称呼它。

int a = 314;
long b = CastTo(a, i=>(long)i);

希望这会有所帮助。

【讨论】:

我不完全明白为什么long b = (long)someInt; 有效,但long b = (long)((object)someInt); 无效。将 int 转换为 object 首先将转换为 long 怎么样?你知道吗? 因为第一个表示数字转换,第二个表示拆箱(从object转换为值类型总是拆箱,从object转换为引用类型总是向下转换) .由于需要进行数字转换,因此导致拆箱的代码不起作用。 (object)someInt 做了一些叫做拳击的事情。它本质上将 int(值类型)包装在对象引用类型中。您实际上是在尝试将引用类型转换为示例中的值类型。 “隐式”一词不属于您的答案。 @Matt 有一个明确的演员表,但仍然失败。

以上是关于泛型:强制转换和值类型,为啥这是非法的?的主要内容,如果未能解决你的问题,请参考以下文章

c#中泛型集合怎样写强制类弄转换

java泛型与object的比较

你真的了解JAVA中的泛型E、T、K、V吗?

关于List泛型的强制转换

学习笔记——泛型

Java强制转换为啥没有四舍五入