静态类型和动态类型各有所长,静态类型能够让编译器帮你找出更多的错误,因为编译器能够在编译时进行大部分的检查工作。C# 是一种静态类型的语言,不过它加入了动态类型的语言特性,可以更高效地解决问题。
一、目录
- 三十八、理解动态类型的优劣
- 三十九、使用动态类型表达泛型类型参数的运行时类型
- 四十、将接受匿名类型的参数声明为 dynamic
- 四十一、用 DynamicObject 或 IDynamicMetaObjectProvider 实现数据驱动的动态类型
- 四十二、如何使用表达式 API
- 四十三、使用表达式将延迟绑定转换为预先绑定
- 四十四、尽量减少在公有 API 中使用动态类型
三十八、理解动态类型的优劣
- C# 动态类型是为了让静态代码能够更加平滑地与其他使用动态类型的环境进行交互,而不是鼓励在一般场景中使用 dynamic 进行动态编程。
- 只要对象在运行时包含成员,那么即可正常使用。
- 若是一个操作数(包括 this)为动态类型,那么返回结果也会是动态类型。不过,最后依然要转换成静态类型,以便被其它 C# 代码所使用,以及被编译器感知。
- 当你需要不知道具体类型的运行时解析方法的时候,动态类型是最佳的工具。如果你能在编译期间明确类型,那么可以使用 lambda 表达式和函数式编程来解决问题。
- 表达式树,一种在运行时创建代码的方法(下面提供示例)。
- 大多数情况,可以使用 Lambda 表达式创建泛型 API,让调用者自己动态定义所需要执行的代码即可。
- 优先使用静态类型,静态类型比动态类型更高效,动态类型和在运行时创建表达式树都会带来性能上的影响,即便这点影响微不足道。
- 若你能控制程序中所有涉及的类型时,可以引入一个接口,而不是动态类型,即基于接口编程,并让所有需要支持该接口行为的类型都实现该接口。通过 C# 类型系统可以减少代码在运行时所产生的错误,编译器也能够生成更加高效的代码。
- 动态类型做法的效率比纯粹的静态类型下降挺大,但实现的难度却比解析表达式树要简单地挺多。
这里,我使用 3 种数字相加的方法,dynamic 动态、Func 委托以及使用表达式树进行相加的区别:
[TestMethod] public void Test() { var result1 = AddDynamic(2, 3); Console.WriteLine((int)result1); var result2 = AddFunc(3, 4, (x, y) => x + y); Console.WriteLine(result2); var result3 = AddExpressionTree(4, 5); Console.WriteLine(result3); } /// <summary> /// Add,动态 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> private dynamic AddDynamic(dynamic a, dynamic b) { return a + b; } /// <summary> /// Add,使用委托 /// </summary> /// <typeparam name="T1"></typeparam> /// <typeparam name="T2"></typeparam> /// <typeparam name="TR"></typeparam> /// <param name="a"></param> /// <param name="b"></param> /// <param name="func"></param> /// <returns></returns> private TR AddFunc<T1, T2, TR>(T1 a, T2 b, Func<T1, T2, TR> func) { return func(a, b); } /// <summary> /// Add,使用表达式树 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> private T AddExpressionTree<T>(T a, T b) { ParameterExpression leftOperand = Expression.Parameter(typeof(T), "left"); ParameterExpression rightOperand = Expression.Parameter(typeof(T), "right"); BinaryExpression body = Expression.Add(leftOperand, rightOperand); Expression<Func<T, T, T>> adder = Expression.Lambda<Func<T, T, T>>(body, leftOperand, rightOperand); Func<T, T, T> theDelegate = adder.Compile(); return theDelegate(a, b); } }
三十九、使用动态类型表达泛型类型参数的运行时类型
- System.Linq.Enumerable.Cast<T> 将序列中的对象转换成 T,从而使得 LINQ 可以配合 IEnumerable 进行工作。
- Convert<T> 要比 Cast<T> 适用性更广,但同时也会执行更多的工作。
四十、将接受匿名类型的参数声明为 dynamic
- 不要过度使用动态类型,因为动态调用会增加系统的额外开销,即便不大。
- 长远来看,具体类型更易于维护,编译器和类型系统也会为其提供更好的支持。
- 扩展方法不能基于动态对象定义。
四十一、用 DynamicObject 或 IDynamicMetaObjectProvider 实现数据驱动的动态类型
- 创建带有动态功能的类型的最简单的方法就是继承 System.Dynamic.DynamicObject。若能直接继承 DynamicObject,那么创建动态类就会比较简单。
- 实现 IDynamicMetaObjectProvider 就意味着需要实现方法 GetmetaObject()。
- 创建动态类型时首选继承,如果必须使用其他基类,可以手工实现 IDynamicMetaObjectProvider 接口,虽然所有的动态类型都会带来性能上的损失,但这种手工实现接口的方式所带来的损失往往更大一些。
这是一个实现动态类型模型的一个示例。除了需要继承 DynamicObject,还需要重写 TryGetMemebr() 和 TrySetMemebr()。
[TestMethod] public void Test1() { dynamic propDynamic = new DynamicPropertyBag(); propDynamic.Now = DateTime.Now; Console.WriteLine(propDynamic.Now); } /// <summary> /// 动态属性绑定模型 /// </summary> internal class DynamicPropertyBag : DynamicObject { private readonly Dictionary<string, object> _storage = new Dictionary<string, object>(); /// <summary> /// 获取属性值 /// </summary> /// <param name="binder"></param> /// <param name="result"></param> /// <returns></returns> public override bool TryGetMember(GetMemberBinder binder, out object result) { var key = binder.Name; if (_storage.ContainsKey(key)) { result = _storage[key]; return true; } result = null; return false; } /// <summary> /// 设置属性值 /// </summary> /// <param name="binder"></param> /// <param name="value"></param> /// <returns></returns> public override bool TrySetMember(SetMemberBinder binder, object value) { var key = binder.Name; try { if (_storage.ContainsKey(key)) { _storage[key] = value; } else { _storage.Add(key, value); } return true; } catch (Exception e) { Console.WriteLine(e); return false; } }
四十二、如何使用表达式 API
- 传统的反射 API 可以用表达式和表达式树进行更好的替代,表达式可以直接编译为委托。
- 接口使我们可以得到一个更为清晰、也更具可维护性的系统,反射是一个很强大的晚期绑定机制(虽然效率有所降低),.NET 框架使用它来实现 Windows 控件和 Web 控件的数据绑定。
这里提供了一个示例:
[TestMethod] public void Test2() { var result = Call<int, string>((x) => (x * 2).ToString("D")); Console.WriteLine(result); } /// <summary> /// 调用 /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="TResult"></typeparam> /// <param name="op"></param> /// <returns></returns> private TResult Call<T, TResult>(Expression<Func<T, TResult>> op) { var exp = op.Body as MethodCallExpression; var result = default(TResult); if (exp == null) { return result; } var methodName = exp.Method.Name; var parameters = exp.Arguments.Select(ProcessArgument); Console.WriteLine($"方法名 {methodName}"); foreach (var parameter in parameters) { Console.WriteLine("参数:"); Console.WriteLine($"\t{parameter.Item1}:{parameter.Item2}"); } return result; } /// <summary> /// 处理参数 /// </summary> /// <param name="expression"></param> /// <returns></returns> private Tuple<Type, object> ProcessArgument(Expression expression) { object arg = default(object); LambdaExpression l = Expression.Lambda(Expression.Convert(expression, expression.Type)); Type parmType = l.ReturnType; arg = l.Compile().DynamicInvoke(); return Tuple.Create(parmType, arg); }
四十三、使用表达式将延迟绑定转换为预先绑定
- 延迟绑定API要使用符号(symbol)信息来实现,而预先编译好的 API 则无需这些信息,表达式 API 正是二者之间的桥梁。
- 延迟绑定常见于 Silverlight 和 WPF 中使用的属性通知接口,通过实现 INotifyPropertyChanged 和 INotifyPropertyChanging 接口来实现属性变更的预绑定。
四十四、尽量减少在公有 API 中使用动态类型
- 优先使用 C# 的静态类型,并尽可能地降低动态类型的作用范围。若是想一直使用动态特性,你应该直接选用一种动态语言,而非 C#。
- 若要在程序中使用动态特性,请尽量不要在公有接口中使用,这样会将动态类型限制在一个单独的对象(或类型)中。
本系列
《Effective C#》快速笔记(一)- C# 语言习惯
《Effective C#》快速笔记(二)- .NET 资源托管
《Effective C#》快速笔记(三)- 使用 C# 表达设计
《Effective C#》快速笔记(五) - C# 中的动态编程
《Effective C#》快速笔记(六) - C# 高效编程要点补充
时间: 2024-10-14 06:10:26