PAYATHON 2026

C# 如何在运行时模拟编译器的隐式和显式类型转换?

数据小许

无法通过一个通用泛型约束,让 (TOut)value 自动适用 C# 的所有隐式和显式转换规则。可以根据运行环境选择做法:运行时支持所需能力时,用表达式树根据源类型和目标类型建立转换委托;需要兼容 Unity 的 AOT 构建时,为所需的类型组合提前生成或注册转换委托。

转换接口需要保留源类型信息。只拿到一个值时,尤其是值为 null 时,无法还原调用位置原本的静态类型。

为什么指定泛型实参也不能让原方法通过编译?

下面的方法必须在定义时通过类型检查:

public TOut Convert<TIn, TOut>(TIn value) => (TOut)value;

编译器检查方法体时,并不知道 TIn 和 TOut 之间一定存在转换。之后调用 Convert<float, int>(10f),也不会让编译器按 float 和 int 重新编译这段源代码。

通过反射调用 MakeGenericMethod 构造封闭泛型方法,同样不能让原本无法编译的方法变成合法方法。

下面的写法虽然能编译,却不能解决数值转换问题:

public static TOut ConvertViaObject<TIn, TOut>(TIn value)
    => (TOut)(object)value;

例如,float 值装箱为 object 后,不能直接拆箱为 int。拆箱和 (int)floatValue 是不同的操作。

可以用表达式树建立显式转换委托

如果目标运行环境支持所需的表达式树执行能力,可以用 Expression.Convert 表达一次转换,再将其编译为委托:

using System;
using System.Linq.Expressions;

public static class RuntimeConversions
{
    public static Func<TIn, TOut> CreateExplicit<TIn, TOut>()
    {
        var value = Expression.Parameter(typeof(TIn), "value");
        var conversion = Expression.Convert(value, typeof(TOut));

        return Expression
            .Lambda<Func<TIn, TOut>>(conversion, value)
            .Compile();
    }
}

调用示例:

var toInt = RuntimeConversions.CreateExplicit<float, int>();
int result = toInt(10f); // 10

表达式参数的类型明确为 float,目标类型为 int,因此可以建立相应的数值转换。

表达式树也支持其转换规则所覆盖的用户定义转换,包括适用的 op_Implicit 和 op_Explicit。如果无法建立转换,通常会在构造表达式时抛出 InvalidOperationException;如果转换已经建立,但某个值无法完成转换,异常可能在委托执行时发生。

应按类型组合复用委托,避免每次转换都重新构造和编译表达式。

Expression.Convert 无法完成 C# 编译器的全部绑定,也不能用来判断是否只允许隐式转换:它可能接受显式转换。完整的 C# 绑定还涉及表达式上下文、常量值等信息。例如,整数常量的某些隐式转换不能仅凭两个 Type 判断。

保留源类型,就能处理带类型的 null

假设类型定义如下:

public sealed class MyType
{
    public static explicit operator int(MyType value)
        => value == null ? -1 : 1;
}

可以这样建立转换:

var toInt = RuntimeConversions.CreateExplicit<MyType, int>();
int result = toInt(null); // -1

虽然传入的值是 null,表达式参数的类型仍是 MyType,因此能找到相应的用户定义转换。

原来的方法只有目标类型参数:

public T Explicit<T>(dynamic value) => (T)value;

当参数接收到 null 时,动态绑定器没有 MyType 这一源类型信息。在 Explicit<int>(default(MyType)) 中,调用表达式原本的 MyType 类型不会随着 null 保留到方法内部。

因此,转换接口应保留 TIn,或者显式接收源 Type。对于 object 类型的容器,仅调用 value.GetType() 也不够:null 无法调用该方法,非 null 对象的运行时类型则可能与原表达式的静态类型不同。

为什么只搜索转换运算符不能覆盖基本类型?

op_Implicit 和 op_Explicit 表示用户定义的转换运算符。C# 内置数值转换不要求以这些普通反射方法的形式存在。

因此,即使完整遍历类型上的方法,也不能靠同一种搜索方式找齐 float、int、long 等类型之间的语言内置转换。缺少 Numerics 接口也不是这个问题的根本原因;这些接口并非完整 C# 转换规则的统一入口。

用户定义转换还涉及源类型、目标类型及适用的标准转换。简单筛选“参数类型完全相等”的方法,无法完整复现编译器的选择过程。

Unity 中如何选择实现?

除了 Unity 版本,还需要确认目标平台和脚本后端。编辑器中运行成功,并不能保证相同的 dynamic 或表达式编译路径适用于目标构建。

如果目标采用 IL2CPP/AOT,应验证实际使用的动态绑定、表达式执行及泛型实例是否受支持。通过反射构造泛型方法,也不意味着目标构建一定具备相应实例所需的可执行代码。

如果类型在编译时已知,转换组合也可以提前收集,预注册委托更容易控制:

using System;
using System.Collections.Generic;

public static class RegisteredConversions
{
    private static readonly Dictionary<(Type, Type), Delegate> ExplicitMap =
        new Dictionary<(Type, Type), Delegate>
        {
            [(typeof(float), typeof(int))] =
                new Func<float, int>(value => (int)value),

            [(typeof(MyType), typeof(int))] =
                new Func<MyType, int>(value => (int)value)
        };

    public static TOut Explicit<TIn, TOut>(TIn value)
    {
        if (!ExplicitMap.TryGetValue(
                (typeof(TIn), typeof(TOut)), out var conversion))
        {
            throw new InvalidOperationException(
                $"No registered conversion: {typeof(TIn)} -> {typeof(TOut)}");
        }

        return ((Func<TIn, TOut>)conversion)(value);
    }
}

调用时应明确保留所需的源类型:

int number = RegisteredConversions.Explicit<float, int>(10f);
int fromNull = RegisteredConversions.Explicit<MyType, int>(null);

这与问题中已有的转换表思路一致。可以通过编辑器工具或构建前代码生成来自动生成所需转换项,减少手写配对。生成后的 (int)value 由 C# 编译器检查,因此每个已注册表达式都采用相应的静态转换规则。转换表应明确覆盖范围;某个转换未注册,不能直接认定语言层面不存在该转换。

如果还需要单独提供“仅隐式转换”的接口,应使用独立的注册表,并通过类似 value => value 的带类型 lambda 让编译器检查隐式转换。不要直接用 Expression.Convert 的结果来判断。

还需要明确哪些转换行为?

  • 溢出处理:需要区分 checked 和 unchecked。表达式树提供 Expression.ConvertChecked,生成代码时也可以显式写出所需上下文。
  • nullable 类型:例如,int? 转 int 在值为 null 时会失败,不能与引用类型的 null 混为一谈。
  • enum 转换:枚举与整数的强制转换不同于按名称解析字符串;字符串解析需要另行定义规则。
  • 引用转换:转换表达式合法,也可能因传入对象的实际类型不符而在执行时失败。
  • 用户定义转换:转换运算符本身可以抛出异常,找到运算符不代表每个输入值都能成功转换。

对于当前需求,可以先用保留 TIn 的表达式树方案验证转换范围。如果目标构建受到 AOT 限制,再把所需转换生成为可静态编译的委托。这样可以处理 null 丢失源类型的问题,也能明确哪些转换已经获得支持。

备注:内容仅供参考。