分享

引用 泛型编程 - xxiu的日志 - 网易博客

 昵称228879 2009-08-17
   啃了啃泛型编程,写点心得,很肤浅,泛型仔细研究起来太复杂,跟反射、委托等等许多东西都有种种关联,慢慢积累吧,今天写点基本的东西。
   我们知道C#中有装箱,拆箱的操作,我们使用ArrayList可以知道,当我们向ArrayList中加入数据时,是以Object类型加入的,取出的时候也是以Object类型取出的,因此我们需要对取出的数据进行强制类型转换才能保证使用,代码如下:

ArrayList al=new ArrayList();

al.Add(123);

int temp=(int)al[0];

Console.WriteLine(temp);

   这样当我们用MSIL反汇编程序看IL代码时会发现,程序进行了装箱,拆箱操作,下图:

引用 泛型编程 - xxiu - 陌上花开   可以清楚的看到,画红圈的boxunbox,就是通常所说的装箱,拆箱操作,引我们老师的一句话:“ArrayList的最大特点就是管你怎么操作都不出错。”弊端在哪呢?如果程序规模庞大,如此频繁的装箱拆箱的操作必然导致程序效率低下,因为Object这个类型实在是过于庞大,是处于顶端的一个类型,这是从效率方面考虑,从另一方面安全性的考虑上来说,频繁的装箱操作及拆箱操作也会引发安全问题,因为你随便往里装什么都无所谓,但是如果不知道装的是什么,你就取不出来(或者说取出来不知道怎么用),因此C#2.0引入泛型,提高了程序效率,并引入了新的编程模式,也就是泛型编程,泛型编程不仅可以应用于需要装箱,拆箱操作的数据,编程时恰当的使用泛型思想也可以减化某些工作的实现。并且C#对泛型的支持,是从编译IL代码、元数据到CLR运行时,实例化则是在JIT编译时,与JAVA的泛型相比是有些不同的。

   泛型有着更强的类型安全,更好的复用,更高的效率,更清晰的约束。下面开始代码与代码解释。

   泛型的基本声明及用法

   代码:

public classStack<T>
{
       private T[] store;
       private int size;
       public Stack()
       {
           store = new T[10];
           size = 0;
       }
       public void Push(T x)
       {
           store[size++] = x;
       }
       public T Pop()
       {
           return store[--size];
       }
}
    一个简单模拟存数据,取数据的泛型类型,代码中<>中的T是这个类的参数类型,写在“<>”中,表示的是这个T是一个晚绑定的类型,也就是在前两次编译时,并不确定T是一个什么类型,前面说过,泛型的实例化是在JIT编译时,这里我是这么理解的,T可以把它当做一个占位符,未实例化之前,它可能是任意类型,泛型类型可以看做一个抽象类,当实例化的时候,应指定T的参数类型,如下:
Stack<int> stack=newStack<int>();

   好了,实例化完成后,T的参数类型就明确了,如此一来,有个问题,我听WebCast的时候只听说需要这么写,那不这么写行不行呢?如果我写

Stack stack=new Stack();

   编译是不会报错的,而且也没有任何的装箱,拆箱操作,Pop出来的值的Type也完全与Push进去的相符合,这个我需要再研究研究,难道编译器这么智能了。。会自己判断了,汗。无关大局,继续。

class C<V>
{
    public Vf1;
    publicD<V> f2;
    public C(Vx)
    {
      this.f1=x;
    }
}

   可以看出泛型类型中还是可以写很多东西的,包括其他泛型类型的对象(泛型类型D的对象f2,并且将自己的参数类型V传递给泛型类型D)。

   泛型类型继承的语法规则

class C1<U,V>{}//合法
classC2:C1<string,int>{} //合法
class C3<U,V>:C1<U,V>{}//合法
classC4<U,V>:C1<string,int>{} //合法
class C5:C1<U,V>{} //非法

   这里跟struct类似,基类的参数要么已经实例化,要么来源于子类的参数。如果两者都不是,那运行时将无法实例化子类对象。

   泛型类型大体上就简单的说完了,下一个:

   泛型接口:

interfaceIList<T>
{
    T[]GetElements();
}
interface IDictionary<K,V>
{
    void Add(Kkey,V value);
}
class MyList<T>:IList<T>,IDictionary<int,T>
{
    public T[]getElements(){}
    public voidAdd(int index,T value){}
}

   注意,接口的参数类型与泛型类型继承中的父类泛型类型类似,当某个类需要实现某个或多个泛型接口时,泛型接口中的参数类型要么已经实例化,要么来源于实现类的参数列表(请注意那个红色的int)。好了,从这个例子可以稍微看出点来,我们可以定义一个可以接收或返回任意数据类型的泛型接口,而实现类只需要根据自身的需要去实现对应数据类型的接口就好了,不仅提高了代码重用,而且提高了效率,当然,这只是泛型接口应用之一。

   继续。

   泛型委托:

delegate boolpredicate<T>(T value);
class X
{
   static void F(int i){}
   static void G(string s){}
    static voidmain()
    {
       predicate<string>p1=G;

       p1(123);
       predicate<int> p2=new predicate<int>(F);

       p2("fuckdawnh");
   }
}

   这段代码需要注意的是红色那段的写法,C#2.0以后支持这种写法,但是实际上编译器做的工作是predicate<string>p1=newpredicate<string>(G);所以我老觉得语言越来越难学,本来就因为C#是建立在高层的语言,底层本来就不容易理解,编译器作者又这么为程序员着想,想方设法为我们节省工作量,是好是坏呢?谁知道……额回到正题,泛型这么一引入,得又省了,定义一个返回或需要任意参数类型的参数(注意,委托支持在返回值类型上使用泛型,且参数类型同样可以附带合法的约束),好了,你需要整型的就写int,你需要字符串就写string,也是一种在参数类型上多态的体现吧。。

   继续,
    泛型方法:

   注意泛型方法既可以包含在泛型类型中,也可以包含在非泛型类型中,不包含在属性、事件、索引器、构造器、析构器。

   ①基本写法:

public class Finder
{
    publicstatic int Find<T>(T[] items,T item)
    {
       for(int i=0;i
       {
           if(itmes[i].Equals(item))
           return i;
       }
        return-1;
    }

}
int i=Finder.Find<T>(newint[]{1,3,4,5,6,8,9},6);

   注意Finder类并不是一个泛型类型,而其中的方法Find却是一个返回int类型,需要T类型数组,T类型变量的泛型方法,注意最后调用Find的写法,这是在非泛型类中的泛型方法的典型例子。

   继续。

   ②泛型方法的重载:

class MyClass
{
    voidF1<T>(T[] a,int i){}
    voidF1<U>(U[] a,int i){} //不构成重载

 

   void F2<T>(int x){};
    void F2(intx){} //构成重载

 

   void F3<T>(T t) where T:A{}
    voidF3<T> where T:B{} //不构成重载
}

   好了,这里需要注意的地方挺多:

       第一个比较好理解,典型的换汤不换药,不构成重载没什么疑问。

       第二组则构成重载,虽然T没在F2方法的参数列表中使用,可是T可以做为第一个F2方法内部成员变量的参数类型,并且同样都是方法F2,调用方法是不同的,调用第一个F2的写法(假如T的类型是string)是对象名.F2<string>(123);而调用第二个F2的写法是 对象名.F2(123);是有区别的。

        第三组也是无法构成重载的,原因是不到运行时,编译器根本无法判断T是不是类型A,也无法判断T是不是类型B,前面已经说过,不到JIT编译,泛型不会被实例化,因此我觉得要做一个程序员,要有两个层次的认识,一是高层认识,就是你需要知道什么场合、什么时间该用什么方法构造你的代码,二是底层认识,你也需要知道底层大概路线是怎么走的,也需要有内存模型,知道栈堆等等都存什么用的。

    继续。

    ③泛型方法的重写:

abstract class Father
{
    publicabstract T F<T,U>(T t,U u) where U:T;
    publicabstract T G<T>(T t) where T:IComparable;
}
class Child:Father

{
    publicoverride X F<X,Y>(X x,Y y){} //正确
    publicoverride T G<T>(T t) where T:IComparable{} //错误!
}

    注意的一点,当重写泛型方法时,此方法的所有约束都是被默认继承的,因此不需要你手动去添加。

    继续下一个大项。

    泛型的约束:    

    对所有泛型类型或泛型方法的类型参数的任何假定,都要基于“显式的约束”,以维护类型安,泛型约束主要有这么几个:①基类约束,②接口约束,③构造器约束,④值/引用类型约束。如果没有显式约束,则泛型类型只能访问Object里的公有方法。一个个看吧:

    ①基类约束

class A{public voidF1(){}}
class B{public void F2(){}}
class C<S,T> where S:A where T:B
{
    static Sa=(S)(new A());

   static T b=(T)(new B());

   static void Main()

   {

        a.F1();

        b.F2();

   }
}

   where后面的S:A就是说这个参数S必须是类型A,参数T必须是类型B,否则将导致运行时异常,当我们对类型C做了基类约束后,那我们便可以通过传递进来的合法参数来调用类型A的F1方法及类型B的F2方法了。注意强制类型转换,这里是将A当成了S的父类,B当成了T的父类。

   ②接口约束

interface IPrintable{voidPrint();}
interface IComparable<T>{int CompareTo(T v)}
interface IKeyProvider<T>{T GetKey();}
class Dictionary<K,V>

   where K:IComparable<K>

   where V:IPrintable,IKeyProvider<K>
{
    //可以调用K类型实例的CompareTo方法

   //可以调用V类型实例的Print,GetKey方法

}

   我们可以看到参数K,V被约束为必须实现了某个接口的类型的实例,因此在该类中我们便可使用该实现类中所实现的接口中的方法了,还是要注意约束中接口中的参数,必须已经实例化或来源于实现类。

   ③构造器约束

class A{publicA(){}}
class B{public B(int i){}}
class C<T> where T:new()
{
    T t=newT();
}
C<A> c=new C<A>();
B<B> c=new C<B>();
//错

   这里只是注意一下where后面的意思就可以了,这里的whereT:new()的意思是,该类型带有一个无参构造函数,我们看到类型B只有一个带有int类型的构造函数,因此如果将B当做类型传入会发生错误。另外注意,如果需要使用Tt=new T();则必须在约束中指定new();否则无法通过编译。

   ④值/引用类型约束

public struct A{}
public class B{}
class C<T> where T:struct
{
}
C<A> c=new C<A>();
C<B> c=new C<B>(); //错误

   这里也只是需要注意一下where后面约束的意思就可以了,whereT:stuct意思是T是一个值类型,因此第二种写法是错的。

   累晕了,写到这泛型的一些基础的东西就说完了,做为C#2.0的核心技术,泛型的应用非常广泛,其复杂程度也是相当之高,光以上这些东西,MSDN里就标出来,这是Level300的,因此还是需要我们多多的积累才能对泛型有一个更全面的认识。

   下一篇准备写写委托与事件,这个东西真是不怎么好理解。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多