编写高效率的C#代码

周末空闲,选读了一下一本很不错的C#语言使用的书,特此记载下便于对项目代码进行重构和优化时查看。

Standing On Shoulders of Giants,附上思维导图,其中标记的颜色越深表示在实际中的实际意义越大。

名称内容和示例
提供API时尽量提供泛型接口Public interface IComparable<T>{ int CompareTo(T other) }
泛型约束尽可能的严格并有效Public delegate T FactoryFunc<T>(); Public static T Factory<T>( FactoryFunc<T> newT) where T:new() { T t = newt();}
通过运行时类型检查具体化泛型算法比如根据不同的集合类型优化相应算法
使用泛型强制执行编译时类型推测Public static T ReadFromStream(XmlReader inputStream) { return (T)factory.Deserialize(inputStream) }
保证自定义泛型类支持可析构的类型参数Public sealed class EngineDriver<T>:IDisposable where T:Engine, new() { public void Dispose(){ var resource = driver as IDisposable; if(resource != null) resource.Dispose(); } }
通过委托在类型参数上定义方法约束Public static T Add<T>(T left, T right, Func<T,T,T> addFunc){ return addFunc(right, left); }
不要在基类和接口上创建具体化的泛型类型尽可能使的基类和接口的适用范围更加的广阔
推荐使用泛型方法,除非类型参数是实例字段Public static T Max<T>(T left, T right) { return Comparer<T>.Default.Compare(left, right) < 0 ? right : left }
推荐使用泛型的Tuple作为输出和引用参数当设置方法的返回值,或者在需要使用ref参数的情形时,使用Tuple<>元组可以使代码更清晰,当然如果参数比较复杂,还是选择建立对应的DTO类型为宜
在泛型接口上增加对应的传统接口这个在大家基础架构时非常重要,每个方法均提供泛型版本和object版本,使得代码有很强的兼容性。 Public static bool CheckEquality(object left, object right) { return left.Equals(right); } Public static bool CheckEquality<T>(T left, T right) where T:IEquatable<T> { return left.Equals(right); }

名称内容和示例
使用线程池代替创建线程经过微软的官方测试,由自己调度线程和使用线程池,在每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 do If(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值的情况。这种情况主要有一下三种方式来处理,最推荐的解决方案为第二种。

  1. 在Form的构造方法中加入 Control.CheckForIllegalCrossThreadCalls =false; (不推荐)

  2. Private void UpdateControl(string msg)

    { Action act = (x)=>{this.controlA.Text = x; } if(controlA.InvokeRequired) { this.controlA.Invoke(act, msg); } else{ act(); } }

  3. 使用BackgroundWorker组件(elide)

名称内容和示例
为序列创建可组合的API, yield return xxxPublic static IEnumerable Square(IEnumerable nums) { foreach(var num in nums) yield return num * num; }
通过Action,Predicate,Functions解耦迭代器Public static IEnumerable Filter(IEnumerable sequence, Predicate filterFunc) { if(filterFunc(int)) yield return item; }
根据请求生成序列[IEnumerable].TakeWhile(num => num < 5);
通过Function参数解耦Public static T Sum(IEnumerable sequence, T total, Func<T,T,T> accumulator) { foreach(T item in sequence){ total = accumulator(total, item); return total; } }
创建清晰,最小化,完整的方法组即在提供方法时,尽可能的保证完备性(支持主要的类型)
推荐定义方法重载操作符还记得在学习C++时,很推荐重载操作符,不过在面向对象语言的今天,使用可读性更强的方法更合理
理解事件是如何增加对象运行时的耦合性public event EventHandler OnProgress; public void DoLotsOfStuff() { for (var i = 0; i < 100; i++) { SomeWork(); var args = new WorkerEventArgs(); args.Percent = i; //关于这个=,我总是不算特别明白,不过记得是线程安全的代码 //可以理解为,使用这个,其他调用这个事件的对象就不会被锁定 var progHandler = OnProgress;   if (progHandler != null) { //注意这里的this progHandler(this, args); } if (args.Cancel) return; } } 这里想补充的是,event属于编译时解耦,你可以看到,该事件的订阅者都没有入侵事件所属的发布者(发布者-订阅者默认),但实际上,在运行时,所有的订阅者其实是和事件紧密关联在一起的,订阅者们修改共享数据的操作存在很大的不确定性。简而言之,事件是编译时解耦,运行时耦合的。
只声明非虚事件对象在.NET中,事件提供了类似属性的简易语法,通过add,remove方法添加相关事件处理程序,其实event就是delegate的包装器,这个特殊的委托便于应用事件处理模型,同时提供线程安全性。由于事件的运行时耦合性,如果使用虚事件容易造成未知的错误, private EventHandler progressEvent; public event EventHandler OnProgress { [MethodImpl(MethodImplOptions.Synchronized)] add { progressEvent += value; } [MethodImpl(MethodImplOptions.Synchronized)] remove { progressEvent -= value; } }
通过异常报告方法契约错误当出现业务异常流程时,推荐抛出异常而不是使用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(this T left, T right) where T : IComparable { return left.CompareTo(right) < 0; }
通过扩展方法增强已经构建的类型这部分很容易理解,比如你使用系统提供的相关类,无法修改源码(虽然已开源),这时为了代码的便捷性和可读性,使用扩展方法增强该类变得非常有效
推荐隐式类型的本地变量简单方便
通过匿名类限制类的可见范围使得代码的封装性更好,更加健壮
为外部的组件创建可组合的API要求提供的API具有更好的健壮性,功能相对完整并独立,复用性更强,例如尽量不要使用可空类型作为接口参数等
避免修改绑定的变量这部分内容涉及闭包,通过以下的例子可以很容易的理解 public void Test() { int index = 0; Func<IEnumerable> sequence = () => Generate(30, () => index ++); index = 20; foreach (var item in sequence()) { Console.WriteLine(item); } } private IEnumerable Generate(int num, Func act) { for (; num > 0; num–) { yield return act(); } }
在匿名类型上定义本地函数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<TResult> Generate<TResult>(int number, Func<TResult> generator) { for(var i = 0; i < number; i++) yield return generator(); }注意到Func<TResult>这个格式没有,和Task<TResult>何其相似,一个是异步返回值,一个是延迟的返回值,仅仅是一个方便理解的小思路哈。
推荐使用lambda表达式代替方法这儿的实际意思是指在使用LINQ时,由于每个查询的局限性,不推荐在查询中调用外部方法,而因尽可能通过LINQ自身来完成相应工作,减少各个查询间的干扰
避免在Func和Action中抛出异常这个也很好理解,由于Action等委托常用于集合操作中,而任何一个一场都会中断整个集合的操作,给集合操作带来了很大的不确定性,并且在并行运算时更加难以控制,因而在Action中把异常捕获并处理掉更加的合理。相信大家在job中常常会遇到循环调用的场景,这是通过返回值将相关的异常信息带回是更合理的处理方式,之后无论是记log还是给相关人发邮件都显得非常的合理
区分预先执行和延迟执行在实际应用时,将正常加载和延迟加载组合使用非常的常见 var method1 = MethodA(); var answer = DoSomething(()=>method1, ()=>MethodB(), ()=>MethodC()); 此外,想说的是,在项目中,比如大部分数据是正常加载,少部分数据使用延迟加载,而一些特殊的场景通过(比如缓存服务器)则使用预热(预先加载)的方式,弄清这里面的逻辑会让这部分的应用更加得心应手
避免捕获昂贵的资源之前介绍了C#编译器如何生成委托和变量是如何在一个闭包的内部被捕获的,下面是一个简单的构建闭包的例子 int counter = 0; IEnumerable<int> numbers = Generate(30, ()=>counter++); 其实际生成的代码如下: private class Closure { public int generatedCounter; public int generatorFunc(){ return generatedCounter ++; } } var c = new Closure(); c.generatedCounter = 0; IEnumerable<int> sequence = Generate(30, new Func<int>(c.generatorFunc)); 通过闭包的形式,我们可以发现其扩展了捕获对象的生命周期,如果这个捕获对象是一个昂贵的资源,比如说是个很大的文件流,那么就可能发生内存泄露的情况。因而在委托中使用本地的资源,一定要非常的当心,比较合理的方式是,将你所需要的内容缓存后释放原始对象。
区别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<Func<int, bool>> IsOdd = val % 2 == 1;Expression<Func<int, bool>> IsLargeNumber = val => val > 99; InvocationExpression callLeft = Expression.Invoke(IsOdd, Expression.Constant(5)); InvocationExpression callRight = Expression.Invoke(IsLargeNumber, Expression.Constant(5)); BinaryExpression Combined = Expression.MakeBinary(ExpressionType.Add, callLeft, callRight); Expression<Func<bool>> typeCombined = Expression.Lamda<Func<bool>>( Combined); Func<bool> compiled = typeCombined.Compile(); Bool result = compiled();

名称内容和示例
最小化可空类型的可见性简单来说,就是减少在公共方法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方法和默认无参的构造函数都是必须的

谢谢大家的阅读,希望自己早日成为一名合格的程序员!

少年辛苦终身事,莫向光阴惰寸功

参考文献:

  1. [美]Bill, Wagner. More Effective C#[M]. 北京:人民邮电出版社, 2009.

原文地址:http://www.cnblogs.com/wanliwang01/p/EffectiveCSharp.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值