C# 泛型学习

对于“泛型”泛型这个词,我很陌生。在学校学习时,也从没听到过这个词。学习“泛型”让我感觉特别费劲。
虽然自己看了泛型相关的书,但还是没看懂。不过自己在网上找了些文章倒对我帮助很大。
下边转一篇我看一以的文章。其内容较简章。适合我快速入门泛型。文章如下:

一.什么是泛形
 微软给出的定义是:"泛型可以让类、结构、接口、委托和方法按它们存储和操作的数据的类型进行参数化"
我对这句话的理解为:
 我们可以让类、结构、接口、委托和方法的字段,参数或者局部变量的类型动态的定义,而不需要为每种类型都进行定义.

二.为什么使用泛型
正如前面所说的我们不必因为类、结构、接口、委托和方法的字段,参数或者局部变量的类型不同,而定义不同的版本.
如果没有泛型的话我没定义Stack类则必须这样定义:

public class Stack
{
    
int[] items;
    
int count;
    
public void Push(int item) {}
    
public int Pop() {}
}
这样我们就定义了一个int型的堆栈,这样看起来还不错,但是如果现在我们不仅需要一个int堆栈还要一个string类型的堆栈呢,这同样很简单我只要再定义一个string类型的堆栈
public class Stack
{
    
string[] items;
    
int count;
    
public void Push(string item) {}
    
public string Pop() {}
}
这样看起来也还不错,还是能很好的完成我们的工作,但是如果我们还要double,byte,Employee.....等等类型的堆栈呢,难道我们都要一个个去定义吗?

我相信聪明的大家肯定不会这样做的,大家都知道.NET有个特殊的 object类,讲到这里我想大家已经猜到了我要干什么了吧。对,我要用Object对象来实现这个堆栈.

 public class Stack
 {
    
object[] items;
   
int count;
 
   
public void Push(object item) {}
 
   
public object Pop() {}
 }
这样总没有什么问题了吧,我只要定义一个Stack类,什么类型都能用了.
问题没有这么简单吧,大家先看下面的代码
//性能问题
Stack stack = new Stack();
stack.Push(
3);
int i = (int)stack.Pop();
//安全问题
Stack stack = new Stack();
stack.Push(
new Customer());
string s = (string)stack.Pop();
当我们i传递给Push方法时则该值将自动被装箱。当以后检索该int时,必须使用显式类型强制转换将其取消装箱.这样的装箱和取消装箱操作由于涉及动态内存分配和运行时类型检查而额外增加了性能开销.

同时,虽然上面的代码错误使用了Stack但是从技术角度讲该代码可以视作是正确的编译器不会报告编译时错误。这个问题在该代码被执行之前不会暴露出来,但在执行该代码时会引发InvalidCastException

显然如果能够指定元素类型Stack类将能够从中受益。

三.创建泛型

泛型提供了一种新的创建类型的机制使用泛型创建的类型将带有类型形参。下面的示例声明一个带有类型形参T的泛型Stack类。类型形参在 <>分隔符中指定并放置在类名后。Stack<T>的实例的类型由创建时所指定的类型确定,该实例将存储该类型的数据而不进行数据类型转换。这有别于同 object之间的相互转换。类型形参T只起占位符的作用,直到在使用时为其指定了实际类型。注意,这里的 T用作内部项数组的元素类型、传递给Push方法的参数类型和Pop方法的返回类型:

 public class Stack<T>
 {
   T[] items;
   
int count;
 
   
public void Push(T item) {}
 
   
public T Pop() {}
 }

四.使用泛型
泛型的堆栈我们是定义好了,那么我们该怎么用他呢?定义都这么简单用起来肯定也不会难的
Stack<int> stack = new Stack<int>();
stack.Push(
3);
int x = stack.Pop();


Stack<int>类型中出现的每个T都被替换为类型实参int。在创建Stack<int>的实例后items数组的本机存储是int[]而不是object[]。无疑,这比非泛型的Stack提供了更高的存储效率。同样,Stack<int> PushPop方法所操作的也是int类型的值。如果将其他类型的值推入堆栈则产生编译时错误。而且在检索值时也不再需要将它们显式强制转换为原始类型。

同时泛型提供了强类型机制,意味着如果将一个int值推入Customer对象的堆栈将导致错误.
这样就定义好了一个int 型的Stack实例.


转自:http://www.cnblogs.com/yanzimywife/archive/2007/03/20/681186.html



我自己也做了一個簡例子,進行學習:例子如下:

    //程序主要功能是:傳入不同類型值,輸出值及類型
    class Program
    {
        
static void Main(string[] args)
        {
            
//實例化類
            Stack<int> iSta = new Stack<int>();
            
//調用類方法,傳入一個整型值
            iSta.ReturnValue(3);

            Stack
<string> sSta = new Stack<string>();
            
//調用類方法,傳入一個字符型值
            sSta.ReturnValue("Good");
            
        }
        
public class Stack<T>
        {
            
public void ReturnValue(T tValue )
            {
                
//輸入出值,及值的類型
                Console.WriteLine("The tVAlue is:{0} and the Type is:{1} ",tValue,tValue.GetType().ToString());                
                Console.ReadKey();
            } 
        }
    }

輸出結果:

    The tVAlue is:3  and the Type is:System.Int32
   
The tVAlue is:Good  and the Type is:System.String


文章二
什么是泛型?

  我们在编写程序时,经常遇到两个模块的功能非常相似,只是一个是处理int数据,另一个是处理string数据,或者其他自定义的数据类型,但我们没有办法,只能分别写多个方法处理每个数据类型,因为方法的参数类型不同。有没有一种办法,在方法中传入通用的数据类型,这样不就可以合并代码了吗?泛型的出现就是专门解决这个问题的。读完本篇文章,你会对泛型有更深的了解。

  为什么要使用泛型

  为了了解这个问题,我们先看下面的代码,代码省略了一些内容,但功能是实现一个栈,这个栈只能处理int数据类型:

public class Stack
{
private int[] m_item;
public int Pop(){...}
public void Push(int item){...}
public Stack(int i)
{
this.m_item = new int[i];
}
}

  上面代码运行的很好,但是,当我们需要一个栈来保存string类型时,该怎么办呢?很多人都会想到把上面的代码复制一份,把int改成string不就行了。当然,这样做本身是没有任何问题的,但一个优秀的程序是不会这样做的,因为他想到若以后再需要long、Node类型的栈该怎样做呢?还要再复制吗?优秀的程序员会想到用一个通用的数据类型object来实现这个栈:

public class Stack
{
private object[] m_item;
public object Pop(){...}
public void Push(object item){...}

 public Stack(int i)
{
this.m_item = new[i];
}

}

  这个栈写的不错,他非常灵活,可以接收任何数据类型,可以说是一劳永逸。但全面地讲,也不是没有缺陷的,主要表现在:

  当Stack处理值类型时,会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。
在处理引用类型时,虽然没有装箱和折箱操作,但将用到数据类型的强制转换操作,增加处理器的负担。

  在数据类型的强制转换上还有更严重的问题(假设stack是Stack的一个实例):

Node1 x = new Node1();
stack.Push(x);
Node2 y = (Node2)stack.Pop();

  上面的代码在编译时是完全没问题的,但由于Push了一个Node1类型的数据,但在Pop时却要求转换为Node2类型,这将出现程序运行时的类型转换异常,但却逃离了编译器的检查。

  针对object类型栈的问题,我们引入泛型,他可以优雅地解决这些问题。泛型用用一个通过的数据类型T来代替object,在类实例化时指定T的类型,运行时(Runtime)自动编译为本地代码,运行效率和代码质量都有很大提高,并且保证数据类型安全。

  使用泛型

  下面是用泛型来重写上面的栈,用一个通用的数据类型T来作为一个占位符,等待在实例化时用一个实际的类型来代替。让我们来看看泛型的威力:

public class Stack<T>
{
private T[] m_item;
public T Pop(){...}
public void Push(T item){...}

 public Stack(int i)
{
this.m_item = new T[i];
}
}

  类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法:

//实例化只能保存int类型的类

Stack<int> a = new Stack<int>(100);
a.Push(10);
a.Push("8888"); //这一行编译不通过,因为类a只接收int类型的数据
int x = a.Pop();

//实例化只能保存string类型的类

Stack<string> b = new Stack<string>(100);
b.Push(10); //这一行编译不通过,因为类b只接收string类型的数据
b.Push("8888");
string y = b.Pop();

  这个类和object实现的类有截然不同的区别:

  1. 他是类型安全的。实例化了int类型的栈,就不能处理string类型的数据,其他数据类型也一样。

  2. 无需装箱和折箱。这个类在实例化时,按照所传入的数据类型生成本地代码,本地代码数据类型已确定,所以无需装箱和折箱。

  3. 无需类型转换。

  泛型类实例化的理论

  C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类,所以不同的封闭类的本地代码是不一样的。按照这个原理,我们可以这样认为:

  泛型类的不同的封闭类是分别不同的数据类型。

  例:Stack<int>和Stack<string>是两个完全没有任何关系的类,你可以把他看成类A和类B,这个解释对泛型类的静态成员的理解有很大帮助。

  泛型类中数据类型的约束

  程序员在编写泛型类时,总是会对通用数据类型T进行有意或无意地有假想,也就是说这个T一般来说是不能适应所有类型,但怎样限制调用者传入的数据类型呢?这就需要对传入的数据类型进行约束,约束的方式是指定T的祖先,即继承的接口或类。因为C#的单根继承性,所以约束可以有多个接口,但最多只能有一个类,并且类必须在接口之前。这时就用到了C#2.0的新增关键字:

public class Node<T, V> where T : Stack, IComparable
where V: Stack
{...}

  以上的泛型类的约束表明,T必须是从Stack和IComparable继承,V必须是Stack或从Stack继承,否则将无法通过编译器的类型检查,编译失败。

  通用类型T没有特指,但因为C#中所有的类都是从object继承来,所以他在类Node的编写中只能调用object类的方法,这给程序的编写造成了困难。比如你的类设计只需要支持两种数据类型int和string,并且在类中需要对T类型的变量比较大小,但这些却无法实现,因为object是没有比较大小的方法的。 了解决这个问题,只需对T进行IComparable约束,这时在类Node里就可以对T的实例执行CompareTo方法了。这个问题可以扩展到其他用户自定义的数据类型。

  如果在类Node里需要对T重新进行实例化该怎么办呢?因为类Node中不知道类T到底有哪些构造函数。为了解决这个问题,需要用到new约束:

public class Node<T, V> where T : Stack, new()

where V: IComparable

  需要注意的是,new约束只能是无参数的,所以也要求相应的类Stack必须有一个无参构造函数,否则编译失败。

  C#中数据类型有两大类:引用类型和值类型。引用类型如所有的类,值类型一般是语言的最基本类型,如int, long, struct等,在泛型的约束中,我们也可以大范围地限制类型T必须是引用类型或必须是值类型,分别对应的关键字是class和struct:

public class Node<T, V> where T : class

where V: struct

  泛型方法

  泛型不仅能作用在类上,也可单独用在类的方法上,他可根据方法参数的类型自动适应各种参数,这样的方法叫泛型方法。看下面的类:

public class Stack2
{
public void Push<T>(Stack<T> s, params T[] p)
{
foreach (T t in p)
{
s.Push(t);
}
}
}

  原来的类Stack一次只能Push一个数据,这个类Stack2扩展了Stack的功能(当然也可以直接写在Stack中),他可以一次把多个数据压入Stack中。其中Push是一个泛型方法,这个方法的调用示例如下:

Stack<int> x = new Stack<int>(100);
Stack2 x2 = new Stack2();
x2.Push(x, 1, 2, 3, 4, 6);

string s = "";

for (int i = 0; i < 5; i++)
{
s += x.Pop().ToString();
} //至此,s的值为64321

  泛型中的静态成员变量

  在C#1.x中,我们知道类的静态成员变量在不同的类实例间是共享的,并且他是通过类名访问的。C#2.0中由于引进了泛型,导致静态成员变量的机制出现了一些变化:静态成员变量在相同封闭类间共享,不同的封闭类间不共享。

  这也非常容易理解,因为不同的封闭类虽然有相同的类名称,但由于分别传入了不同的数据类型,他们是完全不同的类,比如:

Stack<int> a = new Stack<int>();
Stack<int> b = new Stack<int>();
Stack<long> c = new Stack<long>();

  类实例a和b是同一类型,他们之间共享静态成员变量,但类实例c却是和a、b完全不同的类型,所以不能和a、b共享静态成员变量。

  泛型中的静态构造函数

  静态构造函数的规则:只能有一个,且不能有参数,他只能被.NET运行时自动调用,而不能人工调用。

  泛型中的静态构造函数的原理和非泛型类是一样的,只需把泛型中的不同的封闭类理解为不同的类即可。以下两种情况可激发静态的构造函数:

  1. 特定的封闭类第一次被实例化。

  2. 特定封闭类中任一静态成员变量被调用。

  泛型类中的方法重载

  方法的重载在.Net Framework中被大量应用,他要求重载具有不同的签名。在泛型类中,由于通用类型T在类编写时并不确定,所以在重载时有些注意事项,这些事项我们通过以下的例子说明:

public class Node<T, V>
{
public T add(T a, V b) //第一个add
{
return a;
}
public T add(V a, T b) //第二个add
{
return b;
}
public int add(int a, int b) //第三个add
{
return a + b;
}
}

  上面的类很明显,如果T和V都传入int的话,三个add方法将具有同样的签名,但这个类仍然能通过编译,是否会引起调用混淆将在这个类实例化和调用add方法时判断。请看下面调用代码:

Node<int, int> node = new Node<int, int>();
object x = node.add(2, 11);

  这个Node的实例化引起了三个add具有同样的签名,但却能调用成功,因为他优先匹配了第三个add。但如果删除了第三个add,上面的调用代码则无法编译通过,提示方法产生的混淆,因为运行时无法在第一个add和第二个add之间选择。

Node<string, int> node = new Node<string, int>();
object x = node.add(2, "11");

  这两行调用代码可正确编译,因为传入的string和int,使三个add具有不同的签名,当然能找到唯一匹配的add方法。

  由以上示例可知,C#的泛型是在实例的方法被调用时检查重载是否产生混淆,而不是在泛型类本身编译时检查。同时还得出一个重要原则:

  当一般方法与泛型方法具有相同的签名时,会覆盖泛型方法。

  泛型类的方法重写

  方法重写(override)的主要问题是方法签名的识别规则,在这一点上他与方法重载一样,请参考泛型类的方法重载。

  泛型的使用范围

  本文主要是在类中讲述泛型,实际上,泛型还可以用在类方法、接口、结构(struct)、委托等上面使用,使用方法大致相同,就不再讲述。
 

转自:http://blog.csdn.net/licream/archive/2007/08/20/1752155.aspx

原文地址:https://www.cnblogs.com/scottckt/p/955278.html