C#表达式目录树系列之4 -- 解决C#泛型约束与无法创建带参数的泛型实例的矛盾

问题提出

有时候我们在项目中,为了提高代码的可扩展性,有这样的需求。在自己创建的泛型类中,需要将泛型类型直接实例化。C#能够支持,但也是有条件的,例如下面代码:

public class Aircraft<TEngine> where TEngine : classnew(){
	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<TEngine> 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);
		}
	}
}

表达式目录树系列全部文章

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值