深入理解C#:编程技巧总结(一)

具体转载地址见最下文

以下总结参阅了:MSDN文档、《C#高级编程》、《C#本质论》、前辈们的博客等资料,如有不正确的地方,请帮忙及时指出!以免误导!

1..实现多态性的两种方式:继承抽象类、实现接口

其实就是协变的应用,通过把对象向上转型为基类或接口类型,对它调用成员,可实现多态性,即运行时调用的是对应对象的实现版本成员。这两种方式的区别:

继承抽象类:会用掉唯一1次的继承机会,但可以继承任何成员(包括字段),自由度高

实现接口:必须实现所有成员,不能包含字段,但可以实现多个接口

抽象类可以提供成员的具体实现,而接口只负责声明,不能提供任何实现代码

注意:

接口一旦被定义就不应该再被改变,否则所有实现该接口的类型都必须跟着修改。

而抽象类则可以随时添加新的成员,不影响他的子类,还能提供新的额外功能。

多态性示例:(协变与逆变)

//可以返回Stream的任何子类类型

Stream Method1(bool boo)

{ }

//可以接收Stream的任何子类类型的参数

void Method2(Stream stream)

{ }

2.不要创建可变的值类型(结构、枚举),若要改变,请用一个方法来返回一个新实例。要时刻注意频繁的装箱与拆箱对性能的影响

3.仅在能一眼看出变量的类型时,才使用var声明

4.定义值类型时,它的大小不要超过16字节,否则影响性能(频繁复制时),要么改为使用引用类型,要么让它按ref引用传递

5.值类型数组之间不能直接互相转换,可以通过一次中间转换为Array来达到目的,如:

(int[])(Array)new uint[32]

但应注意可能在不同的CLR实现中表现不同!

6.数组与List

如果元素数量固定,且不涉及转型,则使用数组效率更高。

在元素数量可能发生变化的情况下,就不应该使用数组,而应该使用List

无论是数组还是List,元素个数也不能太多,避免成为占用内存超过85000字节的大对象,因为大对象将会被分配到单独的堆进行处理,在回收大对象时效率较低。

7.字符串操作

字符串字面量、字符串常量,直接用"+"相连效率高,因为:string str = "srf"+"ttt"+"ccc";会直接编译成string str = "srftttccc";,同样适用于字符串常量。

尽量避免对变量的装箱:字符串+变量,较好的做法是:字符串+变量.ToString()

频繁操作字符串时用StringBuilder,并制定足够大的容量,而string.Format("{0}{1}{2}",str1,str2,str3);内部也是用StringBuilder。

8.类型转换

字符串转其它基元类型:

默认十进制:用Parse()、TryParse(),如:int.TryParse("24");,其中TryParse效率更高

指定基数进制形式来解析:Convert.ToInt32("0xFF",16);

从字节数组中提取一段,转为基元类型:BitConvert.ToInt32(Byte[] arr, int startIndex);

自定义类型之间的强制转换:

从基类强制转换为子类时,安全的做法是使用"as",若目标为null或类型不兼容转换失败,均会返回null,而不会引发错误,如基类Person,它的子类Man、Women

Person person = new Man();//自动向基类隐式转换,但person的运行时类型仍为Man

Women women = (Women)person; //错误

Women women = person as Women; //women为null ,因为男人不能转换为女人

但需注意"as"只能应用于引用类型或可为null类型。若目标可能为基元类型,则应该通过"is"操作符来过滤

if(!(person is int))

{

   Women women = person as Women;

}

子类与子类之间的横向转换,应该定义转换操作符(关键字implicit、explicit)

9.获取一个可空类型Nullable的值,安全简单的做法是用"??",如 int j = i ?? 0;,普通做法:

if(i.HasValue()) { int j = i.Value; }

10.常量const和只读字段readonly的区别:

const是编译期常量,它总是静态的,编译时直接用实际值填充。而readonly是一个运行时常量。

const只能修饰基元类型、枚举类型、字符串类型,而readonly没有限制。

const一经声明就必须初始化,且之后就无法再改变。而readonly可显式初始化,也可不初始化,它的值可以通过构造函数来改变(即每个实例有自己的readonly只读字段值)

注意:除了构造函数之外,都无法改变readonly的值,对于引用类型是无法改变它的引用,即它只能引用同一对象。但该对象本身是可以被修改的。

11.枚举类型

枚举类型可以为从byte到ulong的基元类型,定义枚举时应该始终为它定义一个零值,因为声明一个枚举变量而未初始化时的默认值将是0

除了0值,要么都不为成员显式赋值,要么就全部赋值(如应用了Flags特性的标志枚举),否则未赋值的成员将等于它前一个成员的值加1,因为枚举成员的值默认是按顺序逐个加1

对枚举应用[Flags]特性,可以定义一个标志枚举,它的成员值通常初始化为2的次幂,之后就可以通过按位运算来判断、合并枚举成员了。

定义一个枚举来专门负责表示状态的信息,这样使代码更易理解。如用枚举成员on、off来代替true、false或0、1

12.如果需要,应该为类型重载常用的运算符和比较运算符,如重载">"以实现person1>person2

13.若该类型有泛型版本,则应该使用泛型版本,因为泛型类型效率更高(避免了装箱、拆箱、类型转换)

14.相等性

值类型:对于值相等的两个值类型变量A、B,"A==B"和"A.Equals(B)"都返回true,而Object.ReferenceEquals(A,B)总是返回false。

引用类型:Object.ReferenceEquals(A,B)比较的是引用是否相等,而默认的A.Equals(B)也是比较的引用,需要重载Equals()方法来实现引用类型之间的"值相等性比较"(如:当person1.ID == person2.ID时,person1.Equals(person2)返回true,来表示他们相等)

注意1:重写了Equals()方法,最好也一起重写GetHashCode()方法,因为对于不同的对象,默认的GetHashCode()返回的值将永远不同,而若把对象作为Dictionary的TKey时,根据TKey取值时,会根据对象的HashCode来比较。所以需要重新GetHashCode(),使得Equals()方法返回true时,GetHashCode()返回的值也相同,这样字典才能正常工作。

注意2:重写了Equals()、GetHashCode()方法,同时也应该实现IEquatable接口,该接口的成员bool Equals(T t1)比Object的Equals(object obj)类型更安全、更高效。

注意3:对于字符串,虽然它也是对象,但当两个字符串所包含的字面值一样时,运行时将只在内存中创建一个该字面值的字符串对象,也就是说所有字面值一样的字符串对象都将引用同一个地址。

15.ToString()方法

应该总是为自定义类型重写Object的ToString()方法,最好还要实现IFormattable接口,该接口的ToString(string format, IFormatProvider formatProvider)提供了根据参数来输出特定的格式化形式。如:

public string ToString(string format, IFormatProvider formatProvider)

{

   switch(format)

   {

       case "CH":

               return this.ToString();

       case "EN":

               return string.Format("{0}{1}",FirstName,LastName);

       ......

   }

}

//调用

Console.WriteLine(person.ToString("EN",null));

16.对象的浅拷贝与深拷贝

浅拷贝:使用Object基类的实例方法MemberwiseClone()来获得对象的一个浅拷贝副本。

深拷贝:通过系列化与反系列化来深拷贝一个对象。

通常做法,如下:接口ICloneable唯一成员是object Clone(),实现该接口只是为了表明该类型的实现可以被拷贝

[Serializable]

class Person : ICloneable

{

public string ID {get;set;}

public int Age {get;set;}

public Work work {get;set;}

//实现ICloneable接口的Clone()

public object Clone()

{

return this.MemberwiseClone();

}

//自定义深拷贝方法

public Person DeepClone()

{

using (Stream objectStream = new MemoryStream())

{

IFormatter formatter = new BinaryFormatter();

formatter.Serialize(objectStream, this);

objectStream.Seek(0, SeekOrigin.Begin);

return formatter.Deserialize(objectStream) as Person;

}

}

}

17.集合的遍历

for循环:采用索引器,for循环的优点是遍历过程中可以修改集合的元素。

foreach循环:采用迭代器,遍历过程中无法对集合增删元素操作,因为迭代器只对原始版本的集合进行遍历,每次迭代都会进行版本判断,若集合发生变化,将抛出异常。- - - - foreach循环的优点是语法更简洁,且迭代完毕后自动调用Dispose()(foreach循环内部使用了try...finally)

18.选择正确的集合:详解请参见《C#高级编程》,书中对集合讲的很细

线性:集合的每个元素都是是1对1的,大部分常用集合都是线性集合

非线性:1对多、多对1、多对多(树、集HashSet、图)

直接存取:具有索引器,元素按索引器排列,访问、查找速度快,在末尾添加删除速度也快,但在中间删除、插入元素效率低(需要移动后面的所有元素)。(数组、List、字符串、结构)

顺序存取:即线性表,可动态扩大或缩小,通过对地址的引用来搜索元素,删除、插入元素效率高,但查找效率低(需要遍历查找)(Stack、Queue、Dictionary、LinkedList等)

多线程集合类:位于System.Collections.Concurrent命名空间中,如ConcurrentBag对应于List、ConcurrentDictionary、ConcurrentStack、ConcurrentQueue

实现自定义集合类时,不要继承自内置的集合类,而应该自行实现相应的泛型接口:

IEnumerable:提供迭代功能

ICollection:提供常用操作

IList

19.泛型

避免为自定义泛型定义静态成员,在不同的类型之间共享静态成员没意义。

记得为泛型参数设定必要的约束,因为约束之后可以使泛型参数成为一个实实在在的"对象",可以访问到约束类型的实例成员,而不做约束的话仅仅是一个object对象

必要时用default(T)为泛型类型变量指定默认值,如T param = default(T);

20.委托

预定义的委托类型能满足大部分日常需求,我们没有必要声明自己的委托类型。

Action,Action:接受0个或多个输入参数,无返回值

Func,Func:接受0个或多个输入参数,带返回值,类型是TResult

Predicate:表示定义一组条件并判断参数是否符合条件

具有特定用途的委托:

事件委托:

public delegate void EventHandler(object sender, EventArgs e);

public delegate void EventHandler(object sender, TEventArgs e);

线程中的委托:

public delegate void ThreadStart(); //无参数

public delegate void ParameterrizedThreadStart(object obj); //参数对象obj

异步回调委托:

public delegate void AsyncCallback(IAsyncResult ar);

21.对于只用一次,且主体语句数量较少的方法,应该使用Lambda表达式,它通常用于注册给委托、或作为其它方法的参数(参数类型是匹配的委托类型)

22.理解委托的本质:

委托是一个类

委托保存着对注册方法的引用(方法指针),多播委托保存着一组方法指针

执行委托,将按顺序调用方法指针指向的方法

对一个委托实例用"="赋值一个新的方法指针时,将会调用构造函数实例化一个新的委托对象

所以在实例化一个委托对象之后后,应该时刻记住使用"+="、"-="来增加、删除新的方法指针

委托类的方法:Invoke()默认调用、在线程池中启用一个新线程调用BeginInvoke()、停止EndInvoke()

23.事件也是委托,加了event关键字是为了限制委托:

禁止了在包含类外部对委托事件对象使用"="赋值,确保不会被覆盖或赋值为null

禁止了在包含类外部对委托事件对象的直接调用,事件的调用应该是包含类的责任

参数1是触发者对象的引用,参数2是EventArgs或其派生类的对象(可包含一些将在事件触发时需要用到的数据)

24.当委托和Lambda小心闭包对象

(特别是在循环体中的循环变量,对于C#5.0的foreach则不必担心)

当Lambda表达式引用了局部变量时,编译器就会自动创建一个闭包对象(如TempClass),该对象的成员包含一个对局部变量的引用(如TempClass.i)、和一个与Lambda表达式等价的方法(如TempClass.add,该方法持有对局部变量的引用)。

而该闭包对象中的方法成员TempClass.add最终被赋给了委托(如MyDel),而委托通常在局部变量的作用域之外才执行。

也就是说,委托中注册的方法持有了对局部变量的引用,形成了像JavaScript中的闭包一样的效果,执行委托方法时,局部变量的值将是最新值,而不是给委托注册方法时的局部变量值。

public static void Main()

{

   Action act=new Action(()=>Console.WriteLine("Begin"));

   for (int i = 0; i < 5; i++)

   {

       act += () => Console.WriteLine(i.ToString());

   }

   act(); //Begin 5 5 5 5 5  因为委托方法持有了对i的引用,当前i的值为5

   Console.ReadKey();

}

public static void Main()

{

   Action act=new Action(()=>Console.WriteLine("Begin"));

   for (int i = 0; i < 5; i++)

   {

       int temp = i; //每次都用一个新的temp变量来保存当前的i值

       act += () => Console.WriteLine(temp.ToString());

   }

   act(); //Begin 0 1 2 3 4

   Console.ReadKey();

}

25.赋值为null,大部分情况下不能提前垃圾回收。

没有必要将没用的实例成员显式赋值为null,因为编译器会忽略该语句。

只有对日后确实没用的静态字段显式赋值为null才有必要,但要确保不会再用到它(或者说不会再用到它的包含类)。

把一个对象赋值为null,它的静态成员不会跟着变为null,因为静态成员跟类的实例无关,它会一直留在内存中,除非显式赋值为null。

后续还有很多其它方面的,如系列化与反系列化,异常处理等,由于篇幅有限,只能等下一篇再发布了

------------------------------------

原文地址:http://www.cnblogs.com/susufufu/p/6263122.html

   

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容