【c#学习笔记十四】泛型的理解和使用

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

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

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

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

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

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

一、为什么要有泛型?

我们在写一些方法时可能会方法名相同,参数类型不同的方法,这种叫做重载。如果只是因为参数类型不同里面做的业务逻辑都是相同的,那可能就是复制粘贴方法,改变参数类型,例如一些排序算法,int、float、double等类型的排序,参数数组存的数据类型不一样,还有像根据索引找到List集合中的对象。可能这个对象是Person、Dog等对象,这样方法改变的只是参数类型,那就是能不能写一个方法,传递不同的参数类型呢?于是乎有了泛型。

二、什么是泛型?

泛型通过参数化类型来实现在同一份代码上操作多种数据类型。例如使用泛型的类型参数T,定义一个类Stack<T>,可以用Stack<int>、Stack<string>或Stack<Person>实例化它,从而使类Stack可以处理int、string、Person类型数据。这样可以避免运行时类型转换或封箱操作的代价和风险,类似C++的模板。泛型提醒的是将具体的东西模糊化,这与后面的反射正好相反。

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

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

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

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

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

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

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

三、泛型的优点

1、代码重用

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

2、类型安全

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

3、性能更佳

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

四、泛型demo

1.泛型类

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

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Generic
{
public class Stack<T>
{
private T[] s;

int pos;

public Stack(int size)
{
s = new T[size];
pos = 0;
}

public void Push(T val)
{
s[pos] = val;
pos++;
}

public T Pop()
{
pos–;
return s[pos];
}

public void display()
{
Console.WriteLine(“Stack Push:”);
foreach (T i in s)
{
Console.WriteLine(i);
}
}
}
}

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Generic
{
class Program
{
static void Main(string[] args)
{
Stack<int> s1 = new Stack<int>(2);
s1.Push(1);
s1.Push(2);
s1.display();

Console.WriteLine(“Stack Pop:”);
Console.WriteLine(s1.Pop());
Console.WriteLine(s1.Pop());

Stack<string> s2 = new Stack<string>(2);
s2.Push(@”One”);
s2.Push(@”Two”);
s2.display();

Console.WriteLine(“Stack Pop:”);
Console.WriteLine(s2.Pop());
Console.WriteLine(s2.Pop());

Console.ReadLine();

}
}
}

上面定义了一个泛型类,主要是维护一个栈,栈里存放T类型的数据,在demo中可以定义int、string类型的栈,这样就很方便,使用一套代码可以维护多种数据类型。如果没有这个可能还要维护double、float等代码。

2.泛型方法

上面是泛型类,主要是在类层面进行参数化,我们还可以在更小的层面,在函数上进行泛型化。

泛型方法的定义规则如下:

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

我们可以在上面Mina类中定义一个静态的泛型方法,用来获取找数值在数组中的位置。

public static int Find<T>(T[] valus, T val)
{
for (int i = 0; i < valus.Length; i++)
{
if (valus[i].Equals(val))
{
return i;
}
}
return -1;
}

我们可以用上面的方法来查找int数组、float数组

int val = 4;
int pos = Find<int>(new int[] {1,2,3,4,5 },val);
Console.WriteLine(string.Format(“int Pos:{0}”,pos));

float val1 = 4;
pos = Find<float>(new float[] { 1, 2, 3, 4, 5 }, val1);
Console.WriteLine(string.Format(“float Pos:{0}”, pos));
Console.ReadLine();

下面是两个demo的输出

五、约束

约束是指对泛型类型参数施加限制,用于限制可以传递到该类型参数的类型种类。如果使用某个约束不允许的类型来实例化,则会产生编译时错误。约束使用where关键字指定。

约束有4种类型:

1.基类约束

指定编译器泛型类型参数必须派生自特定基类

修饰符 class 类名<类型参数列表> where 类型参数:基类名

{ 类体}

2.接口约束

指定编译器泛型类型参数必须派生自特定接口

修饰符 class 类名<类型参数列表> where 类型参数:接口名

{ 类体}

3.默认构造函数约束

指示编译器泛型类型参数公开了默认的公共构造函数(不带任何参数的公共构造函数)

修饰符 class 类名<类型参数列表> where 类型参数:new ()

{ 类体}

4.引用/值类型约束

指示编译器泛型类型参数必须是引用类型或值类型

修饰符 class 类名<类型参数列表> where 类型参数:struct(或class)

{ 类体}

可以对同一类型参数使用多个约束,并且约束自身可以也可以是泛型类型,多个约束之间用逗号隔开。

六、泛型委托

泛型委托主要是想讲一下Action<T>和Func<TResult>两个委托,因为这两个在Linq中是经常见到的。

Action<T>只能委托必须是无返回值的方法

Fun<TResult>只是委托必须有返回值的方法

不管是不是泛型委托,只要是委托委托那能用Lamdba表达式,因为不管Lamdba表达式还是匿名函数其实都是将函数变量化。

下面简单的来做的demo说下两个的用法,这个会了基本linq会了一半了。

Action<string> action = s => {
Console.WriteLine(s);
};
action(“cuiyanwei”);

Func<int, int, int> func = (int a, int b)=>{
return a + b;
};
int result=func(1, 2);
Console.WriteLine(“sum:{0}”,result);

Console.ReadLine();

上面其实都是将函数做为变量,这也是委托的思想。action是实例化了一个只有一个字符串参数没有返回值得函数变量。func是实例化了一个有两个int类型的参数返回值为int的函数变量。下面来看下输出结果:

发表评论

电子邮件地址不会被公开。 必填项已用*标注