1. 简单历史背景
在C#的远古时代,玩集合只有两种乐趣:用数组,或者用那种“类型不深”的集合——比如ArrayList,啥类型都能往里塞。看起来很自由对吧?但你要是把字符串和整数混着加进去——那就乱套了:你没法安全地把元素拿出来,除非加一堆(还不一定靠谱的)类型检查。还有——各种莫名其妙的bug和类型转换(casting)舞蹈。
非generic集合的例子(ArrayList):
using System.Collections;
ArrayList stuff = new ArrayList();
stuff.Add(42);
stuff.Add("你好 C#");
int number = (int)stuff[0]; // OK
string text = (string)stuff[1]; // OK
int fail = (int)stuff[1]; // 崩了!InvalidCastException(运行时错误)
对,编译器不报错——只有运行的时候才炸!就像你打开冰箱,发现里面有个热锅——惊喜!
泛型的本质是啥?
泛型集合(Generics)就是为了让你能创建有类型保证的集合。它有三个核心好处:
- 编译时类型安全。 编译器不会让你乱塞东西进集合。
- 方便: 不用每次都手动类型转换。
- 性能: 不用浪费时间做“装箱和拆箱”(boxing/unboxing)。
2. Generics(泛型)到底是啥?
Generics——就是一种神奇的方式,让你能描述数据结构和方法,它们能和任何类型一起用,但依然是强类型的。
想象一下有个万能箱子,可以装书也能装袜子,但一次只能装一种类型。如果箱子声明成“只装书”——谁也别想往里塞袜子。generic集合也是这样:你要是创建了int的集合,string就绝对进不去。
泛型类的声明例子
public class Box<T>
{
public T Value { get; set; }
}
var boxOfInt = new Box<int> { Value = 42 };
var boxOfString = new Box<string> { Value = "你好 Generics!" };
T——就是“类型参数”,你得告诉箱子要装啥。C#会强制你明确指定类型。
.NET里的泛型集合
在.NET Framework里,几乎所有现代集合都有泛型版本。比如:
- List<T>——动态元素列表,类型是T。
- Dictionary<TKey, TValue>——带键值对的字典。
- Queue<T>、Stack<T>——队列和栈。
- 还有很多别的。
3. 工作原理:Generics的机制
类型参数
当你声明一个像List<int>这样的集合时,C#编译器会专门为int类型生成一个版本(specialization)。你要是声明List<string>,编译器又会为字符串生成一个版本,依此类推。
对你来说,这就像魔法一样:
List<int> numbers = new List<int>();
numbers.Add(1); // 只能加int
List<string> words = new List<string>();
words.Add("hello"); // 只能加string
你要是想加别的类型,编译器立马不乐意:
numbers.Add("fail"); // 编译时就报错!
类型安全:编译时vs运行时
这就意味着,上面那种错误根本不会等到程序运行才爆雷(run-time)。你的集合安全得很,编译器就像门口的地狱三头犬,谁都别想乱来。
底层原理
- 对于引用类型(比如string、object),再也不用多余的类型转换了。
- 对于值类型(int、double),“装箱/拆箱”(boxing/unboxing)的问题没了。
- 在.NET里用generics不会导致代码膨胀——CLR会在JIT编译时优化。
4. Generics怎么提升性能
再来强调下Generics给代码带来的神奇属性:
类型安全(Type Safety):
这绝对是最重要的。有了Generics,编译器就像你的贴身保镖,绝不让“外人”混进你的集合。你可以100%放心,List<Product>里只会有Product对象,不会混进什么奇怪的字符串或数字。这直接消灭了一大堆以前只有运行时才会爆的bug(比如InvalidCastException)。你的代码更靠谱、更可预期。
性能(Performance):
就像前面说的,对于值类型(比如int、double或DateTime),Generics让内存和CPU用得更高效。要是你的集合有几百万个元素或者经常改动,这点就特别关键。你不用再把橙子从箱子倒进袋子再倒回来,直接放进对的箱子就行。
代码复用(Code Reusability):
Generics让你写一次代码,能搞定各种类型,不用重复造轮子。比如你要写个交换两个变量的函数。没Generics你得写SwapInt(ref int a, ref int b)、SwapString(ref string a, ref string b)、SwapProduct(ref Product a, ref Product b),等等。有了Generics你只要写一个:Swap<T>(ref T a, ref T b)。集合也是一样:不用IntList、StringList、ProductList,只要List<T>就够了。这样代码更简洁、好维护、易扩展。
5. 泛型方法和自定义generic类
泛型方法
Generics不只是集合!你可以写自己的泛型方法,啥类型都能用,让代码更通用。
//类型参数T写在方法名后面
public static void Swap<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
// 用法:
int a = 10, b = 20;
Swap(ref a, ref b); // 现在a == 20, b == 10
string one = "一", two = "二";
Swap(ref one, ref two); // 字符串也能用!
注意,C#编译器能根据你传的变量类型自动推断方法的类型参数。所以上面例子里,Swap方法不用额外指定类型。
自定义generic类
你甚至可以自己造“箱子”(generic类)。其实很简单:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
// 用法:
var pair = new Pair<int, string> { First = 42, Second = "答案" };
6. 泛型集合的使用例子
咱们回到之前讲过的“任务清单”(To-Do List)。以前我们用数组存任务,或者直接打印出来。现在——用List<string>动态存。
例子:动态添加任务到列表
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> tasks = new List<string>();
Console.WriteLine("请输入任务(留空回车结束):");
string input;
while (!string.IsNullOrWhiteSpace(input = Console.ReadLine()))
{
tasks.Add(input);
Console.WriteLine("任务已添加!继续输入(留空回车结束):");
}
Console.WriteLine("\n你今天的任务清单:");
foreach (string task in tasks)
{
Console.WriteLine("- " + task);
}
}
}
这里有啥好处?
- 集合会随着你加任务自动扩容。
- 你不可能误加非字符串进列表。
- 可以很方便地遍历和输出内容。
7. 新手常见错误、注意点和建议
从非generic集合转到generic,有时候会有点懵。下面是新手常遇到的几种情况:
- 想加错类型的元素(List<int> numbers = new List<int>(); numbers.Add("hi"); // 报错)。
- 想在一个集合里混合多种类型——这时候你得用基类(比如List<object>)——但之后每次都得手动转回需要的类型。
- 忘了有constraints这种东西,写得太“泛”,结果编译时遇到奇怪的错误。
GO TO FULL VERSION