前言
泛型并不是C#语言一开始就带有的特性,而是在FCL2.0之后实现的新功能。基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用。同时,它减少了泛型类及泛型方法中的转型,确保了类型安全。委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用。事件本身也是委托,它是委托组,C#中提供了关键字event来对事件进行特别区分。一旦我们开始编写稍微复杂的C#代码,就肯定离不开泛型、委托和事件。本章将针对这三个方面进行说明。
这里也有一篇之前我对泛型的简单理解篇 http://www.cnblogs.com/aehyok/p/3384637.html C# 泛型的简单理解(安全、集合、方法、约束、继承)
本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html 。本文主要学习记录以下内容:
建议32、总是优先考虑泛型
建议33、避免在泛型类型中声明静态成员
建议34、为泛型参数设定约束
建议32、总是优先考虑泛型
泛型的优点是多方面的,无论是泛型类还是泛型方法都同时具备可重用性、类型安全和高效率等特性,这都是非泛型类和非泛型方法无法具备的。本建议将从可重用性、类型安全和高效率三个方面来进行剖析在实际的编码过程中为何总是应该优先考虑泛型。
一、可重用性,比如简单的设计一个集合类
public class MyList { int[] items; public int this[int i] { get { return items[i]; } set { this.items[i] = value; } } public int Count { get { return items.Length; } } ////省略一些其他方法 }
该类型只支持整型,如果要让类型支持字符串,有一种方法是重新设计一个类。但是这两个类型的属性和方法都是非常接近的,如果有一种方法可以让类型接收一个通用的数据类型,这样就可以进行代码复用了,同时类型也只要一个就够了。泛型完成的就是这样的功能。
public class MyList<T> { T[] items; public T this[int i] { get { return items[i]; } set { this.items[i] = value; } } public int Count { get { return items.Length; } } ///省略其他方法 }
可以把T理解为一个占位符,在C#泛型编译生成的IL代码中,T就是一个占位符的角色。在运行时,即使编译器(JIT)会用实际代码中输入的T类型来代替T,也就是说,在由JIT生成的本地代码中,已经使用了实际的数据类型。我们可以把MyList<int>和MyList<string>视作两个完全不同的类型,但是,这仅是对本地代码而言的,对于实际的C#代码,它仅仅拥有一个类型,那就是泛型类型MyList<T>。
以上从代码重用性的角度论证了泛型的优点。继续从类型MyList<T>的角度论述,如果不用泛型实现代码重用,另一种方法是让MyList的编码从object的角度去设计。在C#的世界中,所有类型(包括值类型和引用类型)都是继承自object,如果要让MyList足够通用,就需要让MyList针对object编码,代码如下:
public class MyList { object[] items; public object this[int i] { get { return items[i]; } set { this.items[i] = value; } } public int Count { get { return items.Length; } } ////省略一些其他方法 }
这会让以下代码编译通过
MyList list = new MyList(); list[0] = 123; list[1] = "123";
由上面两行代码带来的问题就是非”类型安全性“。该问题实际在建议20 http://www.cnblogs.com/aehyok/p/3641896.html 中已经详细论述过了。让类型支持类型安全,可以让程序在编译期间就过滤掉部分Bug,同时也能让代码规避掉”转型为object类型“或“从object转型为实际类型”所带来的效率损耗。尤其是涉及的操作类型是值类型时,还会带来装箱和拆箱的性能损耗。
例如,上文代码中的
list[1] = 123;
就会带来一次装箱操作,因为它首先倍转型为object,继而存储到items这个object数组中去了。
泛型为C#带来的是革命性的变化,FCL之后的很多功能都是借助泛型才得到了很好的实现,如LINQ。LINQ借助于泛型和扩展方法,有效地丰富了集合的查询功能,同时避免了代码爆炸并提升了操作的性能。我们在设计自己的类型时,应充分考虑到泛型的优点,让自己的类型成为泛型类。
建议33、避免在泛型类型中声明静态成员
在上一个建议中,已经解释了应该将MyList<int> 和MyList<string> 视作两个完全不同的类型,所以,不应将MyList<T>中的静态成员理解成为MyList<int>和MyList<string>共有的成员。
对于一个非泛型类型,以下的代码很好理解:
public class MyList { public static int Count { get; set; } public MyList() { Count++; } } class Program { static void Main(string[] args) { MyList myList1 = new MyList(); MyList mylist2 = new MyList(); Console.WriteLine(MyList.Count); Console.ReadLine(); } }
结果返回为2.
如果将MyList换成泛型类型,看看下面的代码会输出什么呢?
public class MyList<T> { public static int Count { get; set; } public MyList() { Count++; } } class Program { static void Main(string[] args) { MyList<int> myList1 = new MyList<int>(); MyList<int> mylist2 = new MyList<int>(); MyList<string> mylist3 = new MyList<string>(); Console.WriteLine(MyList<int>.Count); Console.WriteLine(MyList<string>.Count); Console.ReadLine(); } }
代码输出为
实际上,随着你为T指定不同的数据类型,MyList<T>相应的也变成了不同的数据类型,在它们之间是不共享静态成员的。
不过,从上文我们也觉察到了,若T所指定的数据类型是一致的,那么两个泛型对象间还是可以共享静态成员的,如上文的myList1和myList2。但是,为了规避因此而引起的混淆,仍旧建议在实际的编码工作中,尽量避免声明泛型类型的静态成员。
上面举的例子是基于泛型类型的,非泛型类型中静态泛型方法看起来很接近该例子,但是应该始终这样来理解:
非泛型类型中的泛型方法并不会在运行时的本地代码中生成不同的类型。
public class MyList { public static int Count { get; set; } public static int Func<T>() { return Count++; } } class Program { static void Main(string[] args) { Console.WriteLine(MyList.Func<int>()); Console.WriteLine(MyList.Func<int>()); Console.WriteLine(MyList.Func<string>()); Console.ReadLine(); } }
输出结果为
建议34、为泛型参数设定约束
”约束“这个词可能会引起歧义,有些人可能认为对泛型参数设定约束是限制参数的使用,实际情况正好相反。没有约束的泛型参数作用很有限,倒是”约束“让泛型参数具有了更多的行为和属性。
public class Salary { /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 基本工资 /// </summary> public int BaseSalary { get; set; } /// <summary> /// 奖金 /// </summary> public int Bouns { get; set; } } public class SalaryComputer { public int Compare<T>(T t1, T t2) { return 0; } }
查看上面定义实体类可以发现,Compare<T>方法的参数t1或参数t2仅仅具有object的属性和行为,所以几乎不能在方法中对它们做任何的操作。但是,在加了约束之后,我们会发现参数t1或参数t2变成了一个有用的对象。由于为其指定了对应的类型,t1和t2现在就是一个Salary了,在方法的内部,它拥有了属性BaseSalary和Bonus,代码如下:
public class SalaryComputer { public int Compare<T>(T t1, T t2) where T:Salary { if (t1.BaseSalary > t2.BaseSalary) { return 1; } else if (t1.BaseSalary == t2.BaseSalary) { return 0; } else { return -1; } } }
那么可以为泛型参数指定那些约束呢?
1、指定参数是值类型(除Nullable外),可以有如下形式:
public void Method1<T>(T t) where T : struct { }
2、指定参数是引用类型,可以有如下形式:
public void Method1<T>(T t) where T : class { } public void Method1<T>(T t) where T : Salary { }
注意object不能用来作为约束。
3、指定参数具有无参数的公共构造函数,可以有如下形式:
public void Method2<T>(T t) where T : new() { }
注意CLR目前只支持无参构造方法约束。
4、指定参数必须是指定的基类、或者派生自指定的基类。
5、指定参数必须是指定的接口、或者实现指定的接口。
6、指定T提供的类型参数必须是为U提供的参数,或者派生自为U提供的参数。
public class Sample<U> { public void Method1<T>(T t) where T : U { } }
7、可以对同一类型的参数设置多个约束,并且约束自身可以是泛型类型。
在编程的过程中应该始终考虑为泛型参数设定约束,正像本建议开始的时候所说,约束使泛型成为一个实实在在的“对象”,让它具有了我们想要的行为和属性,而不仅仅是一个object。