目 录CONTENT

文章目录

CSharp(四十五) 泛型类型的定义、使用和注意事项

C# 中泛型类型的定义、使用和注意事项

一、什么是泛型

泛型,英文叫 Generic

在 C# 中,泛型可以理解为:

写代码时先不固定具体类型,等真正使用时再指定类型。

例如,我们平时写一个保存整数的类:

class IntBox
{
    public int Value { get; set; }
}

这个类只能保存 int

如果还想保存 string,可能又要写一个:

class StringBox
{
    public string Value { get; set; }
}

如果还要保存 doubleStudentProduct,难道每种类型都写一个类吗?

这时就可以使用泛型:

class Box<T>
{
    public T Value { get; set; }
}

使用时再指定具体类型:

Box<int> intBox = new Box<int>();
intBox.Value = 100;

Box<string> stringBox = new Box<string>();
stringBox.Value = "你好";

这里的 T 就是一个类型占位符。

可以这样理解:

T 像一个空位置,先占着,等使用时再把真正的类型放进去。


二、为什么需要泛型

泛型主要解决三个问题:

  1. 代码重复
  2. 类型不安全
  3. 装箱和拆箱带来的性能问题

1. 减少重复代码

如果没有泛型,我们可能要写很多类似的类。

class IntBox
{
    public int Value { get; set; }
}

class StringBox
{
    public string Value { get; set; }
}

class DoubleBox
{
    public double Value { get; set; }
}

它们结构几乎一样,只是类型不同。

用泛型后,一个类就够了:

class Box<T>
{
    public T Value { get; set; }
}

然后:

Box<int> intBox = new Box<int>();
Box<string> stringBox = new Box<string>();
Box<double> doubleBox = new Box<double>();

这就是泛型的第一个好处:

用一份代码,适配多种类型。

2. 提高类型安全

早期没有泛型集合时,经常使用 ArrayList

ArrayList list = new ArrayList();

list.Add(100);
list.Add("hello");
list.Add(true);

它什么都能放进去,看起来很灵活,但也很危险。

取出来时容易出错:

int number = (int)list[1]; // 运行时报错,因为 list[1] 是 string

使用泛型集合后:

List<int> numbers = new List<int>();

numbers.Add(100);
numbers.Add(200);

// numbers.Add("hello"); // 编译时报错

错误会提前在编译阶段发现,而不是等程序运行时才爆炸。

这就是泛型的第二个好处:

类型更明确,错误更早发现。

3. 避免不必要的装箱和拆箱

如果把值类型放进 object,会发生装箱。

object obj = 100; // 装箱
int number = (int)obj; // 拆箱

装箱和拆箱会带来额外性能开销。

使用泛型集合:

List<int> numbers = new List<int>();
numbers.Add(100);

int 会以正确的类型保存,不需要频繁装箱和拆箱。

这就是泛型的第三个好处:

类型安全的同时,性能也更好。


三、泛型类型的基本语法

泛型类型常见语法:

class 类名<T>
{
    // 在类内部可以使用 T
}

例如:

class Box<T>
{
    public T Value { get; set; }

    public void SetValue(T value)
    {
        Value = value;
    }

    public T GetValue()
    {
        return Value;
    }
}

使用:

Box<int> box = new Box<int>();

box.SetValue(123);

int value = box.GetValue();

这里:

  • Box<T> 是泛型类。
  • T 是类型参数。
  • Box<int> 是把 T 替换成 int 后得到的具体类型。
  • SetValue(T value) 中的 T 会变成 int
  • T GetValue() 的返回值也会变成 int

四、T 是什么意思

T 是英文 Type 的缩写,表示类型。

它不是固定关键字,而是一个常见命名习惯。

下面这些都可以:

class Box<T>
{
}

class Box<TValue>
{
}

class Box<TItem>
{
}

甚至这样也能写:

class Box<ABC>
{
}

但不推荐。

常见命名习惯:

名称 含义
T 通用类型
TValue 值类型或值对象
TKey
TResult 返回结果
TItem 集合中的元素
TEntity 实体对象

例如字典:

Dictionary<TKey, TValue>

意思是:

一个键值对集合,TKey 表示键的类型,TValue 表示值的类型。


五、泛型类

泛型类是最常见的泛型类型。

1. 定义泛型类

class Box<T>
{
    public T Value { get; set; }
}

2. 使用泛型类

Box<int> intBox = new Box<int>();
intBox.Value = 100;

Box<string> stringBox = new Box<string>();
stringBox.Value = "C#";

3. 泛型类中的方法

class Box<T>
{
    private T value;

    public void Set(T value)
    {
        this.value = value;
    }

    public T Get()
    {
        return value;
    }
}

使用:

Box<double> box = new Box<double>();

box.Set(3.14);

double result = box.Get();

这个类一旦写成 Box<double>,类中的 T 就都可以理解为 double


六、泛型结构体

结构体也可以是泛型。

struct Pair<T>
{
    public T First { get; set; }
    public T Second { get; set; }
}

使用:

Pair<int> numbers = new Pair<int>
{
    First = 10,
    Second = 20
};

Pair<string> names = new Pair<string>
{
    First = "张三",
    Second = "李四"
};

如果两个值的类型不同,可以写两个类型参数:

struct Pair<TFirst, TSecond>
{
    public TFirst First { get; set; }
    public TSecond Second { get; set; }
}

使用:

Pair<string, int> student = new Pair<string, int>
{
    First = "小明",
    Second = 18
};

七、泛型接口

接口也可以使用泛型。

interface IRepository<T>
{
    void Add(T item);
    T GetById(int id);
}

这个接口表示:

一个仓储接口,可以添加某种类型的数据,也可以根据 id 查询这种类型的数据。

定义实体:

class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
}

实现接口:

class StudentRepository : IRepository<Student>
{
    public void Add(Student item)
    {
        Console.WriteLine("添加学生:" + item.Name);
    }

    public Student GetById(int id)
    {
        return new Student
        {
            Id = id,
            Name = "小明"
        };
    }
}

使用:

IRepository<Student> repository = new StudentRepository();

repository.Add(new Student { Id = 1, Name = "小红" });

Student student = repository.GetById(1);

泛型接口的好处是:

接口规则可以复用,但具体操作的数据类型可以变化。


八、泛型方法

泛型不只可以用在类、结构体、接口上,也可以用在方法上。

1. 定义泛型方法

static void Print<T>(T value)
{
    Console.WriteLine(value);
}

使用:

Print<int>(100);
Print<string>("你好");
Print<double>(3.14);

很多时候,C# 可以自动推断类型:

Print(100);
Print("你好");
Print(3.14);

2. 有返回值的泛型方法

static T GetDefault<T>()
{
    return default(T);
}

使用:

int number = GetDefault<int>();       // 0
string text = GetDefault<string>();   // null
bool flag = GetDefault<bool>();       // false

3. 交换两个变量

static void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

使用:

int x = 10;
int y = 20;

Swap(ref x, ref y);

Console.WriteLine(x); // 20
Console.WriteLine(y); // 10

也可以交换字符串:

string first = "A";
string second = "B";

Swap(ref first, ref second);

这就是泛型方法的好处:

方法逻辑相同,类型可以不同。


九、泛型委托

委托也可以是泛型。

C# 中常见的 ActionFunc 就是泛型委托。

Action<string> print = text =>
{
    Console.WriteLine(text);
};
Func<int, int, int> add = (a, b) =>
{
    return a + b;
};

也可以自定义泛型委托:

delegate TResult Converter<TInput, TResult>(TInput input);

使用:

Converter<string, int> stringToInt = text =>
{
    return int.Parse(text);
};

int number = stringToInt("123");

泛型委托适合表达:

一段可以接收某种类型,并返回另一种类型的行为。


十、多个类型参数

泛型可以有多个类型参数。

例如:

class Result<TData, TError>
{
    public TData Data { get; set; }
    public TError Error { get; set; }
    public bool Success { get; set; }
}

使用:

Result<string, string> result1 = new Result<string, string>
{
    Data = "操作成功",
    Error = null,
    Success = true
};

也可以这样:

Result<Student, string> result2 = new Result<Student, string>
{
    Data = new Student { Id = 1, Name = "小明" },
    Error = null,
    Success = true
};

再看字典:

Dictionary<string, int> scores = new Dictionary<string, int>();

scores["小明"] = 95;
scores["小红"] = 88;

这里:

  • string 是键的类型。
  • int 是值的类型。

十一、常见泛型集合

C# 中最常用的泛型类型之一就是集合。

1. List

List<T> 表示某种类型的列表。

List<int> numbers = new List<int>();

numbers.Add(1);
numbers.Add(2);
numbers.Add(3);

对象列表:

List<Student> students = new List<Student>();

students.Add(new Student { Id = 1, Name = "小明" });
students.Add(new Student { Id = 2, Name = "小红" });

2. Dictionary<TKey, TValue>

Dictionary<TKey, TValue> 表示键值对集合。

Dictionary<string, int> ages = new Dictionary<string, int>();

ages["小明"] = 18;
ages["小红"] = 19;

3. Queue

Queue<T> 表示队列,先进先出。

Queue<string> queue = new Queue<string>();

queue.Enqueue("第一个");
queue.Enqueue("第二个");

Console.WriteLine(queue.Dequeue()); // 第一个

4. Stack

Stack<T> 表示栈,后进先出。

Stack<string> stack = new Stack<string>();

stack.Push("第一个");
stack.Push("第二个");

Console.WriteLine(stack.Pop()); // 第二个

5. HashSet

HashSet<T> 表示不重复集合。

HashSet<int> set = new HashSet<int>();

set.Add(1);
set.Add(1);
set.Add(2);

Console.WriteLine(set.Count); // 2

这些集合中的 <T> 都表示集合中元素的类型。


十二、泛型约束

泛型很灵活,但也有一个问题:

如果不知道 T 到底是什么类型,就不能随便调用它的成员。

例如:

static void PrintName<T>(T item)
{
    // 错误:编译器不知道 T 一定有 Name 属性
    Console.WriteLine(item.Name);
}

因为 T 可能是 intstringDateTime,它们不一定都有 Name 属性。

这时可以使用泛型约束。

泛型约束用 where 关键字。


十三、where T : class

class Repository<T> where T : class
{
    public void Add(T item)
    {
        Console.WriteLine("添加对象");
    }
}

这里:

where T : class

表示:

T 必须是引用类型。

可以使用:

Repository<Student> studentRepository = new Repository<Student>();

不能使用:

// 错误:int 是值类型
Repository<int> intRepository = new Repository<int>();

常见引用类型包括:

  • class
  • string
  • array
  • delegate
  • interface

十四、where T : struct

class ValueBox<T> where T : struct
{
    public T Value { get; set; }
}

这里:

where T : struct

表示:

T 必须是值类型。

可以使用:

ValueBox<int> intBox = new ValueBox<int>();
ValueBox<double> doubleBox = new ValueBox<double>();
ValueBox<DateTime> dateBox = new ValueBox<DateTime>();

不能使用:

// 错误:string 是引用类型
ValueBox<string> stringBox = new ValueBox<string>();

常见值类型包括:

  • int
  • double
  • bool
  • DateTime
  • enum
  • struct

十五、where T : new()

class Factory<T> where T : new()
{
    public T Create()
    {
        return new T();
    }
}

这里:

where T : new()

表示:

T 必须有一个无参数构造方法。

如果没有这个约束,下面代码不能编译:

return new T();

因为编译器不知道 T 能不能被 new 出来。

使用:

class Student
{
    public string Name { get; set; }
}
Factory<Student> factory = new Factory<Student>();

Student student = factory.Create();

如果类只有带参数构造方法,没有无参数构造方法,就不能满足 new() 约束。

注意:

如果同时有多个约束,new() 通常写在最后。

例如:

class Repository<T> where T : class, new()
{
}

十六、where T : 基类

可以限制 T 必须继承某个基类。

class Animal
{
    public string Name { get; set; }

    public void Eat()
    {
        Console.WriteLine(Name + " 正在吃东西");
    }
}
class Dog : Animal
{
}

泛型约束:

class AnimalBox<T> where T : Animal
{
    public void Feed(T animal)
    {
        animal.Eat();
    }
}

因为写了:

where T : Animal

所以在 AnimalBox<T> 中,编译器知道 T 一定是 AnimalAnimal 的子类。

因此可以调用:

animal.Eat();

使用:

AnimalBox<Dog> dogBox = new AnimalBox<Dog>();

dogBox.Feed(new Dog { Name = "小狗" });

十七、where T : 接口

可以限制 T 必须实现某个接口。

interface IPrintable
{
    void Print();
}
class Report : IPrintable
{
    public void Print()
    {
        Console.WriteLine("打印报表");
    }
}

泛型方法:

static void PrintItem<T>(T item) where T : IPrintable
{
    item.Print();
}

使用:

Report report = new Report();

PrintItem(report);

因为 T 被约束为必须实现 IPrintable,所以可以安全调用 Print()

这类约束非常常见。


十八、多个约束

一个类型参数可以有多个约束。

class Repository<T> where T : class, IEntity, new()
{
    public T Create()
    {
        T entity = new T();
        entity.Id = 1;
        return entity;
    }
}

假设接口是:

interface IEntity
{
    int Id { get; set; }
}

这里:

where T : class, IEntity, new()

表示:

  • T 必须是引用类型。
  • T 必须实现 IEntity 接口。
  • T 必须有无参数构造方法。

约束顺序通常是:

where T : class, 接口, new()

或者:

where T : 基类, 接口, new()

十九、default(T)

泛型中经常会看到:

default(T)

它表示:

获取 T 类型的默认值。

不同类型默认值不同:

类型 默认值
int 0
double 0
bool false
char '\0'
引用类型 null
结构体 所有字段都是默认值

示例:

static T GetDefault<T>()
{
    return default(T);
}

使用:

Console.WriteLine(GetDefault<int>());    // 0
Console.WriteLine(GetDefault<bool>());   // False

string text = GetDefault<string>();      // null

新版 C# 中也可以写:

return default;

二十、泛型和 var 的区别

初学者容易把泛型的 Tvar 混淆。

它们完全不是一回事。

1. var 是局部变量类型推断

var age = 18;

编译器会推断 ageint

age 的类型仍然是确定的。

这不等于泛型。

2. T 是类型参数

class Box<T>
{
    public T Value { get; set; }
}

T 是一个类型占位符,使用泛型类时再指定。

Box<int> box = new Box<int>();

对比:

对比项 var 泛型 T
本质 让编译器推断局部变量类型 类型参数
使用位置 方法内部的局部变量 类、接口、方法、委托等定义中
是否创建通用代码
类型何时确定 声明变量时 使用泛型类型或方法时

二十一、泛型和 object 的区别

如果 object 能接收所有类型,为什么还需要泛型?

例如:

class ObjectBox
{
    public object Value { get; set; }
}

使用:

ObjectBox box = new ObjectBox();

box.Value = 100;
box.Value = "hello";

看起来很灵活,但问题很多。

1. 取值需要强制转换

int number = (int)box.Value;

如果类型不对,运行时报错。

2. 编译器不能提前发现错误

box.Value = "hello";

int number = (int)box.Value; // 运行时报错

3. 值类型可能发生装箱拆箱

box.Value = 100; // int 装箱成 object

使用泛型:

class Box<T>
{
    public T Value { get; set; }
}
Box<int> box = new Box<int>();

box.Value = 100;

int number = box.Value;

不需要强制转换,类型也更安全。

一句话:

object 是什么都能装,但不够安全;泛型是使用时指定类型,既灵活又安全。


二十二、泛型类型可以嵌套使用

泛型类型可以互相嵌套。

List<List<int>> matrix = new List<List<int>>();

表示一个二维列表。

Dictionary<string, List<int>> scoreMap = new Dictionary<string, List<int>>();

表示:

键是学生姓名,值是这个学生的多个成绩。

使用:

scoreMap["小明"] = new List<int> { 90, 85, 95 };
scoreMap["小红"] = new List<int> { 88, 92 };

还可以更复杂:

Dictionary<string, List<Student>> classStudents;

表示:

一个班级名对应多个学生。

注意:

泛型嵌套太深会降低可读性,可以考虑定义一个有名字的类来表达业务含义。


二十三、泛型类型推断

泛型方法调用时,很多时候可以省略类型参数。

static void Print<T>(T value)
{
    Console.WriteLine(value);
}

完整写法:

Print<int>(100);
Print<string>("hello");

简写:

Print(100);
Print("hello");

编译器会根据传入参数推断 T 的类型。

但是有些情况不能推断。

例如:

static T CreateDefault<T>()
{
    return default(T);
}

调用时没有参数,编译器无法从参数推断 T

// 错误:无法推断 T
var value = CreateDefault();

必须明确指定:

int number = CreateDefault<int>();
string text = CreateDefault<string>();

二十四、泛型静态成员

泛型类型的静态成员是按具体类型分别保存的。

看例子:

class Counter<T>
{
    public static int Count;
}

使用:

Counter<int>.Count++;
Counter<int>.Count++;

Counter<string>.Count++;

Console.WriteLine(Counter<int>.Count);    // 2
Console.WriteLine(Counter<string>.Count); // 1

为什么?

因为:

Counter<int>

和:

Counter<string>

是两个不同的封闭泛型类型。

所以它们各自有自己的静态字段。

教学时可以这样说:

泛型类换了类型参数,就像生成了不同版本的类;每个版本有自己的静态成员。


二十五、泛型继承

泛型类也可以继承。

1. 子类仍然是泛型

class BaseRepository<T>
{
    public void Add(T item)
    {
        Console.WriteLine("添加数据");
    }
}

class Repository<T> : BaseRepository<T>
{
}

使用:

Repository<Student> repository = new Repository<Student>();

2. 子类指定具体类型

class StudentRepository : BaseRepository<Student>
{
}

这时 StudentRepository 就不是泛型类了,它固定操作 Student

使用:

StudentRepository repository = new StudentRepository();
repository.Add(new Student { Id = 1, Name = "小明" });

3. 子类增加新的类型参数

class CacheRepository<T, TKey> : BaseRepository<T>
{
    public TKey Key { get; set; }
}

泛型继承时要注意:

父类需要什么类型参数,子类要么继续传递,要么指定具体类型。


二十六、泛型和可空类型

C# 中的可空值类型也是泛型。

Nullable<int> number = 10;

它通常写成简写形式:

int? number = 10;

两者含义一样。

int? age = null;

int 本来是值类型,不能直接为 null

但是 int? 可以。

本质上:

int?

就是:

Nullable<int>

这也是泛型在 .NET 中的典型应用。


二十七、协变和逆变的简单了解

这是泛型的进阶内容,初学阶段只需要简单了解。

有些泛型接口支持类型转换,比如:

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;

这是因为 IEnumerable<out T> 中的 out 表示协变。

简单理解:

如果只从泛型对象中读取数据,某些情况下子类型集合可以当成父类型集合使用。

还有一种 in,叫逆变,常见于比较器、委托等场景。

例如:

Action<object> printObject = obj => Console.WriteLine(obj);
Action<string> printString = printObject;

初学阶段不要被这些概念吓到。

先掌握:

  • 泛型类
  • 泛型方法
  • 泛型接口
  • 泛型约束
  • 常用泛型集合

协变和逆变可以以后进阶学习。


二十八、注意事项一:不要滥用泛型

泛型是为了解决“同一套逻辑适配多种类型”的问题。

如果某个类只会处理一种明确类型,就不一定需要泛型。

不推荐:

class StudentService<T>
{
}

如果这个服务永远只处理学生,直接写:

class StudentService
{
}

更清楚。

判断是否需要泛型,可以问自己:

这段代码是否真的要适配多种类型?

如果答案是否定的,就不要为了高级而泛型。


二十九、注意事项二:类型参数命名要清楚

简单泛型可以用 T

class Box<T>
{
}

多个类型参数时,建议用更清楚的名字:

class Result<TData, TError>
{
}

不推荐:

class Result<T, U>
{
}

虽然能运行,但业务含义不明显。

更推荐:

class Result<TData, TError>
{
    public TData Data { get; set; }
    public TError Error { get; set; }
}

清晰命名可以降低学习和维护成本。


三十、注意事项三:泛型里不能随便使用 T 的成员

错误示例:

static void PrintName<T>(T item)
{
    Console.WriteLine(item.Name);
}

原因:

编译器不知道 T 是否一定有 Name 属性。

解决方式一:使用接口约束。

interface IHasName
{
    string Name { get; }
}
static void PrintName<T>(T item) where T : IHasName
{
    Console.WriteLine(item.Name);
}

解决方式二:不使用泛型,直接使用明确类型。

static void PrintName(Student student)
{
    Console.WriteLine(student.Name);
}

原则:

泛型不是动态类型,编译器只允许你使用它能确定存在的成员。


三十一、注意事项四:new T() 需要 new() 约束

错误写法:

static T Create<T>()
{
    return new T();
}

编译器会报错,因为它不知道 T 是否有无参数构造方法。

正确写法:

static T Create<T>() where T : new()
{
    return new T();
}

使用:

Student student = Create<Student>();

注意:

new() 约束要求类型必须有 public 的无参数构造方法。


三十二、注意事项五:class 和 struct 约束不能乱用

where T : class

表示只允许引用类型。

where T : struct

表示只允许非可空值类型。

不要为了“看起来严谨”随便加约束。

例如:

class Box<T> where T : class
{
    public T Value { get; set; }
}

这会导致:

Box<int> box = new Box<int>(); // 错误

如果 Box<T> 本来应该支持 int,这个约束就加错了。

原则:

约束越多,能使用的类型越少;只有确实需要时才加约束。


三十三、注意事项六:泛型集合要指定正确类型

List<int> numbers = new List<int>();

这个列表只能放 int

不能写:

// 错误
numbers.Add("hello");

如果你想保存学生,就应该写:

List<Student> students = new List<Student>();

不要为了偷懒写:

List<object> list = new List<object>();

除非你确实需要保存多种完全不同的类型。

否则 List<object> 会让类型变得不明确,取值时也更容易出错。


三十四、注意事项七:泛型嵌套不要过深

下面这种类型虽然合法,但可读性很差:

Dictionary<string, List<Dictionary<int, List<Student>>>> data;

看到这种代码时,别人很难马上理解它代表什么业务含义。

可以考虑定义类:

class ClassScoreGroup
{
    public string ClassName { get; set; }
    public List<StudentScoreGroup> Groups { get; set; }
}

有名字的类比深层泛型嵌套更适合表达复杂业务。

原则:

泛型可以嵌套,但不要让类型声明变成一长串难读的代码。


三十五、注意事项八:泛型不是越通用越好

有些代码看起来很通用,但实际难以理解。

例如:

class Processor<TInput, TOutput, TContext, TOptions, TResult>
{
}

如果每个类型参数都有明确意义,这样可以。

但如果只是为了“万能”,代码会变得很抽象。

更好的做法是:

  • 先满足当前真实需求。
  • 类型参数有明确责任。
  • 不要为了未来可能发生的变化过早设计。

教学时可以提醒:

泛型是工具,不是装饰。能让代码更清楚才用。


三十六、注意事项九:理解编译时类型安全

泛型的很多错误是在编译阶段发现的。

例如:

List<int> numbers = new List<int>();

numbers.Add("hello");

这段代码编译都过不了。

这是一件好事。

因为越早发现错误,修复成本越低。

对比 ArrayList

ArrayList list = new ArrayList();

list.Add(100);
list.Add("hello");

int number = (int)list[1]; // 运行时才报错

泛型让很多问题提前暴露。

一句话:

泛型不是限制你,而是在编译阶段帮你拦住错误。


三十七、注意事项十:泛型类型参数不是变量

下面写法是错误的理解:

class Box<T>
{
    public void Test()
    {
        // T 不是普通变量,不能这样使用
    }
}

T 表示类型,不表示某个具体对象。

正确用法:

private T value;

public T GetValue()
{
    return value;
}

也就是说:

  • T 可以用在类型位置。
  • T 不能当成一个普通变量直接使用。

例如:

T item;
List<T> list;
Func<T, bool> condition;

这些都是正确的。


三十八、完整示例:泛型缓存类

下面写一个泛型缓存类,用来保存不同类型的数据。

using System;
using System.Collections.Generic;

class Cache<T>
{
    private Dictionary<string, T> data = new Dictionary<string, T>();

    public void Set(string key, T value)
    {
        data[key] = value;
    }

    public T Get(string key)
    {
        if (data.ContainsKey(key))
        {
            return data[key];
        }

        return default(T);
    }
}

class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class Program
{
    static void Main()
    {
        Cache<int> scoreCache = new Cache<int>();
        scoreCache.Set("math", 95);

        int score = scoreCache.Get("math");
        Console.WriteLine(score);

        Cache<Student> studentCache = new Cache<Student>();
        studentCache.Set("student1", new Student { Id = 1, Name = "小明" });

        Student student = studentCache.Get("student1");
        Console.WriteLine(student.Name);
    }
}

这个例子中:

  • Cache<int> 表示缓存整数。
  • Cache<Student> 表示缓存学生对象。
  • 同一套缓存逻辑可以用于不同类型。

三十九、完整示例:泛型仓储

using System;
using System.Collections.Generic;

interface IEntity
{
    int Id { get; set; }
}

class Student : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class Product : IEntity
{
    public int Id { get; set; }
    public string Title { get; set; }
}

class Repository<T> where T : IEntity
{
    private List<T> items = new List<T>();

    public void Add(T item)
    {
        items.Add(item);
    }

    public T GetById(int id)
    {
        foreach (T item in items)
        {
            if (item.Id == id)
            {
                return item;
            }
        }

        return default(T);
    }
}

class Program
{
    static void Main()
    {
        Repository<Student> studentRepository = new Repository<Student>();

        studentRepository.Add(new Student { Id = 1, Name = "小明" });

        Student student = studentRepository.GetById(1);
        Console.WriteLine(student.Name);

        Repository<Product> productRepository = new Repository<Product>();

        productRepository.Add(new Product { Id = 10, Title = "键盘" });

        Product product = productRepository.GetById(10);
        Console.WriteLine(product.Title);
    }
}

这个例子重点看:

class Repository<T> where T : IEntity

它表示:

Repository 可以管理任意实体类型,但这个类型必须实现 IEntity。

因此在 Repository<T> 内部可以安全访问:

item.Id

四十、课堂讲解建议

讲泛型时,可以按这个顺序:

  1. 先讲为什么需要泛型:避免为每种类型写重复代码。
  2. Box<T> 讲清楚 T 是类型占位符。
  3. List<int>List<string> 讲泛型集合。
  4. 对比 object,说明泛型的类型安全。
  5. 讲泛型类、泛型方法、泛型接口。
  6. where 约束,解释为什么泛型里不能随便调用成员。
  7. 最后讲注意事项和常见错误。

学生最容易混淆的点:

  • T 是类型,不是变量。
  • List<int> 只能放 int
  • Func<T> 中的 T 也是类型参数。
  • where T : class 不是让 T 变成某个类,而是限制 T 必须是引用类型。
  • new T() 必须有 where T : new() 约束。

可以用这句话帮助理解:

泛型就是“先写一套通用模板,使用时再把具体类型填进去”。


四十一、练习题

练习 1:定义泛型 Box

定义一个 Box<T> 类,可以保存任意类型的值。

参考答案:

class Box<T>
{
    public T Value { get; set; }
}

使用:

Box<int> intBox = new Box<int>();
intBox.Value = 100;

Box<string> stringBox = new Box<string>();
stringBox.Value = "hello";

练习 2:定义泛型方法 Print

定义一个泛型方法 Print<T>,可以打印任意类型的值。

参考答案:

static void Print<T>(T value)
{
    Console.WriteLine(value);
}

使用:

Print(100);
Print("你好");
Print(3.14);

练习 3:定义泛型交换方法

定义一个泛型方法 Swap<T>,交换两个变量的值。

参考答案:

static void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

练习 4:定义带约束的泛型方法

定义一个接口 IPrintable,然后写一个泛型方法,只允许打印实现了 IPrintable 的对象。

参考答案:

interface IPrintable
{
    void Print();
}
static void PrintItem<T>(T item) where T : IPrintable
{
    item.Print();
}

练习 5:定义泛型仓储

定义一个 Repository<T>,内部使用 List<T> 保存数据,并提供 Add 方法。

参考答案:

class Repository<T>
{
    private List<T> items = new List<T>();

    public void Add(T item)
    {
        items.Add(item);
    }
}

四十二、总结

泛型是 C# 中非常重要的语法,广泛用于集合、委托、接口、方法、框架和业务代码中。

可以记住下面几句话:

  1. 泛型就是先不固定类型,使用时再指定类型。
  2. 泛型可以减少重复代码。
  3. 泛型可以提高类型安全。
  4. 泛型可以避免很多不必要的强制转换。
  5. T 是类型参数,不是普通变量。
  6. 泛型类、泛型方法、泛型接口、泛型委托都很常见。
  7. where 用来给类型参数添加约束。
  8. new T() 需要 where T : new() 约束。
  9. 泛型集合如 List<T>Dictionary<TKey, TValue> 是最常见的泛型应用。
  10. 泛型不是越多越好,只有需要适配多种类型时才使用。

一句话概括:

泛型让我们用一套代码处理多种类型,同时保持类型安全和代码清晰,是 C# 中非常核心的基础能力。

0
博主关闭了当前页面的评论