【学习笔记】C#中的泛型和泛型集合
一、什么是泛型?
泛型是C#语言和公共语言运行库(CLR)中的一个新功能,它将类型参数的概念引入.NET Framework。类型参数使得设计某些类和方法成为可能,例如,通过使用泛型类型参数T,可以大大简化类型之间的强制转换或装箱操作的过程(下一篇将说明如何解决装箱、拆箱问题)。说白了,泛型就是通过参数化类型来实现在同一份代码上操作多种数据类型,利用“参数化类型”将类型抽象化,从而实现灵活的复用。
使用泛型给代码带来的5点好处:1、可以做大限度的重用代码、保护类型的安全以及提高性能。
2、可以创建集合类。
3、可以创建自己的泛型接口、泛型方法、泛型类、泛型事件和泛型委托。
4、可以对泛型类进行约束,以访问特定数据类型的方法。
5、关于泛型数据类型中使用的类型的信息,可在运行时通过反射获取。
例子:
using System; namespace ConsoleApp { class Program { class Test<T> { public T obj; public Test(T obj) { this.obj = obj; } } static void Main(string[] args) { int obj1 = 2; var test = new Test<int>(obj1); Console.WriteLine("int:" + test.obj); string obj2 = "hello world"; var test1 = new Test<string>(obj2); Console.WriteLine("String:" + test1.obj); Console.ReadKey(); } } }
输出结果是:
int:2
String:hello world
分析:
1、 Test是一个泛型类。T是要实例化的范型类型。如果T被实例化为int型,那么成员变量obj就是int型的,如果T被实例化为string型,那么obj就是string类型的。
2、 根据不同的类型,上面的程序显示出不同的值。
二、泛型的主约束和次约束是什么?
六种类型的约束:
T:结构 |
类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。有关更多信息,请参见使用可空类型(C# 编程指南)。 |
T:类 |
类型参数必须是引用类型,包括任何类、接口、委托或数组类型。 |
T:new() |
类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定。 |
T:<基类名> |
类型参数必须是指定的基类或派生自指定的基类。 |
T:<接口名称> |
类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。 |
T:U |
为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束。 |
例子:
1.接口约束。
例如,可以声明一个泛型类 MyGenericClass,这样,类型参数 T 就可以实现 IComparable<T> 接口:
public class MyGenericClass<T> where T:IComparable { }
2.基类约束。
指出某个类型必须将指定的类作为基类(或者就是该类本身),才能用作该泛型类型的类型参数。这样的约束一经使用,就必须出现在该类型参数的所有其他约束之前。
class MyClassy<T, U> where T : class where U : struct { }
3.构造函数约束。
以使用 new 运算符创建类型参数的实例;但类型参数为此必须受构造函数约束 new() 的约束。new() 约束可以让编译器知道:提供的任何类型参数都必须具有可访问的无参数(或默认)构造函数。new() 约束出现在 where 子句的最后。
public class MyGenericClass <T> where T: IComparable, new() { T item = new T(); }
4.对于多个类型参数,每个类型参数都使用一个 where 子句。
interface MyI { } class Dictionary<TKey,TVal> where TKey: IComparable, IEnumerable where TVal: MyI { public void Add(TKey key, TVal val) { } }
5.还可以将约束附加到泛型方法的类型参数。
public bool MyMethod<T>(T t) where T : IMyInterface { }
6. 裸类型约束
用作约束的泛型类型参数称为裸类型约束。当具有自己的类型参数的成员函数需要将该参数约束为包含类型的类型参数时,裸类型约束很有用。
class List<T> { void Add<U>(List<U> items) where U : T {} }
为什么要有约束呢?
当一个泛型参数没有任何约束时,它可以进行的操作和运算是非常有限的,因为不能对实参做任何类型上的保证,这时候就需要用到泛型的约束。泛型的主要约束和次要约束都是指泛型的实参必须满足一定的规范。C#编译器在编译的过程中可以根据约束来检查所有泛型类型的实参并确保其满足约束条件。
一个泛型参数可以至多拥有一个主要约束,主要约束可以是一个引用类型、class或者struct。如果指定一个引用类型,则实参必须是该类型或者该类型派生类型。class规定实参必须是一个引用类型。struct规定了参数必须是一个之类新。以下代码是泛型参数主要约束的示例。
using System; namespace Test { class GenericPrimaryConstraint { static void Main() { Console.Read(); } } //主要约束限定T继承自Exception类型 public class ClassT1<T> where T : Exception { private T myException; public ClassT1(T t) { myException = t; } public override string ToString() { //主要约束保证了myException拥有Source成员 return myException.Source; } } //主要约束限定T是引用类型 public class ClassT2<T> where T : class { private T myT; public void Clear() { //T是引用类型,可以置null myT = null; } } //主要约束限定T是值类型 public class ClassT3<T> where T : struct { private T myT; public override string ToString() { //T是值类型,不会发生NullReferenceException异常 return myT.ToString(); } } }
以上代码,泛型参数具备了主要约束后,就能够在类型中对其进行一定的操作,否则任何算法就只能基于一个System.Object类型的成员。
可以说,主要约束是实参类型的限定,而相对的次要约束,则是指实参实现的接口的限定。对于一个泛型类型,可以有0至无限的次要约束,次要约束规定了参数必须实现所有次要约束中规定的接口。次要约束的语法和主要约束基本一致,区别仅在于提供的不是一个引用类型而是一个或多个接口。
ps:同时拥有主要约束和次要约束的泛型参数,表示实参必须同时满足主要约束和次要约束。
三、什么是泛型集合?
字符串可以说是一个字符的集合,和字符串一样,数据对象也可以是集合的方式存在,所以泛型类对象也可以是集合的方式存在(泛型集合)
同传统的集合相比,泛型集合是一种强类型的集合,它解决了类型安全问题,同时避免了集合中每次的装箱与拆箱的操作,提升了性能。
泛型集合类型:
1. List,这是我们应用最多的泛型种类,它对应ArrayList集合。
2. Dictionary,这也是我们平时运用比较多的泛型种类,对应Hashtable集合。
3. Collection对应于CollectionBase
4. ReadOnlyCollection 对应于ReadOnlyCollectionBase,这是一个只读的集合。
5. Queue,Stack和SortedList,它们分别对应于与它们同名的非泛型类。
性能问题
下面以ArrayList与List<T>为例说明泛型集合的优点及非泛型集合的缺点。例如,有这么一段代码:
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace Web { public partial class b : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { ArrayList numbers = new ArrayList(); numbers.Add(1);//装箱 numbers.Add(2);//装箱 int number = (int)numbers[1];//拆箱 Label1.Text = number.ToString(); } } }
这段代码的背后会发生什么呢?首先,ArrayList中将所有元素都看成Object类型的,是引用类型。调用Add方法增加两个整数,在这个过程中,整数1,2被CLR装箱(boxing)成object类型的,而后二个元素时又被拆箱(unboxing),装箱与拆箱大体上会发生以下过程
1. 在托管堆中非配一个新的object
2. 基于栈(stack-based)的数据必须移动到刚非配的内存区中
3. 当拆箱时,位于堆中的数据又得移动到栈中
4. 堆中无用的数据进行垃圾回收
当涉及大量装箱与拆箱操作时,必然会影响应用程序的性能。而是用泛型的集合类时就会减少装箱与拆箱的工作,当存在大量数据时,自然可以提高很多性能。,比如用
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace Web { public partial class b : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { List<int> numbers = new List<int>();//找不同 numbers.Add(1); numbers.Add(2); int number = numbers[1];//找不同 Label1.Text = number.ToString(); } } }
类型安全问题
对于ArrayList,下面的代码编译时时不会报错的。
ArrayList numbers = new ArrayList(); numbers.Add(22); numbers.Add(35.5); numbers.Add(true); foreach (object item in numbers) { Console.WriteLine((int)item); }
因为可以将int类型,float等数值类型装箱成object,因此即使ArrayList增加的数据类型不一致。编译器也不会提示错误,但是运行时,会报错。
但是如果是用泛型类型比如List<T>,那么在编译时就会进行类型检查。防止运行时错误。
ps:此文章是本人参考网上内容加上自己的理解整合而成,如无意中侵犯了您的权益,请与本人联系。