有时候我们在项目中,为了提高代码的可扩展性,有这样的需求。在自己创建的泛型类中,需要将泛型类型直接实例化。C#能够支持,但也是有条件的,例如下面代码:
public class Aircraft<TEngine> where TEngine : class, new(){
private TEngine engine;
public Aircraft(){
engine = new TEngine();
}
}
上述代码中,飞机类里面附带一个引擎类TEngine的泛型,可以实现不同型号的飞机和引擎的动态组合。
C#支持在类的定义中,直接实例化泛型参数。但是有一个前提,必须要有泛型约束new()。在本例中,真正的引擎类必须要有一个无参数的构造函数,以满足泛型约束new()。
我们只能用这个无参的构造函数在Aircraft类的定义中实例化当前引擎类TEngine。如果需要构建有参数类型的TEngine实例,C#就无法直接支持了。
下面的代码给TEngine带上了一个授权的参数,要求真正的引擎类必须在使用前,检查License授权,并且删除泛型约束new(),代码无法通过编译。
public class Aircraft<TEngine> where TEngine : class{
private TEngine engine;
public Aircraft(License licenseA){
engine = new TEngine(licenseA);
}
}
错误提是如下:
Compilation error (line 58, col 13): Cannot create an instance of the variable type ‘TEngine’ because it does not have the new() constraint
如果按照提示,加上new()的约束,代码如下:
public class Aircraft<TEngine> where TEngine : class, new(){
private TEngine engine;
public Aircraft(License licenseA){
engine = new TEngine(licenseA);
}
}
错误提是如下:
Compilation error (line 58, col 13): ‘TEngine’: cannot provide arguments when creating an instance of a variable type
对于泛型类型,在编译阶段,进行的语法检查,是不允许调用本例中有参数的构造函数的。
这就好比说公司确认一个要招聘的职位,定义了对应聘者的基本要求。但是HR无法定义非常细致的要求,例如应聘者头发颜色,大眼睛大小或皮肤颜色等。HR仅仅确认的是这个职位需要招人,有一个基本的招人原则如工作年限,具备的技能等。具体的细节根据应聘者,因人而异。
同样,C#的泛型约束,也是处于这种考虑。在泛型类Aircraft中,不允许指定过多的细节。泛型类型的真正绑定,是在运行时阶段,根据用户真正定义的引擎类,实现泛型参数的绑定。
这也就是说,如果我们要解决上述问题,只要换一种实例化泛型类型的方式,让其通过编译时的语法检查即可。简单点说就是不要再用new去实例化TEngine。
基于上述思路,可以使用反射或表达式目录树,对泛型参数通过间接的方式去调用含参数的构造方法。解决方案中只是罗列了重要的代码,完整代码请参看代码附录。
我们可以使用反射的Activator类的静态方法CreateInstance创建泛型类TEngine的实例。之所以称之为间接的方式,是因为在反射中,只是指定要调用的构造方法以及它的参数,编译时并不会检查该法是否可以被调用,代码如下:
public class Aircraft<TEngine> where TEngine : class{
private TEngine engine{ get; }
public Aircraft(License licenseA){
engine = Activator.CreateInstance(typeof(TEngine),new Object[]{licenseA}) as TEngine; // 根据运行时传入的具体的引擎类生成对象
}
}
License licenseA = new License(){AuthCode = "A"};
Aircraft<EngineA> a1 = new Aircraft<EngineA>(licenseA);
上述代码中没有任何试图直接通过new关键字调用TEngine含有一个参数的构造方法的行为,所以编译也就不会出错。运行时,CLR会根据用户的代码,将类型EngineA或EngineB绑定到TEngine上,然后通过反射调用EngineA含参数的构建方法,完成类的实力化。完整代码详见代码附录之反射解决方案完整代码。
基于反射的解决方案,性能总是被人诟病。尤其时在高并发条件下。因此提出第二种基于表达式目录树的解决方案。
假如说我们有一个委托如下,我们调用该委托,即可构造出TEngine对象。但是由于是用new关键字,调用的有参数构造方法,所以不能通过编译,也就不能直接用该委托。但是可以通过构造表达式目录树来间接构造该委托,这样就可以通过编译了。
之所以称之为间接的方式,是因为表达式目录树中,只是构造出对应的委托,编译时并不会检查该法是否可以被调用,代码如下:
Func<License,TEngine> engineDelgate = license => new TEngine(license);
具体构建委托的代码如下,完整代码请参看附录
public class Aircraft<TEngine> where TEngine : class{
private TEngine engine;
public Aircraft(License licenseA){
//Destination: license => new TEngine(license)
ParameterExpression paramenter = Expression.Parameter(typeof(License), "license"); // License 参数
ConstructorInfo ctorTEngine = typeof(TEngine).GetConstructor(
BindingFlags.Instance | BindingFlags.Public,
null,
CallingConventions.HasThis,
new[] { typeof(License) }, new ParameterModifier[] {}
); // 指定调用的构建TEngine的构造方法
var newTEngine = Expression.New(ctorTEngine,paramenter);
var lambdaExpression = Expression<Func<License,TEngine>>.Lambda<Func<License,TEngine>>(newTEngine, new ParameterExpression[]{paramenter});
var delgateFunc = lambdaExpression.Compile(); //从表达式目录树生成委托
engine = delgateFunc.Invoke(licenseA); //调用委托,根据运行时传入的具体的引擎类生成对象
}
}
License licenseB = new License(){AuthCode = "B"};
Aircraft<EngineB> b1 = new Aircraft<EngineB>(licenseB);
运行时,CLR会根据用户的代码,将将真实的类型EngineA或EngineB绑定到TEngine上,然后再通过表达式目录树的方法,完成具体业务逻辑。完整代码详见代码附录之表达式目录树解决方案完整代码。
本文中的所有代码都是基于.Net Core 3.1,并未尝试过在其它.Net版本下编译运行,如有问题,敬请谅解。
using System;
namespace GenericExplore{
public class Program
{
public static void Main()
{
License licenseA = new License(){AuthCode = "A"};
Aircraft<EngineA> a1 = new Aircraft<EngineA>(licenseA);
a1.Work("Aircraft A");
License licenseB = new License(){AuthCode = "B"};
Aircraft<EngineB> b1 = new Aircraft<EngineB>(licenseB);
b1.Work("Aircraft B");
}
}
public class License{
public string AuthCode {get;set;} = "default code";
}
public class EngineA{
public string VendorName {get;set;}
private License license;
public EngineA (License license){
this.VendorName = "Factory A";
this.license = license;
}
public int Thrust() => 1000000;
public void Work(string info) {
if (license == null || license.AuthCode != "A"){
Console.WriteLine($"{this.VendorName} are not allowed to use their engine !");
return;
}
Console.WriteLine($"This is {info}.");
Console.WriteLine($"{this.VendorName}'s engine can provide {Thrust()} maxmium thrust.");
}
}
public class EngineB{
public EngineB (License license){
this.VendorName = "Factory B";
this.license = license;
}
public string VendorName {get;set;}
private License license;
public int Thrust() => 2000000;
public void Work(string info) {
if (license == null || license.AuthCode != "B"){
Console.WriteLine($"{this.VendorName} are not allowed to use their engine !");
return;
}
Console.WriteLine($"This is {info}.");
Console.WriteLine($"{this.VendorName}'s engine can provide {Thrust()} maxmium thrust.");
}
}
public class Aircraft<TEngine> where TEngine : class{
private TEngine engine{get;}
public Aircraft(License licenseA){
engine = Activator.CreateInstance(typeof(TEngine),new Object[]{licenseA}) as TEngine;
}
public void Work(string info){
var mInfo = typeof(TEngine).GetMethod("Work",new Type[]{typeof(string)}); //通过反射找到真正的引擎类实例中的Work方法,该方法有一个字符串参数。
mInfo.Invoke(engine,new object[]{info}); //调用Wrok方法,info为实参。
//注意因为现在TEngine只是一个泛型,所以对于它的的实例,不能直接调用任何方法,所以不能写成 engine.Work(info)
}
}
}
using System;
using System.Linq.Expressions;
using System.Reflection;
namespace GenericExplore{
public class Program
{
public static void Main()
{
License licenseA = new License(){AuthCode = "A"};
Aircraft<EngineA> a1 = new Aircraft<EngineA>(licenseA);
a1.Work("Aircraft A");
License licenseB = new License(){AuthCode = "B"};
Aircraft<EngineB> b1 = new Aircraft<EngineB>(licenseB);
b1.Work("Aircraft B");
}
}
public class License {
public string AuthCode {get;set;} = "default code";
}
public class EngineA {
public string VendorName {get;set;}
private License license;
public EngineA (License license){
this.VendorName = "Factory A";
this.license = license;
}
public int Thrust() => 1000000;
public void Work(string info) {
if (license == null || license.AuthCode != "A"){
Console.WriteLine($"{this.VendorName} are not allowed to use their engine !");
return;
}
Console.WriteLine($"This is {info}.");
Console.WriteLine($"{this.VendorName}'s engine can provide {Thrust()} maxmium thrust.");
}
}
public class EngineB{
public EngineB (License license){
this.VendorName = "Factory B";
this.license = license;
}
public string VendorName {get;set;}
private License license;
public int Thrust() => 2000000;
public void Work(string info) {
if (license == null || license.AuthCode != "B"){
Console.WriteLine($"{this.VendorName} are not allowed to use their engine !");
return;
}
Console.WriteLine($"This is {info}.");
Console.WriteLine($"{this.VendorName}'s engine can provide {Thrust()} maxmium thrust.");
}
}
public class Aircraft<TEngine> where TEngine : class{
private TEngine engine;
public Aircraft(License licenseA){
//Destination: license => new TEngine(license)
ParameterExpression paramenter = Expression.Parameter(typeof(License), "license");
ConstructorInfo ctorTEngine = typeof(TEngine).GetConstructor(
BindingFlags.Instance | BindingFlags.Public,
null,
CallingConventions.HasThis,
new[] { typeof(License) }, new ParameterModifier[] {}
);
var newTEngine = Expression.New(ctorTEngine,paramenter);
var lambdaExpression = Expression<Func<License,TEngine>>.Lambda<Func<License,TEngine>>(newTEngine, new ParameterExpression[]{paramenter});
var delgateFunc = lambdaExpression.Compile();
engine = delgateFunc.Invoke(licenseA);
}
public void Work(string info){
/*注意因为现在TEngine只是一个泛型,所以对于它的的实例,不能直接调用任何方法,所以不能写成 engine.Work(info), 要通过构建Action workCall = engine => engine.Work(info)的表达式目录树的方式,间接调用TEngine的方法。*/
ParameterExpression paramenter = Expression.Parameter(typeof(TEngine), "t");
var methodExpression = Expression.Call(
paramenter, // caller
typeof(TEngine).GetMethod("Work",new Type[]{typeof(string)}), // method
new Expression[] // Work method's parameter
{
Expression.Constant(info,typeof(string))
}
);
var workLambda = Expression<Action<TEngine>>.Lambda<Action<TEngine>>(methodExpression,new ParameterExpression[]{paramenter});
var workDelgate = workLambda.Compile();
workDelgate.Invoke(engine);
}
}
}