在C#中,泛型是一个非常核心的特性。它广泛应用与集合、委托、接口、方法、类以及很多.NET基础库中。比如我们经常看到这些写法:
List<T>、Dictionary<TKey,TValue>、Func<T,TResult>、Nullable<T>。这些都和泛型有关。
泛型的出现,大大提高了代码的复用性,类型安全性,可维护性,性能表现。所以,泛型不仅仅是语法层面的知识点,更是一种重要的编程思想。
一、什么是泛型
泛型可以简单理解为:把类型当作参数传递。平时我们调用方法时,传的时数据参数,比如
cs
Add(1,2);
而泛型做的事情是,除了传数据,还可以传类型参数。例如
cs
List<int>
List<string>
这里的int和string就是传给泛型的类型参数。也就是说:
普通参数决定传什么值,泛型参数决定用什么类型。
1.1一个直观理解
你可以把泛型想象成类型占位符。例如
cs
public class Box<T>
{
public T Value { get; set; }
}
这里的T不是某个具体类型,它只是一个占位符。等真正使用的时候,才决定它是什么类型:
cs
Box<int> intBox = new Box<int>();
Box<string> strBox = new Box<string>();
这样同一套代码就能适合于不同类型。
二、为什么需要泛型
这是理解泛型最关键的一步。很多人可能会问:没有泛型也能写程序,为啥还要用泛型?
答案是,因为泛型可以同时解决代码复用,类型安全和性能问题。
2.1不适用泛型时的问题
在泛型出现之前,如果想写一个能存放任意类型的容器,通常会用object。
例如:
cs
public class Box
{
public object Value { get; set; }
}
使用的时候:
cs
Box box1 = new Box();
box1.Value = 100;
Box box2 = new Box();
box2.Value = "Hello";
看起来似乎也能达到通用的目的,但这里有几个问题。
1.类型不安全
cs
Box box = new Box();
box.Value = "Hello";
int num = (int)box.Value; // 运行时出错
因为object可以接收任何类型,所以编译器无法提前检查错误。只有运行时强制转换时,才会发现类型不匹配。
2.需要频繁强制转换
cs
string text = (string)box.Value;
这种写法不仅麻烦,而且容易出错。
3.值类型会发生装箱和拆箱
cs
object obj = 123; // 装箱
int num = (int)obj; // 拆箱
这会带来额外的性能开销。
2.2泛型带来的好处
泛型的出现正式为了改进这些问题。
cs
public class Box<T>
{
public T Value { get; set; }
}
使用时:
cs
Box<int> box = new Box<int>();
box.Value = 100;
int num = box.Value; // 不需要强转
这样就有几个明显的优势,类型明确,编译器检查,不需要强制转换,避免不必要的装箱拆箱。
三、泛型类
泛型最常见的用法之一就是泛型类。
cs
public class Box<T>
{
public T Value { get; set; }
public void ShowValue()
{
Console.WriteLine(Value);
}
}
这里的T表示:Value的类型不固定,具体是什么类型,由创建对象时决定。
就像下面这样:
cs
using System;
public class Box<T>
{
public T Value { get; set; }
public void ShowValue()
{
Console.WriteLine(Value);
}
}
public class Program
{
public static void Main()
{
Box<int> intBox = new Box<int>();
intBox.Value = 123;
intBox.ShowValue();
Box<string> strBox = new Box<string>();
strBox.Value = "你好,泛型";
strBox.ShowValue();
}
}
这个例子在于,我们不需要专门写IntBox和StringBox,只需要写一个Box<T>就能支持不同的类型。这就是泛型代码复用的体现。
四、泛型方法
除了类可以使用泛型,方法也可以使用泛型。
示例:
cs
using System;
public class Program
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
public static void Main()
{
int x = 10;
int y = 20;
Swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}");
string s1 = "Hello";
string s2 = "World";
Swap(ref s1, ref s2);
Console.WriteLine($"s1={s1}, s2={s2}");
}
}
这里的Swap<T>表示这个方法适用于任意类型T,只要两个参数类型一致,就可以使用同一套交换逻辑。
五、泛型接口
接口同样也可以定义为泛型接口。
cs
public interface IRepository<T>
{
void Add(T item);
T GetById(int id);
}
这里表示这个接口可以用于不同的数据类型,T在具体实现时再决定。
示例:
cs
using System;
public interface IRepository<T>
{
void Add(T item);
T GetById(int id);
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public class UserRepository : IRepository<User>
{
public void Add(User item)
{
Console.WriteLine($"添加用户:{item.Name}");
}
public User GetById(int id)
{
return new User { Id = id, Name = "张三" };
}
}
public class Program
{
public static void Main()
{
IRepository<User> repo = new UserRepository();
repo.Add(new User { Id = 1, Name = "李四" });
User user = repo.GetById(1);
Console.WriteLine($"获取到用户:{user.Name}");
}
}
六、泛型委托
委托也可以用泛型。实际上我们常见的Action<T>、Func<T,TResult>、Predicate<T>本质上都是泛型委托。
七、自定义泛型委托
cs
public delegate T Transformer<T>(T input);
表示,输入参数类型是T,返回值类型也是T。
示例
cs
using System;
public delegate T Transformer<T>(T input);
public class Program
{
public static void Main()
{
Transformer<int> square = x => x * x;
Console.WriteLine(square(5));
Transformer<string> toUpper = s => s.ToUpper();
Console.WriteLine(toUpper("hello"));
}
}
八、常见泛型类型参数命名
在写泛型时,虽然参数名可以随便取,但通常会遵循一些约定。
- T:表示一个普通类型参数
- TKey:表示键类型
- TValue:表示值类型
- TResult:表示返回结果类型
- TItem:表示集合中的元素类型
- TSource:表示源数据类型
九、C#中常见的泛型集合
泛型在.NET中最常见的应用就是集合类。在没有泛型的时代,集合通常保存的是object,使用起来不够安全。而泛型集合可以明确元素类型。
9.1List
cs
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
foreach (int n in numbers)
{
Console.WriteLine(n);
}
}
}
9.2Dictionary<TKey,TValue>
cs
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
Dictionary<int, string> students = new Dictionary<int, string>();
students.Add(1, "张三");
students.Add(2, "李四");
Console.WriteLine(students[1]);
Console.WriteLine(students[2]);
}
}
9.3Queue
cs
Queue<string> queue = new Queue<string>();
queue.Enqueue("A");
queue.Enqueue("B");
Console.WriteLine(queue.Dequeue());
9.4Stack
cs
Stack<int> stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
Console.WriteLine(stack.Pop());
十、泛型约束
泛型虽然灵活,但有时候我们不希望类型参数完全任意,而是希望它满足某些条件。这时就需要使用泛型约束,约束的作用就是:限制泛型参数必须符合某些限制。
下面是整理的泛型约束:
10.1where T: calss
表示T必须是引用类型
cs
public class MyClass<T> where T : class
{
}
10.2where T : struct
表示T必须是值类型。
cs
public class MyStructBox<T> where T : struct
{
}
10.3where T : new()
表示T必须有一个公共的无参构造函数
cs
public class Factory<T> where T : new()
{
public T Create()
{
return new T();
}
}
10.4 where T : BaseClass
表示T必须继承某个基类
cs
public class Animal
{
public void Eat()
{
Console.WriteLine("动物在吃东西");
}
}
public class Cage<T> where T : Animal
{
public void Feed(T animal)
{
animal.Eat();
}
}
10.5where T : IInterface
表示T必须实现某个接口
cs
public interface IRun
{
void Run();
}
public class Runner<T> where T : IRun
{
public void Start(T obj)
{
obj.Run();
}
}
10.6多个约束一起使用
cs
public class MyClass<T>
where T : class, new()
{
}
表示T必须是引用类型并且有无参构造函数。
十一、泛型方法的类型推断
调用泛型方法时,很多时候不需要手动写出类型参数,编译器可以自动推断。
示例:
cs
using System;
public class Program
{
public static void Print<T>(T value)
{
Console.WriteLine(value);
}
public static void Main()
{
Print<int>(100);
Print<string>("Hello");
// 也可以省略类型参数
Print(200);
Print("World");
}
}
编译器可以根据传入参数自动推断T的类型。这让泛型在实际使用中更加简洁。
十二、总结
C#泛型是一种非常重要的语言特性,它允许我们把类型作为参数传递,从而实现逻辑复用、类型安全、性能优化。学习泛型时,不能只停留在List<T>这样的表面语法,更要理解其背后的思想。
十三、参考链接
-
Microsoft Learn - 泛型(C# 编程指南)
https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/ -
Microsoft Learn - 泛型类型参数
https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/generic-type-parameters -
Microsoft Learn - 泛型中的约束
https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters