编码规范

本页面的内容:

简介

在Epic内部,我们遵循一些简单的编码标准和规则。书写本文的目的不是为了进行相关讨论或者将其作为一项正在进行中的工作,而是为了反映Epic 的目前使用的编码规范的状态。

编码规范对于程序员来说非常重要,原因如下:

类的组织结构

类的组织结构应该以阅读代码的人而不是以编写代码的人为中心。因为大部分阅读代码的人都要使用类的公共接口,所以在类中应该先声明公共接口,然后是类的私有实现。

版权声明

Epic发布时提供的任何源码文件 (.h, .cpp, .xaml等等) 都必须包含版权声明,将其放置在文件的第一行。版权声明的格式必须完全符合以下格式:

// 版权1998-2014 Epic Games, Inc. 版权所有。

如果没有这行声明或者格式不正确,那么CIS(持续集成系统)将会产生错误并导致集成失败。

命名规范

变量、方法及类的名称应该清晰、明确且具有描述性。名称的作用域越大,取一个符合标准的具有描述性的名称的重要性便越强。避免过度缩写。

所有变量都应该一次仅声明一个,以便可以提供有关这个变量的含义的注释。同时,这也符合JavaDocs 风格的要求。您可以在变量前面使用多行或单行注释,可以留一个空行来给变量分类。

所有返回布尔值的函数都应该询问返回值是真还是假这个问题,比如 "IsVisible()" 或 "ShouldClearBuffer()" 。所有布尔变量都必须以"b"字母为前缀(比如 "bPendingDestruction" 或 "bHasFadedIn")。

过程(没有返回值的函数)命名时应该使用一个强动词后面加一个对象。如果方法的对象正是该方法所属的对象,那么方法将根据上下情境来获得对象,这是种例外情况。命名时要避免使用"Handle"和"Process"开头,这些动词表达的意思模糊不清。

如果一个参数是通过引用传入函数且该函数要向此参数写入值,那么我们鼓励您使用 "Out" 作为函数参数名称的前缀,但这不是强制要求。这样便显而易见地表示出传入到该参数中的值会被该函数所代替。

对于能够返回值的函数的名称,应该描述出该返回值的意思;名称应该可以清楚地表达出要返回的是什么值。这对于布尔函数尤为重要。考虑以下两个示例方法:

    bool CheckTea(FTea Tea) {...} // true 代表什么意思?
    bool IsTeaFresh(FTea Tea) {...} // 名称可以明确表示true代表茶是新鲜的

示例

    /** 茶重量(单位:千克)*/
    float TeaWeight;

    /** 茶叶数量 */
    int32 TeaCount;

    /** true 表示茶叶很香 */
    bool bDoesTeaStink;

    /** 茶叶的非人类可读 FName */
    FName TeaName;

    /** 茶叶的人类可读名称 */
    FString TeaFriendlyName;

    /** 要使用的是茶叶的哪一个类 */
    UClass* TeaClass;

    /** 倒茶的声音 */
    USoundCue* TeaSound;

    /** 茶叶的图片 */
    UTexture* TeaTexture;

基本C++数据类型的可移植别名

请不要在可移植代码中使用C++整型,因为需要根据编译器决定这种数据类型的大小。

注释

注释是一种信息交流;而这种信息交流 至关重要 。关于注释,需要注意以下几点(节选自 Kernighan & Pike 的 编程实践 ):

指南

示例格式

我们使用基于JavaDoc 的系统来自动地从代码中提取注释并构建成文档,所以这就要求遵循一些特定的注释格式规则。

以下示例展示了类、状态、方法和变量的注释格式。请记住注释应该是辅助加强代码的。代码是功能实现,注释表明了代码的目的。请确保在您更改一段代码意图时更新注释。

注意,正如下面Steep和Sweeten方法所呈现的,我们支持两种不同的参数注释风格。Steep所应用的@param风格是传统风格,但对于简单的函数来说,把参数介绍集成到函数的描述性注释中会使其看上去更加清晰,正如Sweeten 中的注释所示。

和UE3编码规范不同,UE4中应该仅包含一次方法注释,即在公开声明方法的地方提供该注释。方法注释应该仅包含和方法的函数调用相关的信息,包括可能和该函数调用相关的方法重载的任何信息。关于那些和函数调用无关的方法及其重载方法的实现细节,应该在方法实现内部进行注释。

    /** 可饮用对象的接口。*/
    class IDrinkable
    {
    public:

        /**
         * 当玩家饮用该对象时调用。
         * @param OutFocusMultiplier - 返回时,将包含乘数以应用于饮用者。
         * @param OutThirstQuenchingFraction - 返回时,将包含饮用者的口渴值的分数值归0 (0-1)。
         */
        virtual void Drink(float& OutFocusMultiplier,float& OutThirstQuenchingFraction) = 0;
    };

    /** 一杯茶 (茶) */
    class FTea : public IDrinkable
    {
    public:

        /**
         * 根据浸泡茶的水的体积和水温来计算茶叶的变换口味值。
         * @param VolumeOfWater - 以毫升计算的用于酿造的水量
         * @param TemperatureOfWater - 以开氏度计算的水温
         * @param OutNewPotency - 在浸泡开始后的茶叶效能,从0.97到1.04
         * @return    会返回每分钟茶叶口味单位值 (TTU) 中茶叶浓度的改变
         */
        float Steep(
            float VolumeOfWater,
            float TemperatureOfWater,
            float& OutNewPotency
            );

        /** 对茶叶添加甜味剂,根据产生相同甜度的蔗糖的量来作为数量,以克计算。*/
        void Sweeten(float EquivalentGramsOfSucrose);

        /**在日本出售的茶叶的价格,以日元为单位。*/
        float GetPrice() const
        {
            return Price;
        }

        // IDrinkable接口
        virtual void Drink(float& OutFocusMultiplier,float& OutThirstQuenchingFraction);

    private:

        /** 以日元计算的茶叶的价值。*/
        float Price;

        /** 茶叶的甜味,根据产生相同甜度的蔗糖的量来作为数量,以克计算。*/
        float Sweetness;
    };

    float FTea::Steep(float VolumeOfWater,float TemperatureOfWater,float& OutNewPotency)
    {
        ...
    }

    void FTea::Sweeten(float EquivalentGramsOfSucrose)
    {
        ...
    }

    void FTea::Drink(float& OutFocusMultiplier,float& OutThirstQuenchingFraction)
    {
        ...
    }

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

方法注释的所有组成部分的意思?

C++ 11和现代语法

虚幻引擎可大量移植到许多C++编译器中,所以我们对于与支持的编译器相兼容功能的使用非常谨慎。有时一些功能相当好用,我们会把它们放在宏中并大量使用它们,但一般我们会等到对所有可能的编译器的支持达到最新标准才开始使用。

我们正在使用特定的对现代编译器进行良好支持的C++ 11语言功能,诸如"auto"关键字,range-based-for以及 lambdas。在一些情况下,我们能够以预处理器条件句的形式对这些功能进行打包(诸如容器中的rvalue引用)。但是,对一些特定的语法功能,我们可能需要尽力避免,除非我们确信这个语法在新平台上可以被识别。

除非是下方特别指出的受支持的现代C++编译器功能,否则您应尽量不使用编译器的特定语法功能,除非它们被封装在预处理器宏或条件句中,并且被保守使用。

原有宏的新关键字

原有的宏'checkAtCompileTime', 'OVERRIDE', 'FINAL'和'NULL'现在可以替换为'static_assert', 'override', 'final' 和 'nullptr'。一些宏可能仍保留定义,因为它们的使用仍较为广泛。

其中的一个例外情况是C++/CX版本中的nullptr(例如Xbox One)实际上是管理的无效引用类型。除了类型和一些模板实例关联外,它与原生C++的nullptr最为兼容,因此出于兼容性的考量,您应该使用TYPE_OF_NULLPTR宏而不是更为常见的decltype(nullptr)。

'auto'关键字

所有的编译器虚幻引擎4对象都支持'auto'关键字,我们建议您在您代码中的合适位置使用。

正如您使用类型名称那样,您可以且应该使用带auto的常量或* 。使用auto关键字将会强制将推测类型变为您想要使用的类型。

我们建议将auto关键字用于迭代器循环(消除样板) - 或更佳: ranged-based for loops - 以及在您初始化变量到新建实例时(消除多余的类型名称)。一些使用更为持续,但您可以随意使用,并且我们也会不断学习并改进其应用。

技巧: 如果您将鼠标悬停于Visual Studio中的变量上,一般它会告知您推测类型。

Range Based For

可以在所有的引擎和编辑器代码中使用,并且我们建议将其用于更容易了解和维护代码的位置。在迁移使用原有TMap迭代器的代码时,请注意原有的作为迭代器类型方式的Key()和Value()函数现在成为底层键值TPair的简单键值域:

    TMap<FString, int32> MyMap;

    // 原有样式
    for (auto It = MyMap.CreateIterator(); It; ++It)
    {
        UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
    }

    // 新建样式
    for (auto& Kvp : MyMap)
    {
        UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), Kvp.Key, *Kvp.Value);
    }

对于单独的迭代器类型,我们也有范围替换:

    // 原有样式
    for (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
    {
        UProperty* Property = *PropertyIt;
        UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
    }

    // 新建样式
    for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
    {
        UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
    }

Lambdas以及匿名函数

现在允许使用Lambdas,但是对于捕获堆栈变量的状态lambdas的使用我们要谨慎 --目前我们仍在学习哪些使用是恰当的。另外,状态lambdas无法指派给我们使用很多的函数指针。

Lambdas的最佳用法是与通用算法中的断言一起使用,举例来说:

    // 搜寻首个名称包含文字"Hello"的对象
    Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT("Hello")); });

    // 以名称的反向来对数组排序
    AnotherArray.Sort([](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });

我们期望在未来建立最佳实践之后来更新本文。

强类型枚举

枚举类受所有编译器的支持,并且我们推荐将其替换原有类型的命名空间枚举,比如常规枚举和UENUMs。比如:

    // 原有枚举
    UENUM()
    namespace EThing
    {
        enum Type
        {
            Thing1,
            Thing2
        };
    }

    // 新建枚举
    UENUM()
    enum class EThing : uint8
    {
        Thing1,
        Thing2
    };

只要它们是基于uint8,它们也被作为UPROPERTYs而受到支持 - 它替换了原有的TEnumAsByte<>解决方案:

    // 原有属性
    UPROPERTY()
    TEnumAsByte<EThing::Type> MyProperty;

    // 新建属性
    UPROPERTY()
    EThing MyProperty;

作为标识使用的枚举类可以通过新建ENUM_CLASS_FLAGS(EnumType)宏来自动定义所有的按位操作符:

    enum class EFlags
    {
        Flag1 = 0x01,
        Flag2 = 0x02,
        Flag3 = 0x04
    };

    ENUM_CLASS_FLAGS(EFlags)

其中的例外情况是在'truth'关联中的标识使用 - 这是对语法的限制,并且可以使用'double bang' 操作符来变通处理:

    // 原有
    if (Flags & EFlags::Flag1)

    // 新增
    if (!!(Flags & EFlags::Flag1))

Move语句

所有的主容器类型- TArray, TMap, TSet, FString -都具有move构造函数以及move赋值运算符。在根据值来传递/返回这些类型时,它们经常会被自动使用,但可以通过使用MoveTemp来明确调用,MoveTemp等同于虚幻引擎4的std::move。

通过值来返回容器或字符串有助于表达而不会使用通常的临时拷贝。值传递以及对MoveTemp的使用的规则仍在建立中,但已经可以在代码库的一些最优化区域中找到。

第三方代码

任何时候当您修改引擎中我们使用的库的代码时,请一定要使用带//@UE4的注释标记该变更,并解释您进行该修改的原因。这样可以更加容易地将变更合并到这个库的新版本中,并且使授权用户可以轻松地找到我们已经进行的任何修改。

引擎中包含的任何第三方代码都应该使用格式化的注释进行标记,以便可以轻松地进行查找。比如:

    // @third party code - BEGIN PhysX
    #include <PhysX.h>
    // @third party code - END PhysX

    // @third party code - BEGIN MSDN SetThreadName
    // [http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx]
    // Used to set the thread name in the debugger
    ...
    //@third party code - END MSDN SetThreadName

代码格式

大括号 { }

大括号冲突是不符合规则的。Epic长期使用的一种模式是将大括号单独放在新的一行上。请继续遵守这个规则。

If - Else

if-else 语句的每个代码执行块都应该位于大括号内。这是为了防止编辑错误 - 如果没有使用大括号,某人可能会不经意地向if代码块中添加另一行代码。而这行代码并不会受到if表达式的控制,这是非常糟糕的。更糟糕的是,当条件化地编译各项时会导致if/else语句中断。所以一定要使用大括号。

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

多重if语句在缩进时应该让每个else if语句的缩进和首个if语句对齐,这样对读者来说结构更加清晰。

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

Tabs(制表符)

Switch 语句

除了空的case(具有相同代码的多个case)外,swittch case 语句应该明确地标注出某个case运行到下一个case。在每个case中或者包含一个break语句或者包含一个向下执行的注释。也可以使用其他的代码控制变换命令(return、continue等)。

通常都有一个default case,它包含一个break语句 - 这样可以防止某人在default语句后再添加新的case。

    switch (condition)
    {
        case 1:
            ...
            // falls through
        case 2:
            ...
            break;
        case 3:
            ...
            return;
        case 4:
        case 5:
            ...
            break;
        default:
            break;
    }

Namespaces(命名空间)

您可以在适当的地方使用命名空间来组织您的类、函数、变量,只要遵循以下规则即可。

C++ 枚举值(命名空间作用域)

物理依赖

一般风格问题

注意,由于最近才添加OVERRIDE 关键字,所以有很多现有代码还不符合这条规则,在任何方便的时候应该向该代码中添加OVERRIDE关键字。