UE5.7.1 源码版 UFUNCTION 参数传递踩坑:为什么 BlueprintImplementableEvent(FString) 会编译失败?

UE5.7.1 源码版 UFUNCTION 参数传递踩坑:为什么 BlueprintImplementableEvent(FString) 会编译失败?
前言最近在使用UE5.7.1 源码版开发 UMG Widget 时遇到了一个比较奇怪的问题。同样是UFUNCTION(BlueprintImplementableEvent)下面两个函数一个可以正常编译一个却直接报错。UFUNCTION(BlueprintImplementableEvent) void SwitchBackgroundStation(int32 StationIndex);正常编译。而下面这个UFUNCTION(BlueprintImplementableEvent) void SetTips(FString Tip);却报出了下面的错误ItemTip.gen.cpp(62): error C2511: void UItemTip::SetTips(const FString) UItemTip 中没有找到重载的成员函数刚开始一直以为是Intermediate 缓存没有清理UHT 没有重新生成Live Coding 导致旧代码残留BlueprintImplementableEvent 使用错误结果全部排查后都不是。最终通过查看 UHT 生成代码以及多组实验终于定位到了真正原因。一、问题复现Widget 定义如下UCLASS() class STEPEDITOR_API UItemTip : public UUserWidget { GENERATED_BODY() public: UFUNCTION(BlueprintImplementableEvent) void SetTips(FString Tip); };编译报错ItemTip.gen.cpp(62): error C2511 void UItemTip::SetTips(const FString Tip) UItemTip 中没有找到重载成员函数注意这里有一个细节自己声明的是void SetTips(FString Tip);而 UHT 生成的是void UItemTip::SetTips(const FString Tip)参数类型已经发生了变化。二、查看 UHT 生成代码打开Intermediate/Build/.../ItemTip.gen.cpp可以看到void UItemTip::SetTips(const FString Tip) { ItemTip_eventSetTips_Parms Parms; Parms.Tip Tip; UFunction* Func FindFunctionChecked(NAME_UItemTip_SetTips); ProcessEvent(Func, Parms); }这里已经明确可以看到FString被 UHT 自动转换成了const FString这也是编译失败的直接原因。三、进一步验证为了确认是不是只有 FString 有问题我又增加了几个测试函数。UFUNCTION(BlueprintImplementableEvent) void TestString(FString Str); UFUNCTION(BlueprintImplementableEvent) void TestText(FText Text); UFUNCTION(BlueprintImplementableEvent) void TestName(FName Name); UFUNCTION(BlueprintImplementableEvent) void TestArray(TArrayint32 Array);结果如下参数类型编译结果int32✅ 正常FString❌ C2511FText❌ C2511FName❌同样需要 const 引用TArray❌ C2511可以发现所有大型对象类型都会出现相同的问题。四、为什么 int32 没问题继续测试UFUNCTION(BlueprintImplementableEvent) void SwitchBackgroundStation(int32 StationIndex);完全正常。原因很简单。对于基础类型int32 float boolUHT 不会修改参数类型。生成代码仍然是void SwitchBackgroundStation(int32 StationIndex)所以不会发生签名不一致。五、BlueprintCallable 呢随后又测试了UFUNCTION(BlueprintCallable) void TestString(FString Str);编译报错LNK2019 无法解析的外部符号 UItemTip::TestString(FString)这个错误和前面的不是同一个问题。原因非常简单BlueprintCallable只是把函数暴露给 Blueprint。它仍然是一个普通 C 函数。因此必须提供 cpp 实现void UItemTip::TestString(FString Str) { }否则一定会出现 LNK2019。所以BlueprintCallable 的 LNK2019 属于正常行为BlueprintImplementableEvent 的 C2511 才是本文讨论的问题。六、解决方案把所有大型对象参数统一改成const 引用。例如UFUNCTION(BlueprintImplementableEvent) void SetTips(const FString Tip); UFUNCTION(BlueprintImplementableEvent) void TestText(const FText Text); UFUNCTION(BlueprintImplementableEvent) void TestName(const FName Name); UFUNCTION(BlueprintImplementableEvent) void TestArray(const TArrayint32 Array);修改以后即可正常编译。七、原因分析从实验结果来看可以得到下面几个结论。1、UHT 会自动优化大型对象参数对于FStringFTextFNameTArrayTMap大部分 UStructUHT 在生成代码时会采用const Type而不是值传递。例如自己写void Foo(FString Str);UHT 实际生成void Foo(const FString Str);这样可以避免 Blueprint 调用时产生一次对象拷贝。2、基础类型不会修改例如int32 float bool依旧保持值传递。因此不会出现签名问题。3、BlueprintCallable 与 BlueprintImplementableEvent 的区别BlueprintCallable属于普通 C 函数。必须自己实现。UFUNCTION(BlueprintCallable) void Foo(int32 Value);必须有void UMyClass::Foo(int32 Value) { }否则一定出现LNK2019BlueprintImplementableEvent实现由 Blueprint 完成。不需要 cpp。但是参数类型必须与 UHT 生成的一致。否则会出现C2511八、推荐写法建议以后所有 UFUNCTION 都遵循 Epic 的代码风格。基础类型int32 float bool FVector FRotator直接值传递即可。例如void Foo(int32 Value);大型对象统一使用 const 引用const FString const FText const FName const TArrayT const TMapK,V const FMyStruct例如UFUNCTION(BlueprintCallable) void SetName(const FString Name); UFUNCTION(BlueprintImplementableEvent) void OnDataLoaded(const TArrayint32 Data); UFUNCTION(BlueprintNativeEvent) void OnTextChanged(const FText Text);这样既符合 Epic 官方源码风格也能避免 UHT 自动生成参数时出现签名不一致的问题。九、最终建议对于UE5.7.1 源码版开发建议统一遵循下面的规范参数类型推荐写法int32int32floatfloatboolboolFStringconst FStringFTextconst FTextFNameconst FNameTArrayconst TArrayTTMapconst TMapK,VUStructconst FMyStruct按照这种方式编写UFUNCTION既符合 Unreal Engine 的代码规范也可以避免参数签名带来的编译问题。