上一篇《》中,我们尝试了反射调用一个返回 DTO 对象的. NET 方法,今天来看看如何在. NET 与 C++ 之间传输集合数据。
先看看. NET 类中的一个返回列表数据的方法:
- //返回List或者数组,不影响 C++调用
- public List GetUsers(string likeName)
- {
- List users = new List();
- for (int i = 0; i < 10; i++)
- {
- IUserInfo userinfo = GetUserByID(i);
- userinfo.Name += likeName;
- users.Add(userinfo);
- }
- //return users.ToArray();
- return users;
- }
- public IUserInfo GetUserByID(int userId)
- {
- IUserInfo userinfo= EntityBuilder.CreateEntity();
- userinfo.ID = userId;
- userinfo.Name = "姓名_" + userId;
- userinfo.Birthday = new DateTime(1980, 1, 1);
- return userinfo;
- }
该方法没有什么复杂业务逻辑,就是将传递进来的参数给 DTO 对象,创建包含 10 个这样的对象的列表并返回而已。
对于 GetUsers 方法,我们可以创建下面的委托方法来绑定:
- Func < String,
- IEnumerable > fun;
注意这里使用的是非泛型的 IEnumerable 接口,在 C++ 需要使用下面这个命名空间:
- using namespace System::Collections;
那么为何不能使用泛型集合呢?
- using namespace System::Collections::Generic;
因为在 C++ 端,没有直接引用用户项目的. NET 程序集,并不知道泛型集合类型的具体类型,IUserInfo 这个接口无法直接访问,好在 IEnumerable<T> 也是继承 IEnumerable 的,所以可以当做非泛型对象在 C++ 中访问,因此创建上面的委托方法是可行的。
下面看看完整的 C++/CLI 反射调用的代码:
- std::list GetUsers(String^ likeName)
- {
- //调用.NET方法,得到结果
- MethodInfo^ method = dotnetObject->GetType()->GetMethod("GetUsers", BindingFlags::Public | BindingFlags::Instance);
- Func^ fun = (Func^)Delegate::CreateDelegate(Func::typeid,
- this->dotnetObject, method);
- IEnumerable^ result = fun(likeName);
- std::list cppResult;
- for each (Object^ item in result)
- {
- Func^ entityProp = EntityHelper::EntityCallDelegate(item);
- CppUserInfo user;
- user.ID = (int)entityProp("ID");
- user.Name = (String^)entityProp("Name");
- user.Birthday = Convert2CppDateTime((DateTime^)entityProp("Birthday"));
- cppResult.push_back(user);
- }
- return cppResult;
- }
在 C++ 中,常常使用 list 来表示一个列表数据,例如上面方法中的代码:
- std::list<CppUserInfo> cppResult;
为此 C++ 需要包含以下头文件:
- #include <list>
要将一个对象添加到列表结尾,像下面这样调用即可:
- cppResult.push_back(user);
在上一篇中已经讲述了如何从. NET 对象转换给 C++ 本地结构体,所以这个转换代码可以直接拿来用,综合起来,要从. NET 集合得到 C++ 的列表对象,像下面这样使用:
- std::list cppResult;
- for each (Object^ item in result)
- {
- Func^ entityProp = EntityHelper::EntityCallDelegate(item);
- CppUserInfo user;
- user.ID = (int)entityProp("ID");
- user.Name = (String^)entityProp("Name");
- user.Birthday = Convert2CppDateTime((DateTime^)entityProp("Birthday"));
- cppResult.push_back(user);
- }
前面讲了从. NET 反射调用获得一个集合,看起来比较容易,但是从 C++ 反射调用时候传递一个集合就不容易了。注意,这里传递的还是. NET 的集合,所以这里需要做 3 件事情:
1,首先构建一个. NET 集合对象;
2,转换 C++ 本机结构数据到. NET 集合元素;
3,反射调用. NET 方法,传递数据过去。
先看要反射调用的. NET 方法定义:
- public bool SaveUsers(IList users)
- {
- UserDb.AddRange(users);
- return true;
- }
方法非常简单,没有什么业务逻辑,接受一个列表接口的数据,然后返回一个布尔值。
在 C++ 端看来,SaveUsers 方法的参数对象是一个泛型集合,但是具体是什么对象并不知道,所以需要反射出泛型集合的类型,同时还需要构建这样一个泛型集合对象实例。
在本例中,要得到 IUserInfo 这个泛型集合的类型,可以通过下面的代码:
- MethodInfo^ method = dotnetObject->GetType()->GetMethod("SaveUsers", BindingFlags::Public | BindingFlags::Instance);
- array^ pars = method->GetParameters();
- Type^ paraType= pars[0]->ParameterType;
- Type^ interfaceType = paraType->GetGenericArguments()[0];
注意上面的代码中使用了 C++/CLI 的数组类型 array<Type^>^ , 而不是 C++ 标准库的数组,因此不要引用下面的命名空间:
- using namespace std;
否则 VS 会提示数组定义缺少参数。
我们使用 List 来做集合对象,在 C# 中,我们可以通过下面的方式得到 List 泛型的类型,然后进一步创建泛型对象实例:
- Type t= 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 对象实例就可以了,完整代码如下:
- static Type^ 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);
- return generListType;
- }
- static IList^ CreateGenericList(Type^ interfaceType)
- {
- Type^ generListType = CreateGenericListType(interfaceType);
- Object^ listObj = System::Activator::CreateInstance(generListType, nullptr);
- IList^ realList = (IList^)listObj;
- return realList;
- }
在方法 CreateGenericListType 得到只是一个泛型 List 的类型,但我们并不知道这个 List 具体的形参类型,所以这个泛型 List 还是无法直接使用,幸好,泛型 List 也是继承自非泛型的 IList 接口的,所以在 CreateGenericList 方法中将泛型 List 对象转换成 IList 接口对象,之后就可以愉快的使用 List 对象了。
- IList ^ realList = CreateGenericList(interfaceType);
- realList - >Add(CurrEntity); //CurrEntity 是interfaceType 类型的动态实体类
在上一篇中,我们在一个. NET 方法中通过接口动态创建实体类,用的是下面的方式:
- IUserInfo userinfo= 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;
- return entity;
- }
注意,由于是反射调用静态方法,并且调用方法时候并不需要参数,所以 Invoke 方法的参数为空。
在 C++/CLI 中,用 nullptr 表示空引用,跟 C# 的 null 作用一样。
SOD 实体类可以通过索引器来访问对象属性,例如下面的 C# 代码:
- int id = (int) CurrEntity["ID"];
- CurrEntity["Name"] = "张三";
- string name = (string) CurrEntity["Name"]; //张三
下面,我们研究如何通过索引器来给实体类的属性赋值:
我们定义一个 EntityHelper 的 C++/CLI 类,在中间添加下面的代码:
- private:
- Type^ entityBuilderType;
- MethodInfo^ mset;
- Object^ _CurrEntity;
- //Action<String^, Object^>^ idxAction;
- void SetPropertyValue(Object^ entity, MethodInfo^ propMethod, String^ propName, Object^ value)
- {
- array^ paraArr = gcnew array{propName, value};
- propMethod->Invoke(entity, paraArr);
- }
- public:
- void set(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);
- }
- void SetPropertyValue(String^ propName, Object^ value)
- {
- this->SetPropertyValue(this->CurrEntity, this->mset, propName, value);
- //参数类型为 Object的委托,可能没有性能优势,反而更慢。
- //this->idxAction(propName, value);
- }
对索引器的访问,实际上就是调用类的 set_Item 方法,VS 编译器会给包含索引器的对象生成这个方法,一般来说我们会对要反射调用的方法创建一个委托,但是实验证明,对索引器使用委托方法调用,反而效率不如直接反射调用,即下面的代码:
- void SetPropertyValue(Object^ entity, MethodInfo^ propMethod, String^ propName, Object^ value)
- {
- array^ paraArr = gcnew array{propName, value};
- propMethod->Invoke(entity, paraArr);
- }
注:C++/CLI 的数组,也可以通过 { } 进行初始化。
一切准备就绪,下面可以通过以下步骤提交集合数据给. NET 方法了:
1,反射. NET 方法,获取参数的泛型形参类型;
2,创建此泛型形参的泛型 List 对象实例;
3,遍历 C++ 集合(列表 list),将结构数据赋值给动态创建的实体类对象;
4,添加动态实体类到泛型 List 对象集合内;
5,反射调用. NET 方法,提交数据。
- //示例1:直接调用.NET强类型的参数方法
- //仅仅适用于有一个参数的情况并且要求是泛型类型参数
- bool SaveUsers(std::list users)
- {
- MethodInfo^ method = dotnetObject->GetType()->GetMethod("SaveUsers", BindingFlags::Public | BindingFlags::Instance);
- array^ pars = method->GetParameters();
- Type^ paraType= pars[0]->ParameterType;
- Type^ interfaceType = paraType->GetGenericArguments()[0];
- IList^ realList = CreateGenericList(interfaceType);
- Object^ userObj = helper->CreateEntityFromInterface(interfaceType);
- for each (CppUserInfo user in users)
- {
- helper->CurrEntity = ((ICloneable^)userObj)->Clone();//使用克隆,避免每次反射
- helper->SetPropertyValue("ID", user.ID);
- helper->SetPropertyValue("Name", gcnew String(user.Name));
- helper->SetPropertyValue("Birthday", Covert2NetDateTime(user.Birthday));
- realList->Add(helper->CurrEntity);
- }
- Object^ result= method->Invoke(dotnetObject, gcnew array{ realList});
- return (bool)result;
- }
看看下面两个委托方法,哪个可以绑定到本文说的这个. NET 方法:
- bool SaveUsers(IList users) {}
- Func,
- bool > fun;
- Func,
- bool > fun2;
很明显,委托方法 fun2 不能绑定,因为参数是 in 的,不是方法 out 的,所以调用的参数类型不能使用派生程度更小的类型;
再看看下面这种情况:
- List GetUsers(string likeName) {}
- Func < string,
- IEnumerable > fun;
- Func < string,
- IEnumerable > fun2;
这里,fun,fun2 都可以绑定到方法上,因为泛型方法的形参作为返回值,是 out 的,可以使用派生程度更小的类型。
这是不是很熟悉的泛型类型的 协变和逆变?
我们知道,反射的时候,利用委托绑定要反射的方法,能够大大提高方法的调用效率,所以对于我们的方法参数,如果调用的时候无法获知具体的类型,从而无法正确构造合适的委托方法,不如退而求其次,让被调用的方法参数采用弱类型方式,这样就可以构造对应的委托方法了。
因此,对我们. NET 方法中的 SaveUsers 进行改造:
- public bool SaveUsers(IList users)
- {
- UserDb.AddRange(users);
- return true;
- }
- public IUserInfo CreateUserObject()
- {
- return EntityBuilder.CreateEntity();
- }
- public bool SaveUsers2(IEnumerable para)
- {
- var users = from u in para
- select u as IUserInfo;
- return SaveUsers (users.ToList());
- }
这里增加一个方法 SaveUsers2,它采用 IEnumerable<Object> , 而不是更为具体的 IList<IUserInfo>,那么采用下面的方式构造方法 SaveUsers2 对应的委托方法就可以了:
- MethodInfo^ method = dotnetObject->GetType()->GetMethod("SaveUsers2", BindingFlags::Public | BindingFlags::Instance);
- Func^,bool>^ fun2 =
- (Func^, bool>^)Delegate::CreateDelegate(System::Func^, bool>::typeid,
- this->dotnetObject, method);
这样要构造一个泛型 List 就不必像之前的方法那么麻烦了:
- System::Collections::Generic::List<Object^>^ list = gcnew System::Collections::Generic::List<Object^>;
反射调用 SaveUser2 完整的代码如下:
- //示例2:调用.NET弱类型的参数方法,以便通过委托方法调用
- //构建委托方法比较容易,适用于参数数量多于1个的情况,
- bool SaveUsers2(std::list users)
- {
- MethodInfo^ method = dotnetObject->GetType()->GetMethod("SaveUsers2", BindingFlags::Public | BindingFlags::Instance);
- Func^,bool>^ fun2 =
- (Func^, bool>^)Delegate::CreateDelegate(System::Func^, bool>::typeid,
- this->dotnetObject, method);
- Object^ userObj = CreateUserObject();
- System::Collections::Generic::List^ list = gcnew System::Collections::Generic::List;
- for each (CppUserInfo user in users)
- {
- helper->CurrEntity = ((ICloneable^)userObj)->Clone();//使用克隆,避免每次反射
- helper->SetPropertyValue("ID", user.ID);
- helper->SetPropertyValue("Name", gcnew String(user.Name));
- helper->SetPropertyValue("Birthday", Covert2NetDateTime(user.Birthday));
- list->Add(helper->CurrEntity);
- }
- bool result = fun2(list);
- return result;
- }
为了测试 C++/CLI 反射调用两种方案(直接反射调用,委托方法调用)的效率,我们循环 1000 次测试,下面是测试代码:
- NetLibProxy::UserProxy^ proxy = gcnew NetLibProxy::UserProxy("..\\NetLib\\bin\\Debug\\NetLib.dll");
- std::list list = proxy->GetUsers("张");
- System::Console::WriteLine("C++ Get List data From .NET function,OK.");
- System::Diagnostics::Stopwatch^ sw = gcnew System::Diagnostics::Stopwatch;
- sw->Start();
- for (int i = 0; i<1000; i++)
- proxy->SaveUsers(list);
- sw->Stop();
- System::Console::WriteLine("1,1000 loop,C++ Post List data To .NET function,OK.use time(ms):{0}",sw->ElapsedMilliseconds);
- sw->Restart();
- for(int i=0;i<1000;i++)
- proxy->SaveUsers2(list);
- sw->Stop();
- System::Console::WriteLine("2,1000 loop,C++ Post List data To .NET function,OK..use time(ms):{0}", sw->ElapsedMilliseconds);
不调试,直接执行:
- C++Get List data From.NET
- function,
- OK.1,
- 1000 loop,
- C++Post List data To.NET
- function,
- OK.use time(ms) : 65 2,
- 1000 loop,
- C++Post List data To.NET
- function,
- OK..use time(ms) : 48
可见,虽然在. NET 程序端,我们使用了弱类型的泛型集合,综合起来还是反射 + 委托方法执行,效率要高。
所以如果你能够适当对要调用的. NET 方法进行封装,那么可采用使用弱类型集合传输数据的方案,否则,就在 C++/CLI 端多写 2 行代码,使用强类型传输数据的方案。
在本篇的方案中,都是 C++ 反射来调用. NET 方法的,如果都是在. NET 应用程序中直接调用或者反射. NET 方法,性能差距有多少呢?
我们模拟文中 C++/CLI 的 UserProxy,写一个. NET 中的 UserProxy:
.Net UserProxy
- struct UserStruct
- {
- public int ID;
- public string Name;
- public DateTime Birthday;
- }
- class UserProxy
- {
- User user;
- public UserProxy()
- {
- user = new User();
- }
- public List GetUsers(string likeName)
- {
- List result = new List();
- var list = user.GetUsers(likeName);
- foreach (var item in list)
- {
- UserStruct us;
- us.ID = item.ID;
- us.Name = item.Name;
- us.Birthday = item.Birthday;
- result.Add(us);
- }
- return result;
- }
- public bool SaveUsers(IList users)
- {
- List list = new List();
- IUserInfo userObj = user.CreateUserObject();
- foreach (var item in users)
- {
- IUserInfo currUser = (IUserInfo)((ICloneable)userObj).Clone();
- currUser.ID = item.ID;
- currUser.Name = item.Name;
- currUser.Birthday = item.Birthday;
- list.Add(currUser);
- }
- bool result = user.SaveUsers(list);
- return result;
- }
- Object CreateUserObject()
- {
- MethodInfo method = user.GetType().GetMethod("CreateUserObject", BindingFlags.Public | BindingFlags.Instance);
- Func fun = (Func)Delegate.CreateDelegate(typeof( Func), user, method);
- return fun();
- }
- //反射+委托
- public bool SaveUsers2(IList users)
- {
- MethodInfo method = user.GetType().GetMethod("SaveUsers2", BindingFlags.Public | BindingFlags.Instance);
- Func, bool> fun2 = (Func, bool>)Delegate.CreateDelegate(typeof( System.Func, bool>),
- user, method);
- List list = new List();
- object userObj = CreateUserObject();
- foreach (var item in users)
- {
- IUserInfo currUser = (IUserInfo)((ICloneable)userObj).Clone();
- currUser.ID = item.ID;
- currUser.Name = item.Name;
- currUser.Birthday = item.Birthday;
- list.Add(currUser);
- }
- bool result = fun2(list);
- return result;
- }
- }
然后同样循环 1000 此调用,直接执行,看执行结果:
- 1,
- 1000 loop,
- .NET Post List data To.NET
- function,
- OK.use time(ms) : 4 2,
- 1000 loop,
- .NET Reflection Post List data To.NET
- function,
- OK.use time(ms) : 14
可见,.NET 平台内调用,反射 + 委托的性能是接近于直接方法调用的。
综合对比,C++/CLI 中反射调用. NET, 比起在. NET 平台内部反射调用,性能没有很大的差距,所以 C++/CLI 中反射调用. NET 是一个可行的方案。
C++/CLI 是一种很好的混合编写本机代码与. NET 托管代码的技术,使用它反射调用. NET 方法也是一种可行的方案,结合的实体类特征,可以更加方便的简化 C++/CLI 反射代码的编写并且提高 C++ 代码与. NET 代码通信的效率。
(全文完)
来源: