用于函数映射的委托字典与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); } }
疑问
- 是否存在比这两种方案更优的实现方式?
- 若没有,使用委托字典相比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
相关产品推荐
相关产品推荐

