C#高效编程
一、 使用readonly而不是const
const是编译时常量,readonly是运行时常量。如果引用了一个库中的const常量,则在更新了程序集,但应用程序没有重新编译时,运行结果会出错
如程序集assembly.dll中有如下声明
1 public static readonly int Start = 1; 2 public const int End = 10;
在ConsoleApp.exe中调用。在正常编译的情况下是没有问题的,但如果更新了assembly.dll中的声明,却不重新编译ConsoleApp.exe,此时End常量的值依旧是10
1 public static readonly int Start = 10; 2 public const int End = 20;
二、使用is和as关键字,而不是使用强制类型转换
强制类型转换不当会引发 InvalidCastException异常。使用is和as可以避免这个问题
三、使用$ - 字符串内插来格式化字符串,而不是string.Format()函数.这个功能需要C#6.0
https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/interpolated
四、尽量使用Conditional特性,减少#if和#endif的使用
平常我们会使用#if和#endif来执行一些Debug环境下执行的代码。这些代码在编译成Release版本时不会执行,但仍然会有一些开销。这时我们就可以使用Conditional特性,使用了Conditional特性的函数只有在某些环境或者设置了某个值之后才能编译成类的一部分,这样就可以减少Release版本的开销。值得注意的,使用Conditional特性必须把以前的调试部分代码封装成函数才行。
需要了解ConditionalAttribute更详细的说明,可以查看MSDN文档 https://docs.microsoft.com/zh-cn/dotnet/api/system.diagnostics.conditionalattribute?redirectedfrom=MSDN&view=netframework-4.8
使用#if和#endif的情况
当定义了DEBUG符号时,控制台会输出Debug和Release。未定义DEBUG符号时,只会输出Release。
1 #define DEBUG 2 3 using System; 4 using System.Collections.Generic; 5 using System.Diagnostics; 6 using System.Linq; 7 using System.Text; 8 using System.Threading.Tasks; 9 10 namespace ConsoleApp5 11 { 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 #if DEBUG 17 Console.WriteLine("Debug"); 18 #endif 19 Console.WriteLine("Release"); 20 21 } 22 23 24 } 25 }
使用Conditional特性的情况
输出情况是跟上面一致的。但当未定义DEBUG符号时,DebugMethod不会被编译进MSIL。可以节省开销
值得注意的是,应用了Conditional特性的函数,返回类型只能为void
1 #define DEBUG 2 3 using System; 4 using System.Collections.Generic; 5 using System.Diagnostics; 6 using System.Linq; 7 using System.Text; 8 using System.Threading.Tasks; 9 10 namespace ConsoleApp5 11 { 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 DebugMethod(); 17 18 Console.WriteLine("Release"); 19 } 20 21 [Conditional("DEBUG")] 22 static void DebugMethod() 23 { 24 Console.WriteLine("Debug"); 25 } 26 27 } 28 }
五、如何正确的对成员变量进行初始化(这个问题困扰了我好久,今天终于有解决方案了)
大多情况下,使用成员初始化器对成员进行初始化。
就是在声明时就进行初始化,而不是放到构造函数。初始化器可以看做是构造函数中初始化语句的另一种表示。初始化器生成的代码会在构造函数前执行。如果是派生类,初始化器会在父类的构造函数前执行,然后再执行子类的构造函数。
public class Patient { List<Bitmap> images = new List<Bitmap>(); string patientName; int patientId; public Patient() { } }
下面三种情况,不推荐使用初始化器语法
1、值类型初始值设为0,引用类型初始值设为null的情况
系统默认的初始化工作将在所有代码执行之前把一切都设置成0或null。系统生成0或者null的这一步操作是位于很底层的实现,会直接使用CPU指令将一整块内存设置为0。
所以以下代码是多余的
1 int num = 0; 2 Student stu = null;
2、需要对这个变量执行不同的初始化方式的情况。初始化器语法的前提是,所有的构造函数都会将变量设置为同样的值。
3、在进行初始化时,可能会引发异常的情况。这种情况下,就需要在构造函数中进行初始化,以便进行相应处理。
六、区分值类型和引用类型
值类型应该是那些小型、轻量级的类型,分配在栈上
引用类型则用来构成类层次结构,分配在堆上。
有如下代码
1 MyType[] arrayOfTypes = new MyType[100];
如果MyType属于值类型,那么将一次分配大小为MyType的100倍空间
如果MyType属于引用类型,那么也仅会分配一次,数组中的每个元素都为空。在初始化数组中的元素时,将执行101次分配,101次分配要比1次分配更占用时间,分配大量的引用 类型将让堆上充满碎片。
还有一点值得注意的是,在集合中查找元素时,如果集合里是值类型,则查找出来的元素跟集合中的元素是不同的引用。如果是引用类型,则查找出来的元素跟集合中的该元素是相同的引用。在修改值的时候,会导致意外的发生。
有如下代码
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Test1 test1 = new Test1() { Name = "Test1"}; 6 Test2 test2 = new Test2() { Name = "Test2"}; 7 8 List<Test1> list1 = new List<Test1>() { test1 }; 9 var temp1 = list1.Find(x => x.Name == "Test1"); 10 Console.WriteLine(object.ReferenceEquals(test1,temp1)); 11 12 temp1.Name = "Modified Test1"; 13 Console.WriteLine(list1.ElementAt(0).Name); 14 15 List<Test2> list2 = new List<Test2>() { test2 }; 16 var temp2 = list2.Find(x => x.Name == "Test2"); 17 Console.WriteLine(object.ReferenceEquals(test2,temp2)); 18 19 temp2.Name = "Modified Test2"; 20 Console.WriteLine(list2.ElementAt(0).Name); 21 22 23 24 } 25 } 26 27 28 struct Test1 29 { 30 public string Name { get; set; } 31 } 32 33 class Test2 34 { 35 public string Name { get; set; } 36 }
运行结果如下
可以看到,查找出来的值类型集合中的元素,跟添加时的元素并不是同一个引用,而且修改值,也不会导致集合中元素的更改。
引用类型则相反。