-
-
- 1.什么是泛型
- 2.构造器的定义
- 3.默认值的指定
- 4.元组
- 5.泛型类
- 5.1 声明泛型类
- 5.2 创建构造类
- 5.3 静态成员
- 5.4 创建变量和实例
- 6.类型参数的约束
- 6.1 Where子句
- 6.2 约束类型和次序
- 7.泛型方法
- 7.1 声明泛型方法
- 7.2 调用泛型方法
- 7.3泛型方法的实例
- 8.扩展方法和泛型类
- 9.泛型结构
- 10.泛型委托
- 11.泛型接口
- 11.1 使用泛型接口的示例
- 11.2 泛型接口的实现必须唯一
- 12.协变与逆变
- 12.1 委托的协变与逆变
- 12.2 接口的协变与逆变
-
1.什么是泛型
泛型(generic)特性提供了一种更优雅的方式,可以让多个类型共享一组代码,泛型允许声明类型参数化的代码,可以使用不同的类型进行实例化,在创建类的实例时指明真实类型。泛型类是类型的模板,关系如下图所示:
C#提供了5中泛型:类、结构、接口、委托和方法。前4种是类型,而方法是成员。下图演示了泛型类型如何用于其他类型。
泛型的简单实例如下所示:
class MyStack<T>
{
int stackPointer = 0;
T[] stactArray;
public void Push(T x) {...} public T Pop() {...} }
泛型的优点如下:
- 保证类型安全
- 减少装箱、拆箱操作,无需从object进行强制类型转换
- 多个类型共享一组代码,提高代码的复用性
命名规范:参数类型形参应包含T前缀。
2.构造器的定义
泛型类或结构的构造器不要求类型参数。
public struct Pair<T>
{
public Pair(T first, T second)
{
First = first;
Second = second; } public T First{ get; set; } public T Second { get; set; } }
3.默认值的指定
public struct Pair<T>
{
public Pair(T first) // struct需要初始化所有字段 { First = first; Second = default(T); // 不指定default报错 } public T First{ get; set; } public T Second { get; set; } }
4.元组
通过元数的不同来重载类型定义:
public class Tuple {...}
public class Tuple<T1>: ... {...} public class Tuple<T1, T2>: ... {...} public class Tuple<T1, T2, T3>: ... {...} public class Tuple<T1, T2, T3, T4>: ... {...} public class Tuple<T1, T2, T3, T4, T5>: ... {...} public class Tuple<T1, T2, T3, T4, T5, T6>: ... {...} public class Tuple<T1, T2, T3, T4, T5, T6, T7>: ... {...} public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest>: ... {...}
class Program
{
static void Main(string[] args)
{
string outparam = ""; int returnvalue = FunOutParamDemo(out outparam); // 使用out拿到多个返回值 Console.WriteLine(returnvalue + " " + outparam); Tuple<int, string> r = FunTupleParamDemo(); // 使用元组拿到多个返回值 Console.WriteLine(r.Item1 + " " + r.Item2); Console.ReadKey(); } public static int FunOutParamDemo(out string o) { o = "returnValue"; return 10; } public static Tuple<int, string> FunTupleParamDemo() { return new Tuple<int, string>(10, "returnValue"); } }
output
10 returnValue
10 returnValue
public static Tuple<int , int> MinMax(int a, int b) { return new Tuple<int, int>(Math.Min(a, b), Math.Max(a, b)); // Item1:Min, Item2:Max } Main: var r = MinMax(1, 2); Console.WriteLine($"min = {r.Item1}, max = {r.Item2}"); // 通过注释注明项 Item# 的意义
output
min = 1, max = 2
其中TRest中可以存储另一个Tuple,由此Tuple可以无限大。
var t = Tuple.Create(0, 1, 2, 3, 4, 5, 6, Tuple.Create(7, 8)); Console.WriteLine($"{t.Item1}, {t.Item2}, {t.Item3}, {t.Item4}, {t.Item5}, " + $"{t.Item6}, {t.Item7}, {t.Rest.Item1.Item1}, {t.Rest.Item1.Item2}");
使用Tuple的Create()工厂方法:
// 用静态 Tuple 重写 MinMax()
public static Tuple<int , int> MinMax(int a, int b) { return Tuple.Create(Math.Min(a, b), Math.Max(a, b)); }
Tuple<string, Contact> t1;
t1 = Tuple.Create("123456", new Contact("kyle")); Tuple<string, Contact> t2; t2 = new Tuple<string, Contact>("123456", new Contact("kyle"));
class Program
{
static void Main(string[] args)
{
Tuple<int> test1 = new Tuple<int>(34); Tuple<string, int> test2 = Tuple.Create("str", 2); Tuple<int, int> test3 = new Tuple<int, int>(2, 2); //8个元素的元组(注意,Tuple<类型...>: 基本"类型"最多7个, 第八个元素类型必须也为元组) Tuple<int, int, int, int, int, int, int, Tuple<int>> test4 = new Tuple<int, int, int, int, int, int, int, Tuple<int>>(1, 2, 3, 4, 5, 6, 7, new Tuple<int>(8)); Console.WriteLine(test1.Item1); Console.WriteLine(test2.Item1 + test2.Item2); Console.WriteLine(test4.Item1 + test4.Item2); Console.WriteLine(test4.Item1 + test4.Item2 + test4.Item3 + test4.Item4 + test4.Item5 + test4.Item6 + test4.Item7 + test4.Rest.Item1); Console.ReadKey(); } }
output
34
str2
3
36
由此可见,使用Tuple的Create()工厂方法无需指定类型参数。
5.泛型类
创建和使用常规非泛型的类有两个步骤:声明类和创建类的实例。而对于泛型类需要先构建实际的类类型,然后才能创建实例。
- 在某些类型上使用占位符来声明一个类
- 为占位符提供真实类型,该类型称为构造类型
- 创建构造类型的实例
泛型类创建流程如下图所示:
5.1 声明泛型类
声明一个简单的泛型类和声明普通类差不多,区别如下:
- 在类名之后放一组尖括号
- 在尖括号中用逗号分隔的占位符字符串来表示提供的类型,这叫做类型参数
- 在泛型类声明的主体中使用类型参数来表示应该替代的类型
如下代码声明了一个叫做SomeClass的泛型类。
class SomeClass <T1, T2>
{
public T1 someVar = new T1(); public T2 otherVar = new T2(); }
在泛型类型申明中并没有特殊关键字,取而代之的是尖括号中的类型参数列表,它可以区分泛型类与普通类的声明。
5.2 创建构造类
一旦创建了泛型类型,就要告诉编译器使用哪些真实类型来替代占位符,替代类型参数的真实类型叫做类型实参。
class SomeClass<T1, T2> {...} // T1,T2是类型参数(开放类型,不可创建实例) SomeClass<short, int> // short,int是类型实参(封闭类型,可创建实例)
5.3 静态成员
泛型类的静态成员只能在类的一个实例中共享,
public class StaticDemo<T>
{
public static int x; } StaticDemo<string>.x = 4; StaticDemo<int>.x = 5; Console.WriteLine(StaticDemo<string>.x);
output
4
5.4 创建变量和实例
如下代码所示非泛型类与泛型类创建对象:
MyNonGenClass myNGC = new MyNonGenClass(); // 非泛型类声明对象
SomeClass<short, int> mySc1 = new SomeClass<short, int>(); //泛型类声明对象 var mySc2 = SomeClass<short, int>(); // 匿名对象
与非泛型类相同,声明和实例可以分开进行:
SomeClass<short, int> mySc1; // 声明
mySc1 = new SomeClass<short, int>(); //实例化
非泛型类与泛型类直接的区别如下表所示。
非泛型 | 泛型 | |
---|---|---|
源代码大小 | 更大:需要为每一种类型编写代码 | 更小:不管多少类型,只需一个是实现 |
可执行大小 | 所有类型的实现代码都会被编译 | 只会根据提供的类型实参具体实现 |
写的难易度 | 易于书写,因为它更具体 | 比较难写,因为它更抽象 |
维护的难易度 | 同一个修改,所有可用类型上都要实现 | 易于维护,只要修改一处 |
6.类型参数的约束
要让泛型变得更有用,需要提供额外的信息让编译器知晓可以接受哪些类型。
如下代码所示,编译器将会产生一个错误信息。
class Simple<T>
{
static void bool LessThan(T i1, T i2) { return i1 < i2; // 错误 } }
可以提供的额外信息叫做约束(constrain),只有符合约束的类型才可以作为类型实参。
6.1 Where子句
约束使用Where子句列出。
- 每一个有约束的类型参数都有自己的where子句
- 如果类型参数有多个约束,则它们在where子句中使用逗号分隔
where子句的语法如下:
where TypeParam : constrain1, constrain2, ...
有关where子句的要点如下。
- 它们在类型参数列表的关闭尖括号后列出
- 它们不使用逗号或其他符号分隔
- 它们可以以任何次序列出
- where是上下文关键字,所以可以在其他上下文中使用
如下所示,其中T1未绑定,T2、T3具有约束。
class MyCalss <T1, T2, T3> where T2: Customer where T3: IComparable {...}
6.2 约束类型和次序
共有5种类型的约束,如下表所示:
约束类型 | 描述 |
---|---|
类名 | 只有这个类型的类或从它继承的类才能用作类型实参 |
class | 任何引用类型,包括类、数组、委托和接口都可以用作类型实参 |
struct | 任何值类型都可以用作类型实参 |
接口名 | 只有这个接口或者实现这个接口的类型才能用作类型实参 |
new() | 任何带有无参公共构造函数的类型都可以用作类型实参,这叫做构造函数约束 |
where子句可以以任何次序列出,但where子句中的约束必须有特定的顺序
- 最多只有一个主约束,如果有则必须放在第一个
- 可以有任意多个接口名约束
- 如果存在构造函数约束,则必须放在最后
约束顺序如下所示:
Primary (0 or 1) | Secondary (0 or more) | Constructor (0 or 1) |
---|---|---|
ClassName class struct | InterfaceName | new() |
7.泛型方法
与其他泛型不一样,方法是成员,不是类型。泛型方法可以在泛型类和非泛型类以及结构和接口中声明,如下图所示:
7.1 声明泛型方法
泛型方法具有类型参数列表和可选的约束。
- 泛型方法有两个参数列表
- 封闭在原括号内的方法参数列表
- 封闭在见括号内的类型参数列表
- 要声明泛型方法需要:
- 在方法名称后和方法参数列表前放置类型参数列表
- 在方法参数列表后放置可选的约束子句
public void PrintData<S, T> (S p, T t) where S: Person {...}
7.2 调用泛型方法
void Do<T1, T2>(T1 t1, T2 t2) { T1 someVar = t1; T2 otherVar = t2; } Do<int, double>(aVal, bVal);
推断类型 编译器有时可以从方法参数中推断出赋给泛型方法的类型实参,
public void MyMethod<T>(T t){...}
int myInt = 5; MyMethod(myInt) // 由MyMethod<int>(myInt)简化
但当泛型方法的方法参数列表为空时,则必须在类型参数列表中指明类型参数。
public void MyMethod<T>()
{
Console.Write(typeof(T));
}
MyMethod<int>();
7.3泛型方法的实例
class Simple
{
public static void ReverseAndPrint<T>(T[] arr)
{
Array.Reverse(arr);
foreach (T item in arr) Console.Write(item.ToString() + " "); Console.WriteLine(); } } class Program { static void Main() { var intArray = new int[] { 3, 5, 7, 9, 11 }; var stringArray = new string[] { "first", "second", "third" }; var doubleArray = new double[] { 1.23, 2.34, 5.33 }; Simple.ReverseAndPrint<int>(intArray); Simple.ReverseAndPrint(intArray); Simple.ReverseAndPrint(stringArray); Simple.ReverseAndPrint(doubleArray); Console.ReadKey(); } }
output:
11 9 7 5 3 3 5 7 9 11 third second first 5.33 2.34 1.23
8.扩展方法和泛型类
泛型类的扩展方法需满足以下要求:
- 必须声明为static
- 必须是静态类的成员
- 第一个参数类型中必须有关键字this,后面是扩展的泛型类的名字
static class ExtendHolder
{
public static void Print<T>(this Holder<T> h) { T[] vals = h.Getvalues(); Console.WriteLine($"{vals[0]}, {vals[1]}, {vals[2]}"); } } 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; } } class Program { static void Main() { var intHolder = new Holder<int>(3, 5, 7); var stringHolder = new Holder<string>("a1", "a2", "a3"); intHolder.Print(); stringHolder.Print(); Console.ReadKey(); } }
output
3, 5, 7
a1, a2, a3
9.泛型结构
与泛型类相似,泛型结构可以有类型参数和约束,其规则与条件也与泛型类相同。
struct PieceOfdata<T>
{
public PieceOfdata(T value) { data = value; } private T data; public T Data { get { return data; } set { data = value; } } } class Program { static void Main() { var intData = new PieceOfdata<int>(10); var stringData = new PieceOfdata<string>("hello"); Console.WriteLine($"intData = {intData.Data}"); Console.WriteLine($"stringdata = {stringData.Data}"); Console.ReadKey(); } }
output
intData = 10
stringdata = hello
10.泛型委托
- 要声明泛型委托,在委托名称后、委托参数列表前的尖括号中放置类型参数列表
- 有两个参数列表:委托形参列表和类型参数列表
- 类型参数的范围包括:
- 返回值
- 形参列表
- 约束子句
delegate void MyDelegate<T>(T value);
class Simple { static public void PrintString(string s) { Console.WriteLine(s); } static public void PrintUpperString(string s) { Console.WriteLine(s.ToUpper()); } } class Program { static void Main() { var myDel = new MyDelegate<string>(Simple.PrintString); myDel += Simple.PrintUpperString; myDel("hi there"); Console.ReadKey(); } }
output
hi there
HI THERE
11.泛型接口
interface IMyIfc<T> // 泛型接口
{
T ReturnIt(T inValue);
}
class Simple<S> : IMyIfc<S> // 泛型类 { public S ReturnIt(S inValue) // 实现泛型接口 { return inValue; } } class Program { static void Main() { Simple<int> trivInt = new Simple<int>(); Simple<string> trivString = new Simple<string>(); Console.WriteLine(trivInt.ReturnIt(5)); Console.WriteLine(trivString.ReturnIt("hello")); Console.ReadKey(); } }
output
5
hello
11.1 使用泛型接口的示例
- 与其他泛型相似,实现不同类型参数的泛型接口是不同的接口
- 可以在非泛型类型中实现泛型接口
interface IMyIfc<T>
{
T ReturnIt(T inValue);
}
class Simple: IMyIfc<int>, IMyIfc<string> { public int ReturnIt(int inValue) { return inValue; } public string ReturnIt(string inValue) { return inValue; } } class Program { static void Main() { Simple simple = new Simple(); Console.WriteLine(simple.ReturnIt(5)); Console.WriteLine(simple.ReturnIt("hello")); Console.ReadKey(); } }
output
5
hello
11.2 泛型接口的实现必须唯一
实现泛型类接口时,必须保证类型实参组合不会在类型中产生两个重复的接口。
class Simple<S>: IMyIfc<int>, IMyIfc<S> //错误,存在潜在冲突 { public int ReturnIt(int inValue) { return inValue; } public S ReturnIt(S inValue) // 当S为int时产生冲突 { return inValue; } }
泛型接口不会与非泛型接口产生冲突,
interface IMyIfc
{
int ReturnIt(int inValue);
}
interface IMyIfc<T> { T ReturnIt(T inValue); } class Simple<S>: IMyIfc<S>, IMyIfc //无冲突 { public S ReturnIt(S inValue) { return inValue; } public int ReturnIt(int inValue) { return inValue + 10 ; } } class Program { static void Main() { Simple<double> simple1 = new Simple<double>(); // 调用泛型接口 Simple<int> simple2 = new Simple<int>(); // 调用非泛型接口 Console.WriteLine(simple1.ReturnIt(5.1)); Console.WriteLine(simple2.ReturnIt(5)); Console.ReadKey(); } }
output
5.1
15
12.协变与逆变
在.NET中,参数类型是协变的,假定有Shape类和Rectangle类,Rectangle派生自Shape类。现声明Display()方法接受Shape类型的对象作为其参数
public void Display(Shape o) {...}
此时Display的参数可以传入派生自Shape基类的任意对象,
var r = new Rectangle( Width = 5, Length = 10 );
Display(r);
方法的返回类型是逆变的。当方法返回一个Shape时,不能把它赋予Rectangle,因为Shape不一定是Rectangle,反之可行,
public Rectangle GetRectangle();
Shape s = GetRectangle();
若果要在泛型中实现协变与逆变,则需要out与in关键字标注。
如果泛型类型用out关键字标注,泛型委托(接口)就是协变的,这也意味着返回类型只能是T。
public delegate T Dele<out T>();
public interface IMyIfc<out T>
如果泛型类型用in关键字标注,泛型委托(接口)就是逆变的,委托(接口)只能把泛型类型T用作其方法的输入。
public delegate T Dele<in T>();
public interface IMyIfc<in T>
协变与逆变的不同如下图所示:
12.1 委托的协变与逆变
将派生类的对象实例赋值给基类的变量,叫做赋值兼容性。
class Animal
{
public int numberOfLegs = 4; } class Dog: Animal { } class Program { static void Main() { Animal a1 = new Animal(); Animal a2 = new Dog(); // 将派生类对象实例赋给基类变量 Console.WriteLine($"number of animal legs: {a1.numberOfLegs}"); Console.WriteLine($"number of dog legs: {a2.numberOfLegs}"); Console.ReadKey(); } }
output
number of animal legs: 4
number of dog legs: 4
下面对代码进行扩展,添加一个委托,
class Animal
{
public int numberOfLegs = 4; } class Dog: Animal { } delegate T Factory<T>(); class Program { static Dog MakeDog() { return new Dog(); } static void Main() { Factory<Dog> dog = MakeDog; // 创建委托对象 Factory<Animal> animal = dog; // 尝试赋值委托对象 Console.WriteLine(animal().numberOfLegs); Console.ReadKey(); } }
以上使用委托赋值失败,是因为Factory<Dog>与Factory<Animal>都派生自delegate类,两个委托对象是同级关系。
如果派生类只是用于输出值,那么这种结构化的委托有效性之间的常数关系叫做协变,使用关键字out指定类型参数的协变。
delegate T Factory<out T>();
在期望传入基类时允许传入派生对象的特性叫做逆变,可以在类型参数中显式使用in关键字来实现。
delegate T Factory<in T>();
12.2 接口的协变与逆变
class Animal { public string Name; }
class Dog : Animal { } interface IMyIfc<out T> { T GetFirst(); } class SimpleReturn<T> : IMyIfc<T> { public T[] items = new T[2]; public T GetFirst() { return items[0]; } } class Program { static void Do(IMyIfc<Animal> returner) { Console.WriteLine(returner.GetFirst().Name); } static void Main() { SimpleReturn<Dog> dogReturner = new SimpleReturn<Dog>(); dogReturner.items[0] = new Dog() { Name = "Bob" }; IMyIfc<Animal> animalReturner = dogReturner; Do(dogReturner); Console.ReadKey(); } }
output
Bob