You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

用于函数映射的委托字典与Switch语句对比及优化问询

实现通用C#对象线性插值(适配Unity ScriptableObject)

问题背景

我希望实现一个无需知晓类实现细节的对象线性插值函数,将其应用于Unity的ScriptableObject类,以实现游戏数据预设间的插值,该问题及概念适用于任意C#对象。

已实现的基础逻辑

通过反射获取类的字段与属性(Settable是FieldInfo和PropertyInfo的包装类),核心遍历逻辑如下:

public static void LerpBetweenObjects<T>(T destinationObj, T startObj, T endObj, float t)
{
    var type = typeof(T);
    var props = type.GetProperties(bindingFlags);
    var fields = type.GetFields(bindingFlags);
    foreach (var item in props)
    {
        PropertyInfo itemLocal = item;
        LerpBetweenMembersRefResult(new Settable(itemLocal), destinationObj, item.GetValue(startObj), item.GetValue(endObj), t);
    }
        
    foreach (var item in fields)
    {
        FieldInfo itemLocal = item;
        LerpBetweenMembersRefResult(new Settable(itemLocal), destinationObj, item.GetValue(startObj), item.GetValue(endObj), t);
    }
}

方案1:Switch语句实现插值

最初采用Switch模式匹配实现类型对应的插值逻辑:

public static void LerpBetweenMembersRefResult(ISettable result, object resultObj, object start, object end, float t)
{
    switch (start)
    {
        case float floatStart:
            result.SetValue(resultObj, Mathf.Lerp(floatStart, (float)end, t));
            break;
        case Color colorStart:
            result.SetValue(resultObj, Color.Lerp(colorStart, (Color)end, t));
            break;
        default:
            result = start;
            break;
    }
}

方案2:委托字典实现插值

参考建议改用委托字典优化:

private static Dictionary<Type, Action<ISettable, object, object, object, float>> lerpFuncsv2 = new Dictionary<Type, Action<ISettable, object, object, object, float>>()
{
    { typeof(float), (resultField, resObj, start, end, t) => resultField.SetValue(resObj, Mathf.Lerp((float)start, (float)end, t)) },
    { typeof(Color), (resultField, resObj, start, end, t) => resultField.SetValue(resObj, Color.Lerp((Color)start, (Color)end, t)) }
};

public static void LerpBetweenMembersRefResult(ISettable result, object resultObj, object start, object end, float t)
{
    if (lerpFuncsv2.TryGetValue(start.GetType(), out Action<ISettable, object, object, object, float> action))
    {
        action?.Invoke(result, resultObj, start, end, t);
    }
}

疑问

  1. 是否存在比这两种方案更优的实现方式?
  2. 若没有,使用委托字典相比Switch语句有哪些优势?

解答

1. 更优的实现方案推荐

有两种更高效且易维护的方案可选:

方案A:自定义插值接口+泛型扩展

定义统一的插值接口,让需要支持插值的类型实现或通过扩展方法适配,既保留通用性,又能避免反射的性能损耗,同时支持自定义插值逻辑:

public interface ILerpable<T>
{
    T Lerp(T end, float t);
}

// 为Unity内置类型添加扩展实现
public static class LerpableExtensions
{
    public static float Lerp(this float start, float end, float t) => Mathf.Lerp(start, end, t);
    public static Color Lerp(this Color start, Color end, float t) => Color.Lerp(start, end, t);
    public static Vector3 Lerp(this Vector3 start, Vector3 end, float t) => Vector3.Lerp(start, end, t);
}

// 修改插值成员函数,优先调用接口逻辑
public static void LerpBetweenMembersRefResult(ISettable result, object resultObj, object start, object end, float t)
{
    // 优先处理实现了ILerpable的类型
    var lerpableType = typeof(ILerpable<>).MakeGenericType(start.GetType());
    if (lerpableType.IsAssignableFrom(start.GetType()))
    {
        var lerpMethod = lerpableType.GetMethod(nameof(ILerpable<object>.Lerp));
        var lerpedValue = lerpMethod.Invoke(start, new object[] { end, t });
        result.SetValue(resultObj, lerpedValue);
        return;
    }

    // fallback到委托字典处理未实现接口的类型
    if (lerpFuncsv2.TryGetValue(start.GetType(), out var action))
    {
        action?.Invoke(result, resultObj, start, end, t);
    }
}

方案B:表达式树缓存反射操作

反射的性能瓶颈主要在GetValue和SetValue,可以用表达式树将反射调用编译为委托并缓存,后续调用性能接近直接调用:

// 缓存字段/属性的获取和设置委托
private static Dictionary<MemberInfo, Func<object, object>> _getters = new Dictionary<MemberInfo, Func<object, object>>();
private static Dictionary<MemberInfo, Action<object, object>> _setters = new Dictionary<MemberInfo, Action<object, object>>();

// 编译获取值的委托
private static Func<object, object> GetGetter(MemberInfo member)
{
    if (_getters.TryGetValue(member, out var getter)) return getter;
    
    var param = Expression.Parameter(typeof(object), "obj");
    Expression body;
    if (member is PropertyInfo prop)
    {
        body = Expression.Property(Expression.Convert(param, prop.DeclaringType), prop);
    }
    else if (member is FieldInfo field)
    {
        body = Expression.Field(Expression.Convert(param, field.DeclaringType), field);
    }
    else
    {
        throw new ArgumentException("Member must be a property or field");
    }
    body = Expression.Convert(body, typeof(object));
    getter = Expression.Lambda<Func<object, object>>(body, param).Compile();
    _getters[member] = getter;
    return getter;
}

// 编译设置值的委托(类似GetGetter逻辑)
// ... 此处省略Set委托的编译代码

// 修改LerpBetweenObjects,使用缓存的委托获取值
public static void LerpBetweenObjects<T>(T destinationObj, T startObj, T endObj, float t)
{
    var type = typeof(T);
    var props = type.GetProperties(bindingFlags);
    var fields = type.GetFields(bindingFlags);
    
    foreach (var prop in props)
    {
        var getter = GetGetter(prop);
        var startValue = getter(startObj);
        var endValue = getter(endObj);
        LerpBetweenMembersRefResult(new Settable(prop), destinationObj, startValue, endValue, t);
    }
    
    foreach (var field in fields)
    {
        var getter = GetGetter(field);
        var startValue = getter(startObj);
        var endValue = getter(endObj);
        LerpBetweenMembersRefResult(new Settable(field), destinationObj, startValue, endValue, t);
    }
}

2. 委托字典相比Switch语句的优势

  • 扩展性更强:无需修改核心插值函数,只需在字典中新增类型-委托对即可支持新类型,完全符合开闭原则。比如新增Vector3插值,直接添加一行字典项,不用修改switch的case分支。
  • 类型处理更灵活:对于派生类型、自定义值类型(或引用类型),字典可以直接按typeof(MyCustomType)注册逻辑,而switch需要逐个添加case,无法动态适配。
  • 代码组织更清晰:所有插值逻辑集中在字典初始化处,而非分散在switch的多个case中,便于统一维护和批量调整。
  • 支持动态注册:可以在运行时动态添加插值逻辑(比如游戏插件扩展自定义数据类型),switch语句无法实现这种动态扩展能力。
  • 性能差异可忽略:字典的TryGetValue性能损耗极小,与switch的模式匹配几乎无差异,而扩展性带来的收益远大于这点性能成本。

内容的提问来源于stack exchange,提问作者sandsalamand

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 12:50:21