C# 泛型 使用详解

总目录


前言

在 C# 编程中,代码的复用性和灵活性是至关重要的。

在传统编程方式中,若需处理不同数据类型的相似逻辑,往往需要为每个类型编写重复代码。例如,针对intstring的集合操作需分别实现,这不仅冗余,还可能导致类型安全隐患。

在C# 2.0引入泛型后,它彻底改变了开发者编写可复用代码的方式。C#泛型(Generics)通过延迟类型指定(或称 类型参数化)的机制,允许开发者编写可复用的类型安全代码,更通过消除装箱拆箱操作显著优化了性能。接下来,我们就来深入探讨一下 C# 泛型的使用吧。


一、什么是泛型

1. 基本概念

  • 泛型(Generics)是一种编程范式,它允许我们在定义类、方法或接口时,使用占位符(类型参数)代替具体的类型。在实际使用时,这些占位符会被具体的类型替换,从而实现类型安全的代码复用。
  • 通过使用泛型,我们可以编写适用于多种数据类型的代码,而无需为每种类型单独写代码,这不仅提高了代码的复用性,还增强了类型安全性和效率。

2. 泛型的优点

  • 类型安全:由于泛型代码在编译时会进行类型检查,因此减少了运行时出现错误的可能性。
  • 性能提升:泛型在运行时会生成特定类型的代码,避免了装箱和拆箱操作,提高了性能。
  • 代码复用:通过泛型,我们可以编写多种数据类型通用的类和方法,减少重复代码。
  • 可读性增强:泛型代码更清晰,意图更明确。

3. 痛点场景

示例:要求实现输入int ,string,datetime类型的值的时候,打印出对应的类型和值

	public class CommonMethod
    {
        //打印int的数据类型和值
        public static void ShowInt(int a)
        {
            Console.WriteLine($"result:type={a.GetType().Name},value={a}");
        }
        
        //打印string的数据类型和值
        public static void ShowString(string s)
        {
            Console.WriteLine($"result:type={s.GetType().Name},value={s}");
        }
        
        //打印DateTime 的数据类型和值
        public static void ShowDateTime(DateTime dt)
        {
            Console.WriteLine($"result:type={dt.GetType().Name},value={dt}");
        }
    }

以上示例中除了传入参数的数据类型不同,其余的处理逻辑相同,明显代码没有得到复用,于是简化代码如下:

        // 打印输入参数的数据类型和值
        public static void ShowObject(object o)
        {
            Console.WriteLine($"result:type={o.GetType().Name},value={o}");
        }

示例中,直接使用object 完成了代码的复用,让代码变得更加简洁通用,但是在这个过程中存在数据类型转化,也就涉及到了装箱和拆箱,严重的影响程序的性能。

那么现在能不能找一个 既能满足 代码复用的需求 又能避免装箱和拆箱带来性能损耗的方法呢?
有,那就是使用泛型,使用泛型优化示例代码:

        //打印输入参数的数据类型和值
        public static void ShowResult<T>(T t)
        {
            Console.WriteLine($"result:type={t.GetType().Name},value={t}");
        }

二、如何使用泛型?

1. 类型参数

  • 类型参数:是指在定义类、接口或方法时使用的占位符,代表实际使用时指定的数据类型。
    • 例如,在List中,T就是类型参数。
    • 类型参数 可以代表任何类型,也可识别任何类型
  • 类型参数命名规范:推荐使用TTKeyTValue等有意义的类型参数名
    • 泛型参数一般用T表示,但是不代表不可以使用别的代表,也可使用V、K等自定义的名称,但是推荐命名的时候使用T或者T开头
  • 类型参数数量:数量不限,业务中需要几个类型参数,就设置几个类型参数,不过建议类型参数不要过多

使用一对尖括号 + 类型参数 T ,如 MyGenericClassList这种形式来定义泛型对象。

2. 泛型类

泛型类是最常见的泛型形式。它允许我们定义一个类,其行为可以独立于具体的数据类型。

1)泛型类

  • 定义泛型类的基本语法如下:
    • 这里T是类型参数,代表任何数据类型。创建对象时,需要指定实际的数据类型。
	public class Box<T>
	{
	    private T _item;
	
	    public void Set(T item)
	    {
	        _item = item;
	    }
	
	    public T Get()
	    {
	        return _item;
	    }
	}

在这个例子中,Box 是一个泛型类,T 是类型参数。我们可以通过指定具体的类型来实例化这个类:

  • 使用泛型类
	Box<int> intBox = new Box<int>();
	intBox.Set(42);
	Console.WriteLine(intBox.Get()); // 输出:42
	
	Box<string> stringBox = new Box<string>();
	stringBox.Set("Hello, World!");
	Console.WriteLine(stringBox.Get()); // 输出:Hello, World!

实例化 泛型对象的时候,必须指明具体的数据类型(如BoxBox),否则是无法实例化的。

2)多类型参数

  • 泛型类可以接受多个类型参数。例如,一个字典类可能需要键和值两种类型:
public class Dictionary<TKey, TValue>
{
    // 实现细节...
}
  • 根据实际业务需求,可以定义多个泛型 类型参数 ,如 MyGeneric
    public class MyGenericClass<T1,T2>
    {
        public void Test(T1 t1,T2 t2)
        {
            Console.WriteLine($"result:T={t1.GetType().Name};V={t2.GetType().Name}");
        }
    }

3) 泛型类和普通类

泛型类定义的时候与普通使用上基本相同,只不过类名后面多了个尖括号,尖括号中放了泛型参数用于占位

//普通类
public class MyClass
//泛型类
public class MyGenericClass<T>

3. 泛型接口

泛型接口(如IEnumerable)支持统一操作不同数据类型的集合。

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

在这个例子中,IRepository 是一个泛型接口,T 是类型参数。我们可以为不同的类型实现这个接口:

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

public class UserRepository : IRepository<User>
{
    public User GetById(int id)
    {
        // 实现逻辑
        return new User { Id = id, Name = "John Doe" };
    }

    public void Add(User item)
    {
        // 实现逻辑
    }

    public void Update(User item)
    {
        // 实现逻辑
    }

    public void Delete(User item)
    {
        // 实现逻辑
    }
}

4. 泛型方法

泛型方法允许我们在方法级别上使用类型参数。这使得方法可以独立于具体类型,从而提高代码的复用性。

1) 泛型方法

除了类之外,我们还可以定义泛型方法,即使它们所在的类不是泛型类:

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

在这个例子中,Swap方法可以交换任意类型的两个变量的值。

public class Utility
{
    public static T GetMax<T>(T a, T b) where T : IComparable<T>
    {
        return a.CompareTo(b) > 0 ? a : b;
    }
}

在这个例子中,GetMax 是一个泛型方法,T 是类型参数。它接受两个参数并返回较大的值。我们可以通过指定具体的类型来调用这个方法:

int maxInt = Utility.GetMax(10, 20);
Console.WriteLine(maxInt); // 输出:20

string maxString = Utility.GetMax("Apple", "Banana");
Console.WriteLine(maxString); // 输出:Banana

2)泛型方法和普通方法

    public class MyGeneric<T>
    {
        public MyGeneric(T t)
        {
            Console.WriteLine($"result:type={t.GetType().Name};value={t}");
        }

        public void Show(T t)
        {
            Console.WriteLine($"result:type={t.GetType().Name};value={t}");
        }

        public void Test<V>(V v)
        {
            
        }
    }

在这个例子中,Show 是 具有泛型参数的 普通方法,Test 是泛型方法,MyGeneric是泛型类。

5. 泛型委托

泛型委托允许我们定义通用的委托类型,其行为可以独立于具体的数据类型。

public delegate T MyDelegate<T>(T a, T b);

public class Program
{
    public static T GetMax<T>(T a, T b) where T : IComparable<T>
    {
        return a.CompareTo(b) > 0 ? a : b;
    }

    static void Main()
    {
        MyDelegate<int> intDelegate = GetMax;
        int maxInt = intDelegate(10, 20);
        Console.WriteLine(maxInt); // 输出:20

        MyDelegate<string> stringDelegate = GetMax;
        string maxString = stringDelegate("Apple", "Banana");
        Console.WriteLine(maxString); // 输出:Banana
    }
}

在这个例子中,MyDelegate 是一个泛型委托,它接受两个参数并返回一个值。我们可以通过指定具体的类型来使用这个委托。

6. 静态成员与泛型

若误以为静态成员跨类型共享,可能导致数据不一致或逻辑错误。示例如下:

public class StaticGeneric<T>
{
    public static int Count;

    public StaticGeneric()
    {
        Count++;
    }
}
public class Program
{
    public static void Main()
    {
        StaticGeneric<int> staticGenericInt=new StaticGeneric<int>();
        Console.WriteLine($"Count = {StaticGeneric<int>.Count}");       //输出:Count = 1

        StaticGeneric<string> staticGenericString=new StaticGeneric<string>();       
        Console.WriteLine($"Count = {StaticGeneric<string>.Count}");    //输出:Count = 1
    }
}

在C#中,静态成员与泛型结合使用时需特别注意以下几点

  • 静态成员不共享:
    • 不同类型参数的泛型实例会生成独立的静态成员,导致数据无法共享。
  • 避免在泛型类型中声明静态成员:
    • 泛型类型(如StaticGeneric)的静态成员与具体类型参数绑定,不同类型参数的实例视为不同类型。
    • StaticGeneric.Count 结果为1,StaticGeneric.Count结果仍为1

7. 泛型与default、dynamic关键字

1)与 default

  • 获取类型默认值

    • 在泛型中处理默认值时,可以使用default来获取类型的默认值。
    • 对于引用类型,默认值为null;对于数值类型,默认值为0。
    public T GetDefault<T>()
    {
    	return default(T); // 引用类型返回null,值类型返回0等
    }
    

    从 C# 7.1 开始,可以直接使用 default 而不带括号来简化语法:

    class GenericExample<T>
    {
    	public T GetDefaultValue()
    	{
        	return default;
    	}
    }
    
  • 设置类型默认值

class GenericExample<T,V>
{
    private T t;
    private V v;
    public GenericExample()
    {
        t = default;
        v = default;
    }
}

2) 与 dynamic

C# 泛型 使用详解_第1张图片
如上图,Add用于计算t1和t2之和的时候,直接使用int sum= t1+t2,会报错,因为还没有实例化,没有指定数据类型,无法直接适用于加法,但是如果使用dynamic,就可以跳过编译类型检查,改为在运行时解析这些操作。 就可以完成相关的业务逻辑。

8. 协变与逆变中的泛型

  • 协变(Covariance):允许子类泛型赋值给父类(IEnumerableIEnumerable
  • 逆变(Contravariance):允许父类泛型赋值给子类(ActionAction

    C#支持通过outin关键字来标记泛型参数是否支持协变或逆变。

    //协变接口
    public interface ICovariant<out T>
    {
        T Get();
    }
    
    //逆变接口
    public interface IContravariant<in T>
    {
        void Set(T item);
    }
    

    集合接口的智能转换:

    // 支持将派生类集合赋值给基类变量
    IEnumerable<string> strings = new List<string>();
    IEnumerable<object> objects = strings; // 协变(Covariance)
    
    // 允许处理基类的接口处理派生类
    Action<object> objectAction = obj => Console.WriteLine(obj);
    Action<string> stringAction = objectAction; // 逆变(Contravariance)
    

    关于 协变与逆变 详见:C# 协变与逆变深入解析

    三、泛型约束

    1. 泛型约束概览

    C# 泛型 使用详解_第2张图片
    常用泛型约束概览

    约束类型 语法 说明
    基类约束 where T : BaseClass T 必须继承自某个基类
    接口约束 where T : IInterface T 必须实现某个接口
    值类型约束 where T : struct T必须是值类型
    引用类型约束 where T : class T必须是引用类型
    无参构造函数 where T : new() T必须有无参构造函数

    2. 使用泛型约束

    • 有时候,我们可能想要对泛型中的类型参数施加一些限制,比如要求该类型必须实现某个接口或继承自某个基类。这时,我们可以使用约束。
    • 泛型约束通过where关键字限制类型参数,增强安全性

    1)单个约束示例


    引用类型约束示例:

    	public class MyClassGeneric<T> where T : class
    	{
    	    public MyClassGeneric(T t)
    	    {
    	        Console.WriteLine($"result:type={t.GetType().Name};value={t}");
    	    }
    	}
    	
    	public class User
    	{
    	    public int Id { get; set; }
    	    public string Name { get; set; }
    	}
    	
    	public class Program
    	{
    	    public static void Main()
    	    {
    	        MyClassGeneric<string> myClassGeneric1 = new MyClassGeneric<string>("test");
    	        MyClassGeneric<User> myClassGeneric2 = new MyClassGeneric<User>(new User() { Id=1,Name="Jack"});
    	    }
    	}
    

    这里的where T : class表示类型参数T必须是引用类型。如示例中 使用int 类型,则会报错。


    值类型约束示例:

        public class MyStructGeneric<T> where T : struct
        {
            public My_Generic(T t)
            {
                Console.WriteLine($"result:type={t.GetType().Name};value={t}");
            }
        }
    	
    	//使用
    	MyStructGeneric<int> myStructGeneric = new MyStructGeneric<int>();
    

    这里的where T : struct表示类型参数T必须是不可为null的值类型。如示例中 使用int 类型,如果使用 string 类型则会报错,因为string 是引用类型。


    接口/基类约束示例:

    public class Box<T> where T : IComparable<T>
    {
        private T _item;
    
        public void Set(T item)
        {
            _item = item;
        }
    
        public T Get()
        {
            return _item;
        }
    }
    

    在这个例子中,Box 的类型参数 T 被限制为必须实现 IComparable 接口。这意味着我们只能使用满足该约束的类型来实例化 Box

        public interface IPeople
        {
            void GetUserInfo();
        }
    
        public class Chinese : IPeople
        {
            public void GetUserInfo()
            {
                //throw new NotImplementedException();
            }
        }
    
        //这个泛型类规定必须是IPeople或者是继承于Ipeople的数据类型才可传入
        public class MyGeneric4<T> where T : IPeople
        {
            public MyGeneric4()
            {
            }
        }
    
    	//使用
    	MyGeneric4<IPeople> myGeneric4 = new MyGeneric4<IPeople>();
    	MyGeneric4<Chinese> my_Generic44 = new MyGeneric4<Chinese>();
    
    

    使用的时候 T 必须是IPeople或者是继承于Ipeople的数据类型才可传入


    无参数构造函数 约束示例:

        public class MyGeneric3<T> where T : new ()
        {
            public MyGeneric3(T t)
            {
                Console.WriteLine($"result:type={t.GetType().Name};value={t}");
            }
        }
        
    	public class User
    	{
    	    public int Id { get; set; }
    	    public string Name { get; set; }
    	}
    	
    	public class Score
        {
            public Score(string code)
            {
                //有参数的构造函数
            }
        }
    
    
    // 如果这样使用就会报错
    // MyGeneric3 my_Generic3 = new MyGeneric3(new Score()); 
    // 这样使用则没有问题
    MyGeneric3<User> my_Generic3 = new MyGeneric3<User>(new User()); 
    

    上例中,如果将Score 类 作为类型参数 传入,则会报错,因为该约束限制类型参数必须 有一个无参数的构造函数

    2)组合约束示例

    public T CreateInstance<T>() where T : Animal, IFly, new()
    {
        return new T();
    }
    
    public class GenericClass<T> where T : IComparable, new()
    {
        public void DoSomethingWithGeneric(T input)
        {
            if (input.CompareTo(default(T)) > 0)
            {
                Console.WriteLine("Greater than default.");
            }
        }
    }
    

    注意:

    • 约束可以组合使用
    • 与其他约束一起使用时,new() 约束必须最后指定。

    三、为什么使用泛型?

    1. 类型安全性

    1) 非泛型集合示例

    非泛型集合(如ArrayList)存储object类型,需显式转换且易引发运行时错误:

    ArrayList list = new ArrayList();
    list.Add(1);
    list.Add("text");
    int num = (int)list[1]; // 运行时异常!
    

    在泛型出现前,ArrayList等集合类以object存储元素,导致:

    ArrayList list = new ArrayList();
    list.Add(1);    // 装箱
    int num = (int)list[0]; // 拆箱 + 类型不安全
    

    2) 泛型集合示例

    泛型集合(如List)在编译时即强制类型匹配,杜绝此类问题。
    泛型集合List彻底解决了这些问题:

    List<int> numbers = new List<int>();
    numbers.Add(42); // 无需装箱
    numbers.Add("text"); // 编译时直接报错!
    int val = numbers[0]; // 直接获取int类型
    

    2. 性能优化

    泛型避免装箱(Boxing)与拆箱(Unboxing)操作。例如,List直接操作值类型,而ArrayList需将int装箱为object,显著提升效率。

    值类型处理效率对比测试:

    操作类型 1000万次操作耗时
    ArrayList 520ms
    List 85ms
    提升幅度 6倍+

    原因剖析:

    • 避免值类型装箱(Heap内存分配)
    • 消除类型检查开销

    性能测试

    public class Program
    {
        static void Main()
        {
            // 使用非泛型集合
            ArrayList arrayList = new ArrayList();
            for (int i = 0; i < 100_0000; i++)
            {
                arrayList.Add(i);
            }
    
            // 使用泛型集合
            List<int> list = new List<int>();
            for (int i = 0; i < 100_0000; i++)
            {
                list.Add(i);
            }
    
            // 测试性能
            Stopwatch sw = Stopwatch.StartNew();
    
            foreach (var item in arrayList)
            {
                int value = (int)item; // 装箱和拆箱操作
            }
            sw.Stop();
    
            Console.WriteLine($"非泛型集合:{sw.ElapsedMilliseconds} ms");
    
            sw.Restart();
            foreach (var item in list)
            {
                int value = item; // 无需装箱和拆箱
            }
            Console.WriteLine($"泛型集合:{sw.ElapsedMilliseconds} ms");
        }
    }
    

    运行结果:

    非泛型集合:28 ms
    泛型集合:7 ms
    

    在这个例子中,使用泛型集合 List 的性能明显优于非泛型集合 ArrayList,因为泛型集合避免了装箱和拆箱操作。

    3. 代码复用性

    通过泛型可编写通用逻辑,适应多种数据类型。例如,泛型方法Swap可交换任意类型的变量:

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

    四、泛型应用场景

    1. 泛型与反射

    反射(Reflection)允许我们在运行时检查和操作类型的信息。泛型与反射结合使用时,可以实现非常灵活的动态行为。

    using System;
    using System.Reflection;
    
    public class Box<T>
    {
        private T _item;
    
        public void Set(T item)
        {
            _item = item;
        }
    
        public T Get()
        {
            return _item;
        }
    }
    
    public class Program
    {
        static void Main()
        {
            Box<int> intBox = new Box<int>();
            intBox.Set(42);
    
            Type boxType = intBox.GetType();
            FieldInfo field = boxType.GetField("_item", BindingFlags.NonPublic | BindingFlags.Instance);
            object value = field.GetValue(intBox);
    
            Console.WriteLine(value); // 输出:42
        }
    }
    

    动态创建泛型实例

    Type openType = typeof(List<>);
    Type closedType = openType.MakeGenericType(typeof(int));
    object list = Activator.CreateInstance(closedType);
    
    Type openType = typeof(Dictionary<,>);
    Type closedType = openType.MakeGenericType(typeof(int), typeof(string));
    object dict = Activator.CreateInstance(closedType);
    

    当编译器无法推断类型时:

    // 错误示例
    var result = CreateInstance(typeof(List<>)); 
    
    // 正确写法
    var listType = typeof(List<>);
    var specificType = listType.MakeGenericType(typeof(int));
    var instance = Activator.CreateInstance(specificType);
    

    2. 泛型在依赖注入中的应用

    通过泛型接口与DI容器结合,实现服务通用化:

    services.AddScoped(typeof(IValidator<>), typeof(ProductValidator));
    

    此配置可为所有实体类型自动提供验证逻辑。

    3. 泛型缓存特性

    public class Cache<T>
    {
        public static DateTime CreatedTime { get; } = DateTime.Now;
        // 每个不同的T类型都会创建独立的静态字段
    }
    

    4. 泛型与设计模式

    仓储模式的现代化实现:

    public interface IRepository<T> where T : class 
    {
        T GetById(int id);
        void Add(T entity);
    }
    
    public class UserRepository : IRepository<User> 
    {
        // 具体实现
    }
    
    // 依赖注入配置
    services.AddScoped<IRepository<User>, UserRepository>();
    

    泛型工厂模式

    public interface IFactory<T>
    {
        T Create();
    }
    
    public class CarFactory : IFactory<Car>
    {
        public Car Create() => new SportsCar();
    }
    

    5. 集合框架

    C#的集合框架大量使用了泛型,如ListDictionary等,它们都提供了类型安全的操作,并避免了装箱拆箱带来的性能损耗。

    var list = new List<int>();
    list.Add(1);
    Console.WriteLine(list[0]); // 输出: 1
    

    6. 自定义泛型

    创建自己的泛型类或方法可以帮助我们编写更具复用性的代码。比如,创建一个简单的缓存机制:

    public class Cache<TKey, TValue>
    {
        private Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();
    
        public void Add(TKey key, TValue value)
        {
            _cache[key] = value;
        }
    
        public TValue Get(TKey key)
        {
            return _cache[key];
        }
    }
    

    五、最佳实践

    1. 合理使用泛型

    • 泛型可以提高代码的复用性和性能,但并不是万能的。在某些场景下,过度使用泛型可能会导致代码难以理解和维护。因此,我们需要根据实际情况合理使用泛型。
    • 避免过度泛型化,避免过度嵌套泛型类型
    • 类型参数命名,使用TTKeyTValue等有意义的类型参数名

    2. 避免过多的类型参数

    • 过多的类型参数会增加代码的复杂性。一般来说,类型参数的数量不应超过 3 个。如果需要更多类型参数,可以考虑将多个类型合并为一个元组或自定义类型。

    3. 使用类型约束

    • 类型约束可以限制类型参数的范围,从而提高代码的安全性和灵活性。合理使用类型约束可以避免不必要的运行时错误。

    结语

    回到目录页:C#/.NET 知识汇总
    希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。


    参考资料:
    .NET泛型集合源码解析
    泛型与设计模式实践
    微软官方泛型文档
    泛型性能优化白皮书
    设计模式中的泛型应用案例集

    你可能感兴趣的:(C#,c#,java,数据库)