C#学习笔记 -- 泛型
2、C#中的泛型
-
泛型可以让多个类型共享一组代码
-
允许声明类型参数化的代码, 用不同的类型来实例化
-
提供5种泛型: 类、结构、接口、委托和方法
class MyStack<T> { int StackPointer = 0; T[] StackArray; public void Push(T t){...} public T Pop(){...} }
3、泛型类
-
泛型类不是实际的类, 而是类的模板
-
必须先构建实际的类类型
-
然后创建这个类类型的引用和实例
(1)声明泛型类
-
在尖括号中用逗号分割的占位符字符串表示需要提供的类型, 叫做类型参数
-
在泛型类声明的主体中使用类型参数来表示代替类型
-
在泛型类的声明中没有特殊的关键字, 取而代之的是尖括号中的类型参数列表
class SomeClass<T1, T2> { public T1 SomeVar; public T2 OtherVar; }
(2)创建构造类型
SomeClass<short, int> someClass = new SomeClass<short, int>();
(3)创建变量和实例
SomeClass<short, int> someClass = new SomeClass<short, int>();
var someVarClass = new SomeClass<short, int>();
-
可以从同一个泛型类穿检出很多不同的类类型, 每一个都是独立的类类型
var first = new SomeClass<short, int>(); var secound = new SomeClass<int, long>();
(4)例子
class MyStack<T> { T[] StackArray; int StackPointer = 0; const int MaxStack = 10; bool IsStackFull { get { return StackPointer == MaxStack; } } bool IsStackEmpty { get { return StackPointer == 0; } } public MyStack() { StackArray = new T[MaxStack]; } public void Push(T x) { if (!IsStackFull) { StackArray[StackPointer++] = x; } } public T Pop() { return StackArray[StackPointer]; if (!IsStackEmpty) { StackPointer--; } } public void Print() { for (int i = StackPointer - 1; i >= 0; i--) { Console.WriteLine($"{StackArray[i]}"); } } }
static void Main(string[] args) { MyStack<int> intStack = new MyStack<int>(); MyStack<string> stringStack = new MyStack<string>(); for (int i = 3; i <= 9 ; i+=2) { intStack.Push(i); } intStack.Print(); stringStack.Push("this is fun"); stringStack.Push("hi there"); stringStack.Print(); }
(5)泛型优势
-
源代码更小, 不管构造的类型有多少, 只需要一个实现
-
可执行文件只会出现构造类型的类型
-
易于维护
4、类型参数的约束
-
约束
-
约束是提供额外的信息, 让编译器知道参数可以接受哪些类型
-
只有符合约束的类型才能替代给定的类书参数来产生构造类型
-
-
泛型类可以处理任何类型
-
只要代码不访问处理一些类型的对象,
-
或者始终是object类型的成员
-
-
符合约束的类型参数交未绑定的类型参数
-
如果代码尝试使用其他成员, 编译器会产生一个错误消息
class Simple<T> { static public bool LessThan(T t1, T t2) { return t1 < t2; //错误 } }
(1)Where
子句
-
使用
where
子句列出-
每一个约束的类型参数都有自己的
where
-
如果形参有多个约束, 他们在
where
中用逗号分隔
-
-
在参数类型列表的关闭尖括号之后列出
-
不同
where
之间不用任何符号 -
可以以任意次序列出
class Sample <T1, T2, T3> where T2 : 约束1, 约束2 where T3 : 约束3, 约束4
(2)约束类型和次序
约束类型 | 标书 |
---|---|
类名 | 只有这个类型的类或他子类才能用作类型实参 |
class | 任何引用类型, 包括类、数组、委托和接口都可以用作类型实参 |
struct | 任何值类型都可以用作类型实参 |
接口名 | 只有这个接口或实现这个几口的类型才能用作类型实参 |
new() | 任何带有无参公共构造函数的类型都可以作为类型实参, 这叫做构造函数约束 |
where子句可以以任何次序列出, 然而where子句中的约束必须有顺序
-
最多只能有一个主约束, 并且放在第一位
-
可以有任意多的接口名称约束
-
如果存在构造函数约束, 必须放在最后
where T : 主约束(0个或1个), 次约束(0个或多个), 构造函数约束(0个或1个)
-
主约束
-
类名
-
class
-
struct
-
-
次约束
-
接口名
-
-
构造函数约束
-
new()
-
class SomeClass<T1, T2> where T1 : IComparable<T1> { }
class SomeClass<T1, T2> where T1 : IComparable<T1> where T2 : IComparable { }
class MyDictionary<KeyType, ValueType> where KeyType : IEnumerable<KeyType>, new() { }
5、泛型方法
(1)声明泛型方法
泛型方法具有类型参数列表和可选的约束
-
泛型方法有两个参数列表
-
封闭在圆括号内的方法参数列表
-
封闭在尖括号内的类型参数列表
-
-
要声明泛型方法, 需要:
-
在方法名称之后和方法参数列表之前防止类型参数列表
-
在方法参数列表后放置可选的约束子句
-
public void PrintData<S, T> (S s, T t) where S : Person
(2)调用泛型方法
调用泛型方法, 应该在方法调用时提供类型实参
static void DoStuff<T1, T2>(T1 t1, T2 t2) { T1 var1 = t1; T2 var2 = t2; Console.WriteLine($"t1:{t1}, t2:{t2}"); }
static void Main(string[] args) { short t1 = 1; int t2 = 2; DoStuff<short, int>(t1, t2); }
X.推断类型
如果为方法传入参数, 编译器有时可以从方法参数的类型中推断出应用作泛型方法的类型参数的类型, 这样就可以使方法调用更简单, 可读性更强, 并且省略尖括号
static void Main(string[] args) { short t1 = 1; int t2 = 2; DoStuff(t1, t2); //类型推断 }
(3)例子
class Simple { public static void ReverseAndPrint<T> (T[] arr) { Array.Reverse(arr); foreach (var item in arr) { Console.Write($"{item.ToString()}, "); } Console.WriteLine(""); } }
static void Main(string[] args) { var intArr = new int[] { 3, 5, 7, 9, 11 }; var stringArr = new string[] { "first", "secound", "third" }; var doubleArr = new double[] { 3.567, 7.891, 2.345 }; Simple.ReverseAndPrint<int>(intArr); Simple.ReverseAndPrint(intArr); Simple.ReverseAndPrint<string>(stringArr); Simple.ReverseAndPrint(stringArr); Simple.ReverseAndPrint<double>(doubleArr); Simple.ReverseAndPrint(doubleArr); }
6、扩展方法和泛型类
-
扩展方法
-
允许将类中的静态方法关联到不同的泛型类上
-
允许我们像调用类构造实例的方法应用来调用方法
-
-
泛型类的扩展方法的要求
-
必须是static的
-
必须是静态类的成员
-
第一个参数类型中必须有关键字this, 后面是扩展的泛型类的名字
-
class Holder<T> { T[] Vals = new T[3]; public Holder(T v0, T v1, T v2) { Vals[0] = v0; Vals[1] = v1; Vals[2] = v2; } public T[] GetValues() { return Vals; } }
static class ExtendHolder { public static void Print<T>(this Holder<T> h) { T[] vals = h.GetValues(); Console.WriteLine($"{vals[0]} \t {vals[1]} \t {vals[2]}"); } }
static void Main(string[] args) { Holder<int> intHolder = new Holder<int>(3, 5, 7); Holder<string> stringHolder = new Holder<string>("a1", "b2", "c3"); intHolder.Print(); stringHolder.Print(); }
7、泛型结构
-
泛型结构可以有参数和约束
-
规则和条件与泛型类一直
struct PieceOfData<T> { public T Data { get; set; } public PieceOfData(T data) { Data = data; } }
static void Main(string[] args) { PieceOfData<int> intData = new PieceOfData<int>(10); PieceOfData<string> stringData = new PieceOfData<string>("hi"); Console.WriteLine($"{intData.Data}"); Console.WriteLine($"{stringData.Data}"); }
8、泛型委托
-
泛型委托和非泛型委托非常相似
-
只不过类型参数决定了能接受什么样的方法
delegate 返回类型 MyDelegate<T, R>(委托形参); delegate R MyDelegate<T, R>(T value);
-
要声明委托, 在委托名称之后、委托参数列表之前的尖括号中放置参数类型
-
类型参数的范围包括:
-
返回类型
-
参数列表
-
约束子句
-
例子1
public delegate void MyDelegate<T>(T value);
class Simple1808 { public static void PrintString(string s) { Console.WriteLine(s); } public static void PrintUpperString(string s) { Console.WriteLine($"{s.ToUpper()}"); } internal static MyDelegate<string> PrintString() { throw new NotImplementedException(); } }
static void Main(string[] args) { MyDelegate<string> myDel = new MyDelegate<string>(Simple1808.PrintString); myDel += Simple1808.PrintUpperString; myDel("hi"); }
例子2
public delegate TR Func1808<T1, T2, TR>(T1 p1, T2 p2);
class Simple1808 { public static string PrintString(int p1, int p2) { int total = p1 + p2; return total.ToString(); } }
static void Main(string[] args) { Func1808<int, int, string> myDel = new Func1808<int, int, string>(Simple1808.PrintString); Console.WriteLine($"{myDel(15, 13)}"); }
9、泛型接口
-
允许编写形参和接口成员返回类型是泛型类型参数的接口
interface IMyIfc<T> { T ReturnIt(T intValue); }
class Simple1809<S> : IMyIfc<S> { public S ReturnIt(S intValue) { return intValue; } }
static void Main(string[] args) { Simple1809<int> simpleInt = new Simple1809<int>(); Simple1809<string> simpleString= new Simple1809<string>(); int vi = simpleInt.ReturnIt(11); string vs = simpleString.ReturnIt("hi"); Console.WriteLine($"{vi}"); Console.WriteLine($"{vs}"); }
(1)例 非泛型类实现同一泛型接口的两个不同接口
-
与其他泛型相似, 用不同类型参数实例化的泛型接口的实例是不同的接口
-
我们可以在非泛型类型中实现泛型接口
interface IMyIfc<T> { T ReturnIt(T inValue); }
//非泛型类实现同一泛型接口的两个不同接口 class Simple180901 : IMyIfc<int>, IMyIfc<string> { public int ReturnIt(int inValue) { return inValue; } public string ReturnIt(string inValue) { return inValue; } }
static void Main(string[] args) { Simple180901 simple180901 = new Simple180901(); Console.WriteLine($"{simple180901.ReturnIt(5)}"); //5 Console.WriteLine($"{simple180901.ReturnIt("hi")}"); //hi }
(2)泛型接口的实现必须唯一
实现泛型类型接口时, 必须保证实参的组合不会在类型中产生两个重复的接口
下列代码中, Simple
类使用了两个IMyIfc接口的实例化
-
第一个是构造类型, 使用类型int进行实例化
-
第二个有一个类型参数, 但不是实参
对于泛型接口, 使用两个相同接口本身没有错
-
但是如此会产生一个潜在的冲突
-
如果把
int
作为类型实参来代替第二个接口中S的话, Simple可能会有两个相同类型的接口, 这不是被允许的
class Simple180902<T> : IMyIfc<int>, IMyIfc<T> //错误 { public int ReturnIt(int inValue) { return inValue; } public T ReturnIt(T inValue) { return inValue; } }
10、可变性: 协变和逆变
(1)协变
-
仅仅将派生类型(子类)用作输出值与构造委托有效性之间的场数关系叫做协变
-
为了让编译器知道这是我们的期望, 使用
out
关键标记委托声明中的类型参数 -
每一种变量都有一种类型
-
可以将派生类型的对象赋值给基类型的变量, 这叫做赋值兼容性(向上转型)
class Animal{} class Dog : Animal{}
static void Main(string[] args) { Animal a1 = new Animal(); Animal a2 = new Dog(); }
X.例子
class Animal181001 { public int Legs = 4; }
class Dog181001 : Animal181001 { }
delegate T Factory<T>();
static void Main(string[] args) { Factory<Dog181001> dogMaker = MakeDog; Factory<Animal181001> animalMaker = dogMaker; //错误 //看上去由子类构造的委托应该可以赋值给父类构造的委托 //实际上虽然`Dog`和`Animal`是父子类, 但是`Factory<Dog>`和`Factory<Animal>`之间没有继承关系 //两个委托对象是同级的, 它们都从delegate类派生, delegate又派生自object }
-
看上去由子类构造的委托应该可以赋值给父类构造的委托
-
实际上虽然
Dog
和Animal
是父子类, 但是Factory<Dog>
和Factory<Animal>
之间没有继承关系 -
两个委托对象是同级的, 它们都从delegate类派生, delegate又派生自object
-
如果类型参数只用作输出值, 则同样的情况也适用于任何泛型委托
-
仅仅将派生类型(子类)用作输出值与构造委托有效性之间的场数关系叫做协变
-
为了让编译器知道这是我们的期望, 使用
out
关键标记委托声明中的类型参数
delegate T Factory<out T>();
static void Main(string[] args) { //- 仅仅将**派生类型(子类)用作输出值与构造委托有效性之间的场数关系叫做协变** //- 为了让编译器知道这是我们的期望, **使用`out`关键标记委托声明中的类型参数 * * //delegate T Factory<out T>(); Factory<Dog181001> dogMaker = MakeDog; Factory<Animal181001> animalMaker = dogMaker; //正确 }
-
在调用委托的时候, 调用代码接受
Dog
类型的对象, 而不是期望的Animal
类型的对象 -
调用代码可以自由地操作对象的
Animal
部分
(2)逆变
-
如下代码声明一个
Action1
的委托, 它接受一个类型参数, 以及一个该类型参数的方法参数, 且不返回值 -
代码还包含了一个叫做
ActOnAnimal
方法构建一个委托, 其签名和void返回类型符合委托声明 -
尝试将这个委托赋值给一个
Action1<Dog>
委托类型的栈变量dog1
delegate void Action1<in T>(T a); //in逆变关键字
class Animal1810 { public int Legs = 4; }
class Dog1810 : Animal1810 { }
static void ActOnAnimal(Animal1810 a) { Console.WriteLine($"{a.Legs}"); } static void Main(string[] args) { Action1<Animal1810> act1 = ActOnAnimal; Action1<Dog1810> dog1 = act1; dog1(new Dog1810()); }
-
使用逆变关键字
in
, 期望传入父类时允许传入子类对象的特性叫做逆变
delegate void Action1<in T>(T a); //in逆变关键字
-
栈上的变量是
void Action1<in T>(T p)
类型的委托, 其类型变量是Dog
类 -
图右边实际构建的委托使用
Animal
类的类型变量来声明, 它是Dog
的父类 -
在调用委托的时候, 调用代码为方法
ActOnAimal
传入Dog
类型变量, 而它期望的是Animal
类型的对象, 方法当然可以像期望的那样自由操作对象的Animal
部分
(3)协变和逆变的不同
-
协变
-
委托返回类型为父类
-
声明泛型委托类型参数为父类
-
实际构建委托参数列表使用子类
-
因为在调用委托的时候, 方法返回指向子类对象的引用, 也是指向父类的引用, 调用代码所期望的
-
-
逆变
-
委托输入参数为子类
-
声明泛型委托类型参数为子类
-
实际构建委托参数列表使用父类
-
因为在调用委托的时候, 调用代码向方法传入了派生类型的对象, 方法期望的只是基类型的对象, 方法完全可以自由操作对象的基类部分
-
(4)接口的协变和逆变
-
接口的逆变和协变同样适用
-
在声明接口中使用
out
/in
关键字
X.例子
interface IMyIfc1810<out T> //协变 { T GetFirst(); }
class SimpleReturn<T> : IMyIfc1810<T> { public T[] items = new T[2]; public T GetFirst() { return items[0]; } }
class Animal1810 { public int Legs = 4; public string Name; }
class Dog1810 : Animal1810 { }
static void ShowNameOfFirst(IMyIfc1810<Animal1810> returner) { Console.WriteLine(returner.GetFirst().Name); } static void Main(string[] args) { SimpleReturn<Dog1810> dogReturner = new SimpleReturn<Dog1810>(); dogReturner.items[0] = new Dog1810() { Name = "BOB" }; IMyIfc1810<Animal1810> animalReturner = dogReturner; ShowNameOfFirst(animalReturner); //BOB }
-
接口使用参数类型T声明泛型接口, 并且指定该类型参数是协变的
-
方法
ShowNameOfFirst
将接口作为咱数, 接受由Animal
类型构建的泛型接口作为参数 -
在主方法中
-
将
Dog
对象创建并初始化实现泛型接口的泛型类SimpleReturn
-
然后把对象赋值给
IMyIfc<Animal>
接口-
赋值左边的类型是接口, 而不是类
-
尽管接口类型不完全匹配, 但是编译器允许这种赋值, 因为在接口声明中定义了
out
协变标识符
-
-
(5)更多
A.自动进行强制类型转换
-
编译器可以自动识别某个已构建的委托是协变还是逆变, 并且自动进行强制类型转换
-
这通常发生在还没有为对象的类型赋值的时候
delegate T Factory<out T>();
static Dog1810 MakeDog() { return new Dog1810(); }
static void Main(string[] args) { Factory<Animal1810> animalMaker1 = MakeDog; //隐式强制转换 Factory<Dog1810> dogMaker = MakeDog; Factory<Animal1810> animalMaker2 = dogMaker; //需要out标识符 Factory<Animal1810> animalMaker3 = new Factory<Dog1810>(MakeDog); //需要out标识符 }
-
第一行代码返回类型是
Dog
对象而不是Animal
对象的方法, 创建了Factory<Animal>
类型的委托-
在创建委托的时候, 赋值运算符右边的方法名还不是委托对象, 因此还没有委托类型
-
此时编译器可以判定这个方法符合委托的类型, 除非其返回类型是
Dog
而不是Animal
-
编译器理解此处为协变关系, 然后构造类型并且把它赋值给变量
-
-
第三、第四行赋值
-
等号右边表达式已经是委托了, 因此具有委托类型
-
需要在委托声明中包含out标识符来通知编译器允许协变
-
B.可变性的注意事项
-
可变性处理是可以使用父类代替子类的安全情况, 反之亦然, 因此可变性只用于引用类型, 以内不能从值类型中派生其他类型
-
使用
in
,out
关键字的显示变化只适用于委托和接口, 不适用于类、结构、方法 -
不包括
in
,out
关键字的委托和接口类型参数是不变的, 这些类型参数不能用于协变或逆变
delegate T Factory<out R, in S, T>(); //out R R类型协变 //in S S类型逆变 //T T类型不变
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了