【读More Effective C#】之一:使用泛型(Use Generics)
前置问题:
什么是泛型?
“泛型是 C# 2.0 的最强大的功能。通过泛型可以定义类型安全的数据结构,而无须使用实际的数据类型。这能够显著提高性能并得到更高质量的代码,因为您可以重用数据处理算法,而无须复制类型特定的代码。”——MSDN
“通过泛型可以定义类型安全类,而不会损害类型安全、性能或工作效率。您只须一次性地将服务器实现为一般服务器,同时可以用任何类型来声明和使用它。为此,需要使用 < 和 > 括号,以便将一般类型参数括起来。”——MSDN
更多信息参考:MSDN
我眼中的泛型
Generic [dʒɪ'nɛrɪk] 意为: 泛化的、一般化的、通用的。很明显“通用的”这个词我们更为熟悉,泛型若是翻译为“通用型”估计中国的程序员会更好理解一些。但是往往实际不是这样,不知道出于什么目的,国外用的很普通的名词到了中国非要用一些晦涩、难懂的词来表达,难道这样才显得有水平?一个很明显的例子就是“BUS”,就是总线,它就是来回传输数据与地址的一个纽带,国外用“公交车”命名不但简单而且十分形象,而我们非要翻译成“总线”这个很难看出神马意思的东东…… 扯远了,赶紧回来 :) 。
说白了,泛型就是将一些通用的东西包装起来,而不用一次次的再去实现,且不用把这些东西包装来包装去,传递的时候全转换成一样东西,用的时候再转换回来。举个栗子:A要将几个东西给不在同一城市的B,怎么办呢?快递呗!快递可以运输(处理)不同的东西,这样就实现了通用,你传递X东西用快递、Y东西也可以用快递、Z当然也可以,但是快递公司现在有个规定,就是必须要把快递的东西分别装在箱子里才能帮你运输(处理),所以我们在交给快递公司的时候得先打包装箱然后签个单子,传递到B后B还得拆开箱子才能用。这样太麻烦啦!而且容易出错啊,万一哪天我包装的时候放错东西了,而箱子也封起来了(快递员真不负责),B收到后发现用不了咋办?要是不用装箱子直接让快递员拿走不是既省时间又安全(快递员很早就可以发现货物不对)吗?
噔 噔 噔噔!Intel出场了?不是,泛型出场了! intel是5个噔,这里是普遍的4噔揭幕配音。 泛型就是一个让“重用数据处理算法,而无须复制类型特定的代码”,它使你无需进行多余的转换,免除拆箱、装箱带来的效率问题,同时还是类型安全的。
泛型都用在哪些地方?
很多初学者第一次接触的泛型应该是泛型集合类List<T> 吧,因为很多文章和论文举得例子大都是用泛型集合类来说的。一般来说,泛型可以用在以下几个方面:
- 泛型接口
其实最明显的例子就是对数据层用的IRepository模式。还有IList<T>、IEnumberable<T>、IQueryable<T>等等
View Code
public interface IRepository<T,TKey>:where T:IEntity
{
void Insert(T model);
void Creat(T model);
void Update(T model);
void Get(TKey id);
} - 泛型类
这个没什么好说的 - 泛型方法
出了在泛型类里出现外,扩展方法中使用也很多 - 泛型委托
.net 内置有两个泛型委托 Func<T1,T2,……TOut> 和Action<T1,T2,……>
好了,开始看书了,本章包含了10个条目:
- 使用1.x框架API的泛型版本
- 恰到好处的定义约束
- 运行时检查泛型参数的类型并提供特定的算法
- 使用泛型强制编译器类型推断
- 确保泛型类型支持可销毁对象
- 使用委托定义类型参数上的方法约束
- 不要为积累或接口创建泛型的特殊实现
- 尽可能使用泛型方法,除非需要将类型参数用于实例的字段中
- 使用泛型元组代替out和ref参数
- 在实现泛型接口的同时也实现传统接口
Item1 使用1.x框架API的泛型版本
理由1:由于.net 前两个版本不支持泛型,所以实现通用方法只能通过System.Object来进行编码,然后通过必要的运行时检查保证程序的正确性,我们就必须小心翼翼的对待每一行代码,很难避免在运行时得到意外的类型输入,继而不可避免的导致运行时错误。而泛型类型的约束让人很难传入错误类型的参数。
理由2:免除了装箱、拆箱操作,虽然这个仅仅影响到值类型。同时也免去了运行时检查参数类型等繁冗操作,从而提高效率。
| 原有版本 | 泛型版本 |
| IEnumerable/Enumerable | IEnumerable<T>/Enumerable<T> |
| IList/ArrayList | IList<T>/List<T> |
| Stack | Stack<T> |
| Queue | Queue<T> |
| IDictionary/HashTable | IDictionary<K,V>/Dictionary<K,V> |
| IComparer | IComparer<T> |
| ICollection | ICollection<T> |
| ---- | IEquatable<T> |
| ---- | SortedList<T> |
| ---- | LinkedList<T> |
Item2 恰到好处的定义约束
类型参数的约束指出了能完成该泛型类工作的类必须具有的行为。约束能够让编译器和用户充分了解到我们对泛型类型参数的假设。编译器会认为该泛型类型参数具有约束所定义的各种功能,另外,编译器还能保证使用该泛型类型的用户所指定的类型参数一定会满足约束的条件,在使用泛型的时候如果使用了类型参数约束以外的类型将会直接得到错误提示,而不用到编译器或运行期才发现错误。
假如我们不使用约束,那么势必要执行大量的强制转换以及运行时检验工作。
约束共有六种:
约束定义的太多或太少都不合适,如果太多说明对泛型类型参数的约束越多,也就表示泛型类能适用的场合越少。相反,过少的约束或者不定义约束,那么该泛型类会被误用、产生异常或其他运行时的错误。如果不定义约束,其他开发人员可能需要猜测你的泛型类的用法,或者需要阅读文档,而使用约束即可让编译器保证程序的正确性,降低运行时错误的数量及误用的可能性。因此,我们仅添加必要的约束即可。
从以上可以看出我们应该采用最小化约束来保证既可以实现约束的优点又避免约束过多导致泛型通用性降低。添加最小化约束最常见的方法是:确保泛型类型不要求其不需要的功能。换句话说,只添加我们在泛型中需要使用的方法的约束。比如:我们需要在泛型中需要调用该参数类型的Equals方法,那么就应该对该泛型添加IEquatable<T>约束。如果我们需要调用该类型的Convert方法,那么就该添加该类型参数实现的Convert的接口约束 IConvert。
是否应该使用new()约束?如果我们在泛型中使用了new T(),那么我们就应该添加new()约束,但是我们可以考量能否用default()方法来代替new(),加入我们可以使用default方法,那么就可以不需要new()约束以此来增加该泛型的通用性。尽量不要使用new()约束,从而不在泛型内部创建类型参数实例,因为会使你的泛型类多一些操作。详情请看Item5 确保泛型类型支持可销毁对象
Item3 运行时检查泛型参数的类型并提供特定的算法
为什么要这么做?因为泛型对所有参数类型使用同一种处理方法,而对有的类型来说,其本身有更好的实现方法,采用同一处理方法会降低程序的效率。书上采用的是对IList、IEnumerable、String执行反转,由于代码过长,我用以下代码示例下如何实现提供特定算法,例子本身没什么意义,只是为了直观说明如何来做。
View Code
1 static void Main(string[] args)
2 {
3 //类型参数为String时 调用该泛型的 string Show(string value)方法
4 MyGenric<string> myGenricString = new MyGenric<string>();
5 //输出结果为 This is a string
6 Console.Write(myGenricString.Show("This is a string"));
7
8 //类型参数为其他时 调用该泛型的 string Show(T t)方法
9 MyGenric<int> myGenricInt = new MyGenric<int>();
10 //输出结果为 This is the default Impl 1
11 Console.WriteLine(myGenricInt.Show(1));
12
13 Console.Read();
14 }
15
16 public class MyGenric<T>
17 {
18
19 public string Show(T t)
20 {
21 return "This is the default Impl " + t.ToString();
22 }
23 public string Show(string value)
24 {
25 return value;
26 }
27 }
Item4 使用泛型强制编译期类型推断
原文的总结:在实现某一算法逻辑时,我们很多时候需要知道传入参数的类型,这时,通常可以创建一个泛型的实现,将方法的类型参数抽象到泛型参数中,随后编译器即可根据泛型参数创建出所需要的类型。
说白了,就是我这方法需要一个参数,而且这个参数是一个类型参数,在使用的时候我得知道它是什么类型,那我就创建个泛型,我的方法需要的类型参数就是这个泛型的类型参数。
Item5 确保泛型类型支持可销毁对象
这一点很好理解,如果泛型类型参数实现了IDisposable,那么我们就可能要多做点事(注意是可能),就是要在泛型中调用该类型参数的Dispose方法,以避免可能出现的资源泄露。
我们如何确定类型参数实现了IDisposable接口?什么情况下需要在泛型中调用Dispose方法?为什么说可能?如何调用Dispose方法?
第一个问题:我们不能确定类型参数是否实现IDisposable接口
第二个问题:只有满足了以下任一条件我们就得想办法调用Dispose方法
- 在泛型某个方法内部创建了参数类型的实例,不管其实现没实现IDisposable(提以下,这个类型参数肯定要有new()约束)
- 泛型类中需要实例化某个类型参数,并将其作为成员变量,这时泛型类本身必须也要实现IDisposable。
第三个问题:如果在泛型的某个方法内部调用了该类型参数的实例,这个实例是以方法参数由外部传入,那就由外部处理吧。我们就不理它了。
第四个问题:
- 如果只是方法中创建了类型参数的实例,那么直接使用using就可以搞定了
public void GetThingsDone()
{
T driver=new T();
using(driver as IDisposable)
{
driver.Dowork();
}
}实际上,无论T是否实现了IDisposable接口,上面的代码都会起到释放driver的作用。如果T实现了IDisposable方法,那么编译器将在代码块结束位置生成对Dispose方法的调用。如果没有实现IDisposable接口,那么该本地变量将为null,不会调用Dispose()方法。
- 如果是泛型类中需要实例化某个类型参数,并将其作为成员变量,那么就要该泛型类就得实现IDisposable接口,在Dispose方法中对这些实例进行销毁。
总结下:事实上,你可以忽略上面所讲的IDisposable,只要你在泛型类的内部创建了类型参数的实例,你总应当尝试去释放它。所以,尽量让变量在外部创建好,通过方法参数传递到泛型类中,同时,尽量不要在约束上添加new()约束。
Item6 使用委托定义类型参数上的方法约束
首先,我对这个是否应该叫约束有点持怀疑态度,因为这里只是将一个匹配泛型类型将要调用的方法用委托的签名来表示。当然,它是起到了必须让外部提供某方法的用处,但这个方法是不是参数类型的,它并不在乎。说的很绕,不好意思。
为什么要采用委托的形式?因为C#的约束机制比较简单,只能通过以前提的6种方法。如果对参数类型某个方法约束的时候,我们就需要建立这个方法的接口,然后用这个接口来进行约束,这样显然会带来很多麻烦。
看看如何使用:
View Code
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 MyGenric<int> myGenricInt = new MyGenric<int>();
6 // myGenricInt.Add(1, 2, (x, y) => (x + y).ToString());
7
8 Func<int, int, string> func = (x, y) => (x + y).ToString();
9 myGenricInt.Add(1, 2, func);
10 }
11
12 }
13 public class MyGenric<T>
14 {
15
16 public string Add(T left,T right,Func<T,T,string> addFunc)
17 {
18 //调用类型参数从外部提供的方法 并在内部进行处理。
19 //这里的处理仅是加上一个字符串。
20 return "The result is "+addFunc(left,right);
21 }
22 }
总结:若你需要仅为某个特定的泛型方法或类创建自定义的接口契约,那么也可以使用委托将该契约声明成为方法的约束。无论是为了指定操作符、静态方法、委托类型或者创建某种规则,都可以用泛型接口来表达这种约束,并通过创建实现了这些接口的辅助方法类型约束来满足约束的条件。

浙公网安备 33010602011771号