编写高质量C#代码必备技巧(上)

以下为《编写高质量代码:改善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 版


  1. string str = "海澜"+666.ToString();string str = "海澜"+666; 效率高,少了一次666值类型的装箱。string.Format和StringBuilder对于字符串的拼接效率更高。(string.Format方法在内部使用StringBuilder进行字符串的格式化)

  2. 类型转换尽量使用FCL中自带的转换方式。例如:int.TryParse("123")

  3. as比is的效率高,as只需要做一次类型兼容和一次null检查,null检查要比类型兼容检查快(因为is为true还要进行as操作)。但是as操作符不能操作基元类型,需要通过is进行判断,例如: obj is int

  4. TryParse比Parse好,主要好在两点:1.不会引发异常。2.转换成功时TryParse比Parse性能有略微提升。转换失败时,TryParse比Parse性能提升600倍左右。

  5. 对于这种int i =-1;-1代表未赋值的魔数(又称魔法值),使用Nullable (可空类型)是一个不错的选择。

  6. const 是天然的 static,编译期常量,所以使用const 变量效率会高。readonly仅仅在构造函数中可赋值或多次赋值。

  7. 将枚举的默认值设置为0.

  8. 避免给枚举类型的元素提供显式的值。例如 enum Week{Monday = 1,Tuesday = 2}

  9. 习惯重载运算符 c = a + b;c= a.add(b);要好

  10. 创建对象时需要考虑是否实现比较器。不过笔者更喜欢一行Lambda配合Linq梭哈。

  11. 区别对待==和Equals,或明确指出这是引用相等Object.ReferenceEquals

  12. 重写Equals时也要重写GetHashCode并确保Hash值相等,例如:(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "_" + this.IDCode).GetHashCode();。因为键值对集合,是根据Key 的HashCode来查找Value。详细请见:Object.GetHashCode 方法

  13. 为频繁打印的类重写ToString。

  14. 正确实现浅拷贝和深拷贝(序列化与反序列化)。

  15. 使用dynamic来简化反射实现

  16. 元素个数小且高频访问的集合用数组,频繁更改长度的集合用List<T>

  17. 多数情况下使用foreach进行循环遍历,并且会自动将代码置入try-catch块,若类型实现了IDispose接口,它会在循环结束后自动调用Dispose方法。

  18. foreach不能代替for,因为foreach使用迭代器进行集合遍历,foreach在FCL提供的迭代器内部维护了一个对集合版本的控
    制,任何增删操作都会是版本号+1,一旦在遍历时MoveNext中检测版本号有变化,就会抛出异常。

  19. 使用更有效的对象和集合初始化。例如:var pTemp = from p in personList2 selectt new {p.Name,AgeScope = p.Age>20?"Old":"Young"};

  20. 使用泛型集合代替非泛型集合。例如:IList<T>代替ArrayList,泛型集合是在原有基础之上的优化,这也是命名空间System.Collections(非泛型)和System.Collections.Generic(泛型)这种父子关系的原因。

  21. 选择正确的集合,每种集合都有他们的优缺点,善用它们。详见:Unity 之数据集合解析

  22. 确保集合的线程安全。使用lock锁或线程安全集合(如:System.Collections.Concurrent命名空间下的ConcurrentDictionary)是一个不错的选择,当然最好的解决方案就是没有线程竞争。

  23. 是如果需要自定义集合类,继承IList<T>比List<T>要好,尽量使用面向接口编程。

  24. 迭代器应该是只读的。

  25. 如果类型的属性中有集合属性,那么应该保证属性对象是由类型本身产生的。例如: var school = new School(new List<Student>{...})比 school.setList(外部产生的集合)要好,因为外部产生的集合不可控。

  26. 使用匿名类型存储并配合Linq查询,会让你的程序更加灵活。【前提与你协作的同事也能看懂】

  27. 在查询中使用Lambda表达式。【前提与你协作的同事也能看懂】

  28. 理解延迟求职和主动求值之间的区别。延迟一切能延迟的。例如:一个类型中某个成员的初始化或者赋值,只有在用到这个成员的时候,才进行这些操作。

  29. 本地查询用IEnumerabel<T>,数据库查询用IQueryable

  30. 使用Linq取代集合中的比较器和迭代器

  31. 在Linq查询中避免不必要的迭代。例如:有时(from c in list where c.Age>=20 select c).First()比from c in list where c.Age==20 select c要效率的多

  32. 总是优先考虑泛型。可以有效的避免装箱拆箱或重复代码。

  33. 避免在泛型类型中声明静态成员。例如:MyList<int>与 MyList< float>中都含有static int count;这很令人迷惑。

  34. 为泛型参数设定约束。 添加约束的泛型产生的作用会更大,例如 where T:Component,所以T类型就具备了组件的性质。

  35. 使用default为泛型类型变量指定初始值。不用担心返回是值类型还是引用类型的困扰。

  36. 使用FCL中的委托声明。Action、Funtion让代码看上去更整洁统一。

  37. 使用Lambda表达式代替方法和匿名方法。Action tempAction = ()=>{Debug.Log("菜鸟海澜");}更整洁

  38. 小心闭包中的陷阱。例如:尤其是for循环中的局部i变量。

  39. 委托的实质就是一个含有方法引用的类。

  40. 使用event关键字为委托施加保护

  41. 实现标准的事件模型。public delegate void EvenHandler(object sender,EventArgs e)

  42. 使用泛型参数兼容泛型接口的不可变性。如果按照如下示例调用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的子类
    }

实际上,只要泛型类型参数在一个接口声明中不被用来作为方法的输入参数,我们都可姑且把它看成是“返回值”类型的。

  1. 让接口中的泛型参数支持协变,也就是如果需要让第42条技巧编译通过。需要为接口添加out关键字public interface IBase<out T>

  2. 理解委托中的协变 协变和逆变 (C#)

  3. 为泛型类型参数指定逆变 协变和逆变 (C#)

  4. 显示释放资源需继承接口IDisposable。补充两个概念,托管资源: 由CLR管理分配和释放的资源,即从CLR里new出来的对象。非托管资源:不受CLR管理的对象,如windows内核对象,或者文件、套接字等。

  5. 即使提供了显示释放方法,也应该在终结器(析构函数)中提供隐式清理(防止忘记)

  6. Dispose方法应允许被多次调用。因为你无法保证调用者真的只会调用一次。

  7. 在Dispose模式中应提取一个受保护的虚方法,也就是在public void Dispose()中调用protected virtual void Dispose(bool disposing)

  8. 在Dispose模式中应区别对待托管资源和非托管资源。也就是protected virtual void Dispose(bool disposing)如果是参数是True则清理托管和非托管资源,如果是False则只清理非托管资源。托管资源由CLR自行清理。

  9. 具有可释放字段的类型或拥有本机资源的类型应该是可释放的。可以理解为,一个类A中持有类B,在类B中有需要释放的非托管资源,所以在类A中的Dispose要有释放类B中非托管资源的操作。

  10. 及时释放资源,避免不要的资源浪费或资源争用(FileStream)

  11. 必要时应将不再使用的对象引用赋值null,局部变量设置null毫无意义,因为无论是否设置为null它都会被回收。而静态字段如果不设置为null,则无法被回收。

  12. 为无用字段标注不可序列化。[NonSerialized]

  13. 利用定特性减少可序列化的字段。如:OnDeserializedAttribute、OnDeserializingAttribute、OnSerializedAttribute、OnSerializingAttribute。

  14. 使用继承ISerializable接口更灵活地控制序列化过程(完全客制化定制序列化方式)。

  15. 实现ISerializable的子类型应负责父类的序列化。

  16. 用抛出异常代替返回错误代码。也就是用catch Exception代替 return errorMsg;并且在catch块中仅仅是发送异常,并不处理异常。

  17. 不要在不恰当的场合下引发异常。一般在以下三种情况下才引发异常,对于可控(系统资源仍可用,资源状态可恢复)的错误,根据情况自行处理,不要引发异常。

  • 第一类情况 如果运行代码后会造成内存泄漏、资源不可用,或者应用程序状态不可恢复,则引发异常。
  • 第二类情况 在捕获异常的时候,如果需要包装一些更有用的信息,则引发异常
  • 第三类情况 如果底层异常在高层操作的上下文中没有意义,则可以考虑捕获这些底层异常,并引发新的有意义的异常
  1. 重新引发异常时使用 Inner Exception

  2. 避免在finally内撰写无效代码。 补充说明

  3. 避免嵌套异常,因为会覆盖掉原本有用的堆栈信息。

  4. 避免“吃掉”异常,这里的“吃掉”指的是需要捕获有意义的异常。

  5. 为循环增加Tester-Doer模式而不是将try-catch置于环内,Try-Parse 模式和Tester-Doer模式是两种替代抛异常的优化方式,起到优化设计性能的作用。

  6. 总是处理未捕获的异常。未捕获异常通常就是运行时期的Bug,我们可以在AppDomain.CurrentDomain.UnhandledException的注册事件方法CurrentDomain_UnhandledException中,将未捕获的异常信息记录在日志中。UnhandledException提供的机制并不能阻止应用程序终止,也就是说,执行CurrentDomain_UnhandledException方法后,应用程序就会终止。

  7. 正确捕获多线程中的异常,例如:

  • 正确
            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);
            }
  1. 慎用自定义异常

  2. 从System.Exception或其他常见的基本异常中派生异常

  3. 应使用finally避免资源泄漏

  4. 避免在调用栈较低的位置记录异常

  5. 区分异步和多线程应用场景

  6. 在线程同步中使用信号量

  7. 避免锁定不恰当的同步

  8. 警惕线程的IsBackgroud

  9. 警惕线程不会立即启动

  10. 警惕线程的优先级

  11. 正确停止线程

  12. 应避免线程数量过多

  13. 使用ThreadPool或BackgroundWorker代替Thread

  14. 用Task代替ThreadPool

  15. 使用Parallel 简化同步状态Task的使用

  16. Paralle简化但不等同于Task默认行为

  17. 小心Parallel中的陷阱

  18. 使用PLINQ,LINQ最基本的功能就是对集合进行遍历查询,并在此基础上对元素进行操作。仔细推敲会发现,并行编程简直就是专门为这一类应用准备的。因此,微软专门为LINQ拓展了一个类ParallelEnumerable(该类型也在命名空间System.Linq中),它所提供的扩展方法会让LINQ支持并行计算,这就是所谓的PLINQ。

  19. Task中的异常处理。 详细说明

  20. 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();  
}
  1. 区分WPE和WinForm的线程模型(untiy可忽略)
  2. 并行并不总是速度更快
  3. 在并行方法体中谨慎使用锁,因为由于锁的存在,系统的开销也增加了,同步带来的线程上下文切换,使我们牺牲了CPU时间与空间性能

更多技巧详见 Unity 之如何写出强壮的代码

推荐阅读更多精彩内容