【UE5 C++课程系列笔记】15——Assert的基本使用
目录
概念
一、Check
二、Verify
三、Ensure
对比
基本使用
一、check的基本使用
二、ensure的基本使用
三、verify的基本使用
概念
assert
可在开发期间帮助检测和诊断不正常或无效的运行时条件。这些条件通常检查是否指针为非空、除数为非零、函数并非递归运行,或代码要求的其他重要假设。但每次检查会使得效率十分低下。某些情况下,assert
会在延迟崩溃发生之前发现导致该崩溃的bug,例如删除未来tick所需的对象,协助开发人员发现引起崩溃的根本原因。assert
的关键特性之一是不存在于发布代码中,这意味着不但不会影响发布产品的性能,也没有任何副作用。对 assert
最简单的理解就是:"断言"必须一律为true,否则程序会停止运行。
虚幻引擎提供 assert
等同项的三个不同族系:check
、verify
和 ensure
。若要检查这些功能背后的代码,可在 Engine/Source/Runtime/Core/Public/Misc/AssertionMacros.h
中找到相关的宏。各个功能的行为略有不同,但它们都是开发期间使用的诊断工具,目标大致相同。
一、Check
Check族系最接近基础 assert
,因为当第一个参数得出的值为false时,此族系的成员会停止执行,且默认不会在发布版本中运行。以下Check宏可用:
宏 | 参数 | 行为 |
---|---|---|
check 或 checkSlow | Expression | 若 Expression 为false,停止执行 |
checkf 或 checkfSlow | Expression 、FormattedText 、... | 若 Expression 为false,则停止执行并将 FormattedText 输出到日志 |
checkCode | Code | 在运行一次的do-while循环结构中执行 Code ;主要用于准备另一个Check所需的信息 |
checkNoEntry | (无) | 若此行被hit,则停止执行,类似于 check(false) ,但主要用于应不可到达的代码路径 |
checkNoReentry | (无) | 若此行被hit超过一次,则停止执行 |
checkNoRecursion | (无) | 若此行被hit超过一次而未离开作用域,则停止执行 |
unimplemented | (无) | 若此行被hit,则停止执行,类似于 check(false) ,但主要用于应被覆盖而不会被调用的虚拟函数 |
Check宏在调试(Debug)、开发(Development)、测试(Test)和发布编辑器(Shipping Editor)版本中运行(以"Slow"结尾的宏除外,其仅在调试(Debug)版本中运行)。定义 USE_CHECKS_IN_SHIPPING
以保留一个true值(通常为 1
),使Check宏可在所有版本中运行。此法在以下情况中十分实用:怀疑Check宏中的代码正在修改值;发现了仅存在于在发布版本中且难以追踪的bug,但认为现有Check宏能找到这些bug。项目发布时应将 USE_CHECKS_IN_SHIPPING
设为默认值 0
。
二、Verify
在大部分版本中,Verify族系的行为与Check族系相同。但即便在禁用Check宏的版本中,Verify宏也会计算其表达式的值。这意味着仅当该表达式需要独立于诊断检查之外运行时,才应使用Verify宏。举例而言,若某个函数执行操作,然后返回 bool
来说明该操作是否成功,则应使用Verify而非Check来确保该操作成功。因为在发布版本中Verify将忽略返回值,但仍将执行操作。而Check在发布版本中根本不调用该函数,所以行为才会有所不同。
宏 | 参数 | 行为 |
---|---|---|
verify 或 verifySlow | Expression | 若 Expression 为false,停止执行 |
verify 或 verifyfSlow | Expression 、FormattedText 、... | 若 Expression 为false,则停止执行并将 FormattedText 输出到日志 |
Verify宏在调试(Debug)、开发(Development)、测试(Test)和发布编辑器(Shipping Editor)版本中完整运行(以"Slow"结尾的宏除外,其仅在调试(Debug)版本中运行)。定义 USE_CHECKS_IN_SHIPPING
来保留一个true值(通常为 1
),从而覆盖此行为。在所有其他情况下,Verify宏将计算其表达式,但不会停止执行或将文本输出到日志。
三、Ensure
Ensure族系类似于Verify族系,但可在出现非致命错误时使用。这意味着,若Ensure宏的表达式计算得出的值为false,引擎将通知崩溃报告器,但仍会继续运行。为避免崩溃报告器收到太多通知,Ensure宏在每次引擎或编辑器会话中仅报告一次。若实际情况需要Ensure宏在每次表达式计算得值为false时都报告一次,则使用"Always"版本的宏。
宏 | 参数 | 行为 |
---|---|---|
ensure | Expression | Expression 首次为false时通知崩溃报告器 |
ensureMsgf | Expression 、FormattedText 、... | Expression 首次为false时通知崩溃报告器并将 FormattedText 输出到日志 |
ensureAlways | Expression | Expression 为false时通知崩溃报告器 |
ensureAlwaysMsgf | Expression , FormattedText , ... | Expression 为false时通知崩溃报告器并将 FormattedText 输出到日志 |
Ensure宏在所有版本中计算其表达式的值,但仅在调试(Debug)、开发(Development)、测试(Test)和发布编辑器(Shipping Editor)版本中联系崩溃报告器。
对比
宏名称 | 调试版本行为 | 发行版本行为 | 侧重点及适用场景 |
---|---|---|---|
check | 条件为 false 时触发断言失败,程序暂停,输出详细错误信息,方便开发者定位问题 | 通常被移除,不进行条件检查,不影响最终产品性能 | 主要用于开发调试阶段,验证代码逻辑假设条件,查找潜在的代码错误 |
verify | 同 check ,条件为 false 时触发断言失败,程序暂停,输出错误信息 | 保留条件检查,触发断言失败后处理方式与调试版本不同(通常不会暂停程序),需开发者提前规划错误处理机制 | 侧重于对关键条件在发行版本中也进行持续监控,保障游戏稳定性,适用于如资源加载、关键配置参数验证等重要情况 |
ensure | 条件为 false 时触发断言失败,尝试采取相对 “温和” 的措施处理错误,如提示信息、尝试恢复操作等,程序不一定暂停 | 类似调试版本,会尝试采取合适的措施应对错误情况,注重用户体验和游戏的可恢复性 | 从用户体验和程序健壮性角度出发,适用于可能出现异常但希望游戏尽量 “容错” 继续运行的情况,如网络通信、外部设备连接等易受影响的功能模块相关验证 |
基本使用
一、check的基本使用
1. 新建一个Actor类,这里命名为“AssertActor”。在“AssertActor.h”中定义一个名为“AttackEnemey”的方法,传入一个Actor类型的引用
再定义一个属性值
在“AssertActor.cpp”中实现“AttackEnemey”函数
2. 创建派生自“AssertActor”的蓝图类“BP_AssertActor”
将“BP_AssertActor”拖入视口
3. 在关卡蓝图中通过按键调用“AttackEnemey”函数
如果“AttackEnemey”函数传入了一个空指针,如下图。此时调用“AttackEnemey”函数,编辑器会崩溃
二、ensure的基本使用
在“AssertActor”中新实现一个“AttackEnemey2”函数,如下图。这里使用“ensure”
在关卡蓝图中通过1键调用“AttackEnemy2”函数
运行后,按下1键,可以看到虽然还是会报错,但是当Continue后编辑器并不会崩溃
三、verify的基本使用
在“AssertActor”中定义修改法力值的函数“ModifyMana”,定义验证法力值的函数“VerifyMana”,设置法力值“Mana”初始大小为50
实现函数“ModifyMana”和“VerifyMana”如下。每次调用“ModifyMana”时将法力值减20。每次调用“VerifyMana”时先调用“ModifyMana”,然后判断“ModifyMana”的返回结果是否大于0
在关卡蓝图中调用“VerifyMana”
调用3次“VerifyMana”后会出现如下错误,但是Continue后还是可以继续执行后续逻辑,不会造成编辑器崩溃。
“AssertActor”完整代码:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AssertActor.generated.h"
UCLASS()
class STUDY_API AAssertActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AAssertActor();
UFUNCTION(BlueprintCallable)
void AttackEnemey(AActor* InActor);
UFUNCTION(BlueprintCallable)
void AttackEnemey2(AActor* InActor);
UFUNCTION(BlueprintCallable)
void VerifyMana();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
int32 ModifyMana();
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
protected:
int32 Health = 100;
int32 Mana = 50;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Assert/AssertActor.h"
// Sets default values
AAssertActor::AAssertActor()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
void AAssertActor::AttackEnemey(AActor* InActor)
{
check(InActor != nullptr);
AAssertActor* MyAssertActor = CastChecked<AAssertActor>(InActor);
MyAssertActor->Health -= 10;
}
void AAssertActor::AttackEnemey2(AActor* InActor)
{
ensure(InActor != nullptr);
AAssertActor* MyAssertActor = Cast<AAssertActor>(InActor);
if (MyAssertActor)
{
MyAssertActor->Health -= 10;
}
else
{
UE_LOG(LogTemp, Error, TEXT("转型失败了!!!"))
}
}
void AAssertActor::VerifyMana()
{
verify(ModifyMana() > 0);
}
// Called when the game starts or when spawned
void AAssertActor::BeginPlay()
{
Super::BeginPlay();
}
int32 AAssertActor::ModifyMana()
{
Mana -= 20;
return Mana;
}
// Called every frame
void AAssertActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
官方文档地址:
https://dev.epicgames.com/documentation/zh-cn/unreal-engine/asserts-in-unreal-engine?application_version=5.3