由于Newlife.XCode组件中大量的地方使用了泛型基类,以前也是很迷糊,似懂非懂。所有春节前整理了一份文档,把泛型基类的知识总结了一下,特意分享出来。本文大部分内容摘自XCode的相关介绍说明以及如下2篇文章,只是整理了一下:
http://www.cnblogs.com/XmNotes/archive/2012/04/23/2466938.html
http://www.cnblogs.com/zgynhqf/archive/2010/09/28/1837567.html
在.NET发展史中,2.0是具有里程碑意义的一个版本。在.NET 2.0带来的诸多新特性中,我认为泛型是最重要的一个。虽然泛型出现已有多年,连Java都早已借鉴引入了泛型(虽然是语法糖),可是用泛型的编程思维方式并没有得到相应的普及。一方面是由于过去大量的Framework仍然是在非泛型时代写成的,另一方面泛型的设计模式没有得到发展,改变的时候该到了。来举一个例子说明这两点。我们如果写过网络数据抓取的代码,应该熟悉这样的代码:
1 var request = WebRequest.Create("http://www.cnblogs.com/") as HttpWebRequest;
或者这么写,也是一样:
1 var request = HttpWebRequest.Create("http://www.cnblogs.com/") as HttpWebRequest;
大家可想过,为什么每次都要as一下?类似的情况还有:
1 var bm = Image.FromFile("e:\\me.jpg") as Bitmap; 2 var bm = Bitmap.FromFile("e:\\me.jpg") as Bitmap;
我想过,但没想明白。上面两种写法,都是调用父类的工厂方法,实际返回了一个子类的实例。显然,即使不了解OCP,凭直觉也应该想到,父类的实现中不应该被子类所决定。写WebRequest和Image的前辈可能也觉得直接返回子类实例不妥,所以阴险地把方法签名的返回类型改成了父类。虽然这种行径值得严重鄙视。但.NET程序员大都是人云亦云,照葫芦画瓢的好学生,所以这个问题多年了也没有修改。理想的设计应该是这样:父类的每个子类,都有独立的工厂方法,返回其自身的实例。这样做法,在泛型出现前非常笨拙,得不偿失,但有了泛型,就可以精巧地实现。
以模拟Image类为例,Image和BitMap实现如下:
1 class Image<T> where T:Image<T>, new() 2 { 3 public string Path { get; set; } 4 public static T FromFile(string path) 5 { 6 return new T() { Path = path }; 7 } 8 } 9 10 class Bitmap:Image<Bitmap> 11 { 12 13 }
Image自身的工厂方法,就没有存在的必要了。
【大石头对X组件泛型基类的举例】泛型基类,类型自己作为泛型基类的泛型参数。2006年我把我们的数据映射框架NewLife.XCode改为泛型基类的写法,两个月后取消了这种做法,因为实在是太诡异了。另外,当时从网络上找到的资料,微软并不推荐这种写法,理由是会让代码变得异常晦涩难懂。
2007年,NewLife.XCode重新开始使用泛型基类,这回总算控制住了。于是,我们的充血实体类基本都是:User:Entity<User>, Log:Entity<Log>
User和Log就“继承”了Entity<>里面静态方法FindAll,并且返回类型是List<User>和List<Log>。
2009年,我们又这样玩:
User<T>:Entity<T> where T:User<T> {// 实体类主体 }
User:User<User>{ // 默认实体 }
User1:User<User1>{// 第一种扩展,比如增加字段属性 }
User2:User<User2>{// 第二种扩展,比如增加业务逻辑 }
XCode从V1.2起,就进入了第二代,关键点就在于泛型基类Entity<TEntity>的使用。在第一代XCode中,因为充血模型,实体类上要附带大量的方法,而当它们的返回类型是实体类或者实体类集合时,这些方法就必须实现于实体类的代码中,实际上是通过代码生成器来生成。在第二代XCode,引入了泛型基类技术,实体类通过泛型参数TEntity指定最终返回类型,编写查询方法的时候,返回类型使用泛型参数TEntity即可。所以,第二代实体类只有属性和索引器,基本不需要生成查询和操作的方法,因为它们都在泛型基类里面实现了。大多数情况下,实体类指定的基类泛型参数就是它自己,因为它需要以它自己作为返回类型。但XCode开发模式是面向对象的,包括实体类,也希望能够继承,增加一些功能,该功能可以通过改变泛型参数来实现。
“绑定子类的泛型层基类”,这个模式在一些著名的框架中也见到过,如果CSLA、BlogEngine。我自己在原来的写的框架中,也用到过。当然了,个人认为是反模式,各们同仁并不一定这样认为,仁者见仁,智者见智了。不过我好几次都是受尽折磨,所以决定写出来给大家分享下心得。“绑定子类的泛型层基类”用在某些地方,而不是用在任何地方.用在ORM上就非常合适.。今天要说的主题正是基于LayerSupertype,并结合了泛型技术而实现的,同样,它还有一个重要的约定:泛型的类型参数必须是最终的子类。看如下一个例子:
1 public abstract class EntityBase<T> where T : EntityBase<T> 2 { 3 public int Id { get; set; } 4 //sth else important...... 5 } 6 public class User : EntityBase<User> 7 { 8 public string Name { get; set; } 9 } 10 11 public class Article : EntityBase<Article> 12 { 13 public string Title { get; set; } 14 }
EntityBase作为所有实体类的基类,提供了统一的实体模板、约定和一些通用的基础实现。基于这个基类的代码重用,使得子类的代码非常简单。这里和普通继承、普通泛型的不同点在于父类在运行时绑定了具体子类的类型。带来的问题 使用这种模式,缺点是显而易见的:
1. 不能直接使用基类进行统一的处理
继续上面的例子,这样的设计,使得我们不能对所有的实体进行统一的处理。由于User和Article的基类其实是两个不同的运行时类型,所以我不能把它们转换为同一个“实体”类型。如:
EntityBase a = new Article(); a = new User();
我甚至都不可能用到抽象的EntityBase类,因为我要使用此类,必须指定具体的子类,但是我如果知道要使用哪个具体的子类,也就没有必要使用它们的基类了。也就是说,根本就不存在实体的抽象类,而EntityBase<T>存在的意义只是为了代码重用。我不知道这是否能看为违反了OO的Liskov替换原则,不过真是难以忍受。
2. 无法直接实现实体的再继承
第二个问题,同样是继承机制的问题。我无法从现在的具体实体类直接进行派生!!!我无法使用这样的语法:GoodArticle : Article。这是因为Article已经“告诉”基类EntityBase<T>绑定子类的类型是Article,而不是GoodArticle,这按照EntityBase<T>设计时的约定“T必须是最终的子类”相矛盾!无法继承……继承作为OO三大特性中的一个,这个问题简直无法忍受。
想办法绕开这两个问题:
其实,上面提到的两个问题,在技术上都是能够找到一些方法来解决的:
1. 无法向基类转换。这个问题产生的原因,主要是因为没有一个“与子类无关的抽象”存在。我们可以为EntityBase<T>添加IEntity接口,这样,所有的子类都能转换为IEntity,也就能进行统一的处理。
2. 无法再继承。要解决这个问题,我们需要把需要进行再继承的类也提取为一个泛型基类和一个继承此基类的空的子类。如:
1 public class Article<T> : EntityBase<T> where T : Article<T> 2 { 3 public string Title { get; set; } 4 } 5 6 public class Article : Article<Article> { } 7 8 public class GoodArticle : Article<GoodArticle> { }
这样的方案好像可以解决,但是这样的设计实在让人难以接受:
*作为设计类库来说,我只是添加了一个单向依赖父类的子类,却不得不修改父类的代码,分离为两个类。
*要不就是所有的类都直接写成一个泛型类+一个空子类的方法。(这个设计丑陋吗?)
*没有解决根本的问题:TopArticle 并不是一个 Article,它只是一个和Article有重用代码的类。
1.对于一项语言特性,一定要有选择的用,要明白它的含义,知道把它放在哪里最合适,而不是一味的在任何地方都使用.
2.User和Article本就不是同一父类的子类,因为它们映射的是数据库中的表,表和表之间是没有继承关系的,只有外键关系;这里的泛型约束就很好的阻止了开发人员为实体类指定派生类.也就是说,这是框架设计者有意为之.
3.泛型实现的是编译时(静态)多态,不是运行时多态,EntityBase<Article> 和 EntityBase<User>其实是不同的两个静态类型,这是在编译器就决定了的,不用等到运行时.
EntityBase<T> ,我们称为泛型模板,注意,有"模板"这个后缀,而EntityBase<User> 则称为泛型模板EntityBase<T>的实例.