泛型特性提供了一种优雅的方式,可以让多个类型共享一组代码
泛型允许声明类型参数化的代码,可以用不同的类型进行实例化
即使用“类型占位符”来写代码,然后在创建实例的时候指明真实的类型
泛型也是一种类型的模板
C#提供了5种泛型:类、结构、接口、委托和方法
通过一个示例来认识泛型:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
//定义一个泛型类
//使用<T>占位符来定义这个类
public class MyClass<T>
{
T Value;
public T GetValue(T x)
{
this.Value = x;
return Value;
}
}
class Program
{
static void Main(string[] args)
{
//实例化自定义的泛型类
//为占位符提供真实类型
//这个类型被称为构造类型
MyClass<int> mc = new MyClass<int>();
var y = mc.GetValue(5); //通过var自动推断类型
Console.WriteLine("值:" + y + ",类型:" + y.GetType());
Console.ReadKey();
}
}
}
解释:
- 在类后面放置<T>
- T代表类型占位符
- T可以用任何标识符,只要让标识符用<>包围起来即可
- 在类的实例中,第一个T都会被替换成实际类型
泛型类的使用
泛型类的声明
class SomeClass<T1, T2>
{
public T1 SomveVar = new T1();
public T2 OtherVar = new T2();
}
说明:
- 在类名后面放一组尖括号
- 在尖括号中使用逗号分隔希望提供的类型的占位符,这叫做类型参数
- 在泛型类声明的主体中使用类型参数来表示替代的类型
- 这里的
T1 ,T2
叫做类型参数的占位符
构造类型
通过列出类名并在尖括号中提供真实类型来替代类型参数(这叫做类型实参),来创建构造类型(用来创建真实对象的模板)
SomeClass<int, short>
上例中,int,short
是类型实参,将会替换类型参数T1, T2
,
SomeClass<int, short>
是一个构造类型,了就是将要被创建的对象的一个模板
变量和实例
SomeClass<int, short> sc1;
sc1 = new SomeClass<int, short>();
//或者
var sc2 = new SomeClass<int, short>();
可以通过构造类型来创建一个泛型类的实例,然后赋值给一个变量;也可以使用var让编译器自动推断对象的类型
一个完整的代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
//声明一个泛型类
class MyStack<T>
{
//声明一个T类型的数组
T[] StackArray;
int StackPointer = 0;
//向数组中添加值
public void Push(T x)
{
if (!IsStackFull)
StackArray[StackPointer++] = x; //向数组中添加一个值
}
//弹出数组中的一个值
public T Pop()
{
return (!IsStackFull)
? StackArray[--StackPointer]
: StackArray[0];
}
//指定数组最大长度
const int MaxStack = 10;
//用于判断数组是否已满
bool IsStackFull
{get { return StackPointer >= MaxStack; }}
//用于判断数组是否为空
bool IsStackEmpty
{get { return StackPointer <= 0; }}
//初始化类
public MyStack()
{
//实例化一个T类型的数组
StackArray = new T[MaxStack];
}
//逆向输出数组中的值
public void Print()
{
for (int i = StackPointer-1; i >= 0; i--)
Console.WriteLine(" value: {0}",StackArray[i]);
}
}
class Program
{
static void Main(string[] args)
{
//创建一个int类型的MyStack对象
MyStack<int> static_int = new MyStack<int>();
//创建一个stirng类型的MyStack对象
MyStack<string> static_stirng = new MyStack<string>();
//添加数值并输出
static_int.Push(3);
static_int.Push(5);
static_int.Push(7);
static_int.Push(9);
static_int.Print();
//添加字符串并输出
static_stirng.Push("Hello");
static_stirng.Push("World");
static_stirng.Print();
Console.ReadKey();
}
}
}
泛型类的特点:
- 不管构造类型的数量有多少,只需要一个实现
- 可执行文件中只要出现有构造类型的类型
- 比较难写,因为它比较抽象
- 但易于维护,因为只需要更改一个地方
类型参数的约束
如果使用泛型类的时候,传递了一个Int与一个string大小比较操作,则会报错;所以需要对参数做出约束
Where子句
类型参数的约束需要用到Where子句
如果形参有多个约束,它们在Where子句中使用逗号分隔
Where子句语法如下:
where TypeParam: contraint, contraint, ...
- TypeParam表示类型参数
- contraint, contraint, ...表示约束列表
- 它们在类型参数列表的关闭尖括号之后列出
- 它们不使用逗号或其他符号分隔
- 它们可以以任何次序列出
- where是上下文关键字,所以可以在其他上下文中使用
示例:
class MyClass<T1, T2, T3>
where T2: Customer
where T3: ICustomer
{
...
}
约束类型和次序
- 类名 - 只有这个类型的类或从它继承的类才能用作类型参数
- class - 任何引用类型,包括类、数组、委托和接口都可以用作类型参数
- struct - 任何值类型都可以用作类型参数
- 接口名 - 只有这个接口或实现这个接口的类型才能用作类型参数
- new() - 任何带有无参公共构造函数的类型都可以用作类型实参;这叫构造函数约束
如果最多只能有一个主约束,如果有则必须放在第一位;可以有任意多的接口约束;如果存在构造函数约束,则必须放在最后
示例:
//T约定:只能是Access类型或者Access的子类型
public class BaseAccess<T> where T : Access
{...}
//T约定:T只能传入接口的本身和实现了此接口的类
public class BaseAccess<T> where T : IAggregateRoot
{...}
//引用类型约束演示
public class BaseAccess<T> where T : class
{...}
//值类型约束演示
public class BaseAccess<T> where T : struct
{...}
//构造器约束
public class BaseAccess<T> where T : new()
{...}
//一个类型占位符有两个约束
//必须是引用类型,必须提供构造函数
public class BaseAccess<T> where T : class,new()
{...}
//K必须约定是一个引用类型
//V必须约定是一个值类型
public class BaseAccess<K, V>
where K : class,new()
where V : struct
{...}
//泛型参数K必须继承V
//K,V必须是引用类型,必须提供构造函数
public class BaseAccess<K, V>
where K : V
where K : class,new()
where V : class,new()
{...}
泛型方法
泛型方法具有两个参数列表:
- 封闭在圆括号内的方法参数列表
- 封闭在尖括号内的类型参数列表
示例:
public void Func<S, T> (S s, T t) where S: Person {...}
解析:
在方法名称之后方法参数列表之前放置类型参数列表
在方法参数列表之后放置可选的约束子句
调用泛型方法
语法:
Func<Person, int>(person, 5);
在调用的时候需要提供实参
一个完整的示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
class Program
{
//泛型方法
//约束T2必须是值类型的
static void Func<T1, T2>(T1 t1, T2 t2) where T2: struct
{
T1 valueA = t1;
T2 valueB = t2;
Console.WriteLine("valueA 值:"+valueA+", 类型:"+valueA.GetType());
Console.WriteLine("valueB 值:" + valueB + ", 类型:" + valueB.GetType());
}
static void Main(string[] args)
{
//如果将第二个参数指定为非值类型的,将会报错
//Func<string, string>("Hello","World");
Func<string, long>("Hello", 2000);
Console.ReadKey();
}
}
}
让编译器推断类型
public void Func<T>(T t)
{
T1 valueA = t1;
Console.WriteLine("valueA 值:"+valueA+", 类型:"+valueA.GetType());
}
int value = 15;
//正常调用泛型方法
Func<int>(value);
//让泛型方法自动推断参数类型以简化代码
Func(value);
扩展方法和泛型类
扩展方法允许将类中的静态方法关联到不同的泛型类上
允许像调用类构造实例的实例方法一样来调用方法
泛型类的扩展方法要求:
- 必须声明static
- 必须是静态类的成员
- 第一个参数类型中必须有关键字this,后面是扩展的泛型类的名字
示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
//创建一个静态类
static class ExtendHoder
{
//创建一个静态的打印方法,用来扩展Holder类
public static void Print<T>(this Holder<T> h)
{
//参数中需要指定被扩展的类,前面必须有this关键字
//访问被扩展的类,从而针对性的做出一些操作
T[] vals = h.GetValues();
Console.WriteLine("{0}, \t{1}, \t{2}", vals[0], vals[1], vals[2]);
}
}
//定义一个基本的泛型类
//本类将会被扩展
class Holder<T>
{
//一个泛型数组
T[] Vals = new T[3];
//初始化泛型数组
public Holder(T v1, T v2, T v3)
{
Vals[0] = v1;
Vals[1] = v2;
Vals[2] = v3;
}
//获取泛型数组方法
public T[] GetValues() { return Vals; }
}
class Program
{
static void Main(string[] args)
{
//实例化一个<int>构造类型的Holder对象
var intHolder = new Holder<int>(3, 5, 7);
//实例化一个<string>构造类型的Holder对象
var stringHolder = new Holder<string>("a1", "b2", "c3");
//Holder类中本身是没有Print方法的
//可以看出,通过扩展,可以在Holder类中访问到扩展的方法
intHolder.Print();
stringHolder.Print();
Console.ReadKey();
}
}
}
在通过Holer
的实例来调用扩展方法Print
的时候,IDE会自动提示它是一个扩展方法
扩展方法的好处是可以在不更改原有方法的情况下,不其增加新的功能
泛型结构
与泛型类相似,泛型结构可以有类型参数和约束。泛型结构的规则和条件与泛型类是一样的。
示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
//定义一个泛型结构
struct MyData<T>
{
T _data;
//初始化结构中的数据
public MyData(T data)
{
this._data = data;
}
//获取数据
public T Data
{
get { return _data; }
set { _data = value; }
}
}
class Program
{
static void Main(string[] args)
{
//实例化两个不同类型的泛型结构结构
MyData<int> md_1 = new MyData<int>(5);
MyData<string> md_2 = new MyData<string>("Hello");
//打印属性值
Console.WriteLine(md_1.Data);
Console.WriteLine(md_2.Data);
Console.ReadKey();
}
}
}
泛型委托
泛型委托与非泛型委托非常相似,不过类型参数决定了能接受什么样的方法
语法:
delegate R DelName<T, R>(T value);
第一个R
表示返回类型;<T, R>
表示类型参数;(T value)
是委托的形参
需要注意的是,这里的类型参数列表负责的范围包括:
- 返回值类型
- 形参列表类型
- 约束子句
示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
//定义一个泛型委托类型
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("{0}", s.ToUpper());
}
}
class Program
{
static void Main(string[] args)
{
//初始化一个泛型委托类型
var myDel = new MyDelegate<string>(Simple.PrintString);
//添加另一个方法
myDel += Simple.PrintUpperString;
//调用委托,并传递参数
myDel("Hello World!");
Console.ReadKey();
}
}
}
另一个示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
//通过类型参数列表来手指定返回类型
delegate R MyDelegate<T, R>(T value);
class Simple
{
//委托的匹配方法
static public string GetString(int x)
{
return x.ToString();
}
}
class Program
{
static void Main(string[] args)
{
//int表示传入的实参类型;string表示返回的类型
var myDel = new MyDelegate<int, string>(Simple.GetString);
Console.WriteLine(myDel(100));
Console.ReadKey();
}
}
}
泛型接口
泛型接口允许我们编写参数和接口成员返回类型是泛型类型参数的接口。泛型接口的声明和非泛型类型接口的声明差不多,但是需要在接口名称之后的尖括号中放置类型参数
示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
// 定义一个泛型接口
interface IMyIfc<T>
{
T GetValue(T value);
}
//想要实现这个泛型接口的类必须是一个泛型类
class MyClass<S> : IMyIfc<S>
{
public S GetValue(S value)
{
return value;
}
}
class Program
{
static void Main(string[] args)
{
var my_class = new MyClass<int>();
var value = my_class.GetValue(100);
Console.WriteLine(value);
var _class = new MyClass<string>();
var _value = _class.GetValue("Hello");
Console.WriteLine(_value);
Console.ReadKey();
}
}
}
泛型接口的另一种用法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
//定义一个泛型接口
interface IMyIfc<T>
{
T GetValue(T value);
}
//显示的告诉要实现的接口是什么类型
//MyClass则不必非得是一个泛型类
//并且可以实现多个类型的泛型接口
class MyClass : IMyIfc<int>, IMyIfc<string>
{
public int GetValue(int value)
{
return value;
}
public string GetValue(string value)
{
return value;
}
}
class Program
{
static void Main(string[] args)
{
var my_class = new MyClass();
var value = my_class.GetValue(100);
Console.WriteLine(value);
var _class = new MyClass();
var _value = _class.GetValue("Hello");
Console.WriteLine(_value);
Console.ReadKey();
}
}
}
当然,上面的示例在使用的时候一定要注意,如果要实现的多个接口有相同类型的,那就会实现出相同签名与返回类型的方法,那到是会出现问题的,所以在使用的时候造成小心 (不过最新的C#已经避免了这种情况)
协变
相关概念:每一个变量都有一种类型,可以将其派生类对象的实例赋值给基类的变量,这叫做赋值兼容性,也就是说一个狗狗类型的对象可以赋值给一个动物类型的变量
示例:下面代码存在错误
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
class Animal { public int Legs = 4; }
class Dog : Animal { }
delegate T Factory<T>();
class Program
{
static Dog MakeDog()
{
return new Dog();
}
static void Main(string[] args)
{
Factory<Dog> dogMaker = new Factory<Dog>(MakeDog);
Factory<Animal> animalMaker = dogMaker;
//这里的错误无法将Factory<Dog>隐式的转换为Factory<Animal>类型
Console.WriteLine(animalMaker().Legs.ToString());
Console.ReadKey();
}
}
}
虽然Dog
类是Animal
的派生类,但是是委托Factory<Dog>
没有从委托Factory<Animal>
派生,所以无法将将Factory<Dog>
隐式的转换为Factory<Animal>
类型,赋值兼容性自然不适用
示例:可以正常通过的代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
class Animal { public int Legs = 4; }
class Dog : Animal { }
delegate T Factory<out T>();
class Program
{
static Dog MakeDog()
{
return new Dog();
}
static void Main(string[] args)
{
Factory<Dog> dogMaker = new Factory<Dog>(MakeDog);
Factory<Animal> animalMaker = dogMaker;
//这里的错误无法将Factory<Dog>隐式的转换为Factory<Animal>类型
Console.WriteLine(animalMaker().Legs.ToString());
Console.ReadKey();
}
}
}
这种情况叫做协变,“协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型,即由小变大
上例通过在类型参数中显性的使用out关键字来支持协变
对于泛型类型参数,out关键字指定该参数是协变的,in关键字指定该参数是逆变的,它们可以在泛型与委托中使用
更多关于协变与逆变的知识,可以参考深入理解 C# 协变和逆变
逆变
了解了协变的道理,自然得知逆变就是由大变小的过程
“逆变”则是指能够使用派生程度更小的类型。
示例:通过在类型参数中显示的使用关键字in来支持逆变
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
class Animal { public int Legs = 4; }
class Dog : Animal { }
class Program
{
delegate void Action1<in T>(T a);
static void ActOnAnimal(Animal a) { Console.WriteLine(a.Legs); }
static void Main(string[] args)
{
Action1<Animal> act1 = ActOnAnimal;
Action1<Dog> dog1 = act1; // 逆变
dog1(new Dog());
Console.ReadKey();
}
}
}
我对out与in的理解
-
out指示类型参数将只作为输出使用,并且如果类型参数T仅在方法的返回值中出现,可通过out关键字来告诉编译器,一些隐式转换是合法的;
delegate T Factory<out T>();
表示T是协变的,所以Factory<Dog> dogMaker = new Factory<Dog>(MakeDog);
相当于转换为Factory<Animal> dogMaker = new Factory<Animal>(MakeDog);
-
in指示类型参数将只作为输入使用,可通过in关键字来告诉编译器,一些隐式转换是合法的;
delegate T Factory<out T>();
表示T是协变的,所以Factory<Animal> dogMaker = new Factory<Animal>(MakeDog);
相当于转换为Factory<Dog> dogMaker = new Factory<Dog>(MakeDog);
接口的协变与逆变
直接上代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
//定义一个Animal基类
class Animal { public string Name; }
//定义一个Dog类,派生于Animal类
class Dog : Animal { }
//定义一个泛型接口,out指定类型参数是协变的,即可向上转换
interface IMyIfc<out T>
{
T GetFirst();
}
//定义一个泛型类,实现IMyIfc接口
class SimpleReturn<T> : IMyIfc<T>
{
public T[] items = new T[2];
public T GetFirst()
{
return items[0];
}
}
class Program
{
//定义一个方法,接收一个IMyIfc<Animal>类型的参数
static void DoSomething(IMyIfc<Animal> returner)
{
Console.WriteLine(returner.GetFirst().Name);
}
static void Main(string[] args)
{
//定义一个Dog类型参数的SimpleReturn类
SimpleReturn<Dog> dogReturner = new SimpleReturn<Dog>();
//将该类中的数组添加一个值,这个值是个有名字的Dog类
dogReturner.items[0] = new Dog() { Name = "Hello"};
//IMyIfc<Animal>类型可以接收SimpleReturn<Dog>类型的对象
//协变的体现
IMyIfc<Animal> animalReturner = dogReturner;
//调用DoSomething方法,传入animalReturner对象
//体现协变
DoSomething(dogReturner);
Console.ReadKey();
}
}
}
某些情况下,编译器可以自动识别某个已构建的委托是协变或是逆变并且自动进行类型强制转换,这通常发生在没有为为对象的类型赋值的时候
示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeForT
{
class Animal { public int Legs = 4; }
class Dog : Animal { }
delegate T Factory<out T>();
class Program
{
static Dog MakeDog()
{
return new Dog();
}
static void Main(string[] args)
{
//隐式的转换,在这里,委托类型中的参数没有out关键字也是可以通过的
Factory<Animal> animalMaker = MakeDog;
//MakeDog等于new Factory<Dog>(MakeDog);而这里Dog参数隐匿的转换为Animal
//显性转换,需要out标识符,如果委托类型中的参数没有out参数是无法通过的
Factory<Dog> dogMaker = MakeDog;
Factory<Animal> animalMaker2 = dogMaker;
//显性转换的另一种写法
Factory<Animal> animalMaker3 = new Factory<Dog>(MakeDog);
Console.ReadKey();
}
}
}
有关可变性的注意事项
- 变化只适用于引用类型,因为不能直接从值类型派生其他类型
- 显示变化使用in和out关键字只适用于委托和接口,不适用于类、结构和方法
- 不包括in和out关键字的委托和接口类型参数叫做不变
网友评论