C#学习记录-泛型

在C#中,泛型是一个非常核心的特性。它广泛应用与集合、委托、接口、方法、类以及很多.NET基础库中。比如我们经常看到这些写法:

List<T>、Dictionary<TKey,TValue>、Func<T,TResult>、Nullable<T>。这些都和泛型有关。

泛型的出现,大大提高了代码的复用性,类型安全性,可维护性,性能表现。所以,泛型不仅仅是语法层面的知识点,更是一种重要的编程思想。

一、什么是泛型

泛型可以简单理解为:把类型当作参数传递。平时我们调用方法时,传的时数据参数,比如

cs 复制代码
Add(1,2);

而泛型做的事情是,除了传数据,还可以传类型参数。例如

cs 复制代码
List<int>
List<string>

这里的int和string就是传给泛型的类型参数。也就是说:

普通参数决定传什么值,泛型参数决定用什么类型。

1.1一个直观理解

你可以把泛型想象成类型占位符。例如

cs 复制代码
public class Box<T>
{
    public T Value { get; set; }
}

这里的T不是某个具体类型,它只是一个占位符。等真正使用的时候,才决定它是什么类型:

cs 复制代码
Box<int> intBox = new Box<int>();
Box<string> strBox = new Box<string>();

这样同一套代码就能适合于不同类型。

二、为什么需要泛型

这是理解泛型最关键的一步。很多人可能会问:没有泛型也能写程序,为啥还要用泛型?

答案是,因为泛型可以同时解决代码复用,类型安全和性能问题

2.1不适用泛型时的问题

在泛型出现之前,如果想写一个能存放任意类型的容器,通常会用object。

例如:

cs 复制代码
public class Box
{
    public object Value { get; set; }
}

使用的时候:

cs 复制代码
Box box1 = new Box();
box1.Value = 100;

Box box2 = new Box();
box2.Value = "Hello";

看起来似乎也能达到通用的目的,但这里有几个问题。

1.类型不安全

cs 复制代码
Box box = new Box();
box.Value = "Hello";

int num = (int)box.Value; // 运行时出错

因为object可以接收任何类型,所以编译器无法提前检查错误。只有运行时强制转换时,才会发现类型不匹配。

2.需要频繁强制转换

cs 复制代码
string text = (string)box.Value;

这种写法不仅麻烦,而且容易出错。

3.值类型会发生装箱和拆箱

cs 复制代码
object obj = 123;   // 装箱
int num = (int)obj; // 拆箱

这会带来额外的性能开销。

2.2泛型带来的好处

泛型的出现正式为了改进这些问题。

cs 复制代码
public class Box<T>
{
    public T Value { get; set; }
}

使用时:

cs 复制代码
Box<int> box = new Box<int>();
box.Value = 100;

int num = box.Value; // 不需要强转

这样就有几个明显的优势,类型明确,编译器检查,不需要强制转换,避免不必要的装箱拆箱。

三、泛型类

泛型最常见的用法之一就是泛型类。

cs 复制代码
public class Box<T>
{
    public T Value { get; set; }

    public void ShowValue()
    {
        Console.WriteLine(Value);
    }
}

这里的T表示:Value的类型不固定,具体是什么类型,由创建对象时决定。

就像下面这样:

cs 复制代码
using System;

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

    public void ShowValue()
    {
        Console.WriteLine(Value);
    }
}

public class Program
{
    public static void Main()
    {
        Box<int> intBox = new Box<int>();
        intBox.Value = 123;
        intBox.ShowValue();

        Box<string> strBox = new Box<string>();
        strBox.Value = "你好,泛型";
        strBox.ShowValue();
    }
}

这个例子在于,我们不需要专门写IntBox和StringBox,只需要写一个Box<T>就能支持不同的类型。这就是泛型代码复用的体现。

四、泛型方法

除了类可以使用泛型,方法也可以使用泛型。

示例:

cs 复制代码
using System;

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

    public static void Main()
    {
        int x = 10;
        int y = 20;
        Swap(ref x, ref y);
        Console.WriteLine($"x={x}, y={y}");

        string s1 = "Hello";
        string s2 = "World";
        Swap(ref s1, ref s2);
        Console.WriteLine($"s1={s1}, s2={s2}");
    }
}

这里的Swap<T>表示这个方法适用于任意类型T,只要两个参数类型一致,就可以使用同一套交换逻辑。

五、泛型接口

接口同样也可以定义为泛型接口。

cs 复制代码
public interface IRepository<T>
{
    void Add(T item);
    T GetById(int id);
}

这里表示这个接口可以用于不同的数据类型,T在具体实现时再决定。

示例:

cs 复制代码
using System;

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

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

public class UserRepository : IRepository<User>
{
    public void Add(User item)
    {
        Console.WriteLine($"添加用户:{item.Name}");
    }

    public User GetById(int id)
    {
        return new User { Id = id, Name = "张三" };
    }
}

public class Program
{
    public static void Main()
    {
        IRepository<User> repo = new UserRepository();
        repo.Add(new User { Id = 1, Name = "李四" });

        User user = repo.GetById(1);
        Console.WriteLine($"获取到用户:{user.Name}");
    }
}

六、泛型委托

委托也可以用泛型。实际上我们常见的Action<T>、Func<T,TResult>、Predicate<T>本质上都是泛型委托。

七、自定义泛型委托

cs 复制代码
public delegate T Transformer<T>(T input);

表示,输入参数类型是T,返回值类型也是T。

示例

cs 复制代码
using System;

public delegate T Transformer<T>(T input);

public class Program
{
    public static void Main()
    {
        Transformer<int> square = x => x * x;
        Console.WriteLine(square(5));

        Transformer<string> toUpper = s => s.ToUpper();
        Console.WriteLine(toUpper("hello"));
    }
}

八、常见泛型类型参数命名

在写泛型时,虽然参数名可以随便取,但通常会遵循一些约定。

  • T:表示一个普通类型参数
  • TKey:表示键类型
  • TValue:表示值类型
  • TResult:表示返回结果类型
  • TItem:表示集合中的元素类型
  • TSource:表示源数据类型

九、C#中常见的泛型集合

泛型在.NET中最常见的应用就是集合类。在没有泛型的时代,集合通常保存的是object,使用起来不够安全。而泛型集合可以明确元素类型。

9.1List

cs 复制代码
using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        List<int> numbers = new List<int>();
        numbers.Add(10);
        numbers.Add(20);
        numbers.Add(30);

        foreach (int n in numbers)
        {
            Console.WriteLine(n);
        }
    }
}

9.2Dictionary<TKey,TValue>

cs 复制代码
using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        Dictionary<int, string> students = new Dictionary<int, string>();
        students.Add(1, "张三");
        students.Add(2, "李四");

        Console.WriteLine(students[1]);
        Console.WriteLine(students[2]);
    }
}

9.3Queue

cs 复制代码
Queue<string> queue = new Queue<string>();
queue.Enqueue("A");
queue.Enqueue("B");
Console.WriteLine(queue.Dequeue());

9.4Stack

cs 复制代码
Stack<int> stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
Console.WriteLine(stack.Pop());

十、泛型约束

泛型虽然灵活,但有时候我们不希望类型参数完全任意,而是希望它满足某些条件。这时就需要使用泛型约束,约束的作用就是:限制泛型参数必须符合某些限制。

下面是整理的泛型约束:

10.1where T: calss

表示T必须是引用类型

cs 复制代码
public class MyClass<T> where T : class
{
}

10.2where T : struct

表示T必须是值类型。

cs 复制代码
public class MyStructBox<T> where T : struct
{
}

10.3where T : new()

表示T必须有一个公共的无参构造函数

cs 复制代码
public class Factory<T> where T : new()
{
    public T Create()
    {
        return new T();
    }
}

10.4 where T : BaseClass

表示T必须继承某个基类

cs 复制代码
public class Animal
{
    public void Eat()
    {
        Console.WriteLine("动物在吃东西");
    }
}

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

10.5where T : IInterface

表示T必须实现某个接口

cs 复制代码
public interface IRun
{
    void Run();
}

public class Runner<T> where T : IRun
{
    public void Start(T obj)
    {
        obj.Run();
    }
}

10.6多个约束一起使用

cs 复制代码
public class MyClass<T>
    where T : class, new()
{
}

表示T必须是引用类型并且有无参构造函数。

十一、泛型方法的类型推断

调用泛型方法时,很多时候不需要手动写出类型参数,编译器可以自动推断。

示例:

cs 复制代码
using System;

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

    public static void Main()
    {
        Print<int>(100);
        Print<string>("Hello");

        // 也可以省略类型参数
        Print(200);
        Print("World");
    }
}

编译器可以根据传入参数自动推断T的类型。这让泛型在实际使用中更加简洁。

十二、总结

C#泛型是一种非常重要的语言特性,它允许我们把类型作为参数传递,从而实现逻辑复用、类型安全、性能优化。学习泛型时,不能只停留在List<T>这样的表面语法,更要理解其背后的思想。

十三、参考链接

相关推荐
武藤一雄2 小时前
WPF Command 设计思想与实现剖析
windows·微软·c#·.net·wpf·.netcore
小邓睡不饱耶2 小时前
实战教程:Python爬取北京新发地农产品价格数据并存储到MySQL
开发语言·python·mysql
EnCi Zheng2 小时前
P1B-Python环境配置基础完全指南-Windows系统安装与验证
开发语言·windows·python
小陈phd2 小时前
多模态大模型学习笔记(十九)——基于 LangChain+Faiss的本地知识库问答系统实战
开发语言·c#
重庆兔巴哥2 小时前
如何检查Java环境变量是否配置成功?
java·开发语言
盐焗西兰花2 小时前
鸿蒙学习实战之路-Share Kit系列(13/17)-配置目标应用名单(企业应用)
学习·华为·harmonyos
yue0082 小时前
C#读取App.Config配置文件
开发语言·c#
武藤一雄2 小时前
WPF 资源解析:StaticResource & DynamicResource 实战指南
微软·c#·.net·wpf·.netcore
2601_948606182 小时前
LaTeX学习笔记:开场白与索引
笔记·学习