什么是泛型?
泛型(Generics)是C#的一种特性,它允许你在编写代码时,不指定具体的类型,而是使用类型参数作为占位符。这样一来,你的代码就可以对多种类型进行复用,增加了灵活性,同时还能保持类型安全。
简单来说,泛型就是一种“模板”,你可以用它来创建可重用、类型安全的代码。
为什么需要泛型?
在没有泛型之前,如果你想创建一个可以存储任意类型对象的集合,比如 ArrayList,你可能会这样做:
ArrayList list = new ArrayList();
list.Add(1);
list.Add("Hello");
list.Add(DateTime.Now);
这样虽然灵活,但有两个主要问题:
-
类型安全问题:从集合中取出元素时,需要进行类型转换,如果转换错误,会导致运行时异常。
-
性能问题:对于值类型(如int),在添加到集合时,需要进行装箱(Boxing);取出时,需要拆箱(Unboxing),这会影响性能。
泛型的优势
- 类型安全:在编译时就能检查类型错误,避免运行时异常。
- 提高性能:避免了装箱和拆箱操作。
- 代码复用:可以编写更通用的代码,对多种类型适用。
使用泛型的示例
C#提供了很多泛型集合类型,比如List、Dictionary<TKey, TValue>等。
示例:使用List
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);foreach (int number in numbers)
{Console.WriteLine(number);
}
在这里,List表示一个存储整数的列表。由于指定了类型参数为int,所以列表中只能添加整数,编译器会帮你检查类型。
如何自定义泛型
接下来,我们看看如何自定义泛型类和泛型方法。
1. 自定义泛型类
示例:创建一个通用的存储盒子
public class Box<T>
{private T item;public void Put(T item){this.item = item;Console.WriteLine($"已将 {item} 放入盒子。");}public T Get(){Console.WriteLine($"从盒子中取出了 {item}。");return item;}
}
解释:
- Box:T是类型参数,表示盒子可以存储任意类型的物品。
- Put方法:用于将物品放入盒子。
- Get方法:用于从盒子中取出物品。
使用泛型类:
// 创建一个存储整数的盒子
Box<int> intBox = new Box<int>();
intBox.Put(123);
int number = intBox.Get();
Console.WriteLine($"取出的数字是:{number}");// 创建一个存储字符串的盒子
Box<string> strBox = new Box<string>();
strBox.Put("Hello, 泛型!");
string message = strBox.Get();
Console.WriteLine($"取出的消息是:{message}");
输出:
已将 123 放入盒子。
从盒子中取出了 123。
取出的数字是:123
已将 Hello, 泛型! 放入盒子。
从盒子中取出了 Hello, 泛型!。
取出的消息是:Hello, 泛型!
2. 自定义泛型方法
有时候,你不需要整个类都是泛型的,只需要某个方法是泛型。
示例:创建一个交换两个变量值的泛型方法
public class Utils
{public static void Swap<T>(ref T a, ref T b){T temp = a;a = b;b = temp;Console.WriteLine($"交换后,a = {a},b = {b}");}
}
解释:
- Swap:定义了泛型方法,其中T是类型参数。
- ref T a:按引用传递参数,使交换在方法外也有效。
使用泛型方法:
int x = 10;
int y = 20;
Console.WriteLine($"交换前,x = {x},y = {y}");
Utils.Swap<int>(ref x, ref y);string s1 = "你好";
string s2 = "世界";
Console.WriteLine($"交换前,s1 = {s1},s2 = {s2}");
Utils.Swap<string>(ref s1, ref s2);
输出:
交换前,x = 10,y = 20
交换后,a = 20,b = 10
交换前,s1 = 你好,s2 = 世界
交换后,a = 世界,b = 你好
3. 泛型约束
有时候,我们希望对类型参数进行限制,确保传入的类型具备某些特性,可以使用泛型约束。
示例:限制类型参数必须实现某个接口
public interface IAnimal
{void Speak();
}public class Dog : IAnimal
{public void Speak(){Console.WriteLine("汪汪!");}
}public class Cat : IAnimal
{public void Speak(){Console.WriteLine("喵喵!");}
}public class AnimalShelter<T> where T : IAnimal
{private List<T> animals = new List<T>();public void AddAnimal(T animal){animals.Add(animal);Console.WriteLine("添加了一只动物。");}public void LetAnimalsSpeak(){foreach (var animal in animals){animal.Speak();}}
}
解释:
- where T : IAnimal:泛型约束,表示T必须是IAnimal接口的实现类。
- AnimalShelter:动物收容所,可以收容任何实现了IAnimal的动物。
使用泛型约束的类:
AnimalShelter<IAnimal> shelter = new AnimalShelter<IAnimal>();
shelter.AddAnimal(new Dog());
shelter.AddAnimal(new Cat());
shelter.LetAnimalsSpeak();
输出:
添加了一只动物。
添加了一只动物。
汪汪!
喵喵!
常用的泛型约束:
- where T : struct:T必须是值类型。
- where T : class:T必须是引用类型。
- where T : new():T必须有一个无参数的公共构造函数。
- where T : 基类名:T必须继承指定的基类。
- where T : 接口名:T必须实现指定的接口。
- where T : U:T必须是类型U或U的子类。
总结
- 泛型让你能够编写可复用、类型安全的代码。
- 自定义泛型类:在类名后使用,在类内部可以使用类型参数T来表示任意类型。
- 自定义泛型方法:在方法名前使用,方法内部可以使用类型参数T。
- 泛型约束:使用where关键字对类型参数进行限制,保证类型安全和功能的正确性。
动手实践
-
题目1:使用泛型类
任务: 创建一个自定义的泛型类
Storage<T>
,该类用于存储和管理一个对象。实现以下方法:StoreItem(T item)
: 存储一个项目。RetrieveItem()
: 返回存储的项目。
要求:
- 创建
Storage<int>
类型的对象,并存储整数42
。 - 创建
Storage<string>
类型的对象,并存储字符串"Hello, Generics"
。 - 打印存储的项目内容。
预期输出:
Stored integer: 42 Stored string: Hello, Generics
题目2:使用泛型方法
任务: 创建一个类
ArrayUtils
,并在其中实现一个泛型方法FindMax<T>(T[] array)
,该方法用于返回数组中最大的元素。要求:
- 使用
FindMax<int>
方法查找整数数组[2, 5, 1, 9, 6]
中的最大值。 - 使用
FindMax<double>
方法查找浮点数数组[1.5, 8.4, 3.2]
中的最大值。 - 打印出每个数组的最大值。
预期输出:
Max in integer array: 9 Max in double array: 8.4
题目3:泛型约束的应用
任务: 创建一个接口
IFlyable
,其中包含一个方法Fly()
。然后,创建一个泛型类FlyingZoo<T>
,该类可以存储实现IFlyable
接口的对象,并提供方法让这些对象执行飞行动作。要求:
- 创建类
Bird
和Airplane
,并实现接口IFlyable
。 - 在
FlyingZoo
中,实现方法Add(T item)
和LetThemFly()
。 - 将3个
Bird
对象和1个Airplane
对象添加到FlyingZoo
中,并执行LetThemFly()
方法。
预期输出:
Bird is flying! Bird is flying! Bird is flying! Airplane is flying in the sky!