深入理解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,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编辑  收藏  举报

导航