深入理解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;
,普通做法:
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,TValue>的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<TKey、TValue>、LinkedList 等) - 多线程集合类:位于System.Collections.Concurrent命名空间中,如ConcurrentBag
对应于List 、ConcurrentDictionary<TKey,TValue>、ConcurrentStack 、ConcurrentQueue
实现自定义集合类时,不要继承自内置的集合类,而应该自行实现相应的泛型接口:
IEnumerable
ICollection
IList
19.泛型
- 避免为自定义泛型定义静态成员,在不同的类型之间共享静态成员没意义。
- 记得为泛型参数设定必要的约束,因为约束之后可以使泛型参数成为一个实实在在的"对象",可以访问到约束类型的实例成员,而不做约束的话仅仅是一个object对象
- 必要时用default(T)为泛型类型变量指定默认值,如T param = default(T);
20.委托
预定义的委托类型能满足大部分日常需求,我们没有必要声明自己的委托类型。
- Action,Action<T1,...,T16>:接受0个或多个输入参数,无返回值
- Func,Func<T1,...,T16,TResult>:接受0个或多个输入参数,带返回值,类型是TResult
- Predicate:表示定义一组条件并判断参数是否符合条件
具有特定用途的委托: - 事件委托:
public delegate void EventHandler(object sender, EventArgs e);
public delegate void EventHandler<TEventArgs>(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。
后续还有很多其它方面的,如系列化与反系列化,异常处理等,由于篇幅有限,只能等下一篇再发布了
posted on 2017-01-08 23:09 SuriFuture 阅读(2281) 评论(3) 编辑 收藏 举报