程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 實例解析C++/CLI之代理與事件

實例解析C++/CLI之代理與事件

編輯:關於C++

在C++/CLI中,代理是對函數進行包裝的對象;而事件是一種為客戶程序提供通知的類機制。

在前幾篇文章中,已經多次演示了如果讓一個句柄在不同的時間,被引用至不同的對象,從而以更抽象的方法來解決程序中的問題,但是,也能使用代理通過函數來達到同樣的效果;代理是包裝了函數的一個對象,且對實例函數而言,也能通過特定的實例,與這些函數發生聯系。一旦一個代理包裝了一個或多個函數,你就能通過代理來調用這些函數,而無須事先了解包裝了哪些函數。

請看例1中的代碼,在標號1中,定義一個代理類型Del,由於使用了上下文關鍵字delegate,所以有點像函數的聲明,但與函數聲明不同的是,此處聲明的是一個代理類型Del的實例,其可包裝進任意接受一個int類型作為參數並返回一個int值類型的函數(任意有效的參數列表及返回類型組合都是允許的)。一旦定義了某種代理類型,它只能被用於包裝具有同樣類型的函數;代理類型可被定義在源文件中或命名空間的范圍內,也能定義在類中,並可有public或private訪問控制屬性。

例1:

using namespace System;
ref struct A
{
  static int Square(int i)
  {
   return i * i;
  }
};
ref struct B
{
  int Cube(int i)
  {
   return i * i * i;
  }
};
/*1*/
delegate int Del(int value);
int main()
{
  /*2*/ Del^ d = gcnew Del(&A::Square);
  /*3*/ Console::WriteLine("d(10) result = {0}", d(10));
  /*4*/ B^ b = gcnew B;
  /*5*/ d = gcnew Del(b, &B::Cube);
  /*6*/ Console::WriteLine("d(10) result = {0}", d(10));
}

靜態函數A::Square與實例函數B::Cube對Del來說,都具有相同的參數類型及返回類型,因此它們能被包裝進同類型的代理中。注意,即使兩個函數均為public,當考慮它們與Del的兼容性時,它們的可訪問性也是不相關的,這樣的函數也能被定義在相同或不同的類中,主要由程序員來選擇。

一旦定義了某種代理類型,就可創建此類型實例的句柄,並進行初始化或賦值操作,如標號2中所示的靜態函數A::Square,及標號5中所示的實例函數B::Cube。(此處只是出於演示的目的,否則把Cube做成實例函數沒有任何好處。)

創建一個代理實例涉及到調用一個構造函數,如果是在包裝一個靜態函數,只需傳遞進一個指向成員函數的指針;而對實例函數而言,必須傳遞兩個參數:一個實例的句柄及指向實例成員函數的指針。

在初始化代理實例之後,就能間接地調用它們包裝的函數了,用法與直接調用原函數一樣,只不過現在用的是代理實例名,如標號3與6,由包裝函數返回的值也是像直接調用函數時那樣獲得。如果一個代理實例的值為nullptr,此時再試圖調用被包裝的函數,會導致System::NullReferenceException類型異常。

以下是輸出:

d(10) result = 100
d(10) result = 1000

傳遞與返回代理

有時,把包裝好的函數傳遞給另一個函數,會非常有用,接受一方的函數並不知道會傳遞過來哪個函數,並且它也無須關心,只需簡單地通過包裝好的代理,間接調用此函數就行了。

下面以集合中元素排序來說明,大多數時候,集合中元素排序所依據的規則,只在對某對元素進行比較的方法上存在區別。如果在運行時提供進行比較的函數,一個排序過程就能用相應定義的比較函數排出任意的順序,請看例2。

例2:

using namespace System;
ref struct StrCompare
{
  static int CompareExact(String^ s1, String^ s2)
  {
   Console::WriteLine("Comparing {0} and {1} " "using CompareExact", s1, s2);
   // ...
   return 0;
  }
  static int CompareIgnoreCase(String^ s1, String^ s2)
  {
   Console::WriteLine("Comparing {0} and {1}" "using CompareIgnoreCase", s1, s2);
   // ...
   return 0;
  }
};
delegate int Compare(String^ s1, String^ s2);
/*1*/
Compare^ FindComparisonMethod()
{
  // ...
}
void Sort(Compare^ compare)
{
  int result;
  /*3*/ result = compare("Hello", "Hello");
  /*4*/ result = compare("Hello", "HELLO");
  /*5*/ result = compare("Hello", "Hell");
}
int main()
{
  /*6*/ Sort(gcnew Compare(&StrCompare::CompareIgnoreCase));
  /*7*/ Sort(FindComparisonMethod());
  /*8*/ FindComparisonMethod()("Red", "RED");
}

Compare代理類型可對任意接受兩個String^參數並返回一個int結果的函數進行包裝,在此,有兩個函數為StrCompare::CompareExact和StrCompare::CompareIgnoreCase。

在標號6中,創建了一個Compare代理類型的實例,用它來包裝StrCompare::CompareIgnoreCase,並把此代理句柄傳遞給Sort函數,其將會利用比較函數進一步進行處理。

正如大家所看到的,Sort可接受一個代理類型的參數--而此參數可像其他函數參數一樣,可為傳值、傳址、傳引用。

在標號7中,調用了FindComparisonMethod函數,其返回一個Del代理類型,接著在標號7及8中調用了包裝過的函數。此處要重點說一下標號8:首先,FindComparisonMethod函數是被調用來獲取代理實例--其常用於調用底層函數;其次,這兩個函數的調用操作符都有同等的優先級,所以它們從左至右調用。

FindComparisonMethod函數中也用了一些邏輯用於確定到底需要包裝哪個函數,此處就未作詳細說明了。

代理類型的兼容性

一個代理類型只與它自身相兼容,與其他任何代理類型都不兼容,即使其他類型的包裝函數均為同一類型。請看例3,非常明顯,代理類型D1與函數A::M1與A::M2兼容,代理類型D2也與這些函數兼容,然而,這兩個代理類型在標號5、6、8、9中並不能互換使用。

例3:

delegate void D1();
delegate void D2();
public struct A
{
  static void M1() { /* ... */ }
  static void M2() { /* ... */ }
};
void X(D1^ m) { /* ... */ }
void Y(D2^ n) { /* ... */ }
int main()
{
  D1^ d1;
  /*1*/ d1 = gcnew D1(&A::M1); //兼容
  /*2*/ d1 = gcnew D1(&A::M2); //兼容
  D2^ d2;
  /*3*/ d2 = gcnew D2(&A::M1); //兼容
  /*4*/ d2 = gcnew D2(&A::M2); //兼容
  /*5*/ d1 = d2; //不兼容
  /*6*/ d2 = d1; //不兼容
  /*7*/ X(d1); //兼容
  /*8*/ X(d2); //不兼容
  /*9*/ Y(d1); //不兼容
  /*10*/ Y(d2); //兼容
}

代理類型的合並

一個代理實例實際上能包裝多個函數,在這種情況下,被包裝的函數集被維護在一個調用列表中,當合並兩個代理實例時,它們的調用列表也以指定的順序連接起來,並產生一個新的列表,而現有的兩個列表並沒有發生改變。當從調用列表中移除一個或多個函數時,也會產生一個新的列表,且原始列表不會發生變化。請看例4中的代碼,每個函數調用後的輸出都寫在相應函數後。

例4:

using namespace System;
delegate void D(int x);
ref struct Actions
{
  static void F1(int i)
  {
   Console::WriteLine("Actions::F1: {0}", i);
  }
  static void F2(int i)
  {
   Console::WriteLine("Actions::F2: {0}", i);
  }
  void F3(int i)
  {
   Console::WriteLine("instance of Actions::F3: {0}", i);
  }
};
int main()
{
  /*1*/ D^ cd1 = gcnew D(&Actions::F1); //包含F1的調用列表
  cd1(10);
  Actions::F1: 10
  /*2*/ D^ cd2 = gcnew D(&Actions::F2); //包含F2的調用列表
  cd2(15);
  Actions::F2: 15
  /*3*/ D^ cd3 = cd1 + cd2; //包含F1 + F2的調用列表
  cd3(20);
  Actions::F1: 20
  Actions::F2: 20
  /*4*/ cd3 += cd1; //包含F1 + F2 + F1的調用列表
  cd3(25);
  Actions::F1: 25
  Actions::F2: 25
  Actions::F1: 25
  Actions^ t = gcnew Actions();
  D^ cd4 = gcnew D(t, &Actions::F3);
  /*5*/ cd3 += cd4; //包含F1 + F2 + F1 + t->F3的調用列表
  cd3(30);
  Actions::F1: 30
  Actions::F2: 30
  Actions::F1: 30
  instance of Actions::F3: 30
  /*6*/ cd3 -= cd1; //移除最右邊的F1
  cd3(35); //調用F1、F2,t->F3
  Actions::F1: 35
  Actions::F2: 35
  instance of Actions::F3: 35
  /*7*/ cd3 -= cd4; //移除t->F3
  cd3(40); //調用F1、F2
  /*8*/ cd3 -= cd1; //移除F1
  cd3(45); //調用F2
  /*9*/ cd3 -= cd2; //移除F2,調用列表現在為空
  /*10*/Console::WriteLine("cd3 = {0}",
  (cd3 == nullptr ? "null" : "not null"));
}
Actions::F1: 40
Actions::F2: 40
Actions::F2: 45
cd3 = null

代理可通過 + 和 += 操作符來合並,如標號3、4中所示。兩個單入口列表會連接成一個新的雙入口列表,以先左操作數,後右操作數的順序。新的列表被cd3引用,而現有的兩個列表並未改變。在此要注意的是,不能合並不同類型的代理。

正如在標號4中所見,同一個函數可在一個調用列表中包裝多次;而在標號5中,也說明了一個調用列表能同時包含類與實例函數。代理可通過 - 或 -= 操作符移除,如標號6中所示。

當同一個函數在調用列表中出現多次時,一個對它的移除請求會導致最右邊的項被移除。在標號6中,這產生了一個新的三入口列表,其被cd3引用,且前一個列表保持不變(因為先前被cd3引用的列表現在引用計數為零,所以會被垃圾回收)。

當一個調用列表中的最後一項被移除時,代理將為nullptr值,此處沒有空調用列表的概念,因為,根本就沒有列表了。

例5中演示了另一個代理合並與移除的例子,正如標號3a與3b中所示,兩個多入口調用列表是以先左操作數,後右操作數的順序連接的。

如果想移除一個多入口列表,只有當此列表為整個列表中嚴格連續的子集時,操作才會成功。例如,在標號4b中,你可以移除F1和F2,因為它們是相鄰的,對標號5b中的兩個F2及標號6b中的F1、F2來說,道理也是一樣的。但是,在標號7b中,列表中有兩個連續的F1,所以操作失敗,而結果列表則是最開始的列表,它包含有4個入口。

例5:

using namespace System;
delegate void D(int x);
void F1(int i) { Console::WriteLine("F1: {0}", i); }
void F2(int i) { Console::WriteLine("F2: {0}", i); }
int main()
{
  D^ cd1 = gcnew D(&F1);
  D^ cd2 = gcnew D(&F2);
  /*1*/ D^ list1 = cd1 + cd2; // F1 + F2
  /*2*/ D^ list2 = cd2 + cd1; // F2 + F1
  D^ cd3 = nullptr;
  /*3a*/ cd3 = list2 + list1; // F2 + F1 + F1 + F2
  cd3(10);
  /*3b*/ cd3 = list1 + list2; // F1 + F2 + F2 + F1
  cd3(20);
  /*4a*/ cd3 = list1 + list2; // F1 + F2 + F2 + F1
  /*4b*/ cd3 -= cd1 + cd2; // F2 + F1
  cd3(30);
  /*5a*/ cd3 = list1 + list2; // F1 + F2 + F2 + F1
  /*5b*/ cd3 -= cd2 + cd2; // F1 + F1
  cd3(40);
  /*6a*/ cd3 = list1 + list2; // F1 + F2 + F2 + F1
  /*6b*/ cd3 -= cd2 + cd1; // F1 + F2
  cd3(50);
  /*7a*/ cd3 = list1 + list2; // F1 + F2 + F2 + F1
  /*7b*/ cd3 -= cd1 + cd1; // F1 + F2 + F2 + F1
  cd3(60);
}

System::Delegate

代理類型的定義,會隱式地創建一個對應的類(class)類型,並且所有的代理類型均從類庫System::Delegate繼承而來。要定義一個這樣的類,唯一的方法就是通過delegate上下文關鍵字。代理類為隱式的sealed,因此它們不能被用作基類。另外,一個非代理類也不能從System::Delegate繼承。

例6演示了幾個Delegate函數的用法:

例6:

using namespace System;
delegate void D(int x);
ref class Test
{
  String^ objName;
  public:
   Test(String^ objName)
   {
    this->objName = objName;
   }
   void M(int i)
   {
    Console::WriteLine("Object {0}: {1}", objName, i);
   }
};
void ProcessList(D^ del, int value, Object^ objToExclude);
int main()
{
  /*1*/ Test^ t1 = gcnew Test("t1");
  D^ cd1 = gcnew D(t1, &Test::M);
  /*2*/ Test^ t2 = gcnew Test("t2");
  D^ cd2 = gcnew D(t2, &Test::M);
  /*3*/ Test^ t3 = gcnew Test("t3");
  D^ cd3 = gcnew D(t3, &Test::M);
  /*4*/ D^ list = cd1 + cd2 + cd3 + cd2;
  /*5a*/ ProcessList(list, 100, nullptr);
  /*5b*/ ProcessList(list, 200, t1);
  /*5c*/ ProcessList(list, 300, t2);
  /*6a*/ D^ cd4 = cd1 + cd2;
  /*6b*/ D^ cd5 = (D^)cd4->Clone();
  /*6c*/ ProcessList(cd4, 5, nullptr);
  /*6d*/ ProcessList(cd5, 6, nullptr);
}
void ProcessList(D^ del, int value, Object^ objToExclude)
{
  /*7*/ if (del == nullptr)
  {
   return;
  }
  /*8*/ else if (objToExclude == nullptr)
  {
   del(value);
  }
  else
  {
   /*9*/ array<Delegate^>^ delegateList = del->GetInvocationList();
   for each (Delegate^ d in delegateList)
   {
    /*10*/ if (d->Target != objToExclude)
    {
     /*11*/ ((D^)d)(value);
    }
   }
  }
}

實例函數Test::M與代理類型D相兼容,當調用時,這個函數只是識別出它調用的對象,並帶有一個整數參數。

在標號1、2、3中,定義了三個Test類型的對象,並把它們各自與實例函數Test:M包裝在單獨的代理類型D中。接著,在標號4中,創建了一個四入口的調用列表。

倘若傳遞進來的調用列表不為空,ProcessList函數將調用在列表中除了特定對象以外的所有函數,例如,在標號5a中,沒有排除任何入口,因此所有的函數都會被調用;在標號5b中,t1被排除在外,而標號5c中,與對象t2有關的兩個入口都被排除了,結果輸出如下:

Object t1: 100
Object t2: 100
Object t3: 100
Object t2: 100
Object t2: 200
Object t3: 200
Object t2: 200
Object t1: 300
Object t3: 300

在標號6b中,調用了Clone創建了代理cd4的一個副本,這個函數返回一個Object^,因此,要把它轉換成D^類型。當原始及克隆的代理在標號6c、6d中調用時,結果輸出如下:

Object t1: 5
Object t2: 5
Object t1: 6
Object t2: 6

關於函數ProcessList,如果參數中的代理實例為nullptr,即沒有調用列表,那它將直接返回;如果排除的對象為nullptr,那麼列表中所有的函數都將被調用;如果存在要排除的對象,就要像標號8中那樣把調用列表當作代理數組取出,接著,在標號9中逐個排查不相符的入口,最後,在標號10中調用余下的這些函數。盡管在調用列表中每個入口都是Del類型,但GetInvocationList返回一個基類Delegate數組,所以在調用每個代理實例之前,需像標號10那樣先轉換成類型D。

事件

在C++/CLI中,事件是一種當某種重要事情發生時,為客戶程序提供通知的機制。鼠標單擊就是事件的一個典型例子,在事件發生之前,有關的客戶程序必須先注冊它們感興趣的事件,如,當檢測到鼠標單擊時,這些程序就會接到通知。

通過添加或刪除一個或多個感興趣的事件,事件列表可在運行時增長或縮減,請看例7中Server類型的定義,在標號1中,Server類定義了代理類型NewMsgEventHandler(一般約定在用於事件處理時,代理類型添加EventHandler的後綴名),接著,在標號2中,定義了一個名為ProcessNewMsg的公共事件(event在此為一個上下文關鍵字)。一個事件必須有一個代理類型,實際上,像這樣的一個事件已經是一個代理實例了,而且因為它被默認初始化為nullptr,所以它沒有調用列表。

例7:

using namespace System;
public ref struct Server
{
  /*1*/ delegate void NewMsgEventHandler(String^ msg);
  /*2*/ static event NewMsgEventHandler^ ProcessNewMsg;
  /*3*/ static void Broadcast(String^ msg)
  {
   if (ProcessNewMsg != nullptr)
   {
    ProcessNewMsg(msg);
   }
  }
};

當通過一條消息調用時,函數Broadcast將調用包裝在ProcessNewMsg調用列表中所有的函數。

Client類定義在例8中,一個Client的類型實例無論何時被創建,它都會通過向為Server::ProcessNewMsg維護的代理列表中添加一個實例函數(它關聯到實例變量),來注冊它所感興趣的新Server消息,而這是通過 += 操作符來完成,如標號5中所示。只要這個入口一直保持在通知列表中,無論何時一個新消息送達Server,注冊的函數都會被調用。

例8:

using namespace System;
public ref class Client
{
  String^ clientName;
  /*4*/ void ProcessNewMsg(String^ msg)
  {
   Console::WriteLine("Client {0} received message {1}", clientName, msg);
  }
  public:
   Client(String^ clientName)
   {
    this->clientName = clientName;
    /*5*/ Server::ProcessNewMsg += gcnew Server::NewMsgEventHandler(this, &Client::ProcessNewMsg);
   }
   /*6*/ ~Client()
   {
    Server::ProcessNewMsg -= gcnew Server::NewMsgEventHandler(this, &Client::ProcessNewMsg);
   }
};

要從通知列表中移除一個入口,可使用 -= 操作符,如標號6定義的析構函數中那樣。

例9:

using namespace System;
int main()
{
  Server::Broadcast("Message 1");
  Client^ c1 = gcnew Client("A");
  Server::Broadcast("Message 2");
  Client^ c2 = gcnew Client("B");
  Server::Broadcast("Message 3");
  Client^ c3 = gcnew Client("C");
  Server::Broadcast("Message 4");
  c1->~Client();
  Server::Broadcast("Message 5");
  c2->~Client();
  Server::Broadcast("Message 6");
  c3->~Client();
  Server::Broadcast("Message 7");
}

例9是主程序,一開始,沒有注冊任何函數,所以當發送第一個消息時,不會獲得任何通知。然而,一旦構造了c1,通知列表就包含了此對象的一個入口,而接下來c2與c3的構造使這個列表增長到3個入口。在這些對象消失時(通過顯式調用析構函數),入口數也相應地減少了,直到最後,一個也不剩,因此當最後一條消息發出時,沒有任何對象在監聽。以下是輸出:

Client A received message Message 2
Client A received message Message 3
Client B received message Message 3
Client A received message Message 4
Client B received message Message 4
Client C received message Message 4
Client B received message Message 5
Client C received message Message 5
Client C received message Message 6

盡管3個對象均為同一類型,但這並不是必須的,只要定義的函數可與NewMsgEventHandler兼容,就能使用任意的類型。

上述例子中使用的事件只不過是微不足道的一個示例,另外要說明一點,與以前文章中說過的屬性一樣,此種類型的事件均以private屬性自動備份,且自動生成添加(add)與移除(remove)存取程序,為自定義這些存取程序,就必須提供這些函數的定義,如例10中所示,名稱add與remove在此為上下文關鍵字。

例10:

public ref struct Server
{
  // ...
  static event NewMsgEventHandler^ ProcessNewMsg {
   void add(NewMsgEventHandler^ n) { /* ... */ }
   void remove(NewMsgEventHandler^ n) { /* ... */ }
  }
  // ...
};

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved