一、C++与C#泛型写法区别
//C++ 写法(你已经会的)
template<tempname T>
class Stack
{
PUblic:
void Push(T x)
{
Data.push_back(x);
}
T top()
{
T x = data.back();
data.pop_back();
return x;
}
private:
vector<T>Data;
}
// 泛型函数
template<tempname T>
void Print(T item)
{
cout<<item<< endl;
}
Stack<int> si; // 实例化
Stack<string> ss;
//c#写法
class Stack<T>
{
private List<T> _data = new(); // new() 目标类型 new,省类型名
public void Push(T x) => _data.Add(x);
public T Pop()
{
var item = _data[^1]; // [^1] = 最后一个,C# 索引运算符
_data.RemoveAt(_data.Count - 1);
return item;
}
public int Count => _data.Count;
}
// 泛型方法 ------ <T> 写在方法名后
void Print<T>(T item) => Console.WriteLine(item);
Stack<int> si = new(); // 实例化,类型靠右边推断
Stack<string> ss = new();
1.1对照图
|--------------------------------------|-------------------------|---------------------------------------|
| C++ | C# | 备注 |
| template<typename T> | class Stack<T> | C# 把 <T> 直接写在类名后,没有 template<> 前缀 |
| Stack<int> | Stack<int> | 实例化语法完全一样 |
| template<typename T> void Print(T) | void Print<T>(T item) | C# 的 <T> 写在方法名和参数列表之间 |
| 编译期源码展开 | 运行时具化 | ⚠️ 见第 3 步,这是灵魂差异 |
💡 一个小甜点:C# 的 new() 叫目标类型 new,Stack<int> si = new(); 里右边不用再写 Stack<int>(),编译器看左边猜右边。C++17 的 Stack<int> si{}; 类似。
二、泛型集合
C# 日常 90% 的泛型用在这两个集合上,记住这个映射就行:
|---------------------------|-------------------------|---------------|
| C++ | C# | 说明 |
| std::vector<T> | List<T> | 动态数组,最常用 |
| std::unordered_map<K,V> | Dictionary<K,V> | 哈希表 |
| std::map<K,V> | SortedDictionary<K,V> | 有序(红黑树) |
| v.push_back(x) | list.Add(x) | |
| v.size() | list.Count | 注意是属性不是方法 |
| vi | listi | 索引访问一样 |
| v.back() | list\^1 | C# 用 ^ 从后往前数 |
List<int> = new() {1,2,3}; //初始器
nums.Add(4);
Console.WriteLine(nums[^1]); // 4,最后一个
Dictionary<string, int> ages = new()
{
["张三"] = 20;
["李四"] = 30;
};
Console.WriteLine(ages["张三"]); // 20
三、核心差异------为什么 C# 泛型不能用 +?
这是今天最该花时间的地方。看这段对比:
4.1 C++ 模板:编译期源码展开(duck typing)
template<typename T>
T Add(T a, T b) { return a + b; } // 只要 T 有 operator+,就能编过
Add(3, 5); // OK,int 有 +
Add(string("a"), string("b")); // OK,string 有 +
Add(Thing{}, Thing{}); // 只要 Thing 重载了 +,就 OK
C++ 编译器看到 Add<int>,就把模板里每个 T 替换成 int,生成一份专门的 Add 代码。它不检查「T 有没有 +」,它只是把代码抄一遍,等替换完再看 int + int 合不合法。模板的本质是高级的文本替换/代码生成。
4.2 C# 泛型:运行时具化(reified),编译期只认一份 IL
T Add<T>(T a, T b) => a + b; // ❌ 编译错误!运算符 '+' 不能应用于类型 'T'
为什么错?因为 C# 编译器在编译 Add<T> 时,它真的不知道 T 是什么。它只编译出一份泛型 IL 代码,这份代码要在运行时、当你传 int 进去时才「具化」成 Add<int>。编译期它没有任何理由相信 T 一定有 +------万一你传个 Person 进来呢?Person + Person 不存在啊。所以编译器直接拒绝。
这就是为什么 C# 泛型叫 reified(具化):泛型类型 List<int> 在运行时是一个真实存在的、具体的类型,而不是编译后就被擦除掉(Java 那种叫类型擦除 erasure)。但代价是------编译期对 T 能做什么一无所知,除非你用约束告诉它。
4.3 这个差异会带来什么后果?
|------------------|---------------|---------------------------------|
| 场景 | C++ 模板 | C# 泛型 |
| a + b | ✅ 只要 T 有 + 就行 | ❌ 直接报错 |
| new T() | ✅ 默认就行 | ❌ 要加 where T : new() 约束 |
| T t = null | ✅ 指针类型可以 | ❌ 要加 where T : class 约束 |
| 调 t.CompareTo(x) | ✅ 有这个方法就行 | ❌ 要加 where T : IComparable<T> |
四、总结
一句话总结:C++ 模板是「先抄代码再检查」,C# 泛型是「先检查约束再用」。C# 想用 T 的任何能力,都必须用 where 约束提前声明。你只要记住「C# 泛型对 T 一无所知」这个直觉就够了。
using System;
using System.Collections.Generic;
// ---------- 1. 泛型类 Stack<T> ----------
class Stack<T>
{
private readonly List<T> _items = new();
public void Push(T item) => _items.Add(item);
public T Pop()
{
if (_items.Count == 0)
throw new InvalidOperationException("栈是空的!");
var top = _items[^1];
_items.RemoveAt(_items.Count - 1);
return top;
}
public T Peek()
{
if (_items.Count == 0)
throw new InvalidOperationException("栈是空的!");
return _items[^1];
}
public int Count => _items.Count;
public bool IsEmpty => Count == 0;
}
// ---------- 2. 泛型方法 ----------
class Printer
{
// <T> 写在方法名后、参数表前。调用时通常可省略,靠参数推断
public static void Print<T>(T item) => Console.WriteLine($"值 = {item},类型 = {typeof(T).Name}");
}
// ---------- 3. 主程序 ----------
class Program
{
static void Main()
{
Console.WriteLine("===== 栈:int 类型 =====");
Stack<int> intStack = new();
intStack.Push(10);
intStack.Push(20);
intStack.Push(30);
Console.WriteLine($"栈顶(不弹出): {intStack.Peek()}");
Console.WriteLine($"弹出: {intStack.Pop()}");
Console.WriteLine($"弹出: {intStack.Pop()}");
Console.WriteLine($"剩余数量: {intStack.Count}");
Console.WriteLine("\n===== 栈:string 类型(同一个类,不同 T)=====");
Stack<string> strStack = new();
strStack.Push("你好");
strStack.Push("世界");
Console.WriteLine($"弹出: {strStack.Pop()}");
Console.WriteLine($"弹出: {strStack.Pop()}");
// intStack.Push("字符串"); // ❌ 编译错误!类型安全:int 栈不能塞 string
// 这就是泛型相比 C++ 模板的最大日常价值:编译期类型检查 + 无装箱
Console.WriteLine("\n===== 泛型方法 =====");
Printer.Print(42); // 推断 T = int
Printer.Print("泛型方法"); // 推断 T = string
Printer.Print(3.14); // 推断 T = double
Console.WriteLine("\n===== List<T> / Dictionary<K,V> 快速体验 =====");
List<string> names = new() { "张三", "李四", "王五" };
names.Add("赵六");
foreach (var n in names) Console.WriteLine($" - {n}");
Dictionary<string, int> score = new()
{
["数学"] = 90,
["语文"] = 85
};
score["英语"] = 95;
Console.WriteLine($"数学成绩: {score["数学"]}");
}
}