本讲用许多代码示例介绍了 C# 语言当中的泛型,主要包括泛型类、接口、结构、委托和方法。
1. 为什么需要泛型?
在之前的教程中,我们使用的自定义类都是具有具体的类型。如果对类型进行抽象,该类型就是泛型,即一种广泛的类型。例如,我们有一个水果篮,水果篮中可以放不同种类的水果,例如苹果、香蕉、菠萝等等。这个水果篮可以放置的水果就是一种泛型,水果可以代指很多不同种类的水果。
为什么需要泛型?泛型的好处之一就是能够提高代码的复用性。例如,我们有一种栈数据结构,栈当中的类型可以是 int
、float
等等,但很多操作都是相同的:入栈、出栈、获取栈大小,所不同的是数据类型的不同。利用泛型,我们只需要实现一套代码,而不需要针对不同的数据类型分别实现各自的一套栈代码。
在 C# 中,泛型又称为参数化类型。泛型可以作用在类、函数、结构、委托和接口,由此衍生出泛型类、泛型函数、泛型结构、泛型委托和泛型接口的概念。
2. 泛型类的定义
2.1 泛型类的定义
2.2 使用泛型类创建变量和实例
在使用泛型类创建变量时,可以使用关键字 var
,让编译器根据 =
右边的类型自动推断变量的类型。
csharp
using System;
class SomeClass<T1, T2>
{
public T1 Property1 { get; set; }
public T2 Property2 { get; set; }
// Constructor
public SomeClass(T1 property1, T2 property2)
{
Property1 = property1;
Property2 = property2;
}
}
class Program
{
static void Main(string[] args)
{
SomeClass<int, float> sc = new SomeClass<int, float>(10, 3.14f);
var sc2 = new SomeClass<string, bool>("Hello", true); // 让编译器推断类型
Console.WriteLine(sc.Property1);
Console.WriteLine(sc.Property2);
Console.WriteLine(sc2.Property1);
Console.WriteLine(sc2.Property2);
}
}
输出:
arduino
10
3.14
Hello
true
需要注意的是,为具体类分配的内存是存储在堆上的。
3. 使用泛型类实现一个简单的栈
csharp
using System;
namespace GenericStackExample
{
// 定义一个泛型栈类
public class Stack<T>
{
// 使用数组来存储栈中的元素
private T[] _items;
// 栈顶元素的索引(初始化为-1表示栈为空)
private int _top;
// 栈的容量
private int _capacity;
// 构造函数,初始化栈的容量
public Stack(int capacity = 10)
{
_capacity = capacity;
_items = new T[capacity];
_top = -1;
}
// 检查栈是否为空
public bool IsEmpty()
{
return _top == -1;
}
// 检查栈是否已满
public bool IsFull()
{
return _top == _capacity - 1;
}
// 入栈操作
public void Push(T item)
{
if (IsFull())
{
throw new InvalidOperationException("Stack is full.");
}
_items[++_top] = item;
}
// 出栈操作
public T Pop()
{
if (IsEmpty())
{
throw new InvalidOperationException("Stack is empty.");
}
return _items[_top--];
}
// 查看栈顶元素(不移除)
public T Peek()
{
if (IsEmpty())
{
throw new InvalidOperationException("Stack is empty.");
}
return _items[_top];
}
// 获取栈的大小(元素数量)
public int Size()
{
return _top + 1;
}
}
class Program
{
static void Main(string[] args)
{
Stack<int> intStack = new Stack<int>(5); // 创建一个整型栈
// 入栈操作
intStack.Push(1);
intStack.Push(2);
intStack.Push(3);
// 访问栈顶元素
Console.WriteLine($"栈顶元素是: {intStack.Peek()}");
// 出栈操作
Console.WriteLine($"出栈元素: {intStack.Pop()}");
// 获取栈的大小
Console.WriteLine($"栈的大小是: {intStack.Size()}");
// 尝试在空栈上执行出栈操作以演示异常
try
{
intStack.Pop();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
3.1 类型参数的约束
现在我们能够设计出泛型类,但是泛型类型它本身提供什么方法,我们是不知道的。例如下面的泛型类:
csharp
class Simple<T>
{
static public bool LessThan(T i1, T i2)
{
return i1 < i2;
}
}
...
但不是所有类型 T 都实现了小于运算符,所以会报出错误: 为此,我们需要告诉编译器关于泛型类型的额外信息,让其知道参数可以接受哪些类型。这些额外的信息叫做约束(constrain)。没有任何约束的类型参数称为未绑定的类型参数(unbounded type parameter)。
3.2 Where 子句
约束使用 where 子句列出:
3.3 约束类型和次序
共有 5 种类型的约束,如下表所示:
约束类型 | 描述 |
---|---|
某个具体的类名 | 只有这个类型的类或从它继承的类才能用作类型参数 |
class |
任何引用类型,包括类、数组、委托和接口都可以用作类型参数 |
struct |
任何值类型都可以用作类型参数 |
接口名 | 只有这个接口或实现这个接口的类型才能用作类型参数 |
new() |
任何带有无参公共构造函数的类型都可以用作类型参数。这叫做构造函数约束 |
对不同类型的 where 约束可以以任何顺序列出。但是,where 子句内的约束必须遵循一定的顺序:
- 最多只能具有一个主约束,有的话必须放到第一位;
- 可以有任意多的接口名约束;
- 如果存在构造函数约束,则必须放到最后。 下面是一些例子:
csharp
class SortedList<S>
where S: IComparable<S>
{ }
class LinkedList<M, N>
where M: IComparable<M>
where N: ICloneable
{ }
class MyDictionary<KeyType, ValueType>
where KeyType: IComparable<KeyType>,
new()
{ }
4. 泛型方法
泛型方法的定义如下:
泛型方法的使用:
csharp
void DoStuff<T1, T2> (T1 t1, T2 t2)
{
T1 someVar = t1;
T2 otherVar = t2;
}
DoStuff<int, string>(10, "Hello");
DoStuff<int, long>(iVal, lVal);
当参数类型列表的类型和方法列表中的类型相同时,在使用泛型方法时可以省略对参数类型的指定,例如:
csharp
public void MyMethod<T> (T val) {}
int myInt = 5;
MyMethod(myInt);
泛型方法的使用示例:
csharp
class Simple
{
static public void ReverseAndPrint<T>(T[] arr)
{
Array.Reverse(arr);
foreach (var item in arr)
{
Console.Write("{0}, ", item.ToString());
Console.WriteLine();
}
}
}
class Program
{
static void Main()
{
// 创建整数、字符串、浮点型数组
int[] intArray = { 1, 2, 3, 4, 5 };
string[] stringArray = { "hello", "world" };
float[] floatArray = { 1.1f, 2.2f, 3.3f };
// 调用泛型方法,反转并打印数组
Simple.ReverseAndPrint(intArray);
Simple.ReverseAndPrint<int>(intArray);
Simple.ReverseAndPrint(stringArray);
Simple.ReverseAndPrint<string>(stringArray);
Simple.ReverseAndPrint(floatArray);
Simple.ReverseAndPrint<float>(floatArray);
}
}
输出结果:
5. 泛型结构
泛型结构和泛型类的定义类似,直接给出例子。
csharp
struct PieceOfData<T>
{
public T _data { get; set; }
public PieceOfData(T data)
{
_data = data;
}
}
class Program
{
static void Main()
{
PieceOfData<int> piece1 = new PieceOfData<int>(10);
PieceOfData<string> piece2 = new PieceOfData<string>("Hello");
Console.WriteLine(piece1._data);
Console.WriteLine(piece2._data);
}
}
6. 泛型委托
委托类型当中可以使用泛型的地方:
- 返回类型
- 类型参数
- where子句
泛型委托的例子:
csharp
delegate void MyDelegate<T>(T value);
class Simple
{
static public void PrintString(string s)
{
Console.WriteLine(s);
}
static public void PrintUpperString(string s)
{
Console.WriteLine(s.ToUpper());
}
}
class Prgram
{
static void Main()
{
MyDelegate<string> d1 = new MyDelegate<string>(Simple.PrintString);
d1 += Simple.PrintUpperString;
d1("Hello");
}
}
输出:
bash
hello
HELLO
7. 泛型接口
csharp
interface IMyIfc<T>
{
T ReturnIt(T invalue);
}
class Simple: IMyIfc<int>, IMyIfc<string>
{
// 因为 Simple 类实现了两个接口,所以它必须实现两个接口的相同方法。
public int ReturnIt(int invalue)
{
return invalue;
}
public string ReturnIt(string invalue)
{
return invalue;
}
}
class Program
{
static void Main()
{
IMyIfc<int> intIfc = new Simple();
IMyIfc<string> stringIfc = new Simple();
Console.WriteLine(intIfc.ReturnIt(10));
Console.WriteLine(stringIfc.ReturnIt("Hello"));
}
}
本章小结:主要通过例子讲解了 C# 语言当中泛型{类、接口、结构、委托、方法}的用法。
各位道友,码字不易,记得一键三连啊。