在C++/CLI中
代理是对函数进行包装的对象
而事件是一种为客户程序提供通知的类机制
在前几篇文章中已经多次演示了如果让一个句柄在不同的时间被引用至不同的对象从而以更抽象的方法来解决程序中的问题但是也能使用代理通过函数来达到同样的效果代理是包装了函数的一个对象且对实例函数而言也能通过特定的实例与这些函数发生联系一旦一个代理包装了一个或多个函数你就能通过代理来调用这些函数而无须事先了解包装了哪些函数
请看例中的代码在标号中定义一个代理类型Del由于使用了上下文关键字delegate所以有点像函数的声明但与函数声明不同的是此处声明的是一个代理类型Del的实例其可包装进任意接受一个int类型作为参数并返回一个int值类型的函数(任意有效的参数列表及返回类型组合都是允许的)一旦定义了某种代理类型它只能被用于包装具有同样类型的函数代理类型可被定义在源文件中或命名空间的范围内也能定义在类中并可有public或private访问控制属性
例
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;
}
};
/**/
delegate int Del(int value);
int main()
{
/**/ Del^ d = gcnew Del(&A::Square);
/**/ Console::WriteLine(d() result = {} d());
/**/ B^ b = gcnew B;
/**/ d = gcnew Del(b &B::Cube);
/**/ Console::WriteLine(d() result = {} d());
}
静态函数A::Square与实例函数B::Cube对Del来说都具有相同的参数类型及返回类型因此它们能被包装进同类型的代理中注意即使两个函数均为public当考虑它们与Del的兼容性时它们的可访问性也是不相关的这样的函数也能被定义在相同或不同的类中主要由程序员来选择
一旦定义了某种代理类型就可创建此类型实例的句柄并进行初始化或赋值操作如标号中所示的静态函数A::Square及标号中所示的实例函数B::Cube(此处只是出于演示的目的否则把Cube做成实例函数没有任何好处)
创建一个代理实例涉及到调用一个构造函数如果是在包装一个静态函数只需传递进一个指向成员函数的指针而对实例函数而言必须传递两个参数一个实例的句柄及指向实例成员函数的指针
在初始化代理实例之后就能间接地调用它们包装的函数了用法与直接调用原函数一样只不过现在用的是代理实例名如标号与由包装函数返回的值也是像直接调用函数时那样获得如果一个代理实例的值为nullptr此时再试图调用被包装的函数会导致System::NullReferenceException类型异常
以下是输出
d() result =
d() result =
传递与返回代理
有时把包装好的函数传递给另一个函数会非常有用接受一方的函数并不知道会传递过来哪个函数并且它也无须关心只需简单地通过包装好的代理间接调用此函数就行了
下面以集合中元素排序来说明大多数时候集合中元素排序所依据的规则只在对某对元素进行比较的方法上存在区别如果在运行时提供进行比较的函数一个排序过程就能用相应定义的比较函数排出任意的顺序请看例
例
using namespace System;
ref struct StrCompare
{
static int CompareExact(String^ s String^ s)
{
Console::WriteLine(Comparing {} and {} using CompareExact s s);
//
return ;
}
static int CompareIgnoreCase(String^ s String^ s)
{
Console::WriteLine(Comparing {} and {} using CompareIgnoreCase s s);
//
return ;
}
};
delegate int Compare(String^ s String^ s);
/**/
Compare^ FindComparisonMethod()
{
//
}
void Sort(Compare^ compare)
{
int result;
/**/ result = compare(Hello Hello);
/**/ result = compare(Hello HELLO);
/**/ result = compare(Hello Hell);
}
int main()
{
/**/ Sort(gcnew Compare(&StrCompare::CompareIgnoreCase));
/**/ Sort(FindComparisonMethod());
/**/ FindComparisonMethod()(Red RED);
}
Compare代理类型可对任意接受两个String^参数并返回一个int结果的函数进行包装在此有两个函数为StrCompare::CompareExact和StrCompare::CompareIgnoreCase
在标号中创建了一个Compare代理类型的实例用它来包装StrCompare::CompareIgnoreCase并把此代理句柄传递给Sort函数其将会利用比较函数进一步进行处理
正如大家所看到的Sort可接受一个代理类型的参数而此参数可像其他函数参数一样可为传值传址传引用
在标号中调用了FindComparisonMethod函数其返回一个Del代理类型接着在标号及中调用了包装过的函数此处要重点说一下标号首先FindComparisonMethod函数是被调用来获取代理实例其常用于调用底层函数其次这两个函数的调用操作符都有同等的优先级所以它们从左至右调用
FindComparisonMethod函数中也用了一些逻辑用于确定到底需要包装哪个函数此处就未作详细说明了
代理类型的兼容性
一个代理类型只与它自身相兼容与其他任何代理类型都不兼容即使其他类型的包装函数均为同一类型请看例非常明显代理类型D与函数A::M与A::M兼容代理类型D也与这些函数兼容然而这两个代理类型在标号中并不能互换使用
例
delegate void D();
delegate void D();
public struct A
{
static void M() { /* */ }
static void M() { /* */ }
};
void X(D^ m) { /* */ }
void Y(D^ n) { /* */ }
int main()
{
D^ d;
/**/ d = gcnew D(&A::M); //兼容
/**/ d = gcnew D(&A::M); //兼容
D^ d;
/**/ d = gcnew D(&A::M); //兼容
/**/ d = gcnew D(&A::M); //兼容
/**/ d = d; //不兼容
/**/ d = d; //不兼容
/**/ X(d); //兼容
/**/ X(d); //不兼容
/**/ Y(d); //不兼容
/**/ Y(d); //兼容
}
代理类型的合并
一个代理实例实际上能包装多个函数在这种情况下被包装的函数集被维护在一个调用列表中当合并两个代理实例时它们的调用列表也以指定的顺序连接起来并产生一个新的列表而现有的两个列表并没有发生改变当从调用列表中移除一个或多个函数时也会产生一个新的列表且原始列表不会发生变化请看例中的代码每个函数调用后的输出都写在相应函数后
例
using namespace System;
delegate void D(int x);
ref struct Actions
{
static void F(int i)
{
Console::WriteLine(Actions::F: {} i);
}
static void F(int i)
{
Console::WriteLine(Actions::F: {} i);
}
void F(int i)
{
Console::WriteLine(instance of Actions::F: {} i);
}
};
int main()
{
/**/ D^ cd = gcnew D(&Actions::F); //包含F的调用列表
cd();
Actions::F:
/**/ D^ cd = gcnew D(&Actions::F); //包含F的调用列表
cd();
Actions::F:
/**/ D^ cd = cd + cd; //包含F + F的调用列表
cd();
Actions::F:
Actions::F:
/**/ cd += cd; //包含F + F + F的调用列表
cd();
Actions::F:
Actions::F:
Actions::F:
Actions^ t = gcnew Actions();
D^ cd = gcnew D(t &Actions::F);
/**/ cd += cd; //包含F + F + F + t>F的调用列表
cd();
Actions::F:
Actions::F:
Actions::F:
instance of Actions::F:
/**/ cd = cd; //移除最右边的F
cd(); //调用FFt>F
Actions::F:
Actions::F:
instance of Actions::F:
/**/ cd = cd; //移除t>F
cd(); //调用FF
/**/ cd = cd; //移除F
cd(); //调用F
/**/ cd = cd; //移除F调用列表现在为空
/**/Console::WriteLine(cd = {}
(cd == nullptr ? null : not null));
}
Actions::F:
Actions::F:
Actions::F:
cd = null
代理可通过 + 和 += 操作符来合并如标号中所示两个单入口列表会连接成一个新的双入口列表以先左操作数后右操作数的顺序新的列表被cd引用而现有的两个列表并未改变在此要注意的是不能合并不同类型的代理
正如在标号中所见同一个函数可在一个调用列表中包装多次而在标号中也说明了一个调用列表能同时包含类与实例函数代理可通过 或 = 操作符移除如标号中所示
当同一个函数在调用列表中出现多次时一个对它的移除请求会导致最右边的项被移除在标号中这产生了一个新的三入口列表其被cd引用且前一个列表保持不变(因为先前被cd引用的列表现在引用计数为零所以会被垃圾回收)
当一个调用列表中的最后一项被移除时代理将为nullptr值此处没有空调用列表的概念因为根本就没有列表了
例中演示了另一个代理合并与移除的例子正如标号a与b中所示两个多入口调用列表是以先左操作数后右操作数的顺序连接的
如果想移除一个多入口列表只有当此列表为整个列表中严格连续的子集时操作才会成功例如在标号b中你可以移除F和F因为它们是相邻的对标号b中的两个F及标号b中的FF来说道理也是一样的但是在标号b中列表中有两个连续的F所以操作失败而结果列表则是最开始的列表它包含有个入口
例
using namespace System;
delegate void D(int x);
void F(int i) { Console::WriteLine(F: {} i); }
void F(int i) { Console::WriteLine(F: {} i); }
int main()
{
D^ cd = gcnew D(&F);
D^ cd = gcnew D(&F);
/**/ D^ list = cd + cd; // F + F
/**/ D^ list = cd + cd; // F + F
D^ cd = nullptr;
/*a*/ cd = list + list; // F + F + F + F
cd();
/*b*/ cd = list + list; // F + F + F + F
cd();
/*a*/ cd = list + list; // F + F + F + F
/*b*/ cd = cd + cd; // F + F
cd();
/*a*/ cd = list + list; // F + F + F + F
/*b*/ cd = cd + cd; // F + F
cd();
/*a*/ cd = list + list; // F + F + F + F
/*b*/ cd = cd + cd; // F + F
cd();
/*a*/ cd = list + list; // F + F + F + F
/*b*/ cd = cd + cd; // F + F + F + F
cd();
}
System::Delegate
代理类型的定义会隐式地创建一个对应的类(class)类型并且所有的代理类型均从类库System::Delegate继承而来要定义一个这样的类唯一的方法就是通过delegate上下文关键字代理类为隐式的sealed因此它们不能被用作基类另外一个非代理类也不能从System::Delegate继承
例演示了几个Delegate函数的用法
例
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 {}: {} objName i);
}
};
void ProcessList(D^ del int value Object^ objToExclude);
int main()
{
/**/ Test^ t = gcnew Test(t);
D^ cd = gcnew D(t &Test::M);
/**/ Test^ t = gcnew Test(t);
D^ cd = gcnew D(t &Test::M);
/**/ Test^ t = gcnew Test(t);
D^ cd = gcnew D(t &Test::M);
/**/ D^ list = cd + cd + cd + cd;
/*a*/ ProcessList(list nullptr);
/*b*/ ProcessList(list t);
/*c*/ ProcessList(list t);
/*a*/ D^ cd = cd + cd;
/*b*/ D^ cd = (D^)cd>Clone();
/*c*/ ProcessList(cd nullptr);
/*d*/ ProcessList(cd nullptr);
}
void ProcessList(D^ del int value Object^ objToExclude)
{
/**/ if (del == nullptr)
{
return;
}
/**/ else if (objToExclude == nullptr)
{
del(value);
}
else
{
/**/ array<Delegate^>^ delegateList = del>GetInvocationList();
for each (Delegate^ d in delegateList)
{
/**/ if (d>Target != objToExclude)
{
/**/ ((D^)d)(value);
}
}
}
}
实例函数Test::M与代理类型D相兼容当调用时这个函数只是识别出它调用的对象并带有一个整数参数
在标号中定义了三个Test类型的对象并把它们各自与实例函数Test:M包装在单独的代理类型D中接着在标号中创建了一个四入口的调用列表
倘若传递进来的调用列表不为空ProcessList函数将调用在列表中除了特定对象以外的所有函数例如在标号a中没有排除任何入口因此所有的函数都会被调用在标号b中t被排除在外而标号c中与对象t有关的两个入口都被排除了结果输出如下
Object t:
Object t:
Object t:
Object t:
Object t:
Object t:
Object t:
Object t:
Object t:
在标号b中调用了Clone创建了代理cd的一个副本这个函数返回一个Object^因此要把它转换成D^类型当原始及克隆的代理在标号cd中调用时结果输出如下
Object t:
Object t:
Object t:
Object t:
关于函数ProcessList如果参数中的代理实例为nullptr即没有调用列表那它将直接返回如果排除的对象为nullptr那么列表中所有的函数都将被调用如果存在要排除的对象就要像标号中那样把调用列表当作代理数组取出接着在标号中逐个排查不相符的入口最后在标号中调用余下的这些函数尽管在调用列表中每个入口都是Del类型但GetInvocationList返回一个基类Delegate数组所以在调用每个代理实例之前需像标号那样先转换成类型D
事件
在C++/CLI中事件是一种当某种重要事情发生时为客户程序提供通知的机制鼠标单击就是事件的一个典型例子在事件发生之前有关的客户程序必须先注册它们感兴趣的事件如当检测到鼠标单击时这些程序就会接到通知
通过添加或删除一个或多个感兴趣的事件事件列表可在运行时增长或缩减请看例中Server类型的定义在标号中Server类定义了代理类型NewMsgEventHandler(一般约定在用于事件处理时代理类型添加EventHandler的后缀名)接着在标号中定义了一个名为ProcessNewMsg的公共事件(event在此为一个上下文关键字)一个事件必须有一个代理类型实际上像这样的一个事件已经是一个代理实例了而且因为它被默认初始化为nullptr所以它没有调用列表
例
using namespace System;
public ref struct Server
{
/**/ delegate void NewMsgEventHandler(String^ msg);
/**/ static event NewMsgEventHandler^ ProcessNewMsg;
/**/ static void Broadcast(String^ msg)
{
if (ProcessNewMsg != nullptr)
{
ProcessNewMsg(msg);
}
}
};
当通过一条消息调用时函数Broadcast将调用包装在ProcessNewMsg调用列表中所有的函数
Client类定义在例中一个Client的类型实例无论何时被创建它都会通过向为Server::ProcessNewMsg维护的代理列表中添加一个实例函数(它关联到实例变量)来注册它所感兴趣的新Server消息而这是通过 += 操作符来完成如标号中所示只要这个入口一直保持在通知列表中无论何时一个新消息送达Server注册的函数都会被调用
例
using namespace System;
public ref class Client
{
String^ clientName;
/**/ void ProcessNewMsg(String^ msg)
{
Console::WriteLine(Client {} received message {} clientName msg);
}
public:
Client(String^ clientName)
{
this>clientName = clientName;
/**/ Server::ProcessNewMsg += gcnew Server::NewMsgEventHandler(this &Client::ProcessNewMsg);
}
/**/ ~Client()
{
Server::ProcessNewMsg = gcnew Server::NewMsgEventHandler(this &Client::ProcessNewMsg);
}
};
要从通知列表中移除一个入口可使用 = 操作符如标号定义的析构函数中那样
例
using namespace System;
int main()
{
Server::Broadcast(Message );
Client^ c = gcnew Client(A);
Server::Broadcast(Message );
Client^ c = gcnew Client(B);
Server::Broadcast(Message );
Client^ c = gcnew Client(C);
Server::Broadcast(Message );
c>~Client();
Server::Broadcast(Message );
c>~Client();
Server::Broadcast(Message );
c>~Client();
Server::Broadcast(Message );
}
例是主程序一开始没有注册任何函数所以当发送第一个消息时不会获得任何通知然而一旦构造了c通知列表就包含了此对象的一个入口而接下来c与c的构造使这个列表增长到个入口在这些对象消失时(通过显式调用析构函数)入口数也相应地减少了直到最后一个也不剩因此当最后一条消息发出时没有任何对象在监听以下是输出
Client A received message Message
Client A received message Message
Client B received message Message
Client A received message Message
Client B received message Message
Client C received message Message
Client B received message Message
Client C received message Message
Client C received message Message
尽管个对象均为同一类型但这并不是必须的只要定义的函数可与NewMsgEventHandler兼容就能使用任意的类型
上述例子中使用的事件只不过是微不足道的一个示例另外要说明一点与以前文章中说过的属性一样此种类型的事件均以private属性自动备份且自动生成添加(add)与移除(remove)存取程序为自定义这些存取程序就必须提供这些函数的定义如例中所示名称add与remove在此为上下文关键字
例
public ref struct Server
{
//
static event NewMsgEventHandler^ ProcessNewMsg {
void add(NewMsgEventHandler^ n) { /* */ }
void remove(NewMsgEventHandler^ n) { /* */ }
}
//
};