1. 程式人生 > 其它 >在C++中反射呼叫.NET(三) 使用非泛型集合的委託方法C++中的列表物件list C++傳遞集合資料給.NET建立泛型List例項反射靜態方法反射呼叫索引器當委託遇到協變和逆變C++/CLI

在C++中反射呼叫.NET(三) 使用非泛型集合的委託方法C++中的列表物件list C++傳遞集合資料給.NET建立泛型List例項反射靜態方法反射呼叫索引器當委託遇到協變和逆變C++/CLI

在.NET與C++之間傳輸集合資料

上一篇《在C++中反射呼叫.NET(二)》中,我們嘗試了反射呼叫一個返回DTO物件的.NET方法,今天來看看如何在.NET與C++之間傳輸集合資料。

使用非泛型集合的委託方法

先看看.NET類中的一個返回列表資料的方法:

 //返回List或者陣列,不影響 C++呼叫
        public List<IUserInfo> GetUsers(string likeName)
        {
            List<IUserInfo> users = new List<NetLib.IUserInfo>();
            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<IUserInfo>();
            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++中的列表物件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;

            for each (Object^ item in result)
            {
                Func<String^, Object^>^ 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<CppUserInfo> cppResult;

            for each (Object^ item in result)
            {
                Func<String^, Object^>^ 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);
            }

 C++傳遞集合資料給.NET

前面講了從.NET反射呼叫獲得一個集合,看起來比較容易,但是從C++反射呼叫時候傳遞一個集合就不容易了。注意,這裡傳遞的還是.NET的集合,所以這裡需要做3件事情: 1,首先構建一個.NET集合物件; 2,轉換C++本機結構資料到.NET集合元素; 3,反射呼叫.NET方法,傳遞資料過去。

先看要反射呼叫的.NET方法定義:

        public bool SaveUsers(IList<IUserInfo> users)
        {
            UserDb.AddRange(users);
            return true;
        }

方法非常簡單,沒有什麼業務邏輯,接受一個列表介面的資料,然後返回一個布林值。

在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++標準庫的陣列,因此不要引用下面的名稱空間:

using namespace std;

否則VS會提示陣列定義缺少引數。

建立泛型List例項

我們使用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<Object^>^ paraArr = gcnew array<Object^>{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<Object^>^ paraArr = gcnew array<Object^>{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<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);

            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<Object^>{ realList});
            return (bool)result;
        }

使用弱型別集合傳輸資料

當委託遇到協變和逆變

看看下面兩個委託方法,哪個可以繫結到本文說的這個.NET方法:

bool SaveUsers(IList<IUserInfo> users){ }
Func<List<IUserInfo>,bool> fun;
Func<List<Object>,bool> fun2;

很明顯,委託方法 fun2不能繫結,因為引數是 in 的,不是方法out的,所以呼叫的引數型別不能使用派生程度更小的型別;

再看看下面這種情況:

List<IUserInfo> GetUsers(string likeName){ }

Func<string,IEnumerable<IUserInfo>> fun;
Func<string,IEnumerable> fun2;

這裡,fun,fun2都可以繫結到方法上,因為泛型方法的形參作為返回值,是out的,可以使用派生程度更小的型別。

這是不是很熟悉的泛型型別的 協變和逆變?

我們知道,反射的時候,利用委託繫結要反射的方法,能夠大大提高方法的呼叫效率,所以對於我們的方法引數,如果呼叫的時候無法獲知具體的型別,從而無法正確構造合適的委託方法,不如退而求其次,讓被呼叫的方法引數採用弱型別方式,這樣就可以構造對應的委託方法了。 因此,對我們.NET方法中的 SaveUsers 進行改造:

 public bool SaveUsers(IList<IUserInfo> users)
        {
            UserDb.AddRange(users);
            return true;
        }

        public IUserInfo CreateUserObject()
        {
            return EntityBuilder.CreateEntity<IUserInfo>();
        }

        public bool SaveUsers2(IEnumerable<Object> 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<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 = gcnew System::Collections::Generic::List<Object^>;

反射呼叫SaveUser2完整的程式碼如下:

//示例2:呼叫.NET弱型別的引數方法,以便通過委託方法呼叫
        //構建委託方法比較容易,適用於引數數量多於1個的情況,
        bool SaveUsers2(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 = gcnew System::Collections::Generic::List<Object^>;

            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 反射效能測試

為了測試 C++/CLI 反射呼叫兩種方案(直接反射呼叫,委託方法呼叫)的效率,我們迴圈1000次測試,下面是測試程式碼:

NetLibProxy::UserProxy^ proxy = gcnew NetLibProxy::UserProxy("..\NetLib\bin\Debug\NetLib.dll");
std::list<CppUserInfo> 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行程式碼,使用強型別傳輸資料的方案。

與.NET直接呼叫和反射的效能比較

在本篇的方案中,都是C++反射來呼叫.NET方法的,如果都是在.NET應用程式中直接呼叫或者反射.NET方法,效能差距有多少呢? 我們模擬文中 C++/CLI的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<UserStruct> GetUsers(string likeName)
        {
            List<UserStruct> result = new List<NetApp.UserStruct>();
            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<UserStruct> users)
        {
            List<IUserInfo> list = new List<IUserInfo>();
            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<Object> fun = (Func<Object>)Delegate.CreateDelegate(typeof( Func<Object>), user, method);
            return fun();
        }

        //反射+委託
        public bool SaveUsers2(IList<UserStruct> users)
        {
            MethodInfo method = 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 = new List<IUserInfo>();
            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方法也是一種可行的方案,結合PDF.NET SOD框架的實體類特徵,可以更加方便的簡化C++/CLI反射程式碼的編寫並且提高C++程式碼與.NET程式碼通訊的效率。

(全文完)