UDN
Search public documentation:

CodingStandardCH
English Translation
日本語訳
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 主页 > 虚幻脚本 > 编码标准

编码标准


概述


在 Epic,我们有一些简单的编码标准和规则。大多数这些示例都是关于虚幻脚本的,不过他们同时也适用于 C++ 编码。该文档目的不是为了讨论或者进行开发,更确切地说,它可以反映 Epic 当前编码标准的状态。提供它作为供授权用户使用的规则。注意:该引擎不可以完全反映这个标准;我们有成百上千行以前遗留的代码,我们会在添加新代码或重构旧代码时进行更新。

代码规则很重要,原因如下:

  • 一套软件 80% 的生命周期消耗都用于维护。
  • 几乎任何软件原创作者不会在整个生命周期过程中都进行维护。
  • 代码规则会改进软件的可读性,使工程师可以更加快速彻底地理解新代码。我们肯定会雇佣新工程师并让他们实习超过这个项目的生命周期时间,而且我们可能会使用我们对于接下来几个项目中在引擎方面所进行的新的修改。
  • 如果我们决定将源代码公布到 mod 开发者社区,那么我们需要它能够通俗易懂。
  • 许多这些规则对于交叉编译器兼容性来说确实是必需的。

类组织:类名称是名词


  • 类注释块
  • 类声明
  • 变量声明
  • C++ 专用:静态和常数变量
  • Public 变量
  • Protected 变量
  • Private 变量
  • cpptext
  • Constructor(构造函数)和 Destructor(析构函数)
  • 可以包含 BeginPlay( )、Destroyed( ) 等等
  • 根据功能进行分组的方法和 state
  • 根据功能分组的 state 无关使用方法(与任何 state 无关)
  • 与进入或退出某个 state 有关的无 state 的方法应该就在 state 前面。
  • 如果有任何 state 相关方法和自动 state 的 state,那么应该定义第一个状态
  • defaultproperties(默认属性)

State 组成部分:状态名称是形容词


  • 状态注释块(类似于方法注释)
  • 方法
  • Begin: 代码

方法组成部分:方法是动词


  • 方法注释块
  • 局部变量

变量:使用 C++ 中合适类型!


  • UBOOL 代表布尔值(4 字节)。 BOOL 将不会进行编译。
  • TCHAR 代表字符变量(“从不”指定 TCHAR 的大小)
  • BYTE 代表无符号的字节(1 字节)
  • SBYTE 代表带符号的字节(1 字节)
  • WORD 代表无符号的“短整型”(2 字节)
  • SWORD 代表带符号的“短整型”(2 字节)
  • UINT 代表无符号整型(4 字节)
  • INT 代表带符号整型(4 字节)
  • QWORD 代表无符号“四倍长字”(8 字节)
  • SQWORD 代表带符号的“四倍长字”(8 字节)
  • FLOAT 代表单精度浮点(4 字节)
  • DOUBLE 代表双精度浮点(8 字节)
  • PTRINT 代表可能具有指针的整型变量(“从不”指定 PTRINT 的大小)

注释


注释是信息交流;信息交流 很重要 。 有关注释要记住的一些事情(节选自 Kernighan & Pike 的 编程实践 ):

  • 编写自存档代码:
t = s + l - b; TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;

  • 不要赘述明显的地方;留下注释或说明一些有用的事情:
// increment iLeaves // we know there is another tea leaf
Leaves++; Leaves++;

  • 不要注释有问题的代码 - 重新编写!
// total number of leaves is sum of  
// small and large leaves less the  
// number of leaves that are both  
t = s + l - b; TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;

  • 不要与代码冲突:
// never increment iLeaves! // we know there is another tea leaf
Leaves++; Leaves++;

我们的文档类型基于Javadoc,但是 C# 项目例外(请参阅这部分)。最终,我们会设法找到自动生成的文档。

以下示例会显示类、状态、方法和变量注释的格式。 记住注释应该增加代码。 代码会记录执行而注释会记录意图。 请确保在您更改一段代码意图时更新注释。

class Tea extends WarmBeverage
   native, perobjectconfig, transient;
/**
 * 这个类会进行很多有条理的操作。它会使用
 * 一些其他类进行其他有条理的操作。
 */

/** 它会在瓷器中存储茶的值。 */
var float Price;


/**
 * state Brewing
 * 创建这个 state 代表茶杯中的 Brewing。
 * entered: 在默认状态中调用 AddBoilingWater。
 * exited:  通过 Pour 或太多时间消耗(会进入
 * GettingBitter)
 */
state Brewing
{

   /**
    * Steep 会为已经指定
    * 用来浸泡的水的体积和温度的茶计算 delta 味道值。
    *
    * @param    VolumeOfWater - 用于酿造的水量
    *           毫升的正数(没有勾选!)
    *
    * @param    TemperatureOfWater - 其中水的温度
    *           度(开尔文单位)。 273 < temperatureOfWater(水的温度) < 380
    *
    * @param    NewPotency - 浸泡开始后茶的效能;
    *           应该在 0.97 到 1.04 之间
    *
    * @return   会返回在茶味道中浓度的改变
    *           每分钟单位数 (TTU)
    *
    */

   function float Steep (float VolumeOfWater, float TemperatureOfWater,
                         out float NewPotency)
   {
   }
}

类注释中都包括哪些内容?

  • 这个类解决的问题的说明。 为什么创建这个类?
声明注释中包括哪些内容?
  • 该声明解决的问题的说明。 为什么该对象有这样的声明。 注意:在上面的类材料的排列顺序中,与该声明相关联不在声明中的函数应该就显示在声明注释前面。
该函数注释的所有部分分别代表什么意思?
  • Brewing::Steep 代表 Steep 方法在 Brewing 声明中。 它会在虚幻脚本注释中使用 C++ 范围解析操作符。
  • 函数的意图会在下面进行说明。 它会记录 这个函数解决的问题。 正像上面所说的那样,注释会记录 意图 和代码文档执行。
  • 会将所有输入参数的列表输入到该方法。 每个参数应该包括测量单位,预计值的范围、“不可能”值以及状态/错误代码的意思。
  • 输入/输出(因为不需要所以这里不包括)会记录为可能被该函数改变的函数提供值的参数。 记录为输入和输出 两种 参数。
  • 输出会列出所有只输出的参数(忽略其中由调用者提供的值)。 应该包含返回值代表的含义以及发生任何错误/故障时值所发生的情况。
  • 返回会在记录输出变量时记录预计返回值。

一些特殊的注释。 注意在斜杠和单词之间没有空格;它不需要大量点击就可以轻松搜索这些注释。
//@debug 调试声明以便在可以发布之前进行删除
//@todo 仍然还有要进行的工作,紧跟着是说明

注意,调试代码通常应该留在引擎外面。如果您必须将其留在引擎内,那么它应该封装在 if(0) 块中而不是 #if 0 块中,这样它会一直进行编译,但是仍然在外面进行优化。包括说明它是调试代码的注释,而且包含您的名称以及调试代码代表的含义。

修改第三方代码:

每当您将代码修改为我们在引擎(wxWindows、FaceFX、Novodex 等等)中使用的数据库时,请确保使用 //@UE3 注释标记您的变更,以及您为什么进行这个变更的说明。 这样可以更加容易地将变更合并到这个数据库的新版本中,并且使授权用户可以轻松地找到我们已经进行的任何修改。

C# 的其他标准


与其为 C# 使用注释的

/** */
格式,使用可以正确生成代码帮助文件的
///
格式。 比如:

namespace UnrealLogging
{
   /// <summary>
   /// 表示记入日志的消息的等级
   /// </summary>
   public enum LogLevel { LOG_Error, LOG_Warning, LOG_Debug };

   /// <summary>
   /// 通用日志界面。执行该界面可以为任何目标环境提供日志支持。
   /// </summary>
   public interface ILogger
   {
      /// <summary>
      /// 将消息写入执行的日志支持
      /// </summary>
      /// <param name="MsgLevel">写入的消息的严重等级</param>
      /// <param name="StrMsg">要写出的消息</param>
      void Log(LogLevel MsgLevel,string StrMsg);

      /// <summary>
      /// 设置对于日志适用的过滤方法。没有消息的等级比在这里设置的高会记入到日志。
      /// </summary>
      /// <param name="MaxLevel">要写出的最高日志等级</param>
      void SetFilter(LogLevel MaxLevel);
   }
}

需要的项目设置

启用 XML 文档文件并在 Perforce 中保持随时进行更新。

启用这个选项将没有代码的文档视为错误。

命名规则


变量、函数、声明和类名称应该是清晰、明确和描述性的。名称的范围越大,符合标准的描述性名称的重要性越强。避免缩写过于泛滥。

所有变量都应该一次声明一个变量,这样可以提供有关这个变量的含义的注释。此外,JavaDocs 类型需要它。您可以在变量前面使用多行或单行注释,而对于组变量来说空白行是可选项。

变量名称应该使用以下大小写格式:ThisIsAGoodVariableName。不要使用 "thisIsABadName"(a.k.a. 骆驼拼写法)或者 "this_is_a_bad_name"(下划线)。

所有布尔变量都应该询问 true/false 问题。 所有返回布尔变量的函数应该进行相同的操作。所有布尔变量必须带有 b 前缀。

/** 茶重量(以千克为单位) */
var float TeaWeight;

/** 茶叶子数 */
var int TeaNumber;

/** TRUE 表示茶有味道 */
var bool bDoesTeaStink;

/** 非人类可读取的茶的 FName */
var name TeaName;

/** 茶的人类可读名称 */
var String TeaName;

结构类型附加类的名称转向变量末尾:

/** 要使用的是茶的哪一个类 */
var class<Tea> TeaClass;

/** 倒茶的声音 */
var Sound TeaSound;

/** 茶的图片 */
var Texture TeaTexture;

对于 C++ 代码,默认情况下使所有类变量为 private 变量。 只有在需要与脚本(例如,在 XxxClasses.h 中生成的脚本)进行交互的情况下才使它们是 public 变量。

程序(没有返回值的函数)应该在对象后面使用一个醒目的动词。 如果方法的对象是它在其中的对象的情况例外;然后从上下文可以理解这个对象。 名称要避免包括这些以 "Handle" 和 "Process" 开头的动词,这些动词含糊不清。

Tea SomeT;
SteepTea(SomeT);    // 对象处理的方法名称类型
SomeT.Pour();       // 在 Tea 上调用的方法;动词就足够

返回某个值的函数应该会说明这个返回值;名称应该明确表示该函数会返回哪些值。 这对于布尔函数尤为重要。考虑以下两个示例方法:

function bool CheckTea(Tea t) {...} // TRUE 代表什么意思?
function bool IsTeaFresh(Tea t) {...} // 名称可以明确 TRUE 代表茶事新鲜的

声明和类名称应该以大写字母开头并使用内部大小写提高可读性。类名称应该是名词,而声明名称应该表示进行的状态(例如,形容词)。注意,绝对不可以在没有类的情况下使用状态,这样通常会有一个内含的对象。

使用您传入的所有变量。如果您没有使用一个值,请从参数列表中删除它并保留参数类型但是要注释掉这个参数:

void Update(FLOAT DeltaTime, UBOOL /*bForceUpdate*/);

虚幻脚本:通过引用传递的参数应该使用 out_ 前缀:

function PassRefs (out float out_wt, float ht, out int out_x)

Const

每当您可以在 C++ 中使用 const,请使用 const。 尤其是有关函数参数和类方法,const 是与编译器指令一样多的文档。

虚拟

在可以覆盖父代类中的虚拟函数的衍生类中声明一个虚拟函数时,您 必须 使用虚拟关键字。 尽管因为‘虚拟’行为是继承而来的,根据 C++ 标准它是可选项,但是在所有虚拟函数声明使用虚拟关键字的情况下代码会清楚得多。

执行块


大括号 { }

大括号战争是犯规的。Epic 具有一种长期使用在新的一行上放置大括号的模式。 请继续遵守这个模式。

if - else

if-else 声明中的每个执行块都应该在大括号中。这会防止编辑错误 - 在没有使用大括号时,某个人可能会无意向一个 if 块中添加另外一行。这一行将不会由 if 表达式进行控制,它将会出现问题。 在有条件地已编译项使 if/else 声明中断时情况会更糟糕。 所以通常使用大括号。

if (bHaveUnrealLicense)
{
   InsertYourGameHere();
}
else
{
   CallMarkRein();
}

一个多路 if 声明应该使用与第一个 if 相同的缩进量对每个 else if 进行缩进;这样可以使结构更清楚便于渲染:

if (TannicAcid < 10)
{
   log("Low Acid");
}
else if (TannicAcid < 100)
{
   log("Medium Acid");
}
else
{
   log("High Acid");
}

选项卡

由执行块提供的缩进代码。虚幻脚本类成员函数不需要进行缩进,因为在块中没有封入类。使用选项卡缩进代码,而不是空格。这样,每个人都可以在他们喜欢的选项卡等级查看代码。

开关声明

除了空的案例(代码相同的多个案例)外,切换案例声明应该明确标注一个案例落空到下一个案例。两个案例都包括一个中断和一个落空注释。其他代码控制转移命令(返回、继续等等)也都是正常的。

通常有一个默认案例,而且包括一个中断 - 只是以防有人在默认案例后面添加一个新的案例。

switch (condition)
{
   case 1:
      --code--;
      // 失败
   case 2:
      --code--;
      break;
   case 3:
      --code--;
      return;
   case 4:
   case 5:
      --code--;
      break;
   default:
      break;
}

defaultproperties(默认属性)

应该按顺序列出默认属性。首先是继承的变量,然后是类变量。请记住,配置和本地化的变量不可以再在 UE3 的 defaultproperties 块中进行定义。

defaultproperties
{
   // 从 Object(对象)中继承的变量
   bGraphicsHacks=TRUE

   // 从 WarmBeverage 中继承的变量
   bThirsty=FALSE

   // 类变量‏
   bTeaDrinker=TRUE
}

布尔表达式


在 C++ 中,布尔表达式通常将 TRUE 或 FALSE 作为值使用。不要使用 true 或 false,这可能会导致严重的编译器/定义问题。不要使用 0 或 1,因为它们读取起来不够清楚。

在 UnrealScript 中,您应该使用 true 和 false(小写形式)。

不要害怕创建一些局部布尔变量可以使代码的可读性更强。在下面的第二个示例中,变量名称可以使通知在什么条件下调用 DoSomething() 这件事变得很容易。

if ((Blah.BlahP.WindowExists.Etc && stuff) &&
    !(Player exist && Gamestarted && player still has pawn &&
    IsTuesday())))
{
   DoSomething();
}

应该替换为

local bool bLegalWindow;
local bool bPlayerDead;

bLegalWindow = (Blah.BlahP.WindowExists.Etc && stuff);
bPlayerDead = (Player exist && Gamestarted &&
               player still has pawn && IsTuesday());

if ( bLegalWindow && !bPlayerDead )
{
   DoSomething();
}

注意:在操作符(该操作符结束了前一行) 后面 中断表达式,然后在对应的圆括号内部对齐接下来的行。

同样,没有可以“非常简单”地提出来放入函数中的代码。 如果您在多个函数中使用的是 bLegalWindow 变量,那么将其提出放入它自己的函数中可能行得通(假定参数容易得到)。

常见典型问题:


  • 避免从一个函数中获得多个返回值 。除非它会使您的代码过于混乱,否则使用一个单独的返回声明。它会使代码更易于维护,因为跟踪执行的路径、对调试进行变更等等变得更容易。

  • 绝对不要将结构体传入到 debugf/warnf/appErrorf 函数中。 用作* FNames 和 FStrings 使它们可以使用 %s 进行打印。

  • 最小化依赖距离 。当代码依赖于具有特定值的变量时,尝试在使用它之后马上就设置该变量的值。在执行块的顶部初始化变量,而且不要将其用于一百行的代码,会提供很大空间在不需要意识到依赖性的情况下意外地更改值。让它在下一行上可以更清楚地了解为什么初始化变量要使用这种方法以及在什么情况下使用这种方法。 Berkeley 研究显示,一个声明超过 7 行的变量,从使用角度看,被正确使用的几率更高 (> 80%)。

  • 函数长度 。 函数不得再超出 60 行的长度。 这个数字 任意的,但是整个函数要适合于一页单独打印的页面。 如果您发现您的函数比这个数字长,那么考虑一下怎么样才能进行重构。 使用小型函数和内联函数避免调用的性能消耗。

  • 解决编译器警告。 编译器警告消息表示某些内容与它本应该显示的内容不同。 修复编译器所抱怨的地方。 如果您确实无法解决这个问题,那么使用 #pragma 禁止输入警告;这样t这是对上一次访问的修复。

  • 绝对不允许将 FLOAT 隐性转换成 INT。 这是一个慢操作,而且不会在所有的编译器上编译。 相反,通常使用 appTrunc() 函数转换为 INT。 这样可以确保交叉编译器功能并生成更快速的代码。

C++ 中的命名空间


  • 您可以使用命名空间适当组织您的类、函数和变量,前提是要遵守下面的规则。

  • 目前虚幻代码没有封装在全局命名空间中。您需要注意发生全局范围内的碰撞,尤其是在引入第三方代码的时候。

  • 不要在全局范围内书写 "using" 声明,即使是在 .cpp 文件中(它会使我们的“统一”编译系统产生问题。)

  • 在其他命名空间或者函数体中可以书写 "using" 声明。

  • 注意,如果您在命名空间中书写了 "using",那么它会遗留给在同一个平移单位中出现该命名空间的其他地方。不过,只要您能够保持一致,也不会出现问题。

  • 如果您遵守上面的规则,那么您只有在头文件中才可以安全地使用 "using"。

  • 注意,提前声明的赖幸需要在它们各自的命名空间中进行声明,否则将会造成链接错误。

  • 如果您在命名空间中声明了大量类/类型,那么在其他全局范围类中使用这些类型会变得很困难。(例如,函数签名出现在类声明中时将需要使用明确的命名空间。)

  • 您只可以对您的范围内的命名空间中的指定变量使用 "using"(例如,using Foo::FBar),但是我们通常在虚幻代码中不这么做。

  • 通常可以使用它封装命名空间中的枚举变量声明(C#-型范围规划)。请查看以下实例。

命名空间示例(作用域范围内的枚举变量)

/**
 * 在命名空间中定义枚举值实现 C#-型枚举范围规划
 */
namespace EColorChannel
{
    /** 将 EColorChannel::Type 声明为该枚举变量的实际类型 */
    enum Type
    {
        /** 红色通道 */
        Red,

        /** 绿色通道 */
        Green,

        /** 蓝色通道 */
        Blue
    };
}


/**
 * 给定一个颜色通道后,会返回这个颜色的名称字符串
 *
 * @param   InColorChannel   要返回名称的颜色通道
 *
 * @return  这个颜色通道的名称
 */
FString GetNameForColorChannel( const EColorChannel::Type InColorChannel )
{
    FString Name;

    switch( InColorChannel )
    {
        case EColorChannel::Red:
            Name = TEXT( "Red" );
            break;

        case EColorChannel::Green:
            Name = TEXT( "Green" );
            break;

        case EColorChannel::Blue:
            Name = TEXT( "Blue" );
            break;
    }

    return Name;
}

示例:


/**
 * 这个类会处理各种波形活动。 它可以管理波形数据,
 * 这些数据会在任何给定时间显示在任何给定游戏台上。
 * 它可以供玩家控制器调用,为给定的游戏台 开始/停止/暂停 波形。
 * 它可以供 UXboxViewport 调用使当前低频噪音状态信息
 * 将它应用于游戏台。可以通过计算在波形样本数据中定义的函数进行这项操作。<BR>
 *
 * Copyright:    Copyright (c) 2005<BR>
 * Company:      Epic Games Inc.<BR>
 */
class WaveformManager extends Object within PlayerController
   native
   transient
   poolable(1,0);

/**
 * 玩家是否已经禁用游戏台低频噪音 (TCR C5-3)。这个注释很长,所以它的确需要多行显示。
 */
var bool bAllowsForceFeedback;

/** 当前播放的波形 */
var ForceFeedbackWaveform FFWaveform;

/**  它是否由玩家控制器暂停 */
var bool bIsPaused;

/** 正在进行播放的当前波形样本 */
var int CurrentSample;

/** 自从波形开始所消耗的时间量 */
var float ElapsedTime;

/** 根据(用户可以设置范围)缩放所有波形的量 */
var float ScaleAllWaveformsBy;

/**
 * 将波形设置为供游戏台播放
 *
 * @param Waveform 要播放的波形数据
 */
simulated final function PlayWaveform(ForceFeedbackWaveform Waveform)
{
   // 清空当前样本和持续时间,如果已经暂停那么取消暂停
   CurrentSample = 0;
   ElapsedTime = 0.0;
   bIsPaused = FALSE;
   // 确保波形有效
   if (Waveform != None && Waveform.Samples.Length > 0 &&
      bAllowsForceFeedback )
   {
      // 设置要播放的波形
      FFWaveform = Waveform;
   }
   else
   {
      FFWaveform = None;
   }
}

/**
 * 通过清空波形停止波形
 */
simulated final function StopWaveform()
{
   // 删除当前波形
   FFWaveform = None;
}

/**
 * 暂停/取消暂停游戏台的波形回放
 *
 * @param bPause TRUE 会暂停,FALSE 会重新开始
 */
simulated final function PauseWaveform(optional bool bPause)
{
   // 为游戏台设置暂停的状态
   bIsPaused = bPause;
}

defaultproperties
{
   bAllowsForceFeedback=TRUE
   ScaleAllWaveformsBy=1.0
}

C++ 函数


/**
 * 为每一个子相机特效运行后期渲染并累积结果。
 * 这样做,我们可以通过添加自发光
 * 结果保存额外的全屏渲染,而且我们还可以在自发光上免费获得 2 次渲染模糊。
 *
 * @param Viewport   我们正在其中进行描画的视口
 * @param RI      用于描画的渲染界面
 */
void UUCScreenEmissiveWithBloom::PostRender(UViewport* Viewport,FRenderInterface* RI)
{
   guard(UUCScreenEmissiveWithBloom::PostRender);

   checkSlow( EmissiveCamEffect && BloomCamEffect );

   // 请确保自发光后期渲染不会将它的
   // 结果描画到后台缓冲
   EmissiveCamEffect->DisplayOutput = 0;
   EmissiveCamEffect->PostRender( Viewport, RI );

   // 将自发光相机特效中的结果设置为
   // 要使用的光溢出相机特效
   BloomCamEffect->EmissiveResultTarget =
EmissiveCamEffect->GetEmissiveResultTarget();
   BloomCamEffect->PostRender( Viewport, RI );

   unguard;
}


/**
 * 会增加附加在链接 0 中的整型变量,然后
 * 将其与附加在链接 1 中的整型变量进行比较,
 * 并根据比较结果触发脉冲。
 */
void USeqCond_Increment::Activated()
{
   check(VariableLinks.Num() >= 2 && "Decrement requires at least 2 variables");

   // 获得我们的两个整型变量
   INT Value1 = 0;
   INT Value2 = 0;
   TArray<INT*> IntValues1;
   GetIntVars( IntValues1, TEXT("Counter") );
   TArray<INT*> IntValues2;
   GetIntVars( IntValues2, TEXT("Comparison") );
   // 增加所有计数器变量
   for (INT VarIdx = 0; VarIdx < IntValues1.Num(); VarIdx++)
   {
      *(IntValues1(VarIdx)) += IncrementAmount;
      Value1 += *(IntValues1(VarIdx));
   }
   // 通过将所有链接的变量叠加起来获得实际变量
   for (INT VarIdx = 0; VarIdx < IntValues2.Num(); VarIdx++)
   {
      Value2 += *(IntValues2(VarIdx));
   }
   // 比较变量和固定输出脉冲
   OutputLinks(Value1 <= Value2 ? 0 : 1).bHasImpulse = 1;
}

C++ 类


/**
 * 所有临时数组的基础类。会对动态数组的分配进行处理并
 * 使用一种算法确定要分配多少“闲置”内存来阻止
 * 由于模式增长效率较低而进行大量的数据复制操作。
 */
class FArray
{
public:
   /**
    * 允许访问数组块
    *
    * @return 指向组成数组的内存块的指针
    */
   void* GetData()
   {
      return Data;
   }

   /**
    * 会验证指定的索引是否在数组边界内部
    *
    * @return 如果索引有效则返回 TRUE(真),否则返回 FALSE(假)
    */
   UBOOL IsValidIndex( INT i ) const
   {
      return i>=0 && i<ArrayNum;
   }

   // ...

protected:
   /** 指向代表数组的内存块的指针 */
   void*    Data;
   /** 当前在数组中的元素数 */
   INT      ArrayNum;
   /** 可以在不修改分配的控件的大小的情况下数组可以具有的元素数 */
   INT      ArrayMax;
};