C#基础:泛型的理解和使用

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#基础:泛型的理解和使用相关的知识,希望对你有一定的参考价值。

  日常生活中的事物都是有类型的,比如我们说“一个女人”,那么“女”就是这个人的类型。我们可以说“女人都是水做的”,那么听者都知道这是在说“女”这种类型的人。再比如你去肉店买肉,你可以对老板说“我要十斤猪肉”,那么老板一定知道你是在要“猪”这种类型的肉。

  日常生活中的这些语言都是带有类型的,但是在日常生活中还有一些语言是不带类型的。比如我们经常说“人是贪婪的”,这里的人就没有类型之分,听者都知道是指所有的人;我们也可以在肉店里指着猪肉说“给我来十斤肉”,肉店老板同样知道你要的是猪肉。

  程序语言必须能够对现实中的数据进行表述,对于C#语言来讲可以使用数据类型对数据进行精确的描述。事实上这种程序语言被称作强类型语言,在这样的语言当中出现的数据都必须带有数据,这样的语言还有很多,比如C++、Java、Python等。与强类型语言对应的得是弱类型语言,比如VB、javascript等,他们没有数据类型概念。从肉店买肉这个例子我们可以看出这两种类型的各自的优缺点。

  强类型语言显然可以精确的表达逻辑但表达过于罗嗦,无论是肉店老板还是旁边的人听到“我要十斤猪肉”这句话都可以精确的知道你的意思。弱类型语言的特点就是表达简洁但逻辑容易发生混乱,比如你还可以指着猪肉说“来十斤”,很显然你的话只有肉店老板先看懂你的手势才能懂,容易引起逻辑的混乱。

  计算机程序是推理性语言,中间某一行逻辑出错都会导致最终的结果出现错误,所以从这个角度出发,显然在买猪肉这个问题上强类型语言获胜。我们再来看关于人的那个表述,对于“人是贪婪的”这句话,是在描述一种通用性的规律。

  对于这个问题用传统的强类型语言来描述就是“女人是贪婪的,男人是贪婪的”,这样说显然非常啰嗦,这也是强类型语言都存在一个缺陷。比如在程序中经常会用到某些通用的算法,用强类型语言编写这些通用的算法会和上面出现一样的情况,需要每种数据类型都提供一个相同的算法。泛型技术就是用可以用来解决此类问题。

  重点:

  Ø 理解泛型的概念

  Ø 泛型的定义及其应用

  Ø 泛型类

  预习功课:

  Ø 泛型的概念

  Ø 如何定义泛型及其应用

  Ø 如何使用泛型类

  9.1 为什么使用泛型

  假如让你用C#编写一个求两个数和的方法,你会怎么做?若求的两个数是整数,可以定义如下方法:

  C#

  int Add(int a,int b)

  { return a+b; }

  若求的是两个double型的数的和,可以定义如下方法:

  C#

  static double Add(double a,double b)

  { return a+b; }

  若是字符串型的数值进行相加,那么你就可以定义如下方法:

  C#

  static double Add(string a,string b)

  {

  return double.Parse(a)+double.Parse(b);

  }

  假如有一天程序需要升级,你需要其他数据类型求和的算法,日不char、long、decimal等,那你怎么办?继续重载吗?还是想一个更好更通用的方法?我们可能会想到使用object类,于是你写了下面这个通用的算法:

  C#

  staticobject Add(object a,object b)

  {

  //decimal为最大的数值类型,所以使用它

  return decimal.Parse(a)+decimal.Parse(b);

  }

  static voidMain(string[]args)

  {

  decimal r1=(decimal)Add(3,3);

  decimal r2=(decimal)Add(3.3,3.3);

  decimal r3=(decimal)Add("3.3","3.3");

  Console.WriteLine("{0},{1},{2}",r1,r2,r3) ;

  }

  staticobject Add(object a,object b)

  {

  returnConvert.ToDecimal(a)+Convert.ToDecimal(b);

  }

  运行结果:

  6,6.6,6.6

  这里用到的技术就是装箱和拆箱,Add方法首先将所有数据类型的数据进行装箱,这样就统一了它们的类型,然后再进行类型转换和计算,计算结果再拆箱就是要求的结果。实际上就是“泛型”思想的一个应用,这里用一个通用方法解决了几乎任何数值类型两个数的求和操作。所以可以说,对于这个求和算法来讲是通用的、泛型的(不需要特定数据类型)。

  但是我们从上面的代码页可以看到问题,就是它执行了频繁的装箱和拆箱操作,我们知道这些操作是非常损耗性能的。另外,装箱和拆箱的代码也显得比较“难看”

  因为每次都要进行强类型转换,有没有更好的方式让我们编写这种通用算法呢?于是,C#从2.0版本开始引入了泛型技术,泛型能够给我们带来的两个明显好处是—代码清晰和减少了装箱、拆箱。

  9.2 C#泛型简介

  利用泛型解决交换两数的泛型方法的例子:

  C#

  using System;

  class Program

  {

  static void Main(string[]args)

  {

  int i=1,j=2;

  Console.WriteLine("交换前:{0},{1}",i,j);

  Swap(ref I,ref j); //交换两个数

  Console.WriteLine("交换后:{0}",i,j);

  }

  //交换两个数的泛型算法

  static void Swap(ref T a,ref T b)

  {

  T temp=a ;

  a=b ;

  b=temp ;

  }

  }

  运行结果:

  交换前:1,2

  交换后:2,1

  这个交换算法不仅支持任何数字类型,它还支持你在程序中能用到得任何类型。注意,泛型不属于任何命名空间,准确的讲,泛型是一种编译技术。在书写算法的时候,泛型技术允许我们使用一种类型占位符(或称之为类型参数,这里使用的占位符是“T”)作为类型的标识符,而不需要指定特定类型。

  当我们在调用这个算法的时候,编译器使用指定的类型代替类型占位符建立一个针对这种类型的算法。这就是泛型技术,它允许你编写算法的时候不指定具体类型,但调用的时候一定要指定具体类型,编写算法的时候使用“<>”来指定类型占位符,调用的时候一般也使用“<>”来指定具体的数据类型。

  上面这个例子中的Swap,指定了这个泛型方法的占位符是“T”,指定后我们就可以认为有了这么一个数据类型,该类型就是T类型,然后这个T类型既可以作为参数的数据类型又可以作为方法的返回值类型,还可以在方法内部作为局部变量的数据类型。当我们通过Swap(ref i,ref j)来调用这个泛型方法时,在编译时Swap方法中所有出现“T”的地方都会被“int”类型所代替,也就相当于我们建立了

  int型的交换方法,如:

  C#

  static void Swap(ref int a,ref int b)

  {

  int temp=a;

  a=b;

  b=temp;

  }

  l 代码重用

  泛型最突出优点就是可以代码重用。从上面举的交换算法的例子你也可以看出节省了多少代码。对于一个程序员来讲,写的好的算法是很重要的财富,例如我们一直在使用各种类库,这些类库实际上就是一些优秀的程序员封装的,我们直接调用就是一个代码重用的过程。

  l 类型安全

  类型安全的含义是类型之间的操作必须是兼容的,反之就是类型不安全。类型不安全的代码会在运行时出现异常,比如两个数相加的算法,Convert.ToDecimal(a),a是object类型,a可以是数值“3.3”,a也可以是普通字符串“hello”,如果a是后者那么执行类型转换时必定会出异常,所以说使用Convert.ToDecimal(a)是类型不安全的做法,同样那个求和的方法也是类型不安全的方法。泛型本质上还是强类型的,如果你使用一个不兼容的类型来调用泛型算法,编译器是会报错的,所以说泛型是类型安全的。

  l 性能更佳

  相比装箱和拆箱,泛型效率更高一些。因装箱时系统需要分配内存,而拆箱时需要类型转换,这两个操作都是极其耗费性能的。特别是在执行一些大数据量的算法时(比如排序、搜索等)装箱和拆箱性能损耗尤其严重,因此,在C#中提倡使用泛型。

  9.3 泛型定义及其应用

  使用泛型可以定义泛型方法、泛型类、泛型接口等。在这些泛型结构的定义中,泛型类型参数(或叫占位符)是必须指定的,类型参数所包含的类型就是我们定义的泛型类型,我们可以一次性定义多个泛型类型,如泛型方法Swap三个泛型类型。类型参数一般放在所定义的类、方法、接口等标识符后面,并且包含在“<>”里面。

  泛型类型名称的写法也有一定的规则:

  l 泛型类型名称必须是由字母、数字、下划线组成,并且必须以字符或下划线开头。比如_T、T、TC都是有效的泛型类型名称。

  l 务必使用有意义泛型类型名称,除非单个字母名称完全可以让人了解它表示的含义,如T.

  l 当类型参数里只有单个泛型类型时,考虑使用T作为泛型类型名,如class Note。

  l 提倡作为泛型类型名的前缀,如Tkey,TValue。

  前面举例子的时候,一般使用了泛型类型T,但从本质上讲我们可以使用满足上面要求的任何单词。实际上,泛型类型名和类名或接口名的定义规则基本一样。

  9.4 泛型结构体

  结构是值类型,通常可以定义结构类型来表示一些简单的对象。比如,我们前面接触的系统结构体Point、DateTime等。但是,这些结构体通常都存储一种类型的数据,我们可以定义一个泛型结构体,它将可以保存任何数据,定义规则:

  C#

  struct 结构名 <泛型类型列表>

  {

  结构体;

  }

  要注意泛型类型标识符的定义只能放在结构名的后面,下面我们定义了一个

  Point类型的泛型结构体,此时该结构体的X、Y可以保存任何数值类型的坐标数据。代码如下:

  C#

  classProgram

  {

  //定义泛型结构体和泛型类型T

  struct Point

  {

  public T X;

  public T Y;

  }

  //测试泛型结构体

  static voidMain(string[]args)

  {

  //给T类型指定数据类型为int型

  Point a =newPoint();

  X=1;

  a. Y=2;

  Console.WriteLine("{0},{1}",a.X,a.Y);

  }

  }

  运行结果:

  1,2

  9.5 泛型类

  泛型类封装不属于特定具体数据类型的数据或操作。泛型类最常见的就是泛型集合类,如链表、哈希表、堆栈、队列、树等。对于集合的操作,如从集合中添加、移除、排序等操作大体上都以相同方式进行的,与所存储数据类型无关,即可使用泛型技术。

  在泛型类中使用的数据类型,可以是泛型类型也可以是普通的。一般规则是,类中使用的泛型类型越多,代码就会变得越灵活,重用性就越好。但是要注意,类中如果有太多的泛型类型也会使其他开发人员难以阅读或理解该类。要定义类的泛型类型也是在类名后面通过"<>"定义,类的其他元素除了方法外都不能定义自己的泛型类型,但可以使用该类定义的泛型类型。泛型类定义规则如下:

  C#

  class 类名<泛型类型列表>

  {

  //类体

  }

  //示例代码:

  usingSystem;

  classProgram

  {

  //定义泛型类和泛型类型T

  private class Node

  {

  private T data;

  public Node(T t)

  {

  data=t;

  }

  public T Data

  {

  get{return data;}

  set{data=value;}

  }

  }

  static void Main()

  {

  Nodenode=newNode(10000);

  Console.WriteLine("数据:{0}",node.Data);

  Nodesnode=newNode("壹万");

  Console.WriteLine("数据:{0}",snode.Data);

  }

  }

  运行结果:

  数据:10000

  数据:壹万

  如前所述,类中的成员有很多,如字段、属性、方法、事件、索引器等,其中除了方法之外,其他的类成员都不能自定义的泛型类型,只能使用定义类的时候定义的泛型类型或系统数据类型:

  C#

  classStudent

  {

  private T name; //姓名

  private U[]score; //各个科目的分数数组

  private int ucode; //编号使用系统数据类型

  public U this[int n] //返回一个分数

  {

  get{return score[n];

  }

  }

  类中的方法可以是泛型的,泛型方法的定义规则如下:

  访问修饰符 返回类型 方法名<泛型类型列表>(方法参数列表)

  如:

  C#

  public voidShow(T a){}

  此泛型方法的使用时要给T指定一个实际的数据类型,如:

  C#

  Show("hello");

  其中方法的泛型类型列表中定义的泛型类型可以出现在方法的任何位置,包括返回值、参数、方法内,当然也可以不出现,比如下面这些都是合法的:

  C#

  public TGet(T a) {return default(T) ;}

  public intGet

  public TGet(int a) {return default(T);}

  这上面用了default关键字,这个关键字可以取当前类型的默认初始值,这个关键字对于引用类型会返回null,对于数值类型会返回零。

  另外,类中也可以出现泛型的重载方法,如:

  C#

  voidDoWork(){}

  voidDoWork(){}

  voidDoWork(){}

  由于方法是在类中,所以泛型方法中的数据类型又三种情况,一种是类的泛型类型,一种是泛型方法自身的泛型类型,另外还可以是系统数据类型。泛型方法和非泛型方法或属性、索引器可以互相调用。如:

  C#

  classStudent

  {

  private U id;

  private string name;

  public void ShowHello()

  {

  this.Show("hello"); //调用泛型方法

  }

  public void ShowId()

  {

  this.Show(id);

  }

  private void Show(S msg)

  {

  Console.WriteLine(msg);

  }

  }

  类的泛型类型只能用于本类,方法的泛型类型只能用于本方法。不管谁定义的泛型,一旦定义了泛型类型,你可以就当泛型类型是一个真实的类型来用了。

  9.6 典型的泛型类

  .Net框架类库中,System.Collections.Generic和System.Collections.ObjectModel命名空间中,分别定义了大量的泛型类和泛型接口,这些泛型类多为集合类,因为泛型最大的应用正体现于再集合中对于不同类型对象的管理。

  下表列出了,.Net框架中常用的泛型类和泛型接口:

  泛型类说明

  List对应于ArrayList集合类,可以动态调整集合容量,通过索引方式访问对象,支持排序、搜索和其他常见操作。

  SortedList对应于SortedList集合类,表示Key/Value对集合,类似于SortedDictionary集合类,而SortedList在内存上更有优势。

  Queue对应于Queue集合类,是一种先进先出的集合类,常应用于顺序存储处理。

  Stack对应于Stack集合类,是一种后进先出的集合类。

  Collection对应于CollectionBase集合类,是用于自定义泛型集合的基类,提供了受保护的方法来实现定制泛型集合的行为Collection的实例是可修改的。

  Dictionary对应于Hashtable集合类,表示Key/Value对的集合类,Key必须是唯一的,其元素类型既不是Key的类型,也不是Value的类型,而是KeyValuePair类型。

以上是关于C#基础:泛型的理解和使用的主要内容,如果未能解决你的问题,请参考以下文章

c#基础泛型

C#泛型基础知识点总结

C#泛型

C#泛型实例详解

C# 泛型的使用

C#基础篇——委托