一篇文章搞懂C#中的泛型类/泛型方法/泛型接口
链接: 源码
提起泛型类,很多人就头疼,我也头疼。在C#中这个概念很重要,重要的向定义一个int数值类型一样,但是这个内容又不像if···else那样容易理解。我花费了两天的时间,把整个知识点梳理了一遍,希望讲清楚,也当给自己做个笔记。
泛型类(Generic Classes)
泛型类是一种可以处理多种数据类型的数据结构或算法模板。它允许在定义类时使用一个或多个类型参数(通常用大写字母表示,如T
、U
等),这些参数在实例化类时由具体类型替换。通过使用泛型类,可以编写一次代码,为多种数据类型提供服务,提高了代码的复用性和类型安全性。
泛型类定义
泛型类定义的基本语法如下:一个普通类后面携带类型参数就是泛型类了
public class ClassName<T, U, ...>
{
// 类的成员(字段、属性、方法、事件等)
// 可以使用类型参数T、U等来定义成员
private T _fieldOfT;
public U PropertyOfU {
get; set; }
public void MethodWithGenericTypeParameters(T arg1, U arg2) {
... }
}
泛型类使用
定义一个简单的泛型栈类:在这个例子中,Stack<T>
是一个泛型类,T
是类型参数。使用这个类时,可以根据需要传入具体的类型,如Stack<int>
、Stack<string>
等。例子中使用的类型参数是int
Stack<int> stack = new Stack<int>();
stack.Push(0);
stack.Push(1);
public class Stack<T>
{
private List<T> _items = new List<T>();
public void Push(T item)
{
_items.Add(item);
}
public T Pop()
{
if (_items.Count == 0)
throw new InvalidOperationException("Stack is empty.");
T item = _items[_items.Count - 1];
_items.RemoveAt(_items.Count - 1);
return item;
}
}
泛型方法(Generic Methods)
泛型方法定义
泛型方法是在方法级别上应用泛型的概念,即在方法签名中使用类型参数,使其能够处理多种数据类型,而无需所在的类或结构是泛型的。换句话说:泛型方法既可以存在泛型类中,也可以存在普通类中.
泛型方法可以提高代码的灵活性和复用性,特别是在方法内部处理不同类型数据时。
泛型方法定义的基本语法如下:
public ReturnType MethodName<T, U, ...>(ParameterType parameter1, ...)
{
// 方法体
}
泛型方法的使用
我们在一个普通类中定义一个泛型方法,用于交换两个变量的值:
{
int a = 15;
int b = 6;
Console.WriteLine($"交换前:a={
a},b={
b}");
Swap<int>(ref a, ref b);
Console.WriteLine($"交换后:a={
a},b={
b}");
}
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
在这个例子中,Swap<T>
是一个泛型方法,它接受两个ref
参数,类型均为T
。调用时可以传入任何类型的变量,如int
、string
等,方法会自动处理相应类型的交换操作。
泛型委托
C# 中的泛型委托(Generic Delegates)是一种允许在委托的签名中使用类型参数的特殊类型的委托。通过使用泛型委托,您可以创建可以引用任何符合特定方法签名的委托,其中该方法签名包含泛型类型参数。也就是说泛型委托必须搭配泛型方法来使用.
泛型委托的声明方式与泛型类或泛型方法的声明方式类似,通过在委托声明中引入类型参数来实现。这些类型参数在委托签名中被用作占位符,实际的类型将在创建委托实例时提供。
下面是一个简单的示例,演示了如何声明和使用泛型委托:
// 声明泛型委托
public delegate void GenericAction<T>(T value);
// 使用泛型委托的类
public class ExampleClass
{
// 泛型方法,与泛型委托的签名匹配
public static void PrintValue<T>(T value)
{
Console.WriteLine(value);
}
}
class Program
{
static void Main()
{
// 创建泛型委托实例,引用 ExampleClass.PrintValue 方法
GenericAction<int> intAction = ExampleClass.PrintValue;
GenericAction<string> stringAction = ExampleClass.PrintValue;
// 调用委托实例
intAction(42); // 输出:42
stringAction("Hello"); // 输出:Hello
}
}
在上面的示例中,我们首先声明了一个泛型委托 GenericAction<T>
,它接受一个类型为 T
的参数,并没有返回值。然后,我们创建了一个包含泛型方法 PrintValue<T>
的类 ExampleClass
,该方法的签名与泛型委托的签名匹配。
在 Main
方法中,我们创建了两个泛型委托实例:intAction
和 stringAction
。intAction
委托实例用于引用 ExampleClass.PrintValue
方法,其中 T
被替换为 int
类型;而 stringAction
委托实例则用于引用同一个方法,但 T
被替换为 string
类型。
最后,我们通过调用这两个委托实例来执行 ExampleClass.PrintValue
方法,并分别传递了一个整数和一个字符串作为参数。输出结果分别为 42
和 Hello
。
通过使用泛型委托,可以创建更加灵活和可重用的代码,因为委托可以引用任何符合特定泛型方法签名的方法。这对于创建泛型算法、回调和事件处理程序等场景非常有用。
泛型接口
泛型接口是C#中一种特殊的接口类型,它允许在定义接口时使用类型参数,使得实现该接口的类或结构可以为这些类型参数指定具体的类型。泛型接口为程序员提供了创建适用于多种数据类型且具有良好类型安全性的接口的能力,从而提升代码的复用性和灵活性。
泛型接口的定义
泛型接口的定义与泛型类和泛型委托类似,使用类型参数(通常用大写字母表示,如T
、U
等)来指定接口方法、属性、索引器等的参数类型和返回类型。泛型接口的基本语法如下:
public interface InterfaceName<out T1, in T2, ..., Tn>
{
ReturnType MethodName(ParameterType parameter1, ...);
PropertyType Property {
get; set; }
IndexerType this[IndexerParameterType indexer] {
get; set; }
// 其他接口成员
}
其中:
InterfaceName
:自定义的接口名称。T1, T2, ..., Tn
:泛型类型参数。ReturnType
,PropertyType
,IndexerType
,IndexerParameterType
,ParameterType
:接口成员的返回类型、属性类型、索引器类型、索引器参数类型和方法参数类型。
**示例:**定义一个泛型接口,表示可以比较两个元素的容器:
public interface IGeneric<T>
{
T Method(T input);
}
在这个例子中,IComparableContainer<in T>
是一个泛型接口,它有两个方法:Contains(T item)
用于检查容器是否包含指定元素,Compare(T item1, T item2)
用于比较两个元素的大小关系。
泛型接口的实现
要实现泛型接口,需要在类或结构声明中指定接口名称及相应的类型参数。实现类或结构必须提供接口中所有成员的实现。
**示例:**实现IComparableContainer<in T>
泛型接口:
public class Generic<T> : IGeneric<T>
{
public T Method(T input)
{
// 这里可以是对T类型数据的任何操作,例如返回相同的值
return input;
}
}
在这个例子中,CustomList<T>
类实现了IComparableContainer<in T>
接口,为Contains(T item)
和Compare(T item1, T item2)
方法提供了具体实现。
协变与逆变
在C#中,协变(Covariance)和逆变(Contravariance)是泛型编程的两个重要概念,它们允许在委托和接口中使用更加灵活的类型参数。这主要涉及到委托的返回类型和接口的方法参数类型的灵活性。为了后面讲解,我们先写一个基类 Animal,再写一个子类dog,该子类继承于父类。
public class Animal
{
public void Play()
{
Console.WriteLine("动物正在唱歌");
}
public virtual void Eat()
{
Console.WriteLine("动物正在吃东西");
}
}
public class Dog:Animal
{
public void Play()
{
Console.WriteLine("小狗正在唱歌");
}
public override void Eat()
{
Console.WriteLine("小狗正在吃东西");
}
}
协变
协变允许您将一个返回派生类实例的委托赋值给一个返回基类实例的委托。简而言之,协变处理的是返回类型。
说人话就是:允许子类对象赋值给父类对象. 将Dog类的实例赋值给Animal类实例.
在C#中,协变是通过使用out
关键字在泛型接口或委托的类型参数上声明的。
逆变
逆变允许您将接受基类实例作为参数的委托赋值给一个接受派生类实例作为参数的委托。这处理的是参数类型。
说人话就是:**允许父类对象赋值给子类对象.**将Animal类的实例赋值给Dog类实例.
在C#中,逆变是通过使用in
关键字在泛型接口或委托的类型参数上声明的。
实例
定义一个泛型接口ITest, 通过关键字in和out来声明参数是协变还是逆变. Test类实现了ITest接口.
//关键字in是逆变,out是协变
public interface ITest<in TIn, out TOut>
{
void Show(TIn value);
TOut Get();
}
public class Test<T1, T2> : ITest<T1, T2>
{
public void Show(T1 value)
{
Console.WriteLine(value.GetType().Name);
}
public T2 Get()
{
Console.WriteLine(typeof(T2).Name);
return default(T2);
}
}
接下来, 通过不同的类型参数实例化Test类, 指向(赋值给)接口ITest
{
//如何理解协变与逆变?
//协变与逆变隶属于泛型接口,使我们更加灵活的使用接口的参数
//父类:Animal 子类:Dog
//协变:子类可以赋值给父类,用关键字out声明
//逆变:父类可以赋值给子类,用关键字in声明
ITest<Dog, Animal> test1 = new Test<Dog, Animal>(); //正常传递参数
ITest<Dog, Animal> test2 = new Test<Dog, Dog>(); //协变
ITest<Dog, Animal> test3 = new Test<Animal, Animal>(); //逆变
ITest<Dog, Animal> test4 = new Test<Animal, Dog>(); //逆变 + 协变
test1.Show(new Dog());
test2.Show(new Dog());
ITest<Animal, Dog> test5 = new Test<Animal, Dog>(); //正常传递参数
/*
错误,ITest类第一个类型参数是in,代表逆变,逆变可以将父类赋值给子类,而这里却将子类赋值给父类,逻辑相悖
ITest<Animal, Dog> test6 = new Test<Dog,Dog>();
*/
/*
错误,ITest类第二个类型参数是out,代表协变,协变可以将子类赋值给父类,而这里却将父类赋值给子类,逻辑相悖
ITest<Animal, Dog> test7 = new Test<Animal, Animal>();
*/
/*
ITest类第一个类型参数是in,代表逆变,逆变可以将父类赋值给子类,而这里却将子类赋值给父类,逻辑相悖
ITest类第二个类型参数是out,代表协变,协变可以将子类赋值给父类,而这里却将父类赋值给子类,逻辑相悖
ITest<Animal, Dog> test8 = new Test<Dog, Animal>();
*/
Animal animal = new Dog(); //将子类赋值给父类,这里实际生成的类是Dog类
animal.Play(); //由编译器时决定,为了提高运行效率,调用普通方法时,使用的是父类方法
animal.Eat(); //由运行时决定,为了多态灵活,使用的是子类重写的虚方法
}
协变和逆变在事件处理、LINQ查询、异步编程等场景中非常有用。它们允许您创建更加灵活和可重用的代码,而无需创建大量的特定类型委托或接口。通过使用协变和逆变,您可以编写泛型代码,该代码能够处理不同类型的对象,同时保持类型安全。
请注意,协变和逆变仅适用于引用类型。值类型(如int、double等)不支持协变和逆变,因为它们是按值传递的,并且不会在运行时进行装箱或拆箱。
泛型约束
C#泛型约束是编程语言中的一种机制,用于在定义泛型类、结构、接口、方法或委托时,对使用这些泛型的类型参数施加特定的限制。这些限制确保了类型参数必须满足特定的条件,从而允许编译器进行更严格的类型检查,提高代码的类型安全性,并且使得在泛型代码内部可以安全地访问特定类型成员或执行特定操作。
通过 where
关键字和相应的约束条件,限制类型参数只能是满足特定条件的类型。这些条件可以包括:
常见约束类型
-
基类约束(
where T : BaseType
):要求类型参数
T
必须是某个特定基类(如BaseType
)的子类或直接是该基类本身。这样就可以在泛型代码中安全地使用该基类的所有公共成员,因为编译器知道所有传递给T
的类型都会继承这些成员。//定义一个基类 public class BaseClass { public virtual void BaseMethod() { Console.WriteLine("这是基类方法"); } } //定义一个派生类 public class DerivedClass : BaseClass { public override void BaseMethod() { Console.WriteLine("这是子类方法"); } } //定义一个有基类约束的泛型类 public class GenericClass<T> where T : BaseClass { public void DoSomethingWithT(T instance) { instance.BaseMethod(); //调用约束类的方法 } } class Program { static void Main() { GenericClass<BaseClass> genericInstance = new GenericClass<BaseClass>();//实例化一个泛型类,约束要求类型参数是BaseClass genericInstance.DoSomethingWithT(new BaseClass()); //传递泛型约束类实例 genericInstance.DoSomethingWithT(new DerivedClass()); //传递泛型约束类子类实例 GenericClass<DerivedClass> genericInstance2 = new GenericClass<DerivedClass>();//实例化一个泛型类,约束要求类型参数是DerivedClass genericInstance.DoSomethingWithT(new BaseClass()); //传递泛型约束类实例 genericInstance.DoSomethingWithT(new DerivedClass()); //传递泛型约束类子类实例 } }
-
接口约束(
where T : InterfaceType
):类似于基类约束,但要求 类型参数
T
必须实现指定的接口(如InterfaceType
)。这意味着传入的类型必须提供了接口中定义的所有方法、属性等成员。//Program.cs GenericClass2<DataProvider> genericClass = new GenericClass2<DataProvider>();//类型参数DataProvider实现了IDataProvider接口 genericClass.DoSomethingWithT(new DataProvider()); //定义接口 public interface IDataProvider { public void Method(); } //接口实现类 public class DataProvider : IDataProvider { public void Method() { Console.WriteLine("该方法正在获取数据"); } } // 有泛型约束的泛型类 public class GenericClass2<T> where T : IDataProvider { public void DoSomethingWithT(T instance) { instance.Method(); //调用接口中的方法 } }
-
多个接口约束(
where T : InterfaceType1, InterfaceType2
):可以同时指定多个接口,类型参数
T
必须同时实现所有列出的接口。和上面的一个接口约束一样,只不过要求类型参数必须同时实现多个接口. -
引用类型约束(
where T : class
):限制
T
必须是一个引用类型(如类、接口、委托或数组),不能是值类型(如整数、结构体等)。//Program.cs //参数类型传递一个类 GenericClass3<ReferenceData> genericClass = new GenericClass3<ReferenceData>(); genericClass.DoSomethingWithT(new ReferenceData()); //尝试传递一个值类型,会报错 // GenericClass3<int> genericClass2 = new GenericClass3<int>(); //类型“int”必须是引用类型才能用作泛型类型或方法“GenericClass3 < T >”中的参数“T” // 有泛型约束的泛型类 public class GenericClass3<T> where T : class { public void DoSomethingWithT(T instance) { Console.WriteLine($"传递进来的类型参数是:{ typeof(T).Name}") ; //调用接口中的方法 } } //值类型 public class ReferenceData { public int Id; public string Name; }
-
值类型约束(
where T : struct
):要求
T
必须是一个值类型,且不能是Nullable<T>
类型。这通常用于确保类型参数是诸如整数、浮点数、枚举或用户定义的结构体。//Program.cs //参数类型传递值 GenericClass4<MyStruct> genericClass = new GenericClass4<MyStruct>(); genericClass.DoSomethingWithT(new MyStruct()); //尝试传递一个引用类型,会报错 //GenericClass4<ReferenceData> genericClass2 = new GenericClass4<ReferenceData>(); // 有泛型约束的泛型类 public class GenericClass4<T> where T : struct { public void DoSomethingWithT(T instance) { Console.WriteLine($"传递进来的参数类型是:{ typeof(T).Name}") ; //调用接口中的方法 } } //值类型 public struct MyStruct { public int Id; public string Name; }
-
非nullable值类型约束(
where T : notnull
)(C# 8.0 及以上):确保
T
是一个不可为 null 的类型,适用于引用类型和值类型。对于引用类型,意味着不能为null
;对于值类型,等同于struct
约束。 -
new() 约束(
where T : new()
):new()` 约束是泛型编程中的一种特殊约束,它要求类型参数必须有一个无参数的公共构造函数。这样,你就可以在泛型类或方法的内部创建该类型的新实例。
//类型参数传递一个含有无参构造函数的类 GenericClass5<NoParasStruct> genericClass = new GenericClass5<NoParasStruct>(); //类型参数传递一个含有无参构造函数的类 //报错->“ParasStruct”必须是具有公共的无参数构造函数的非抽象类型,才能用作泛型类型或方法“GenericClass5 < T >”中的参数“T” 泛型 D:\Desk\一个教英语的程序员\源码\learn - English - learn - c - sharp\泛型类和泛型方法\Program.cs 107 活动 //GenericClass5<ParasStruct> genericClass2 = new GenericClass5<ParasStruct>(); // 有泛型约束的泛型类 public class GenericClass5<T> where T : new() { public T CreateInstance() { return new T(); // 使用 new() 约束创建 T 类型的新实例 } } //无参构造函数类 public class NoParasStruct { //public NoParasStruct() { } //无参构造函数默认可以不写 } //有参构造函数类 public class ParasStruct { public string Name { get; set; } public ParasStruct(string name) { Name = name; } }
约束的应用场景
-
确保方法或操作的有效性:通过约束,编译器能够验证泛型代码中对类型参数的操作是否合法,例如比较、排序、转换等。
-
增强编译时类型检查:约束可以防止在运行时因类型不匹配而导致的异常,提高程序的稳定性和可靠性。
-
减少类型转换:有了约束,编译器可以在编译时确定类型兼容性,避免不必要的强制类型转换。
-
提高代码可读性和维护性:约束清晰地表达了对类型参数的期望,有助于其他开发者理解泛型类或方法的意图和适用范围。