周末空闲,选读了一下一本很不错的C#语言使用的书,特此记载下便于对项目代码进行重构和优化时查看。
Standing On Shoulders of Giants,附上思维导图,其中标记的颜色越深表示在实际中的实际意义越大。
名称 | 内容和示例 |
提供API时尽量提供泛型接口 | Public interface IComparable |
泛型约束尽可能的严格并有效 | Public delegate T FactoryFunc |
通过运行时类型检查具体化泛型算法 | 比如根据不同的集合类型优化相应算法 |
使用泛型强制执行编译时类型推测 | Public static T ReadFromStream(XmlReader inputStream) { return (T)factory.Deserialize(inputStream) } |
保证自定义泛型类支持可析构的类型参数 | Public sealed class EngineDriver |
通过委托在类型参数上定义方法约束 | Public static T Add |
不要在基类和接口上创建具体化的泛型类型 | 尽可能使的基类和接口的适用范围更加的广阔 |
推荐使用泛型方法,除非类型参数是实例字段 | Public static T Max |
推荐使用泛型的Tuple作为输出和引用参数 | 当设置方法的返回值,或者在需要使用ref参数的情形时,使用Tuple<>元组可以使代码更清晰,当然如果参数比较复杂,还是选择建立对应的DTO类型为宜 |
在泛型接口上增加对应的传统接口 | 这个在大家基础架构时非常重要,每个方法均提供泛型版本和object版本,使得代码有很强的兼容性。 Public static bool CheckEquality(object left, object right) { return left.Equals(right); } Public static bool CheckEquality |
名称 | 内容和示例 |
使用线程池代替创建线程 | 经过微软的官方测试,由自己调度线程和使用线程池,在每10万个计算消耗的平均时长比较中,前者所消耗时长为后者三倍,因而选用线程池作为默认多线程处理机制是合理的选择 Private static double ThreadPoolThreads(int numThreads) { var start = new Stopwatch(); Using(var e = new AutoResetEvent(false)){ int workerThreads = numThreads; start.Start();//watch.ElapsedMilliseconds, watch.Restart(), watch.Stop();for(var I = 0; I < numThreads; thread++) ThreadPool.QueueUserWorkItem( (x)=>{ // to doIf(Interlocked.Decrement(ref workThreads) == 0) { e.Set(); } }); }} |
使用后台工作者组件对象用于处理多线程通信 | 现在已经不再使用后台Worker,而推荐使用Task任务模型替代它,其逻辑为 |
将lock作为优先级最高的同步原语 | 使用lock相当于使用了Monitor.Enter和Exit,不过要方便很多,使用的是临界区的概念。Public int TotalNum {get{ lock(syncObj) return total; } set{ lock(syncObj) total++;} } |
Lock中方法体尽可能精简 | 在使用lock时,一定不要使用lock(this)和lock(typeof(MyType))的形式,这会造成很多的问题,必须保证锁的对象不是公开无法被外部使用的,常见的对方法加锁的形式有: 1.使用特性,[MethodImpl(MethodImplOptions.Synchronized)] 2.使用私有变量作为锁变量 private object syncHandler = new object(); 此外还有一种复杂点的形式如下。 Private object syncHandle; Private object GetSyncHandle(){InterLocked.CompareExchange(ref syncHandle, new object(), null); } |
避免在临界区中调用未知代码 | 比如不要在临界区中使用事件,因为事件的处理方法由调用方注册,是未知的,会造成相关的问题,一定要保证临界区中方法的确定性 |
理解在WinForm和WPF中的跨线程调用 | 做过WinForm编程的亲,一定遇到过一个InvalidOperationException,内容为跨线程操作非法,访问Control的线程不是创建线程,这其实是Winform、WPF等框架对UI的保护,避免多个不同线程修改UI值的情况。这种情况主要有一下三种方式来处理,最推荐的解决方案为第二种。
|
名称 | 内容和示例 |
为序列创建可组合的API, yield return xxx | Public static IEnumerable |
通过Action,Predicate,Functions解耦迭代器 | Public static IEnumerable |
根据请求生成序列 | [IEnumerable |
通过Function参数解耦 | Public static T Sum |
创建清晰,最小化,完整的方法组 | 即在提供方法时,尽可能的保证完备性(支持主要的类型) |
推荐定义方法重载操作符 | 还记得在学习C++时,很推荐重载操作符,不过在面向对象语言的今天,使用可读性更强的方法更合理 |
理解事件是如何增加对象运行时的耦合性 | public event EventHandler |
只声明非虚事件对象 | 在.NET中,事件提供了类似属性的简易语法,通过add,remove方法添加相关事件处理程序,其实event就是delegate的包装器,这个特殊的委托便于应用事件处理模型,同时提供线程安全性。由于事件的运行时耦合性,如果使用虚事件容易造成未知的错误, private EventHandler |
通过异常报告方法契约错误 | 当出现业务异常流程时,推荐抛出异常而不是使用TryXXX组合的方式,因为这样代码更加简单易懂。当然在与业务无关的,如简单数据转换的场景下,使用TryXXX是很好的选择 |
确定属性的行为和数据一样 | 让属性尽可能的简单,不要将复杂逻辑放在属性,如果需要可以通过提供相应方法的方式,使得代码更加通俗易懂,且使得调用人坚信属性的调用不会造成任何的性能影响 |
区分继承和组合 | 在适当的场景下,用组合代替继承是常见的代码设计模式,这样可以减少类的污染,在选用策略模式的场景下,组合使用的非常的多,常见的形式如下: public interface IContract{ void SampleImplMethod(); }public class MyInnerClass:IContract{ public void SampleImplMethod (){ //elided }} public class MyOuterClass:IContract{ private IContract impl = new MyInnerClass(); public void SampleImplMethod (){ impl.SampleImplMethod(); }} |
名称 | 内容和示例 |
通过扩展方法扩展接口 | Public static bool LessThan |
通过扩展方法增强已经构建的类型 | 这部分很容易理解,比如你使用系统提供的相关类,无法修改源码(虽然已开源),这时为了代码的便捷性和可读性,使用扩展方法增强该类变得非常有效 |
推荐隐式类型的本地变量 | 简单方便 |
通过匿名类限制类的可见范围 | 使得代码的封装性更好,更加健壮 |
为外部的组件创建可组合的API | 要求提供的API具有更好的健壮性,功能相对完整并独立,复用性更强,例如尽量不要使用可空类型作为接口参数等 |
避免修改绑定的变量 | 这部分内容涉及闭包,通过以下的例子可以很容易的理解 public void Test() { int index = 0;Func |
在匿名类型上定义本地函数 | public void Test01() { var randomNumbers = new Random(); var sequence = (from x in Generate(100, () => randomNumbers.NextDouble() * 100) let y = randomNumbers.NextDouble() * 100 select new { x, y }).TakeWhile(point => point.x < 75); foreach (var item in sequence) { Console.WriteLine(item); }} |
不要重载扩展方法 | 由于个人创建扩展方法的普遍性和完备性不强,重载此类方法容易降低程序的健壮性 |
名称 | 内容和示例 |
理解查询表达式如何映射到方法调用 | 简单来说,我们所写的LINQ语句都会先转化为对应的扩展方法,然后再解析相关的表达式树最后生成对应语句。 var people = from e in employees where e.Age > 30 orderby e.LastName, e.FirstName, e.Age select e; var people = employees.Where(e=>e.Age > 30).OrderBy(e=>e.LastName).ThenBy(e=>e.FirstName).ThenBy(e=>e.Age); |
推荐Lazy延迟加载查询 | 延迟加载表示数据到真正使用时再去获取,这个概念不太容易理解,简单来说,我们的获得集合函数调用实际上只是生成相应的查询语句,但并未实际执行,获得任何对象,只有在我们对其经行迭代等操作时,才真正的加载数据。这些概念其实都和委托紧密相关,从逻辑上讲就是加了一个新的层次,函数本身(可以说是其指针、地址)是一个层次,函数的实际调用又是一个层次,在javascript也有相似的概念,就比如FunctionA和FunctionA()的区别。 Private static IEnumerable |
推荐使用lambda表达式代替方法 | 这儿的实际意思是指在使用LINQ时,由于每个查询的局限性,不推荐在查询中调用外部方法,而因尽可能通过LINQ自身来完成相应工作,减少各个查询间的干扰 |
避免在Func和Action中抛出异常 | 这个也很好理解,由于Action等委托常用于集合操作中,而任何一个一场都会中断整个集合的操作,给集合操作带来了很大的不确定性,并且在并行运算时更加难以控制,因而在Action中把异常捕获并处理掉更加的合理。相信大家在job中常常会遇到循环调用的场景,这是通过返回值将相关的异常信息带回是更合理的处理方式,之后无论是记log还是给相关人发邮件都显得非常的合理 |
区分预先执行和延迟执行 | 在实际应用时,将正常加载和延迟加载组合使用非常的常见 var method1 = MethodA(); var answer = DoSomething(()=>method1, ()=>MethodB(), ()=>MethodC()); 此外,想说的是,在项目中,比如大部分数据是正常加载,少部分数据使用延迟加载,而一些特殊的场景通过(比如缓存服务器)则使用预热(预先加载)的方式,弄清这里面的逻辑会让这部分的应用更加得心应手 |
避免捕获昂贵的资源 | 之前介绍了C#编译器如何生成委托和变量是如何在一个闭包的内部被捕获的,下面是一个简单的构建闭包的例子 int counter = 0; IEnumerable |
区别IEnumerable和IQueryable的数据源 | 由于IQueryable数据源其实是对IEnumerable数据源的封装和增强,简答来说,IQueryable对象的相关数据处理操作的性能要远高于IEnumerable对象,因而如果实际的返回值为IQueryable对象,那么不要经行相关的转化,当然也可以通过typeA as IQueryable来尝试转化,如果本来就是IQueryable对象则直接返回,反之对其进行封装后返回 |
通过Single()和First()方法强行控制查询的语义 | 这个就是让我们的查询语句通过语义来指导查询,尽早的抛出异常 var stus = (from p in Students where p.Score > 60 orderby p.ID select p).Skip(2).First(); |
推荐存储Expression<.>替代Func<> | 这部分很有意思,当然理解难度也不小,毕竟Expression完全可以实现一个简单的编译器了,真心强大。我们所使用的LINQ完全是建立在其上的,这儿只做个很粗略的学习,作为未来加强学习的引子,可以看到,Expression表达式树是Func的抽象 Expression |
名称 | 内容和示例 |
最小化可空类型的可见性 | 简单来说,就是减少在公共方法API的输入参数和输出返回值中使用可空类型,因而这样会加大方法的调用难度。当然在内部方法和实体类(包括代码生成的实体类)中使用还是非常方便有效的 |
给部分类和部分方法建立构造器,设值器和事件处理器 | 这个主题常出现在有代码生成器出现的场景,比如说使用代码生成工具生成DAO层,其中只包含最基础的CRUD操作,当扩展时,我们如果直接修改类文件,那么当下一次数据库修改,再次生成代码时就可能出现代码覆盖等错误,因而在这种情况下我们会考虑使用分布类(说实话分布方法,我自己也没怎么用过,记得在以前做C++时用过类似external关键字引用外部方法的情形,形式上有点像)。这是需要注意的是,工具生成类和扩展类(一般来说类名相同,但文件名加上Ext并放入对应层次文件夹中)的设计,需要仔细考虑默认构造方法、属性值设置器、事件处理器等类成员的构建。 |
将数组参数限制为参数数组 | 由于数组的不确定性,因而不推荐将数组作为参数(指的是不同类型的数据放入一个object[]中,使得方法的使用非常容易出错,当然泛型的数据集合等除外),而推荐params的形式来传递相应数据,这样API参数在不存在或者提供null值时也不会报错。 Private static void Write(params object[] params) { foreach(object o in params) Console.WriteLine(o); } |
避免在构造器中调用虚方法 | 这其实是个很有用的建议,尤其是在构建集成关系复杂的基类及其派生类时,由于子类、父类构造方法调用顺序原因,很容造成初始化和赋值的错误,用一个简单的例子来说明这个问题,借用书中的一句原话,"一个对象在其所有构造器执行完成前并没有完整的被构建" class A { protected A() { MethodA(); } protected virtual void MethodA(){ Console.WriteLine("MethodA in A"); } } class B : A{ private readonly string msg = "set by initializer"; public B(string msg){ this.msg = msg; } protected override void MethodA(){ Console.WriteLine(msg); } } class Program{ static void Main(string[] args){ B b = new B("Constructed in main"); } } 这儿的结果是"set by initializer",首先调用B的构造方法,由于msg是readonly赋值木有成功,然后调用父类无参构造方法,实际调用子类MethodA有以上结果。这部分在实际中我也曾犯过相似的错误,需要非常小心。 |
对大对象考虑使用弱引用 | 弱引用的概念接触的相对较少,实际就是将直接引用转化为间接引用 Var weakR = new WeakReference(largeObj); largeObj = null; 咋一看,感觉确实不太好明白,这儿的意图是首先将大对象的引用(指针)放入一个包装类型,成为弱引用,之后将直接引用对象释放,这样就形成弱引用,利于垃圾回收,其使用场景主要针对没有提供IDispose接口的大对象。说实话,在实际中,我也没有这样使用过,之后尝试后再给大家分享。 |
推荐对易变量和不可序列化的数据使用隐式属性 | 简单来说,就是在非Serializable对象中推荐使用priavte set,可以保护数据安全并便于提供验证等方法。当然在支持序列化时,public的set方法和默认无参的构造函数都是必须的 |
谢谢大家的阅读,希望自己早日成为一名合格的程序员!
少年辛苦终身事,莫向光阴惰寸功J
参考文献:
出 处:http://www.cnblogs.com/wanliwang01/
特别说明:转发是为了方便日后翻阅,并无任何侵权或其他恶意企图。