以下为《编写高质量代码:改善C#程序的157个建议》作者【陆陆敏技】的读书总结,添加了笔者自己的理解或示例。
首先简述几个概念:
FCL:(Framework Class Library)即Framework类库。
基元类型:.NET 中,编译器直接支持的数据类型称为基元类型(primitive type).基元类型和.NET框架类型(FCL)中的类型有直接的映射关系,例如:在C#中,int直接映射为System.Int32类型。
[编译器]直接支持的类型。
sbyte / byte / short / ushort /int / uint / long / ulong / char / float / double / bool
友情提示:Linq配合Lambda,会让你的Code简洁许多,这也是C#发展历史上非常重要的升级之一
C# 3.0 版
-
string str = "海澜"+666.ToString();
比string str = "海澜"+666;
效率高,少了一次666值类型的装箱。string.Format和StringBuilder对于字符串的拼接效率更高。(string.Format方法在内部使用StringBuilder进行字符串的格式化) -
类型转换尽量使用FCL中自带的转换方式。例如:
int.TryParse("123")
-
as比is的效率高,as只需要做一次类型兼容和一次null检查,null检查要比类型兼容检查快(因为is为true还要进行as操作)。但是as操作符不能操作基元类型,需要通过is进行判断,例如:
obj is int
-
TryParse比Parse好,主要好在两点:1.不会引发异常。2.转换成功时TryParse比Parse性能有略微提升。转换失败时,TryParse比Parse性能提升600倍左右。
-
对于这种
int i =-1;
-1代表未赋值的魔数(又称魔法值),使用Nullable (可空类型)是一个不错的选择。 -
const 是天然的 static,编译期常量,所以使用const 变量效率会高。readonly仅仅在构造函数中可赋值或多次赋值。
-
将枚举的默认值设置为0.
-
避免给枚举类型的元素提供显式的值。例如
enum Week{Monday = 1,Tuesday = 2}
-
习惯重载运算符
c = a + b;
比c= a.add(b);
要好 -
创建对象时需要考虑是否实现比较器。不过笔者更喜欢一行Lambda配合Linq梭哈。
-
区别对待==和Equals,或明确指出这是引用相等
Object.ReferenceEquals
-
重写Equals时也要重写GetHashCode并确保Hash值相等,例如:
(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "_" + this.IDCode).GetHashCode();
。因为键值对集合,是根据Key 的HashCode来查找Value。详细请见:Object.GetHashCode 方法 -
为频繁打印的类重写ToString。
-
正确实现浅拷贝和深拷贝(序列化与反序列化)。
-
使用dynamic来简化反射实现
-
元素个数小且高频访问的集合用数组,频繁更改长度的集合用List
-
多数情况下使用foreach进行循环遍历,并且会自动将代码置入try-catch块,若类型实现了IDispose接口,它会在循环结束后自动调用Dispose方法。
-
foreach不能代替for,因为foreach使用迭代器进行集合遍历,foreach在FCL提供的迭代器内部维护了一个对集合版本的控
制,任何增删操作都会是版本号+1,一旦在遍历时MoveNext中检测版本号有变化,就会抛出异常。 -
使用更有效的对象和集合初始化。例如:
var pTemp = from p in personList2 selectt new {p.Name,AgeScope = p.Age>20?"Old":"Young"};
-
使用泛型集合代替非泛型集合。例如:IList
代替ArrayList,泛型集合是在原有基础之上的优化,这也是命名空间System.Collections(非泛型)和System.Collections.Generic(泛型)这种父子关系的原因。 -
选择正确的集合,每种集合都有他们的优缺点,善用它们。详见:Unity 之数据集合解析
-
确保集合的线程安全。使用lock锁或线程安全集合(如:
System.Collections.Concurrent命名空间下的ConcurrentDictionary
)是一个不错的选择,当然最好的解决方案就是没有线程竞争。 -
是如果需要自定义集合类,继承IList
比List 要好,尽量使用面向接口编程。 -
迭代器应该是只读的。
-
如果类型的属性中有集合属性,那么应该保证属性对象是由类型本身产生的。例如:
var school = new School(new List<Student>{...})比 school.setList(外部产生的集合)
要好,因为外部产生的集合不可控。 -
使用匿名类型存储并配合Linq查询,会让你的程序更加灵活。【前提与你协作的同事也能看懂】
-
在查询中使用Lambda表达式。【前提与你协作的同事也能看懂】
-
理解延迟求职和主动求值之间的区别。延迟一切能延迟的。例如:一个类型中某个成员的初始化或者赋值,只有在用到这个成员的时候,才进行这些操作。
-
本地查询用IEnumerabel
,数据库查询用IQueryable -
使用Linq取代集合中的比较器和迭代器
-
在Linq查询中避免不必要的迭代。例如:有时
(from c in list where c.Age>=20 select c).First()比from c in list where c.Age==20 select c
要效率的多 -
总是优先考虑泛型。可以有效的避免装箱拆箱或重复代码。
-
避免在泛型类型中声明静态成员。例如:
MyList<int>与 MyList< float>
中都含有static int count;
这很令人迷惑。 -
为泛型参数设定约束。 添加约束的泛型产生的作用会更大,例如
where T:Component
,所以T类型就具备了组件的性质。 -
使用default为泛型类型变量指定初始值。不用担心返回是值类型还是引用类型的困扰。
-
使用FCL中的委托声明。Action、Funtion让代码看上去更整洁统一。
-
使用Lambda表达式代替方法和匿名方法。
Action tempAction = ()=>{Debug.Log("菜鸟海澜");}
更整洁 -
小心闭包中的陷阱。例如:尤其是for循环中的局部
i
变量。 -
委托的实质就是一个含有方法引用的类。
-
使用event关键字为委托施加保护
-
实现标准的事件模型。
public delegate void EvenHandler(object sender,EventArgs e)
-
使用泛型参数兼容泛型接口的不可变性。如果按照如下示例调用
TestFuction(tempSon);
就会报错:CS1503 C# 参数 1: 无法从“Base<Son>”转换为“IBase<Father>”
,换句话说:编辑器认为IBase<Father>
与IBase<Father>
没有任何关系
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
void Start()
{
Base<Son> tempSon = new Base<Son>();
TestFuction(tempSon);//报错
TestFuctionGeneric(tempSon);//正确
}
public void TestFuction(IBase<Father> parameter)
{
}
public void TestFuctionGeneric<T>(IBase<T> parameter)
{
}
public Father GetFather()
{
return new Son(); //Son是Fatrher的子类
}
}
public interface IBase<T>
{
void Play();
}
public class Base<T> : IBase<T>
{
public void Play()
{
}
}
public class Father { }
public class Son : Father { }
介绍几个概念
协变:让返回值类型返回比声明的类型派生程度更大的类型,就是“协变”。例如:
public Father GetFather()
{
return new Son(); //Son是Fatrher的子类
}
实际上,只要泛型类型参数在一个接口声明中不被用来作为方法的输入参数,我们都可姑且把它看成是“返回值”类型的。
-
让接口中的泛型参数支持协变,也就是如果需要让第42条技巧编译通过。需要为接口添加out关键字
public interface IBase<out T>
-
理解委托中的协变 协变和逆变 (C#)
-
为泛型类型参数指定逆变 协变和逆变 (C#)
-
显示释放资源需继承接口IDisposable。补充两个概念,托管资源: 由CLR管理分配和释放的资源,即从CLR里new出来的对象。非托管资源:不受CLR管理的对象,如windows内核对象,或者文件、套接字等。
-
即使提供了显示释放方法,也应该在终结器(析构函数)中提供隐式清理(防止忘记)
-
Dispose方法应允许被多次调用。因为你无法保证调用者真的只会调用一次。
-
在Dispose模式中应提取一个受保护的虚方法,也就是在
public void Dispose()
中调用protected virtual void Dispose(bool disposing)
-
在Dispose模式中应区别对待托管资源和非托管资源。也就是
protected virtual void Dispose(bool disposing)
如果是参数是True则清理托管和非托管资源,如果是False则只清理非托管资源。托管资源由CLR自行清理。 -
具有可释放字段的类型或拥有本机资源的类型应该是可释放的。可以理解为,一个类A中持有类B,在类B中有需要释放的非托管资源,所以在类A中的Dispose要有释放类B中非托管资源的操作。
-
及时释放资源,避免不要的资源浪费或资源争用(FileStream)
-
必要时应将不再使用的对象引用赋值null,局部变量设置null毫无意义,因为无论是否设置为null它都会被回收。而静态字段如果不设置为null,则无法被回收。
-
为无用字段标注不可序列化。[NonSerialized]
-
利用定特性减少可序列化的字段。如:OnDeserializedAttribute、OnDeserializingAttribute、OnSerializedAttribute、OnSerializingAttribute。
-
使用继承ISerializable接口更灵活地控制序列化过程(完全客制化定制序列化方式)。
-
实现ISerializable的子类型应负责父类的序列化。
-
用抛出异常代替返回错误代码。也就是用catch Exception代替 return errorMsg;并且在catch块中仅仅是发送异常,并不处理异常。
-
不要在不恰当的场合下引发异常。一般在以下三种情况下才引发异常,对于可控(系统资源仍可用,资源状态可恢复)的错误,根据情况自行处理,不要引发异常。
- 第一类情况 如果运行代码后会造成内存泄漏、资源不可用,或者应用程序状态不可恢复,则引发异常。
- 第二类情况 在捕获异常的时候,如果需要包装一些更有用的信息,则引发异常
- 第三类情况 如果底层异常在高层操作的上下文中没有意义,则可以考虑捕获这些底层异常,并引发新的有意义的异常
-
重新引发异常时使用 Inner Exception
-
避免在finally内撰写无效代码。 补充说明
-
避免嵌套异常,因为会覆盖掉原本有用的堆栈信息。
-
避免“吃掉”异常,这里的“吃掉”指的是需要捕获有意义的异常。
-
为循环增加Tester-Doer模式而不是将try-catch置于环内,Try-Parse 模式和Tester-Doer模式是两种替代抛异常的优化方式,起到优化设计性能的作用。
-
总是处理未捕获的异常。未捕获异常通常就是运行时期的Bug,我们可以在AppDomain.CurrentDomain.UnhandledException的注册事件方法CurrentDomain_UnhandledException中,将未捕获的异常信息记录在日志中。UnhandledException提供的机制并不能阻止应用程序终止,也就是说,执行CurrentDomain_UnhandledException方法后,应用程序就会终止。
-
正确捕获多线程中的异常,例如:
- 正确
Thread t = new Thread((ThreadStart)delegate
{
try
{
throw new Exception("多线程异常");
}
catch (Exception error)
{
MessageBox.Show("工作线程异常:" + error.Message + Environment.NewLine + error.StackTrace);
}
});
t.Start();
- 错误
try
{
Thread t = new Thread((ThreadStart)delegate
{
throw new Exception("多线程异常");
});
t.Start();
}
catch (Exception error)
{
MessageBox.Show(error.Message + Environment.NewLine + error.StackTrace);
}
-
慎用自定义异常
-
从System.Exception或其他常见的基本异常中派生异常
-
应使用finally避免资源泄漏
-
避免在调用栈较低的位置记录异常
-
区分异步和多线程应用场景
-
在线程同步中使用信号量
-
避免锁定不恰当的同步
-
警惕线程的IsBackgroud
-
警惕线程不会立即启动
-
警惕线程的优先级
-
正确停止线程
-
应避免线程数量过多
-
使用ThreadPool或BackgroundWorker代替Thread
-
用Task代替ThreadPool
-
使用Parallel 简化同步状态Task的使用
-
Paralle简化但不等同于Task默认行为
-
小心Parallel中的陷阱
-
使用PLINQ,LINQ最基本的功能就是对集合进行遍历查询,并在此基础上对元素进行操作。仔细推敲会发现,并行编程简直就是专门为这一类应用准备的。因此,微软专门为LINQ拓展了一个类ParallelEnumerable(该类型也在命名空间System.Linq中),它所提供的扩展方法会让LINQ支持并行计算,这就是所谓的PLINQ。
-
Task中的异常处理。 详细说明
-
Parallel中的异常处理
static void Main(string[] args)
{
try
{
var parallelExceptions = new ConcurrentQueue<Exception>();
Parallel.For(0, 1, (i) =>
{
try
{
throw new InvalidOperationException("并行任务中出现的异常");
}
catch (Exception e)
{
parallelExceptions.Enqueue(e);
}
if (parallelExceptions.Count > 0)
throw new AggregateException(parallelExceptions);
});
}
catch (AggregateException err)
{
foreach (Exception item in err.InnerExceptions)
{
Console.WriteLine("异常类型:{0}{1}来自:
{2}{3}异常内容:{4}", item.InnerException.GetType(),
Environment.NewLine, item.InnerException.Source,
Environment.NewLine, item.InnerException.Message);
}
}
Console.WriteLine("主线程马上结束");
Console.ReadKey();
}
- 区分WPE和WinForm的线程模型(untiy可忽略)
- 并行并不总是速度更快
- 在并行方法体中谨慎使用锁,因为由于锁的存在,系统的开销也增加了,同步带来的线程上下文切换,使我们牺牲了CPU时间与空间性能
非常不错 满满干货