第16篇 C# 零基础入门——泛型入门
欢迎来到 C# 零基础入门系列的第16篇!在前面的15篇中,我们已经掌握了控制台交互、变量与常量、条件判断、循环结构、数组、List 集合、Dictionary 字典、字符串处理、异常处理、面向对象入门(类与对象、封装、继承、多态)以及接口与抽象类等核心知识。
但不知道你有没有注意到一个"痛点":我们之前写的很多方法,为了处理不同类型的数据,不得不写几乎一模一样的代码。比如一个"交换两个变量值"的方法,int 类型要写一个,string 类型又要写一个,double 类型再来一个……代码重复得让人抓狂。
今天登场的这位"大佬"——泛型(Generic),就是来解决这个痛点的。泛型是 C# 中最重要的特性之一,你之前学过的 List<T>、Dictionary<K,V> 全部都用到了泛型。理解了泛型,你才算真正跨进了"高级 C# 开发"的大门。本篇将系统讲解泛型的基本语法、泛型约束、泛型与面向对象的关系,以及泛型在实战中的经典用法。准备好了吗?让我们开始吧!
一、什么是泛型(Generic)?
泛型的概念
泛型(Generic)是一种"用类型做参数"的编程技巧。你可以把泛型理解为一个"类型占位符"——你在定义类、方法或接口时,先用一个代号(比如 T)来代替具体的类型,等实际使用时再告诉它具体是什么类型。
💡 生动类比:泛型就像一个"万能模具"。你不需要为每个不同尺寸的杯子都重新设计一套模具,你只需要做一个"可调尺寸的模具"(泛型),然后根据需求填入具体的尺寸(具体类型),就能生产出各种大小的杯子。
没有泛型时的痛点
// ❌ 没有泛型:每种类型都要写一遍几乎一样的代码
class IntStack
{
private int[] _items;
private int _count;
public void Push(int item) { ... }
public int Pop() { ... }
public int Count { get { return _count; } }
}
class StringStack
{
private string[] _items;
private int _count;
public void Push(string item) { ... } // 几乎一模一样!
public string Pop() { ... } // 几乎一模一样!
public int Count { get { return _count; } }
}
class DoubleStack
{
private double[] _items;
private int _count;
public void Push(double item) { ... } // 又写一遍!
public double Pop() { ... } // 又写一遍!
public int Count { get { return _count; } }
}
看到问题了没有?IntStack、StringStack、DoubleStack 三个类的代码几乎一模一样,只是数据类型不同。这不仅代码冗余,而且维护起来极其痛苦——修一个 bug 得改三个类。
用泛型解决痛点
// ✅ 用泛型:只写一次,适用于任何类型
// T 是"类型参数"的占位符(约定用大写字母T表示Type)
class Stack<T>
{
private T[] _items; // 用T代替具体的int/string/double
private int _count;
public void Push(T item) // 方法参数也用T
{
// … 具体的压栈逻辑(与类型无关)
Console.WriteLine($"压入元素:{item},类型是{T}");
}
public T Pop() // 返回值也用T
{
// … 具体的弹栈逻辑
return default(T); // default(T) 表示T类型的默认值(引用类型为null,值类型为0)
}
public int Count { get { return _count; } }
}
// 使用:创建不同种类的Stack,代码完全一样!
Stack<int> intStack = new Stack<int>(); // 整数栈
Stack<string> stringStack = new Stack<string>(); // 字符串栈
Stack<double> doubleStack = new Stack<double>(); // 双精度栈
intStack.Push(100);
stringStack.Push("Hello");
doubleStack.Push(3.14);
⚠️ 关键点:泛型的本质就是"把类型当作参数",让同一套代码可以处理多种类型,既消除了代码重复,又保证了类型安全(编译期检查,不需要运行时强制类型转换)。
二、泛型的基本语法
泛型类的定义
泛型类的定义在类名后面加上 <T> 尖括号,T 是类型参数的占位符(你可以用任意合法的标识符,但惯例用 T、TKey、TValue 等)。
// 定义一个泛型容器类:Box<T>
// T 是类型参数占位符,代表"某种类型"
class Box<T>
{
private T _content; // 用T声明字段,具体类型由使用时决定
// 构造函数也用T
public Box(T content)
{
_content = content;
Console.WriteLine($"创建了一个Box,里面装的是{T}类型的数据");
}
// 获取内容的方法
public T GetContent()
{
return _content;
}
// 设置内容的方法
public void SetContent(T content)
{
_content = content;
}
// 显示内容的类型信息
public void ShowTypeInfo()
{
// typeof(T) 可以在运行时获取T的实际类型
Console.WriteLine($"当前Box中存储的类型是:{typeof(T).Name}");
}
}
// 使用泛型类
Box<int> intBox = new Box<int>(42); // 创建一个装int的Box
Box<string> stringBox = new Box<string>("你好,泛型!"); // 创建一个装string的Box
Box<bool> boolBox = new Box<bool>(true); // 创建一个装bool的Box
Console.WriteLine(intBox.GetContent()); // 输出: 42
Console.WriteLine(stringBox.GetContent()); // 输出: 你好,泛型!
Console.WriteLine(boolBox.GetContent()); // 输出: True
intBox.ShowTypeInfo(); // 输出: 当前Box中存储的类型是:Int32
stringBox.ShowTypeInfo(); // 输出: 当前Box中存储的类型是:String
多类型参数的泛型类
泛型类可以有多个类型参数,用逗号分隔。
// 定义一个泛型键值对类:KeyValuePair<K, V>
// K 代表Key的类型,V 代表Value的类型
class KeyValuePair<K, V>
{
public K Key { get; set; }
public V Value { get; set; }
public KeyValuePair(K key, V value)
{
Key = key;
Value = value;
}
public override string ToString()
{
return $"Key={Key}, Value={Value}";
}
}
// 使用:可以指定不同的类型组合
KeyValuePair<string, int> pair1 = new KeyValuePair<string, int>("张三", 20);
KeyValuePair<int, string> pair2 = new KeyValuePair<int, string>(1, "学生");
KeyValuePair<string, double> pair3 = new KeyValuePair<string, double>("数学", 95.5);
Console.WriteLine(pair1); // 输出: Key=张三, Value=20
Console.WriteLine(pair2); // 输出: Key=1, Value=学生
Console.WriteLine(pair3); // 输出: Key=数学, Value=95.5
💡 命名惯例:
- T:通用的类型参数(Type的第一个字母)
- TKey、TValue:表示键值对中的键和值的类型
- TInput、TOutput:表示输入和输出的类型
- 多个类型参数按字母顺序排列:T, U, V, W
泛型结构体和泛型接口
泛型不仅可以用在类上,还可以用在结构体(struct)和接口(interface)上。
// 泛型结构体
struct Result<T>
{
public bool Success { get; set; }
public T Data { get; set; }
public string Message { get; set; }
public Result(bool success, T data, string message)
{
Success = success;
Data = data;
Message = message;
}
}
// 使用泛型结构体
Result<int> intResult = new Result<int>(true, 100, "操作成功");
Result<string> stringResult = new Result<string>(false, null, "文件未找到");
Console.WriteLine(intResult.Success); // 输出: True
Console.WriteLine(intResult.Data); // 输出: 100
Console.WriteLine(stringResult.Message); // 输出: 文件未找到
// 泛型接口
interface IRepository<T>
{
void Add(T entity);
void Delete(T entity);
T GetById(int id);
IEnumerable<T> GetAll();
}
// 类实现泛型接口
class UserRepository : IRepository<User>
{
// 这里T已经被替换为User,所以方法签名是确定的
public void Add(User entity) { Console.WriteLine($"添加用户:{entity.Name}"); }
public void Delete(User entity) { Console.WriteLine($"删除用户:{entity.Name}"); }
public User GetById(int id) { return new User { Name = "张三" }; }
public IEnumerable<User> GetAll() { return new List<User>(); }
}
class User
{
public string Name { get; set; }
}
三、泛型方法
不仅类可以是泛型的,方法也可以是泛型的。泛型方法在方法名前面加上 <T> 类型参数声明。
泛型方法的基本用法
class Program
{
// 泛型方法:交换两个变量的值
// <T> 声明这是一个泛型方法,T是类型参数
static void Swap<T>(ref T a, ref T b)
{
T temp = a; // 用T声明临时变量
a = b;
b = temp;
}
// 泛型方法:获取数组中的最大值
static T GetMax<T>(T[] array) where T : IComparable<T>
{
if (array == null || array.Length == 0)
throw new ArgumentException("数组不能为空");
T max = array[0];
for (int i = 1; i < array.Length; i++)
{
// 因为T实现了IComparable<T>,所以可以用CompareTo比较
if (array[i].CompareTo(max) > 0)
max = array[i];
}
return max;
}
static void Main()
{
// 交换int变量
int x = 10, y = 20;
Swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}"); // 输出: x=20, y=10
// 交换string变量
string a = "Hello", b = "World";
Swap(ref a, ref b);
Console.WriteLine($"a={a}, b={b}"); // 输出: a=World, b=Hello
// 获取int数组最大值
int[] numbers = { 3, 7, 2, 9, 5 };
int maxNum = GetMax(numbers);
Console.WriteLine($"最大值:{maxNum}"); // 输出: 最大值:9
// 获取string数组最大值(按字典序)
string[] words = { "apple", "banana", "cherry" };
string maxWord = GetMax(words);
Console.WriteLine($"字典序最大:{maxWord}"); // 输出: 字典序最大:cherry
}
}
类型推断(Type Inference)
C# 编译器可以根据方法参数的类型自动推断泛型参数,不需要你显式写出类型。
// 显式指定类型(C# 3.0之前必须这样写)
Swap<int>(ref x, ref y); // 必须写明<int>
// 类型推断(C# 3.0+ 支持)
Swap(ref x, ref y); // 编译器自动推断T是int,不需要写<int>
// 编译器也能推断其他泛型方法
int maxNum = GetMax(new int[] { 3, 7, 2, 9, 5 }); // 自动推断T=int
string maxWord = GetMax(new string[] { "a", "b", "c" }); // 自动推断T=string
💡 类型推断的小技巧:当方法有多个类型参数时,编译器可能无法完全推断,这时需要显式指定部分类型参数。但大多数简单场景下,编译器都能自动推断,让你少打很多代码。
四、泛型约束(Generic Constraints)
有时候你定义的泛型方法或泛型类,需要对类型参数 T 施加一些限制。比如你希望 T 必须是实现了某个接口的类型,或者必须是引用类型等。这时就需要用到泛型约束。
常用的泛型约束
| where T : class | T 必须是引用类型(类、接口、数组、delegate) | class Box<T> where T : class |
| where T : struct | T 必须是值类型(struct、enum、nullable) | class Box<T> where T : struct |
| where T : new() | T 必须有无参构造函数 | class Box<T> where T : new() |
| where T : 基类名 | T 必须是该基类或其派生类 | class Box<T> where T : Animal |
| where T : 接口名 | T 必须实现该接口 | class Box<T> where T : IComparable |
| where T : U | T 必须是 U 或其派生类 | class Box<T, U> where T : U |
📌 注意:class 和 struct 约束不能同时使用(一个类型不可能既是引用类型又是值类型)。new() 约束必须放在所有约束的最后。
泛型约束实战示例
// 约束1:where T : class — T必须是引用类型
class ReferenceBox<T> where T : class
{
private T _value;
public void Set(T value)
{
_value = value; // 引用类型可以是null,所以这里可以安全地赋值
}
public T Get()
{
return _value; // 引用类型返回可能是null
}
}
// 约束2:where T : struct — T必须是值类型
class ValueBox<T> where T : struct
{
private T _value;
private bool _hasValue = false;
public void Set(T value)
{
_value = value;
_hasValue = true;
}
public T Get()
{
if (!_hasValue)
throw new InvalidOperationException("没有设置值");
return _value;
}
}
// 约束3:where T : new() — T必须有无参构造函数
class Factory<T> where T : new()
{
public T Create()
{
// 因为有new()约束,编译器保证T有无参构造函数,所以可以new
return new T();
}
}
// 约束4:where T : 基类名 — T必须是Animal或其派生类
class AnimalHolder<T> where T : Animal
{
protected T _animal;
public AnimalHolder(T animal)
{
_animal = animal;
}
// 因为T是Animal,所以可以调用Animal的Speak方法
public void MakeAnimalSpeak()
{
_animal.Speak();
}
}
// 约束5:where T : 接口名 — T必须实现IComparable
class SortableBox<T> where T : IComparable<T>
{
public T Max(T a, T b)
{
// 因为T实现了IComparable<T>,所以可以调用CompareTo
return a.CompareTo(b) > 0 ? a : b;
}
}
// 使用示例
class Animal
{
public virtual void Speak() { Console.WriteLine("动物发出声音"); }
}
class Dog : Animal
{
public override void Speak() { Console.WriteLine("汪汪汪!"); }
}
// 测试
ReferenceBox<string> refBox = new ReferenceBox<string>();
refBox.Set("Hello");
Console.WriteLine(refBox.Get()); // 输出: Hello
ValueBox<int> valBox = new ValueBox<int>();
valBox.Set(42);
Console.WriteLine(valBox.Get()); // 输出: 42
Factory<Dog> dogFactory = new Factory<Dog>();
Dog dog = dogFactory.Create();
dog.Speak(); // 输出: 汪汪汪!
SortableBox<int> box = new SortableBox<int>();
Console.WriteLine(box.Max(10, 20)); // 输出: 20
多个约束的组合
// 一个泛型类可以同时有多个约束,用逗号分隔
// 注意:new() 必须放在最后
class MultiConstraint<T, U>
where T : class, new() // T是引用类型且有无参构造函数
where U : struct, IComparable<U> // U是值类型且实现IComparable
{
private T _ref;
private U _val;
public MultiConstraint()
{
_ref = new T(); // ✅ 因为有new()约束
_val = default(U); // ✅ 因为有struct约束,T一定有默认值
}
public U Compare(U a, U b)
{
// ✅ 因为有IComparable<U>约束
return a.CompareTo(b) > 0 ? a : b;
}
}
五、泛型与面向对象的关系
泛型和面向对象(继承、多态)配合使用,能写出非常强大且灵活的设计。
泛型 + 继承
泛型类可以被继承,子类可以指定具体的类型参数,也可以继续保留泛型参数。
// 基础泛型类
class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
}
// 泛型 Repository 基类
abstract class Repository<T> where T : BaseEntity
{
protected List<T> _items = new List<T>();
public virtual void Add(T item)
{
_items.Add(item);
Console.WriteLine($"添加了{T}类型的实体,ID={item.Id}");
}
public virtual T GetById(int id)
{
return _items.FirstOrDefault(x => x.Id == id);
}
public List<T> GetAll()
{
return _items;
}
}
// 具体实现类:指定T为User
class UserRepository : Repository<User>
{
// 继承基类的所有方法,也可以重写
public override void Add(User item)
{
Console.WriteLine("[UserRepository] 开始添加用户…");
base.Add(item); // 调用基类实现
Console.WriteLine("[UserRepository] 用户添加完成");
}
// 可以添加User特有的方法
public List<User> FindByName(string name)
{
return _items.Where(u => u.Name == name).ToList();
}
}
// 也可以继续泛型化
class CachedRepository<T> : Repository<T> where T : BaseEntity
{
private Dictionary<int, T> _cache = new Dictionary<int, T>();
public override T GetById(int id)
{
// 先从缓存查找
if (_cache.TryGetValue(id, out T cachedItem))
{
Console.WriteLine($"缓存命中!ID={id}");
return cachedItem;
}
// 缓存未命中,从数据库查找
T item = base.GetById(id);
if (item != null)
_cache[id] = item;
return item;
}
}
// 定义实体类
class User : BaseEntity
{
public string Name { get; set; }
public int Age { get; set; }
}
class Product : BaseEntity
{
public string Name { get; set; }
public decimal Price { get; set; }
}
// 使用
UserRepository users = new UserRepository();
users.Add(new User { Id = 1, Name = "张三", Age = 20 });
// 输出: [UserRepository] 开始添加用户…
// 输出: 添加了User类型的实体,ID=1
// 输出: [UserRepository] 用户添加完成
User user = users.GetById(1);
Console.WriteLine($"{user.Name},{user.Age}岁"); // 输出: 张三,20岁
泛型 + 多态
泛型类型在运行时会被"具体化",不同具体类型的泛型类之间没有继承关系,但可以通过接口实现多态。
// 定义泛型接口作为多态的"桥梁"
interface IRepository
{
void Add();
string GetTypeDescription();
}
// 泛型类实现非泛型接口(实现代码复用)
abstract class RepositoryBase<T> : IRepository where T : BaseEntity, new()
{
protected List<T> _items = new List<T>();
// 非泛型接口的实现
public void Add()
{
T item = new T();
_items.Add(item);
Console.WriteLine($"添加了{T}类型的实体");
}
public abstract string GetTypeDescription();
// 泛型方法(保留类型信息)
public T GetById(int id)
{
return _items.FirstOrDefault(x => x.Id == id);
}
}
class UserRepository2 : RepositoryBase<User>
{
public override string GetTypeDescription()
{
return "用户仓库,存储User对象";
}
}
class ProductRepository2 : RepositoryBase<Product>
{
public override string GetTypeDescription()
{
return "商品仓库,存储Product对象";
}
}
// 多态使用:统一操作不同类型的仓库
IRepository repo1 = new UserRepository2();
IRepository repo2 = new ProductRepository2();
repo1.Add(); // 输出: 添加了User类型的实体
repo2.Add(); // 输出: 添加了Product类型的实体
Console.WriteLine(repo1.GetTypeDescription()); // 输出: 用户仓库,存储User对象
Console.WriteLine(repo2.GetTypeDescription()); // 输出: 商品仓库,存储Product对象
💡 重要理解:Repository<User> 和 Repository<Product> 在运行时是两个完全不同的类型,它们之间没有继承关系。但通过一个非泛型的基类或接口,我们可以把它们统一起来操作,这就是泛型+多态的威力。
六、常用泛型类型速查
C# 框架中大量使用了泛型,你最常遇到的泛型类型包括:
List — 泛型列表
// List<T> 是最常用的泛型集合,相当于"可以自动扩容的数组"
List<string> names = new List<string>();
names.Add("张三");
names.Add("李四");
names.Add("王五");
// 访问元素(通过索引)
Console.WriteLine(names[0]); // 输出: 张三
// 遍历
foreach (string name in names)
{
Console.WriteLine(name);
}
// 常用方法
names.Insert(0, "赵六"); // 在指定位置插入
names.Remove("李四"); // 删除指定元素
int count = names.Count; // 获取元素数量
bool has = names.Contains("王五"); // 检查是否包含某元素
names.Sort(); // 排序(T必须实现IComparable)
names.Reverse(); // 反转顺序
Dictionary<K, V> — 泛型字典
// Dictionary<K, V> 存储键值对,通过Key快速查找Value
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["张三"] = 20;
ages["李四"] = 25;
ages["王五"] = 30;
// 通过Key获取Value
int age = ages["张三"]; // 20
// 检查是否包含某个Key
if (ages.ContainsKey("李四"))
{
Console.WriteLine($"李四的年龄是:{ages["李四"]}");
}
// 遍历键值对
foreach (KeyValuePair<string, int> pair in ages)
{
Console.WriteLine($"{pair.Key} 今年 {pair.Value} 岁");
}
// 常用方法
ages.Remove("王五"); // 删除键值对
ages.TryGetValue("赵六", out int age2); // 安全获取(Key不存在时不报错)
KeyValuePair<K, V> — 键值对结构体
// Dictionary遍历时返回的就是KeyValuePair
Dictionary<string, string> capitals = new Dictionary<string, string>();
capitals["中国"] = "北京";
capitals["日本"] = "东京";
foreach (KeyValuePair<string, string> kv in capitals)
{
Console.WriteLine($"{kv.Key}的首都是{kv.Value}");
}
// 输出:
// 中国的首都是北京
// 日本的首都是东京
Nullable — 可空类型
// Nullable<T> 让值类型也能表示null
int? nullableInt = null; // 等价于 Nullable<int>
double? nullableDouble = 3.14;
// 检查是否有值
if (nullableInt.HasValue)
{
Console.WriteLine(nullableInt.Value);
}
else
{
Console.WriteLine("nullableInt 是 null"); // 会输出这行
}
// 空合并运算符 ?? 的用法
int result = nullableInt ?? 0; // 如果nullableInt是null,则用0代替
Console.WriteLine(result); // 输出: 0
double result2 = nullableDouble ?? 0.0;
Console.WriteLine(result2); // 输出: 3.14
📌 小贴士:T? 是 Nullable<T> 的简写语法,两者完全等价。int? 就是 Nullable<int> 的缩写。
七、新手常见错误
错误1:对值类型使用 null
// ❌ 错误:普通的值类型(int, struct等)不能赋值为null
int num = null; // 编译错误!int不能为null
// ✅ 正确:使用可空类型 Nullable<T>
int? num = null; // ✅ 值类型也可以为null了
错误2:忘记泛型约束导致无法调用类型特定的方法
// ❌ 错误:没有约束T,无法调用CompareTo等方法
class BadComparer<T>
{
public T Max(T a, T b)
{
// 编译错误!T没有定义CompareTo方法
return a.CompareTo(b) > 0 ? a : b;
}
}
// ✅ 正确:添加IComparable约束
class GoodComparer<T> where T : IComparable<T>
{
public T Max(T a, T b)
{
return a.CompareTo(b) > 0 ? a : b; // ✅ 编译通过
}
}
错误3:new() 约束位置错误
// ❌ 错误:new() 必须放在所有约束的最后
class BadFactory<T> where new() : class // 编译错误!
// ✅ 正确:new() 放在最后
class GoodFactory<T> where T : class, new() // ✅ 正确
{
public T Create() { return new T(); }
}
错误4:泛型类型在运行时不保留泛型信息(泛型擦除的误解)
📌 重要澄清:C# 的泛型不会像 Java 那样在运行时擦除类型信息。List<int> 和 List<string> 在运行时是完全不同的类型,你可以用 typeof() 获取泛型参数信息。
List<int> intList = new List<int>();
List<string> strList = new List<string>();
Console.WriteLine(intList.GetType()); // 输出: System.Collections.Generic.List`1[System.Int32]
Console.WriteLine(strList.GetType()); // 输出: System.Collections.Generic.List`1[System.String]
Console.WriteLine(intList.GetType() == strList.GetType()); // 输出: False(类型不同!)
// 可以获取泛型定义和实际类型参数
Console.WriteLine(intList.GetType().GetGenericTypeDefinition()); // List`1[T]
Console.WriteLine(intList.GetType().GetGenericArguments()[0]); // Int32
错误5:在泛型类中使用 sizeof 运算符
// ❌ 错误:sizeof 只能用于已知的值类型
class BadSize<T>
{
public int GetSize()
{
return sizeof(T); // 编译错误!T可能是引用类型
}
}
// ✅ 正确:用约束限制为值类型
class GoodSize<T> where T : struct
{
public int GetSize()
{
return sizeof(T); // ✅ 因为有struct约束,T一定是值类型
}
}
八、综合小练习:泛型缓存系统
需求
设计一个泛型缓存系统,要求:
完整代码
using System;
using System.Collections.Generic;
using System.Linq;
// ========== 1. 定义可缓存实体接口 ==========
interface ICacheable
{
string CacheKey { get; }
DateTime ExpiresAt { get; }
}
// ========== 2. 定义缓存项 ==========
class CacheItem<T> : ICacheable where T : class
{
public T Data { get; set; } // 缓存的数据
public string CacheKey { get; set; } // 缓存键(实现ICacheable接口)
public DateTime ExpiresAt { get; set; } // 过期时间
public DateTime CreatedAt { get; set; } // 创建时间
public bool IsExpired => DateTime.Now > ExpiresAt;
public CacheItem(T data, TimeSpan expiresIn)
{
Data = data;
CacheKey = typeof(T).Name + "_" + Guid.NewGuid().ToString("N")[..8];
CreatedAt = DateTime.Now;
ExpiresAt = DateTime.Now.Add(expiresIn);
}
public override string ToString()
{
return $"[CacheItem] Key={CacheKey}, Type={typeof(T).Name}, " +
$"Created={CreatedAt:HH:mm:ss}, Expired={ExpiresAt:HH:mm:ss}, " +
$"Valid={IsExpired == false}";
}
}
// ========== 3. 定义泛型缓存管理器 ==========
class CacheManager<T> where T : class, new()
{
private Dictionary<string, CacheItem<T>> _cache = new Dictionary<string, CacheItem<T>>();
// 添加缓存项
public bool Add(T data, TimeSpan expiresIn)
{
string key = typeof(T).Name + "_default";
// 如果key已存在,先删除旧的
if (_cache.ContainsKey(key))
{
_cache.Remove(key);
}
CacheItem<T> item = new CacheItem<T>(data, expiresIn);
_cache[key] = item;
Console.WriteLine($"✅ 缓存已添加:{item.CacheKey}");
return true;
}
// 获取缓存项
public T Get(string key, out bool isExpired)
{
isExpired = false;
if (!_cache.ContainsKey(key))
{
Console.WriteLine($"❌ 缓存未命中(Key不存在):{key}");
return null;
}
CacheItem<T> item = _cache[key];
if (item.IsExpired)
{
_cache.Remove(key);
Console.WriteLine($"⏰ 缓存已过期并被清除:{key}");
isExpired = true;
return null;
}
Console.WriteLine($"✅ 缓存命中:{key}");
return item.Data;
}
// 删除缓存项
public bool Remove(string key)
{
if (_cache.Remove(key))
{
Console.WriteLine($"🗑️ 缓存已删除:{key}");
return true;
}
Console.WriteLine($"❌ 缓存不存在:{key}");
return false;
}
// 清理所有过期的缓存项
public int CleanExpired()
{
var expiredKeys = _cache.Where(kvp => kvp.Value.IsExpired)
.Select(kvp => kvp.Key)
.ToList();
foreach (string key in expiredKeys)
{
_cache.Remove(key);
}
Console.WriteLine($"🧹 清理了 {expiredKeys.Count} 个过期缓存项");
return expiredKeys.Count;
}
// 获取缓存数量
public int Count => _cache.Count;
// 显示所有缓存项信息
public void ShowAll()
{
Console.WriteLine($"\\n— 当前缓存 ({Count} 项) —");
foreach (var kvp in _cache)
{
Console.WriteLine(kvp.Value);
}
Console.WriteLine("— 结束 —\\n");
}
}
// ========== 4. 定义实体类(实现ICacheable) ==========
class User : ICacheable
{
public string CacheKey => $"User_{Id}";
public DateTime ExpiresAt { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
{
return $"User(Id={Id}, Name={Name}, Age={Age})";
}
}
class Product : ICacheable
{
public string CacheKey => $"Product_{Id}";
public DateTime ExpiresAt { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public override string ToString()
{
return $"Product(Id={Id}, Name={Name}, Price={Price})";
}
}
// ========== 5. 使用缓存系统 ==========
class Program
{
static void Main()
{
// 创建用户缓存和产品缓存(两个独立的缓存管理器)
CacheManager<User> userCache = new CacheManager<User>();
CacheManager<Product> productCache = new CacheManager<Product>();
// 添加用户到缓存(有效期5分钟)
User user = new User { Id = 1, Name = "张三", Age = 20 };
userCache.Add(user, TimeSpan.FromMinutes(5));
// 添加产品到缓存(有效期10分钟)
Product product = new Product { Id = 1, Name = "笔记本电脑", Price = 5999m };
productCache.Add(product, TimeSpan.FromMinutes(10));
// 查看缓存状态
userCache.ShowAll();
productCache.ShowAll();
// 获取缓存数据
bool expired;
User cachedUser = userCache.Get("User_1", out expired);
if (cachedUser != null)
{
Console.WriteLine($"获取到用户:{cachedUser.Name},{cachedUser.Age}岁");
}
Product cachedProduct = productCache.Get("Product_1", out expired);
if (cachedProduct != null)
{
Console.WriteLine($"获取到产品:{cachedProduct.Name},{cachedProduct.Price}元");
}
// 清理过期缓存
userCache.CleanExpired();
productCache.CleanExpired();
}
}
九、小结
核心要点回顾
| 泛型是什么 | 用类型参数代替具体类型,实现代码复用 + 类型安全 |
| 泛型类 | class Box<T>,在类名后加 <T> |
| 泛型方法 | T Max<T>(T a, T b),在方法名后加 <T> |
| 类型推断 | 编译器根据参数自动推断类型,无需显式指定 |
| 泛型约束 | where T : class/struct/new()/接口/基类,限制T的取值范围 |
| 泛型 + 继承 | 泛型类可被继承,子类可指定具体类型或继续泛型化 |
| 泛型 + 多态 | 通过非泛型接口/基类实现泛型类型的多态操作 |
| 常用泛型类型 | List<T>、Dictionary<K,V>、Nullable<T>、KeyValuePair<K,V> |
最佳实践总结
恭喜你完成了第16篇!泛型是 C# 中最强大也最常用的特性之一,几乎你后面学到的每一个高级特性都会和泛型打交道。下一篇我们将深入泛型的进阶话题——协变与逆变,让你彻底理解泛型类型的转换规则。我们下一篇见!



