在C++中反射调用.NET的方法(三)
在.NET与C++之间传输集合数据
上一篇《在C++中反射调用.NET(二)》中,我们尝试了反射调用一个返回DTO对象的.NET方法,今天来看看如何在.NET与C++之间传输集合数据。
使用非泛型集合的委托方法
先看看.NET类中的一个返回列表数据的方法:
//返回List或者数组,不影响C++调用 publicList<IUserInfo>GetUsers(stringlikeName) { List<IUserInfo>users=newList<NetLib.IUserInfo>(); for(inti=0;i<10;i++) { IUserInfouserinfo=GetUserByID(i); userinfo.Name+=likeName; users.Add(userinfo); } //returnusers.ToArray(); returnusers; } publicIUserInfoGetUserByID(intuserId) { IUserInfouserinfo=EntityBuilder.CreateEntity<IUserInfo>(); userinfo.ID=userId; userinfo.Name="姓名_"+userId; userinfo.Birthday=newDateTime(1980,1,1); returnuserinfo; }
该方法没有什么复杂业务逻辑,就是将传递进来的参数给DTO对象,创建包含10个这样的对象的列表并返回而已。
对于GetUsers方法,我们可以创建下面的委托方法来绑定:
Func<String,IEnumerable>fun;
注意这里使用的是非泛型的IEnumerable接口,在C++需要使用下面这个命名空间:
usingnamespaceSystem::Collections;
那么为何不能使用泛型集合呢?
usingnamespaceSystem::Collections::Generic;
因为在C++端,没有直接引用用户项目的.NET程序集,并不知道泛型集合类型的具体类型,IUserInfo这个接口无法直接访问,好在IEnumerable<T>也是继承IEnumerable的,所以可以当做非泛型对象在C++中访问,因此创建上面的委托方法是可行的。
C++中的列表对象list
下面看看完整的C++/CLI反射调用的代码:
std::list<CppUserInfo>GetUsers(String^likeName) { //调用.NET方法,得到结果 MethodInfo^method=dotnetObject->GetType()->GetMethod("GetUsers",BindingFlags::Public|BindingFlags::Instance); Func<String^,IEnumerable^>^fun=(Func<String^,IEnumerable^>^)Delegate::CreateDelegate(Func<String^,IEnumerable^>::typeid, this->dotnetObject,method); IEnumerable^result=fun(likeName); std::list<CppUserInfo>cppResult; foreach(Object^iteminresult) { Func<String^,Object^>^entityProp=EntityHelper::EntityCallDelegate(item); CppUserInfouser; user.ID=(int)entityProp("ID"); user.Name=(String^)entityProp("Name"); user.Birthday=Convert2CppDateTime((DateTime^)entityProp("Birthday")); cppResult.push_back(user); } returncppResult; }
在C++中,常常使用list来表示一个列表数据,例如上面方法中的代码:
std::list<CppUserInfo>cppResult;
为此C++需要包含以下头文件:
#include<list>
要将一个对象添加到列表结尾,像下面这样调用即可:
cppResult.push_back(user);
在上一篇中已经讲述了如何从.NET对象转换给C++本地结构体,所以这个转换代码可以直接拿来用,综合起来,要从.NET集合得到C++的列表对象,像下面这样使用:
std::list<CppUserInfo>cppResult; foreach(Object^iteminresult) { Func<String^,Object^>^entityProp=EntityHelper::EntityCallDelegate(item); CppUserInfouser; user.ID=(int)entityProp("ID"); user.Name=(String^)entityProp("Name"); user.Birthday=Convert2CppDateTime((DateTime^)entityProp("Birthday")); cppResult.push_back(user); }
C++传递集合数据给.NET
前面讲了从.NET反射调用获得一个集合,看起来比较容易,但是从C++反射调用时候传递一个集合就不容易了。注意,这里传递的还是.NET的集合,所以这里需要做3件事情:
1,首先构建一个.NET集合对象;
2,转换C++本机结构数据到.NET集合元素;
3,反射调用.NET方法,传递数据过去。
先看要反射调用的.NET方法定义:
publicboolSaveUsers(IList<IUserInfo>users) { UserDb.AddRange(users); returntrue; }
方法非常简单,没有什么业务逻辑,接受一个列表接口的数据,然后返回一个布尔值。
在C++端看来,SaveUsers方法的参数对象是一个泛型集合,但是具体是什么对象并不知道,所以需要反射出泛型集合的类型,同时还需要构建这样一个泛型集合对象实例。
在本例中,要得到IUserInfo这个泛型集合的类型,可以通过下面的代码:
MethodInfo^method=dotnetObject->GetType()->GetMethod("SaveUsers",BindingFlags::Public|BindingFlags::Instance); array<ParameterInfo^>^pars=method->GetParameters(); Type^paraType=pars[0]->ParameterType; Type^interfaceType=paraType->GetGenericArguments()[0];
注意上面的代码中使用了C++/CLI的数组类型array<Type^>^,而不是C++标准库的数组,因此不要引用下面的命名空间:
usingnamespacestd;
否则VS会提示数组定义缺少参数。
创建泛型List实例
我们使用List来做集合对象,在C#中,我们可以通过下面的方式得到List泛型的类型,然后进一步创建泛型对象实例:
Typet=typeof(List<>);
但是,对应的C++/CLI写法却无法通过编译:
Type^t=List<>::typeid;
VS总是提示List缺少类型参数,不过像下面这样子是可以的:
Type^t2=List<IUserInfo>::typeid;
但是IUserInfo类型正是我们要动态反射的,事先并不知道,所以一时不知道在C++/CLI中如何构建List泛型的具体实例,MS你不能这么坑好么?
既然无法直接解决,只好曲线救国了,通过类型名字,来创建类型:
String^listTypeName=System::String::Format("System.Collections.Generic.List`1[{0}]",interfaceType->FullName);
可惜,这种方式不成功,只好一步步来了,先创建基本的List泛型类型:
String^listTypeName="System.Collections.Generic.List`1"; Type^listType=System::Type::GetType(listTypeName);
成功,在此基础上,创建真正的泛型List对象实例就可以了,完整代码如下:
staticType^CreateGenericListType(Type^interfaceType) { //直接这样创建泛型List不成功: //String^listTypeName=System::String::Format("System.Collections.Generic.List`1[{0}]",interfaceType->FullName); String^listTypeName="System.Collections.Generic.List`1"; Type^listType=System::Type::GetType(listTypeName); Type^generListType=listType->MakeGenericType(interfaceType); returngenerListType; } staticIList^CreateGenericList(Type^interfaceType) { Type^generListType=CreateGenericListType(interfaceType); Object^listObj=System::Activator::CreateInstance(generListType,nullptr); IList^realList=(IList^)listObj; returnrealList; }
在方法CreateGenericListType得到只是一个泛型List的类型,但我们并不知道这个List具体的形参类型,所以这个泛型List还是无法直接使用,幸好,泛型List也是继承自非泛型的IList接口的,所以在CreateGenericList方法中将泛型List对象转换成IList接口对象,之后就可以愉快的使用List对象了。
IList^realList=CreateGenericList(interfaceType);
realList->Add(CurrEntity);//CurrEntity是interfaceType类型的动态实体类
反射静态方法
在上一篇中,我们在一个.NET方法中通过接口动态创建实体类,用的是下面的方式:
IUserInfouserinfo=EntityBuilder.CreateEntity<IUserInfo>();
CreateEntity是EntityBuilder的静态方法,现在我们需要在C++/CLI中,反射调用此方法。
为什么要反射创建实体类?
因为CreateGenericList(interfaceType)创建的是一个泛型List对象,要求它的成员是一个实体类。
Object^CreateEntityFromInterface(Type^interfaceType) { MethodInfo^method=this->entityBuilderType->GetMethod("CreateEntity",BindingFlags::Public|BindingFlags::Static); MethodInfo^genMethod=method->MakeGenericMethod(interfaceType); Object^entity=genMethod->Invoke(nullptr,nullptr); this->CurrEntity=entity; returnentity; }
注意,由于是反射调用静态方法,并且调用方法时候并不需要参数,所以Invoke方法的参数为空。
在C++/CLI中,用nullptr表示空引用,跟C#的null作用一样。
反射调用索引器
SOD实体类可以通过索引器来访问对象属性,例如下面的C#代码:
intid=(int)CurrEntity["ID"]; CurrEntity["Name"]="张三"; stringname=(string)CurrEntity["Name"];//张三
下面,我们研究如何通过索引器来给实体类的属性赋值:
我们定义一个EntityHelper的C++/CLI类,在中间添加下面的代码:
private: Type^entityBuilderType; MethodInfo^mset; Object^_CurrEntity; //Action<String^,Object^>^idxAction; voidSetPropertyValue(Object^entity,MethodInfo^propMethod,String^propName,Object^value) { array<Object^>^paraArr=gcnewarray<Object^>{propName,value}; propMethod->Invoke(entity,paraArr); } public: voidset(Object^value) { this->mset=_CurrEntity->GetType()->GetMethod("set_Item",BindingFlags::Public|BindingFlags::Instance); //this->idxAction=(Action<String^,Object^>^)Delegate::CreateDelegate(Action<String^,Object^>::typeid,_CurrEntity,this->mset); } voidSetPropertyValue(String^propName,Object^value) { this->SetPropertyValue(this->CurrEntity,this->mset,propName,value); //参数类型为Object的委托,可能没有性能优势,反而更慢。 //this->idxAction(propName,value); }
对索引器的访问,实际上就是调用类的set_Item方法,VS编译器会给包含索引器的对象生成这个方法,一般来说我们会对要反射调用的方法创建一个委托,但是实验证明,对索引器使用委托方法调用,反而效率不如直接反射调用,即下面的代码:
voidSetPropertyValue(Object^entity,MethodInfo^propMethod,String^propName,Object^value) { array<Object^>^paraArr=gcnewarray<Object^>{propName,value}; propMethod->Invoke(entity,paraArr); }
注:C++/CLI的数组,也可以通过{}进行初始化。
一切准备就绪,下面可以通过以下步骤提交集合数据给.NET方法了:
1,反射.NET方法,获取参数的泛型形参类型;
2,创建此泛型形参的泛型List对象实例;
3,遍历C++集合(列表list),将结构数据赋值给动态创建的实体类对象;
4,添加动态实体类到泛型List对象集合内;
5,反射调用.NET方法,提交数据。
//示例1:直接调用.NET强类型的参数方法 //仅仅适用于有一个参数的情况并且要求是泛型类型参数 boolSaveUsers(std::list<CppUserInfo>users) { MethodInfo^method=dotnetObject->GetType()->GetMethod("SaveUsers",BindingFlags::Public|BindingFlags::Instance); array<ParameterInfo^>^pars=method->GetParameters(); Type^paraType=pars[0]->ParameterType; Type^interfaceType=paraType->GetGenericArguments()[0]; IList^realList=CreateGenericList(interfaceType); Object^userObj=helper->CreateEntityFromInterface(interfaceType); foreach(CppUserInfouserinusers) { helper->CurrEntity=((ICloneable^)userObj)->Clone();//使用克隆,避免每次反射 helper->SetPropertyValue("ID",user.ID); helper->SetPropertyValue("Name",gcnewString(user.Name)); helper->SetPropertyValue("Birthday",Covert2NetDateTime(user.Birthday)); realList->Add(helper->CurrEntity); } Object^result=method->Invoke(dotnetObject,gcnewarray<Object^>{realList}); return(bool)result; }
使用弱类型集合传输数据
当委托遇到协变和逆变
看看下面两个委托方法,哪个可以绑定到本文说的这个.NET方法:
boolSaveUsers(IList<IUserInfo>users){} Func<List<IUserInfo>,bool>fun; Func<List<Object>,bool>fun2;
很明显,委托方法fun2不能绑定,因为参数是in的,不是方法out的,所以调用的参数类型不能使用派生程度更小的类型;
再看看下面这种情况:
List<IUserInfo>GetUsers(stringlikeName){} Func<string,IEnumerable<IUserInfo>>fun; Func<string,IEnumerable>fun2;
这里,fun,fun2都可以绑定到方法上,因为泛型方法的形参作为返回值,是out的,可以使用派生程度更小的类型。
这是不是很熟悉的泛型类型的协变和逆变?
我们知道,反射的时候,利用委托绑定要反射的方法,能够大大提高方法的调用效率,所以对于我们的方法参数,如果调用的时候无法获知具体的类型,从而无法正确构造合适的委托方法,不如退而求其次,让被调用的方法参数采用弱类型方式,这样就可以构造对应的委托方法了。
因此,对我们.NET方法中的SaveUsers进行改造:
publicboolSaveUsers(IList<IUserInfo>users) { UserDb.AddRange(users); returntrue; } publicIUserInfoCreateUserObject() { returnEntityBuilder.CreateEntity<IUserInfo>(); } publicboolSaveUsers2(IEnumerable<Object>para) { varusers=fromuinpara selectuasIUserInfo; returnSaveUsers(users.ToList()); }
这里增加一个方法SaveUsers2,它采用IEnumerable<Object>,而不是更为具体的 IList<IUserInfo>,那么采用下面的方式构造方法SaveUsers2对应的委托方法就可以了:
MethodInfo^method=dotnetObject->GetType()->GetMethod("SaveUsers2",BindingFlags::Public|BindingFlags::Instance); Func<System::Collections::Generic::IEnumerable<Object^>^,bool>^fun2= (Func<System::Collections::Generic::IEnumerable<Object^>^,bool>^)Delegate::CreateDelegate(System::Func<Collections::Generic::IEnumerable<Object^>^,bool>::typeid, this->dotnetObject,method);
这样要构造一个泛型List就不必像之前的方法那么麻烦了:
System::Collections::Generic::List<Object^>^list=gcnewSystem::Collections::Generic::List<Object^>;
反射调用SaveUser2完整的代码如下:
//示例2:调用.NET弱类型的参数方法,以便通过委托方法调用 //构建委托方法比较容易,适用于参数数量多于1个的情况, boolSaveUsers2(std::list<CppUserInfo>users) { MethodInfo^method=dotnetObject->GetType()->GetMethod("SaveUsers2",BindingFlags::Public|BindingFlags::Instance); Func<System::Collections::Generic::IEnumerable<Object^>^,bool>^fun2= (Func<System::Collections::Generic::IEnumerable<Object^>^,bool>^)Delegate::CreateDelegate(System::Func<Collections::Generic::IEnumerable<Object^>^,bool>::typeid, this->dotnetObject,method); Object^userObj=CreateUserObject(); System::Collections::Generic::List<Object^>^list=gcnewSystem::Collections::Generic::List<Object^>; foreach(CppUserInfouserinusers) { helper->CurrEntity=((ICloneable^)userObj)->Clone();//使用克隆,避免每次反射 helper->SetPropertyValue("ID",user.ID); helper->SetPropertyValue("Name",gcnewString(user.Name)); helper->SetPropertyValue("Birthday",Covert2NetDateTime(user.Birthday)); list->Add(helper->CurrEntity); } boolresult=fun2(list); returnresult; }
性能测试
C++/CLI反射性能测试
为了测试C++/CLI反射调用两种方案(直接反射调用,委托方法调用)的效率,我们循环1000次测试,下面是测试代码:
NetLibProxy::UserProxy^proxy=gcnewNetLibProxy::UserProxy("..\\NetLib\\bin\\Debug\\NetLib.dll"); std::list<CppUserInfo>list=proxy->GetUsers("张"); System::Console::WriteLine("C++GetListdataFrom.NETfunction,OK."); System::Diagnostics::Stopwatch^sw=gcnewSystem::Diagnostics::Stopwatch; sw->Start(); for(inti=0;i<1000;i++) proxy->SaveUsers(list); sw->Stop(); System::Console::WriteLine("1,1000loop,C++PostListdataTo.NETfunction,OK.usetime(ms):{0}",sw->ElapsedMilliseconds); sw->Restart(); for(inti=0;i<1000;i++) proxy->SaveUsers2(list); sw->Stop(); System::Console::WriteLine("2,1000loop,C++PostListdataTo.NETfunction,OK..usetime(ms):{0}",sw->ElapsedMilliseconds);
不调试,直接执行:
C++GetListdataFrom.NETfunction,OK. 1,1000loop,C++PostListdataTo.NETfunction,OK.usetime(ms):65 2,1000loop,C++PostListdataTo.NETfunction,OK..usetime(ms):48
可见,虽然在.NET程序端,我们使用了弱类型的泛型集合,综合起来还是反射+委托方法执行,效率要高。
所以如果你能够适当对要调用的.NET方法进行封装,那么可采用使用弱类型集合传输数据的方案,否则,就在C++/CLI端多写2行代码,使用强类型传输数据的方案。
与.NET直接调用和反射的性能比较
在本篇的方案中,都是C++反射来调用.NET方法的,如果都是在.NET应用程序中直接调用或者反射.NET方法,性能差距有多少呢?
我们模拟文中C++/CLI的UserProxy,写一个.NET中的UserProxy:
structUserStruct { publicintID; publicstringName; publicDateTimeBirthday; } classUserProxy { Useruser; publicUserProxy() { user=newUser(); } publicList<UserStruct>GetUsers(stringlikeName) { List<UserStruct>result=newList<NetApp.UserStruct>(); varlist=user.GetUsers(likeName); foreach(variteminlist) { UserStructus; us.ID=item.ID; us.Name=item.Name; us.Birthday=item.Birthday; result.Add(us); } returnresult; } publicboolSaveUsers(IList<UserStruct>users) { List<IUserInfo>list=newList<IUserInfo>(); IUserInfouserObj=user.CreateUserObject(); foreach(variteminusers) { IUserInfocurrUser=(IUserInfo)((ICloneable)userObj).Clone(); currUser.ID=item.ID; currUser.Name=item.Name; currUser.Birthday=item.Birthday; list.Add(currUser); } boolresult=user.SaveUsers(list); returnresult; } ObjectCreateUserObject() { MethodInfomethod=user.GetType().GetMethod("CreateUserObject",BindingFlags.Public|BindingFlags.Instance); Func<Object>fun=(Func<Object>)Delegate.CreateDelegate(typeof(Func<Object>),user,method); returnfun(); } //反射+委托 publicboolSaveUsers2(IList<UserStruct>users) { MethodInfomethod=user.GetType().GetMethod("SaveUsers2",BindingFlags.Public|BindingFlags.Instance); Func<System.Collections.Generic.IEnumerable<Object>,bool>fun2=(Func<System.Collections.Generic.IEnumerable<Object>,bool>)Delegate.CreateDelegate(typeof(System.Func<System.Collections.Generic.IEnumerable<Object>,bool>), user,method); List<IUserInfo>list=newList<IUserInfo>(); objectuserObj=CreateUserObject(); foreach(variteminusers) { IUserInfocurrUser=(IUserInfo)((ICloneable)userObj).Clone(); currUser.ID=item.ID; currUser.Name=item.Name; currUser.Birthday=item.Birthday; list.Add(currUser); } boolresult=fun2(list); returnresult; } } .NetUserProxy
然后同样循环1000此调用,直接执行,看执行结果:
1,1000loop,.NETPostListdataTo.NETfunction,OK.usetime(ms):4 2,1000loop,.NETReflectionPostListdataTo.NETfunction,OK.usetime(ms):14
可见,.NET平台内调用,反射+委托的性能是接近于直接方法调用的。
综合对比,C++/CLI中反射调用.NET,比起在.NET平台内部反射调用,性能没有很大的差距,所以C++/CLI中反射调用.NET是一个可行的方案。
总结
C++/CLI是一种很好的混合编写本机代码与.NET托管代码的技术,使用它反射调用.NET方法也是一种可行的方案,结合PDF.NETSOD框架的实体类特征,可以更加方便的简化C++/CLI反射代码的编写并且提高C++代码与.NET代码通信的效率。