代理

代理允许您在C++对象上以通用的但类型安全的方式调用成员函数。通过使用代理,您可以将其动态地绑定到任何对象的成员函数上,然后在该对象上调用函数,即时调用者不知道该对象的类型也没关系。

复制代理对象是非常安全的。代理可以进行值传递,但是一般不推荐这样做,因为这样就必须在堆上为其分配内存。因此 任何时候您都应该通过引用传递代理。

既支持单播代理也支持多播代理,同时还支持可以安全地序列化到磁盘上的“动态”代理。

声明代理

通过使用以下提供的任何一个宏都可以声明代理。所使用的宏由要绑定到该代理上的函数的签名决定。系统预定义了各种通用函数签名的组合,您可以根据这些组合声明代理类型、给返回值及参数填入您需要的任何类型的类型名称。现在支持使用以下任何条件组合的代理签名:

  • 返回一个值的函数

  • 多达4个"负载" 变量

  • 多达8个函数参数

  • 声明为 'const'的函数

使用该表格来查找声明您的代理时要使用的声明宏。

函数签名 声明宏
void Function() DECLARE_DELEGATE( DelegateName )
void Function( <Param1> ) DECLARE_DELEGATE_OneParam( DelegateName, Param1Type )
void Function( <Param1>, <Param2> ) DECLARE_DELEGATE_TwoParams( DelegateName, Param1Type, Param2Type )
void Function( <Param1>, <Param2>, ... ) DECLARE_DELEGATE_<Num>Params( DelegateName, Param1Type, Param2Type, ... )
<RetVal> Function() DECLARE_DELEGATE_RetVal( RetValType, DelegateName )
<RetVal> Function( <Param1> ) DECLARE_DELEGATE_RetVal_OneParam( RetValType, DelegateName, Param1Type )
<RetVal> Function( <Param1>, <Param2> ) DECLARE_DELEGATE_RetVal_TwoParams( RetValType, DelegateName, Param1Type, Param2Type )
<RetVal> Function( <Param1>, <Param2>, ... ) DECLARE_DELEGATE_RetVal_<Num>Params( RetValType, DelegateName, Param1Type, Param2Type, ... )

同时也提供了针对多播代理、动态代理及封装代理的宏变种:

  • DECLARE_MULTICAST_DELEGATE...

  • DECLARE_DYNAMIC_DELEGATE...

  • DECLARE_DYNAMIC_MULTICAST_DELEGATE...

  • DECLARE_DYNAMIC_DELEGATE...

  • DECLARE_DYNAMIC_MULTICAST_DELEGATE...

宏签名声明可以在全局范围内、命名空间内、或者甚至可以在一个类声明中(但不能在函数体内)。

请参照动态代理多播代理 获得关于声明这些代理类型的更多信息。

绑定代理

代理系统理解某些对象类型,且当使用这些对象时会启用附加功能。如果您把一个代理绑定到一个UObject或共享的指针类上,代理系统可以保持一个到该对象的弱引用,以便当该对象在该代理的底层被销毁了时,您可以通过调用 IsBound()ExecuteIfBound() 函数处理这些情况。注意,所支持的各种对象类型有特定的绑定语法。

函数 描述
Bind() 绑定到一个现有的代理对象上。
BindRaw() 绑定到一个原始的C++指针全局函数代理上。原始指针不使用任何引用,所以如果从代理的底层删除了该对象,那么调用它可能是不安全的。因此,当调用Execute()时一定要小心!
BindSP() 绑定一个基于共享指针的成员函数代理。共享指针代理保持到您的对象的弱引用。您可以使用 ExecuteIfBound() 来调用它们。
BindUObject() 绑定一个基于UObject的成员函数代理。UObject 代理保持到您的对象的弱引用。您可以使用 ExecuteIfBound() 来调用它们。
UnBind() 解除绑定该代理。

请参照 DelegateSignatureImpl.inl (located in ..\UE4\Engine\Source\Runtime\Core\Public\Templates\) 文件获得关于这些函数的变种、参数及实现相关的信息。

负载数据

当绑定一个代理时,您可以随之传入负载数据。这些负载数据是指当调用任何绑定函数时直接传入到该绑定函数的任意变量。这非常有用,因为它允许您在绑定时在代理本身中存储变量。所有的代理类型(除了"动态代理")都自动支持负载变量。该示例向代理中传入了两个自定义变量,一个布尔型变量一个是32位的整型变量。那么,当调用该代理时,这些参数将会传入到您的绑定函数中。在代理类型变量参数后面,总是可以添加额外的变量参数。

MyDelegate.BindRaw( &MyFunction, true, 20 );

执行代理

绑定到代理上的函数可以通过调用代理的 Execute() 函数进行执行。在执行这些函数之前您必须检查是否已经“绑定”了代理。这是为了使得代码更加安全,因为可能代理有时会具有未初始化且后续要访问的返回值及输出参数。执行一个未绑定的代理实际上有时会扰乱内存。您可以调用 IsBound() 来判断执行该代理是否安全。同时,对于没有返回值的代理,您可以调用 ExecuteIfBound() 函数,但是一定要注意那些可能未初始化的输出参数。

执行函数 描述
Execute()
ExecuteIfBound()
IsBound()

请参照 多播代理 获得执行多播代理的详细信息。

应用示例

假设您有一个类,它具有您想在任何地方都可以访问的一个方法。

class FLogWriter
{
    void WriteToLog( FString );
};

要想调用WriteToLog函数,我们需要创建针对该函数签名的代理类型。要想完成这个处理,您需要使用以下其中一个宏声明该代理。比如,以下是一种简单的代理类型:

DECLARE_DELEGATE_OneParam( FStringDelegate, FString );

这创建了一个称为 'FStringDelegate' 的代理类型,取入一个 'FString' 类型的参数。

以下示例展示了如何在一个类中使用这个 'FStringDelegate' :

class FMyClass
{
    FStringDelegate WriteToLogDelegate;
};

这允许您的类存放一个到任何类中的一个方法的指针。这个类了解关于该代理的唯一一件事情是:它是函数签名。

现在,要想分配该代理,只需简单地创建您的代理类的一个实例,传入具有该方法的类作为模板参数。您也可以传入您的对象实例和该方法的实际函数地址。所以,这里我们将创建 'FLogWriter' 类的一个实例,然后创建那个对象实例的 'WriteToLog' 方法的代理。

FSharedRef< FLogWriter > LogWriter( new FLogWriter() );

WriteToLogDelegate.BindSP( LogWriter, &FLogWriter::WriteToLog );

您已经动态地把一个代理绑定到一个类的方法上了! 非常简单,对吧?

注意, 'BindSP' 中的 'SP' 部分代表 '共享指针' ,因为我们正在绑定到一个由共享指针拥有的对象上。不同的对象类型有不同的函数版本,比如BindRaw()和BindUObject()。

现在,FMyClass就可以调用您的'WriteToLog'方法了,甚至它不需要知道关于 'FLogWriter' 的任何信息。要想调用您的代理,仅需使用 'Execute()' 方法。

WriteToLogDelegate.Execute( TEXT( "Delegates are spiffy!" ) );

如果在将函数绑定到代理上之前调用了 Execute() 函数,那么将会触发产生断言。因此大部分情况下,您需要进行这样的处理:

WriteToLogDelegate.ExecuteIfBound( TEXT( "Only executes if a function was bound!" ) );