让我们一起写出更有效的CSharp代码吧,少年们!

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了让我们一起写出更有效的CSharp代码吧,少年们!相关的知识,希望对你有一定的参考价值。

周末空闲,选读了一下一本很不错的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<string> act = (x)=>{this.controlA.Text = x; }

    if(controlA.InvokeRequired)

    { this.controlA.Invoke(act, msg); }

    else{ act(); }

    }

  3. 使用BackgroundWorker组件(elide)

 

技术分享

名称

内容和示例

为序列创建可组合的API, yield return xxx

Public static IEnumerable<int> Square(IEnumerable<int> nums)

{ foreach(var num in nums) yield return num * num; }

通过Action,Predicate,Functions解耦迭代器

Public static IEnumerable<T> Filter<T>(IEnumerable<T> sequence, Predicate<T> filterFunc)

{ if(filterFunc(int)) yield return item; }

根据请求生成序列

[IEnumerable<int>].TakeWhile(num => num < 5);

通过Function参数解耦

Public static T Sum<T>(IEnumerable<T> sequence, T total, Func<T,T,T> accumulator)

{ foreach(T item in sequence){ total = accumulator(total, item); return total; } }

创建清晰,最小化,完整的方法组

即在提供方法时,尽可能的保证完备性(支持主要的类型)

推荐定义方法重载操作符

还记得在学习C++时,很推荐重载操作符,不过在面向对象语言的今天,使用可读性更强的方法更合理

理解事件是如何增加对象运行时的耦合性

public event EventHandler<WorkerEventArgs> 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<WorkerEventArgs> progressEvent;

public event EventHandler<WorkerEventArgs> 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<T>(this T left, T right) where T : IComparable<T>

{ return left.CompareTo(right) < 0; }

通过扩展方法增强已经构建的类型

这部分很容易理解,比如你使用系统提供的相关类,无法修改源码(虽然已开源),这时为了代码的便捷性和可读性,使用扩展方法增强该类变得非常有效

推荐隐式类型的本地变量

简单方便

通过匿名类限制类的可见范围

使得代码的封装性更好,更加健壮

为外部的组件创建可组合的API

要求提供的API具有更好的健壮性,功能相对完整并独立,复用性更强,例如尽量不要使用可空类型作为接口参数等

避免修改绑定的变量

这部分内容涉及闭包,通过以下的例子可以很容易的理解

public void Test()

{

int index = 0;

Func<IEnumerable<int>> sequence = () => Generate(30, () => index ++);

index = 20;

foreach (var item in sequence())

{ Console.WriteLine(item); }

}

private IEnumerable<int> Generate(int num, Func<int> 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方法和默认无参的构造函数都是必须的

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

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

 

参考文献:

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

以上是关于让我们一起写出更有效的CSharp代码吧,少年们!的主要内容,如果未能解决你的问题,请参考以下文章

努力吧少年错过的只有更努力来弥补

Code皮皮虾双栈 + 贪心 思路讲解代码实现——>有效的括号字符串

一起Talk Android吧(第四百一十九回:让时钟走起来)

一起Talk Android吧(第五百二十七回:监听网络状态)

一起talk C栗子吧(第一百五十四回:C语言实例--socket通信地址系统调用二)

一起Talk Android吧(第三百八十一回:通知的发送方式)