CSharp泛型学习笔记

一、为什么需要泛型

先看一个很常见的场景:有几段代码,逻辑完全一样,只是传入的参数类型或返回类型不同

public string GetClassType(Teacher teacher)
{
    return teacher.GetType().Name;
}

public string GetClassType(Student student)
{
    return student.GetType().Name;
}

public string GetClassType(Worker worker)
{
    return worker.GetType().Name;
}

public int GetInt(string str)
{
    return int.Parse(str);
}

public float GetFloat(string str)
{
    return float.Parse(str);
}

上面的代码非常啰嗦:为了处理 TeacherStudentWorker 三种类型,写了三个几乎一样的方法;为了转 intfloat,又写了两个方法。

我们当然希望:只写一份逻辑,就能适用于各种类型。泛型就是为此而生的。

二、先用 object 行不行

有人会想:既然 object 是所有类的基类,任何类型都能当 object 传进去,那用一个 object 参数不就通用了吗?

public string GetClassType(object obj)
{
    return obj.GetType().Name;
}

这个思路能用,但有两个明显的坑:

1. 性能问题:装箱与拆箱

值类型(如 int)传给 object 参数时,会发生装箱(boxing):int -> object;取出来转回实际类型时,又发生拆箱(unboxing):object -> int。装箱拆箱会额外分配内存、有性能损耗。

2. 类型安全问题:编译能过,运行可能崩

因为参数是 object,编译器无法在编译期检查类型,很多错误只能等到运行时才暴露,直接抛异常。

泛型正好能同时解决这两个问题:编译期就确定类型(类型安全),且不会装箱拆箱(性能好)

三、泛型的定义

泛型(generic) 是 C# 2.0 推出的语法,专门用于处理"多段代码在不同数据类型上执行相同指令"的情况。

核心思想:用一个占位符 T 代替实际数据类型,等到真正调用时,才指定具体类型。

按定义的位置不同,可以定义:

类型 写法示例
泛型方法 GetUserName<T>()
泛型类 BaseModel<T>
泛型接口 IData<T>
泛型委托 CustHandler<int, string>
// 泛型方法
public class CommonClass
{
    public string GetClassType<T>(T t)
    {
        return t.GetType().Name;
    }
}

// 泛型类
public class MyList<T>
{
    List<T> list = new List<T>();

    public void Add(T t)
    {
        list.Add(t);
    }
}

泛型思想一句话总结:延迟一切可以延迟的 —— 把类或方法中"数据类型"的确定,延迟到调用时才指定。

四、泛型的特性

  • 最大限度重用代码保护类型安全提高性能
  • 最常见的用途是创建集合类
  • .NET 类库在 System.Collections.Generic 命名空间下提供了 List<T>Dictionary<TKey, TValue> 等泛型集合,建议用它代替旧的 System.Collections 集合;
  • 可以自定义泛型接口、泛型类、泛型方法、泛型事件、泛型委托;
  • 可以对泛型加约束where)来限定类型范围;
  • 泛型类型参数的信息可以在运行时通过反射获取。

五、泛型方法

泛型方法:在声明中包含类型参数的方法,用一个方法满足不同类型需求。

写法就是方法名后面加尖括号 <T>

public string GetClassType<T>(T t)
{
    return t.GetType().Name;
}

int intVal = 5;
GetClassType<int>(intVal);   // 显式指定类型参数

其中 T 就是类型参数:声明时不写死类型,用 T 占位;调用时才用实际类型替换。

类型推断

如果方法的参数类型就是类型参数,调用时可以省略类型参数,编译器会根据传入参数自动推断:

int intVal = 5;
GetClassType(intVal);        // 省略 <int>,编译器自动推断为 int

类型推断对静态方法和实例方法都适用。但要注意:编译器只能根据传入的参数推断,无法仅根据约束或返回值推断。

所以下面这两种情况不能省略类型参数,必须显式指定:

// 1. 没有参数,无法推断
public string GetClassType<T>()
{
    return typeof(T).Name;
}

// 2. 有参数,但参数类型不是类型参数 T,无法推断
public T ToType<T>(string str)
{
    Type type = typeof(T);
    return (T)Convert.ChangeType(str, type);
}

// 调用时必须显式写类型
string name = GetClassType<Student>();
int num = ToType<int>("3.9");

六、泛型类

泛型类用于封装"不特定于某个具体类型"的操作,最常见用途是列表、哈希表、堆栈、队列、树等集合。因为这些集合无论存什么类型,"添加、删除、计数"的操作方式都基本一样。

先看一个只针对 IncomeInfo 的具体类:

public class IncomeList
{
    List<IncomeInfo> list = new List<IncomeInfo>();

    public void Add(IncomeInfo income)
    {
        list.Add(income);
    }

    public void Delete(IncomeInfo income)
    {
        list.Remove(income);
    }

    public int GetCount()
    {
        return list.Count;
    }
}

如果还要处理 ItemInfoExpendInfo 等类型,就要重复写一堆类似的类。改成泛型类,一份搞定:

public class DataList<T>
{
    List<T> list = new List<T>();

    public void Add(T t)
    {
        list.Add(t);
    }

    public void Delete(T t)
    {
        list.Remove(t);
    }

    public int GetCount()
    {
        return list.Count;
    }
}

// 使用
DataList<ItemInfo> itemList = new DataList<ItemInfo>();
ItemInfo item1 = new ItemInfo() { ItemId = 1, ItemName = "收入" };
ItemInfo item2 = new ItemInfo() { ItemId = 2, ItemName = "支出" };
itemList.Add(item1);
itemList.Add(item2);
itemList.Delete(item1);
int itemCount = itemList.GetCount();

自定义泛型类的三个注意点

  1. 类型参数的数量:可参数化的类型越多越灵活、可重用性越高,但不能过度(泛化太多反而难读、难维护);
  2. 约束:要给类型参数加什么约束,以限定类型范围(有些类型不适合当前类的方法);
  3. 继承拆分:可以把泛型行为拆分成基类和子类。

七、常见集合类型

7.1 ArrayList(不建议使用)

ArrayList非泛型的动态数组,能动态增删元素。但它的 Add 方法接受 object,什么类型都能加。

问题:值类型存取会发生装箱拆箱,有性能损耗;且它不是类型安全的。

ArrayList arrList = new ArrayList();
arrList.Add(12);
arrList.Add(35.6);
arrList.Add(new IncomeInfo() { IncomeId = 1, IncomeName = "领工资" });
arrList.RemoveAt(1);
int iVal = Convert.ToInt32(arrList[0]);   // 拆箱

结论:不建议用 ArrayList,建议用 List<T>

7.2 List<T>(首选)

List<T> 是泛型集合,强类型、类型安全,可动态增删元素,适合存储数目不固定的一组信息。是存储列表数据的首选。

常用操作:

List<int> ids = new List<int>();
ids.Add(101);                             // 添加到末尾
ids.Add(102);
ids.AddRange(new int[] { 103, 104, 105 }); // 一次添加多个
ids.Remove(103);                           // 移除指定元素
ids.RemoveAt(2);                           // 移除第 3 个元素
bool bl = ids.Contains(105);               // 判断是否存在
int newId = ids.Find(id => id == 103);     // 查找元素
int index = ids.IndexOf(104);              // 获取索引
List<int> newList = ids.Where(id => id > 103).ToList();  // 条件筛选
ids.Sort();                                // 排序
ids.Reverse();                             // 翻转

什么时候用 List? 暂存编号集合、对象列表、选中的数据等,是存储列表数据的首选。

7.3 Dictionary<TKey, TValue>

表示键值对集合,从一组"键"映射到一组"值"。也是强类型、类型安全的。通过键检索值速度非常快。

要点:

  • 键必须唯一,值可以重复;
  • 键和值都是类型参数,可以是任意类型(stringint、自定义类型等)。
Dictionary<int, string> dicList = new Dictionary<int, string>();
dicList.Add(101, "收入");
dicList.Add(102, "收入");
dicList.Add(103, "支出");
dicList.Add(104, "收入");
dicList.Add(105, "支出");

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

// 方式一:遍历每个键值对
foreach (KeyValuePair<int, string> kv in dicList)
{
    if (kv.Value == "收入")
    {
        itemIds.Add(kv.Key);
    }
}

// 方式二:遍历每个键
foreach (int key in dicList.Keys)
{
    if (dicList[key] == "收入")
    {
        itemIds.Add(key);
    }
}

// 判断键是否存在,再取值
if (dicList.ContainsKey(104))
{
    string val = dicList[104];
}

什么时候用 Dictionary? 存储一系列有对应关系的信息,比如"编号 → 名称"、"ID → 对象"等,需要根据键快速查值。

八、泛型约束(where)

泛型定义中可以用 where 子句,给类型参数加约束,限定类型的范围

约束类型包括:接口约束、基类约束、引用类型约束、值类型约束、无参构造函数约束。

约束规则(必记)

  1. 可以同时使用多个约束;
  2. 值类型约束与引用类型约束不能同时使用;
  3. 引用类型约束不能与基类约束同时使用;
  4. 如果有基类约束和其他约束,基类约束必须写在最前面
  5. 多个约束里如果有 new()new() 必须是最后一个

8.1 值类型约束(where T : struct)

限定实际类型不能是引用类型(即必须是值类型)。

public T ToType<T>(string str) where T : struct
{
    return (T)Convert.ChangeType(str, typeof(T));
}

float f1 = ToType<int>("3.9");       // 正确,int 是值类型
string s = ToType<string>("aaaa");   // 错误,string 是引用类型

8.2 引用类型约束(where T : class)

限定实际类型必须是引用类型

public class Record<T> where T : class
{
    List<T> list = new List<T>();

    public void AddRecord(T t)
    {
        list.Add(t);
    }
}

Record<IncomeInfo> income = new Record<IncomeInfo>();   // 正确
Record<double> record1 = new Record<double>();          // 错误,double 是值类型

8.3 基类约束(where T : BaseModel)

限定实际类型必须是某个基类或其子类

public class BaseModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Amount { get; set; }
}

public class IncomeInfo : BaseModel { }

public void GetRecordInfo<T>(T t) where T : BaseModel
{
    Console.WriteLine("编号:" + t.Id + ",名称:" + t.Name + ",金额:" + t.Amount);
}

IncomeInfo incomeInfo = new IncomeInfo();
incomeInfo.Id = 1;
incomeInfo.Name = "工资收入";
incomeInfo.Amount = 2000;
GetRecordInfo(incomeInfo);   // 正确,IncomeInfo 是 BaseModel 的子类

有了基类约束,编译器就能确定 T 一定有 IdNameAmount 这些成员,于是可以在方法里直接访问它们。

8.4 接口约束(where T : IRecord)

限定实际类型必须实现了某个接口

public interface IRecord
{
    decimal CalTotalAmount(decimal amount);
}

public class ExpendInfo : BaseModel, IRecord
{
    public decimal Price { get; set; }

    public decimal CalTotalAmount(decimal amount)
    {
        return GenericMethod.totalAmount - amount;
    }
}

// 接口约束 + 基类约束同时使用
public decimal GetTotalAmount<T>(T t) where T : BaseModel, IRecord
{
    return t.CalTotalAmount(t.Amount);
}

ExpendInfo exInfo = new ExpendInfo();
exInfo.Id = 2;
exInfo.Name = "借钱给朋友";
exInfo.Amount = 1000;
decimal totalAmount = GetTotalAmount(exInfo);

有了接口约束,编译器就能确定 T 实现了 IRecord,可以放心调用 CalTotalAmount 方法。

8.5 无参构造函数约束(where T : new())

限定实际类型必须有无参构造函数,这样就能在泛型里用 new T() 创建对象。

public T CreateObj<T>() where T : new()
{
    T t = new T();
    return t;
}

IncomeInfo income = CreateObj<IncomeInfo>();

九、一个综合示例

把泛型方法、泛型类、约束串起来,写一个能跑通的小程序:

using System;
using System.Collections.Generic;

class Program
{
    // 泛型类:封装通用的增删查
    public class DataList<T>
    {
        private List<T> list = new List<T>();

        public void Add(T t) => list.Add(t);
        public void Remove(T t) => list.Remove(t);
        public int Count => list.Count;
        public List<T> GetAll() => list;
    }

    static void Main()
    {
        DataList<string> names = new DataList<string>();
        names.Add("张三");
        names.Add("李四");
        names.Remove("张三");
        Console.WriteLine("剩余数量:" + names.Count);   // 输出:1

        // 泛型方法
        int num = ParseValue<int>("3.9");
        Console.WriteLine("解析结果:" + num);           // 输出:3
    }

    // 值类型约束的泛型方法
    static T ParseValue<T>(string str) where T : struct
    {
        return (T)Convert.ChangeType(str, typeof(T));
    }
}

十、面试题

1. 什么是泛型?它解决了什么问题?

:泛型是用一个类型参数(如 T)代替具体类型,延迟到调用时才确定实际类型的机制。它解决的是"多段代码逻辑相同、只是类型不同"的重复编写问题,能最大限度重用代码。

2. 泛型相比用 object 参数有什么优势?

  • 类型安全:编译期就能检查类型,避免运行时才报错;
  • 性能更好:避免值类型的装箱拆箱;
  • 代码更简洁:调用方不用手动做类型转换(object 需要强转)。

3. 泛型约束有哪些?各自的语法和作用?

约束 语法 作用
值类型约束 where T : struct T 必须是值类型
引用类型约束 where T : class T 必须是引用类型
基类约束 where T : Base T 必须是某基类或其子类
接口约束 where T : IRecord T 必须实现某接口
无参构造函数约束 where T : new() T 必须有无参构造函数,可用 new T()

4. 泛型约束有哪些规则?

  1. 可同时使用多个约束;
  2. 值类型约束和引用类型约束不能同时用;
  3. 引用类型约束不能与基类约束同时用;
  4. 有基类约束时,基类约束必须写在最前面;
  5. 多个约束里若有 new(),它必须是最后一个。

5. 泛型方法的类型推断在什么情况下不能用?

:编译器只能根据传入参数推断类型参数,无法仅根据约束或返回值推断。所以下面两种情况必须显式指定类型参数:

  • 方法没有参数(如 GetClassType<T>());
  • 有参数但参数类型不是类型参数 T(如 ToType<T>(string str))。

6. List<T> 和 ArrayList 有什么区别?

  • ArrayList 是非泛型集合,元素是 object,不类型安全,值类型会装箱拆箱,有性能损耗;
  • List<T> 是泛型集合,强类型、类型安全、无装箱拆箱,性能更好。

所以现在推荐用 List<T>,不建议用 ArrayList

7. Dictionary 的键有什么要求?键重复会怎样?

:Dictionary 的键必须唯一(值可以重复)。如果 Add 一个已存在的键,会抛出 ArgumentException 异常。因此添加前通常先用 ContainsKey 判断,或使用索引器 dict[key] = value 赋值(键存在则覆盖值,不存在则新增)。

8. 值类型约束和引用类型约束能同时用吗?

:不能。where T : structwhere T : class 互斥,同时使用会导致编译错误。

9. 泛型方法和泛型类里的 T 能继承或实现接口吗?如何让编译器知道 T 有某个成员?

T 本身不能直接继承或实现,但可以通过约束让编译器"知道"T 有哪些成员。比如加基类约束 where T : BaseModel,编译器就确定 T 有 BaseModel 的成员;加接口约束 where T : IRecord,就能调用 IRecord 的方法。没有约束时,T 只能当 object 用,只能访问 object 的成员。

10. 泛型的类型参数信息在运行时能拿到吗?怎么拿?

:能。C# 的泛型在运行时保留类型信息(不像 Java 的泛型擦除),可以通过反射获取。比如在泛型方法里用 typeof(T) 拿到 T 对应的 Type 对象。

11. 泛型类可以被继承吗?继承时类型参数如何处理?

:可以。继承时有两种方式:

  • 继承时指定具体类型:class MyList : List<int>,此时子类不再是泛型类;
  • 继承时继续传递类型参数:class MyList<T> : List<T>,子类仍是泛型类。

12. 为什么说"延迟一切可以延迟的"是泛型的核心思想?

:泛型不要求编写时就知道具体类型,而是用 T 占位,把"确定类型"这件事推迟到真正使用时。这样一份泛型代码就能服务于任意类型,达到代码复用、类型安全、高性能的目的。

posted @ 2026-08-15 15:16  春煦景明  阅读(3)  评论(0)    收藏  举报