C#教程 - 泛型(Generic Types)
更新记录
转载请注明出处:https://www.cnblogs.com/cqpanda/p/16690994.html
2022年9月18日 发布。
2022年9月10日 从笔记迁移到博客。
泛型(Generic Types)说明
一般情况下,我们都是先声明一个类型,然后使用这个类型再去实例化一个对象
我们把类中的具体类型再提出出来,使具体类型可以适应不同的类型
这需要添加一个额外的抽象层,称为泛型
泛型(generic)将类中的具体类型进行类型参数化(type-parameterized)
没有绑定类型参数的泛型类型叫做open generic types
绑定类型参数的泛型类型叫做bound Generic Types
泛型生成类型,类型再生成实例
可定义:类(class)、结构(struct)、接口(interface)、委托(delegate)、方法(method)的泛型
泛型类型参数只可以由:classes,structs,interfaces,delegates,methods引入
其他类型和成员可以只可以使用类型参数
注意:
构造、析构函数不需要使用类型参数
泛型类型约束会被继承。而泛型方法不会被继承
泛型类型中不可以使用操作符
使用原因:
提高运行效率,没有装箱拆箱效率损失
类型安全,泛型限定类型
缓解代码膨胀
支持多种类型
提高可维护度
不使用原因:
代码复杂度提高
泛型类
声明泛型类类型
在类名后添加一组尖括号
在尖括号内使用类型占位符来表示类型,用逗号分隔
在泛型类声明的主体中使用类型参数来表示应该代替的类型
使用泛型创建类类型
用真实的类型代替尖括号中的类型参数,称为类型实参(type argument)
通过提供类型实参,编译器将为我们生成一个真实的类
注意:
泛型类型 与 普通类型 不会产生冲突:
class A {}
class A<T> {}
class A<T1,T2> {}
创建类实例
和普通类创建实例没有太多差异
泛型还可以生成不同的类型,类型再生成不同的实例
实例:泛型结构
public struct Nullable<T>
{
public T Value { get; }
}
实例:泛型栈
public class Stack<T>
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
var stack = new Stack<int>();
stack.Push (5);
stack.Push (10);
int x = stack.Pop(); // x is 10
int y = stack.Pop(); // y is 5
实例:泛型类型参数重载
class A {}
class A<T> {}
class A<T1,T2> {}
实例:获得泛型类型的类型
class A<T> {}
class A<T1,T2> {}
Type a1 = typeof (A<>); // Unbound type (notice no type arguments).
Type a2 = typeof (A<,>); // Use commas to indicate multiple type args.
Type a3 = typeof (A<int,int>); //bound type
实例:在泛型类内部获得类型
class B<T> { void X() { Type t = typeof (T); } }
泛型生成的类型和非泛型类型的区别
泛型类型参数注意
泛型类型参数(占位符)只可以在 泛型类 和 泛型方法中声明
可以定义classes, structs, interfaces, delegates的泛型类型
只有泛型类 和 泛型方法(Generic Methods)可以引入类型参数(type parameters)
属性(Properties)、索引(indexer)、事件(event)、属性(property)
字段(fields)、构造函数(constructors)、运算符重载(operators)
都不可以引入类型参数,只可以使用类预定义的类型参数
静态成员处理
using System;
namespace ConsoleApp1
{
//测试用的泛型类
class PandaTest<T>
{
public static int Count = 0;
}
class Program
{
static void Main(string[] args)
{
//新建泛型类进行测试
++PandaTest<int>.Count;
++PandaTest<string>.Count;
Console.WriteLine(PandaTest<int>.Count); //1
Console.WriteLine(PandaTest<string>.Count); //1
//再次累加
++PandaTest<int>.Count;
++PandaTest<string>.Count;
Console.WriteLine(PandaTest<int>.Count); //2
Console.WriteLine(PandaTest<string>.Count); //2
//wait
Console.ReadKey();
}
}
}
内部成员类型转换
因为T类型在类定义时是不确定的,所以在转换时使用object作为中间层
class PandaTest<T>
{
public T SomeData;
public int DoSomething()
{
return (int)(object)this.SomeData;
}
}
内部类型比较问题
因为类型定义的时候,无法知道类型是否可以比较
所以在占位符类型定义时,使用约束使其继承IComparable
class PandaTest<T>
where T : IComparable<T>
{
public T DoSomething(T a, T b)
{
return a.CompareTo(b) > 0 ? a : b;
}
}
泛型方法
泛型方法可以在泛型和非泛型的类、结构、接口中声明
泛型方法也可以使用约束
声明泛型方法
在方法名之后放置类型参数列表
在方法参数之后放置可选的约束子句
调用泛型方法
还可以不指定类型参数,编译器会根据方法的参数推断类型参数
泛型结构
和泛型类基本相同
声明泛型结构类型
struct Panda<T1, T2>
{
public T1 arg1 { get; set; }
public T2 arg2 { get; set; }
public Panda(T1 arg1, T2 arg2)
{
this.arg1 = arg1;
this.arg2 = arg2;
}
}
实例化泛型结构类型
Panda<int, string> obj1 = new Panda<int, string>(666, "Panda");
Panda<string, string> obj2 = new Panda<string, string>("6", "P");
泛型委托
在委托名之后放置类型参数列表
注意:类型参数的范围包括:返回值、参数、约束
泛型接口
和普通接口类似,在接口名之后放置尖括号和类型参数即可
类实现泛型接口
注意:类实现泛型接口,类可以是:非泛型的 或 泛型的
一个类型实现同一个泛型接口的不同实际类型
泛型默认值(default Generic Value)
在泛型编程结构中可以使用default关键字声明默认值
语法:
default(T);
从C# 7.1开始可以省略类型标识
default;
实例:
class Test<T>
{
public T SomeMethod(T arg1)
{
arg1 = default;
return arg1;
}
}
实例:
array[i] = default(T);
array[i] = default; //从 C# 7.1 开始
类型参数约束(Generic Constraints)
约束说明
约束(constrain):为类型参数提供额外的信息,让编译器知道参数可以接受那些类型
使用where子句
每一个类型参数的约束使用一个单独的where子句
如果一个类型有多个约束,可以使用逗号进行分隔
where子句放在类型参数列表的关闭尖括号之后
where子句不需要使用逗号分隔
where子句的次序不限制
格式:
多个where子句
约束的类型:
where T : class? //
where T : unmanaged //必须是值类型,如果是结构类型不可以包含引用类型
where T : base-class // Base-class constraint
where T : interface // Interface constraint
where T : class // Reference-type constraint
where T : class? // (See "Nullable reference types")
where T : struct // Value-type constraint (excludes Nullable types)
where T : unmanaged // Unmanaged constraint
where T : new() // Parameterless constructor constraint
where U : T // Naked type constraint
where T : notnull // Non-nullable value type, or from C# 8
// a non-nullable reference type.
=========这里需要合并
注意:
最多只能有一个主约束,如果有则放在第一位
主约束可以是值类型、引用类型、类名
可以有任意多的接口约束
如果有构造函数约束,则必须放在最后
委托、数组、枚举类型不可以作为约束条件
构造函数约束只能有一个
约束的顺序:
约束顺序实例:
约束实例
//接口约束
class ClassName<T1...>
where T1 : 接口名
//类类型约束
class ClassName<T1...>
where T1 : 类名
//类约束
class ClassName<T1...>
where T1 : ClassName
//结构约束
class ClassName<T1...>
where T1 : StructName
//构造器约束
class ClassName<T1...>
where T1 : new()
//多个约束的语法:
class ClassName<T1,T2>
where T1: struct //没有逗号哟
where T2: class
注意:没有委托和枚举类型的约束
泛型类型间继承(Subclassing Generic Types)
子类化泛型类型(Subclassing Generic Types)
子类可以继承父类泛型的类型参数或者不继承泛型的类型参数
实例:
class Stack<T> {...}
//泛型类型 继承 泛型类型
class SpecialStack<T> : Stack<T> {...}
//非泛型类型 继承 泛型类型
class IntStack : Stack<int> {...}
//泛型类型(多个参数) 继承 泛型类型
class List<T> {...}
class KeyedList<T,TKey> : List<T> {...}
实例:子类继承父类的泛型类型参数
class Stack<T> {...}
class SpecialStack<T> : Stack<T> {...}
实例:子类不继承父类的泛型类型参数
class IntStack : Stack<int> {...}
实例:子类继承父类的泛型类型参数并添加泛型类型参数
class List<T> {...}
class KeyedList<T,TKey> : List<T> {...}
自引用泛型声明(Self-Referencing Generic Declarations)
可以在泛型的定义中引用自身
实例:在实现IEquatable泛型接口中引用Balloon自身
public interface IEquatable<T> { bool Equals (T obj); }
public class Balloon : IEquatable<Balloon>
{
public string Color { get; set; }
public int CC { get; set; }
public bool Equals (Balloon b)
{
if (b == null) return false;
return b.Color == Color && b.CC == CC;
}
}
实例:定义泛型类型时,引用自身
class Foo<T> where T : IComparable<T> { ... }
class Bar<T> where T : Bar<T> { ... }
泛型内的静态数据(Static Data)
每种泛型类型的具体类型的静态数据共用
静态数据对于每个封闭类型都是唯一的
实例:
class Bob<T> { public static int Count; }
Console.WriteLine (++Bob<int>.Count); // 1
Console.WriteLine (++Bob<int>.Count); // 2
Console.WriteLine (++Bob<string>.Count); // 1
Console.WriteLine (++Bob<object>.Count); // 1
实例:实例化泛型类型参数后,再使用静态数据
class Bob<T> { public static int Count; }
class Test
{
static void Main()
{
Console.WriteLine (++Bob<int>.Count); // 1
Console.WriteLine (++Bob<int>.Count); // 2
Console.WriteLine (++Bob<string>.Count); // 1
Console.WriteLine (++Bob<object>.Count); // 1
}
}
类型参数与转换(Type Parameters and Conversions)
泛型类型中使用强制转换会导致编译不通过
进行转换可以使用的方法:
使用as来进行引用转换
转为object后再进行转换
实例:泛型类型中使用强制转换
StringBuilder Foo<T> (T arg)
{
if (arg is StringBuilder)
return (StringBuilder) arg; // Will not compile
}
实例:使用as进行引用转换
StringBuilder Foo<T> (T arg)
{
StringBuilder sb = arg as StringBuilder;
if (sb != null) {
return sb;
}
}
实例:转为object后再进行转换
StringBuilder Foo<T> (T arg)
{
if (arg is StringBuilder)
return (StringBuilder)(object)arg; // Will compile
}
扩展方法和泛型类
扩展方法可以和泛型类一起结合使用
声明泛型扩展方法:
和泛型方法没有多大差异,在方法中包含类型参数,在参数中包含类型参数即可
泛型与嵌套类型结合
实例:泛型与嵌套类型结合
class Stack<T>
{
Stack<U> FilteredStack<U>() where U : T {...}
}
泛型本质
泛型在CIL中的表示也是一个类
值类型参数的泛型类型会对每个值类型进行生成类,相同的值类型的使用一个类
引用类型参数的泛型类型,会生成一个object引用参数
当有不同引用参数时,直接改变object引用即可,不会生成多个类
编译器可以进行类型推断,部分情况下可以省略类型参数
C#泛型 和 C++模板 比较
C#泛型在源代码和runtime中都存在
C++模板只在源代码中存在
规范/最佳实践
类型参数使用T开头,并使用明确表达参数意思的标识符
类型参数指明约束
将同名的泛型类型放在一个文件中
考虑定义自己的委托类型的可读性是否超过了使用预定义的通用委托类型的便利性
typeof和未绑定的泛型类型(Unbound Generic Types)
未绑定(Open generic)的类型在运行(runtime)时可以用typeof运算符获得
实例:
class A<T> {}
class A<T1,T2> {}
Type a1 = typeof (A<>); // Unbound type
Type a2 = typeof (A<,>); // Indicates 2 type args
Type a3 = typeof (A<int,int>); //specify a closed type
Console.Write (a2.GetGenericArguments().Count()); // 2
协变(Covariance)
说明
假设A可以转为B,如果X可转换为X,则X具有协变类型参数
协变可以理解为和谐的变化,这样方便记忆
支持协变的类型有:Interfaces、Delegate、Array
协变不是自动的,需要手动去给类型参数设置out关键字
注意:
对于C#的协方差(和逆方差)概念
“convertible”表示通过隐式引用转换(如子类B或实现B)进行转换
即只在引用类型中发送,不包括数值转换、装箱转换和自定义转换
定义泛型类型:
delegate T DelegateName<T>();
则DelegateName
称为DelegateName
协变修饰符out
当son转为father时,读取转换后的实例成员的倒是没有什么。但对转为father后的实例的成员进行赋值将出现类型不安全,比如为其赋值son类型的值。所以协变参数使用out修饰,表示只能进行读取输出不能写入。这样C#就支持协变,但禁止协变参数进行赋值。这样就能保证协变的安全
实例
示意图:
协变的限制
协变的类型参数只能作为输出参数。不能作为输入参数
协变只能在引用类型中发生
只有泛型接口和泛型委托允许协变,泛型类和泛型结构不允许协变
逆变(Contravariance)
说明
逆变和协变是相反的
如果Father类型可以强制转换为Son类型(Son类继承自Father类)
定义泛型类型
Delegate void DelegateName<T>(T argName);
如果DelegateName
协变修饰符in
当Father转为Son时,写入转换后的实例成员的倒是没有什么(Son可以转为Father)
但对转为Son后的Father实例进行读取将出现类型不安全
所以逆变参数使用in修饰,表示只能进行写入不能读取
这样C#就支持逆变,但禁止逆变参数进行读取。这样就能保证逆变的安全
实例
示意图:
实例:
IPushable<Animal> animals = new Stack<Animal>();
IPushable<Bear> bears = animals; // Legal
bears.Push (new Bear());
逆变的限制
逆变的类型参数只能作为输入参数。不能作为输出参数
逆变只能在引用类型中发生
只有泛型接口和泛型委托允许协变。泛型类和泛型结构不允许协变
协变和逆变总结
泛型性能优化
考虑将ArrayList、Queue、Stack、Hastable对象转为对应的泛型对象,减少装箱拆箱性能损耗,提高类型安全
本文来自博客园,作者:重庆熊猫,转载请注明原文链接:https://www.cnblogs.com/cqpanda/p/16690994.html