miniblink49 内置 gmock 1.6 官方 FAQ 深度解读:从模板报错到 Matcher 迁移的完整实战指南
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
本指南以仓库内
v8_6_7/testing/gmock/docs/v1_6/FrequentlyAskedQuestions.md为骨架,围绕 Google Mock(gmock)使用中最高频的 20 余个问题展开——包括 mock 方法为何不生效、自定义 Matcher 在 1.4.0 之后的 API 迁移、MOCK_METHODn宏语法设计、MSVC 编译警告、以及"新期望覆盖旧期望"等核心行为规则。全文结合 miniblink49 仓库中随 V8 6.7 一起分发的 gmock 源码(gmock-matchers.h、gmock-actions.h、gmock_doctor.py)逐条印证,读完即可独立排查 mock 失效、模板报错、期望不匹配等实战问题。
一、先说结论:这份 FAQ 在仓库里的位置与价值
在 miniblink49 仓库中,Google Mock 1.6 版本的完整文档随 V8 6.7 测试框架一起被保留在 v8_6_7/testing/gmock/docs/ 目录下,其中 FrequentlyAskedQuestions.md 汇集了 gmock 用户最常踩的坑:既有「方法没有被 mock 住」这类入门级问题,也有「自定义 Matcher 升级后无法编译」这类深度迁移问题。与它配套的 ForDummies.md、CookBook.md、CheatSheet.md 分别负责入门、进阶与速查,而本文则聚焦 FAQ 中所有问题的原因分析与可复制的解决方案。
二、方法没有被 mock 住?先检查它是不是 virtual
FAQ 的第一个问题看似初级,却是无数 mock 失效事故的根源:
当我对 mock 对象调用一个方法时,真实对象的方法被调用了。问题出在哪?
答案只有一句话:要被 mock 的方法必须是virtual的(除非你使用"高性能依赖注入"技巧来 mock 非虚方法,该技巧的细节记载在 CookBook.md 的 "Mocking Nonvirtual Methods" 一节)。
原理上,gmock 的MOCK_METHODn宏会在你的 mock 类中生成一个重写(override)基类虚函数的派生实现。如果基类方法不是虚函数,派生类的同名方法就无法参与虚函数表分派,通过基类指针/引用调用时自然落到真实实现上。因此排查顺序是:
- 确认被测代码是通过接口(基类指针/引用)调用;
- 确认接口方法声明为
virtual; - 确认 mock 类确实以
public方式继承该接口。
三、升级 gmock 后自定义 Matcher 编译失败:1.4.0 之后的 API 迁移指南
FAQ 用最大篇幅回答了这个问题:Google Mock 1.4.0 之后,为了让 Matcher 能更高效地生成信息丰富的诊断消息,Matcher 接口发生了破坏性变更——凡是直接实现MatcherInterface或使用MakePolymorphicMatcher()的自定义 Matcher 都需要迁移;而用MATCHER*宏族定义的 Matcher不受影响。
3.1 迁移规则一:Matches()改名为MatchAndExplain()
旧写法(1.4.0 之前):
using ::testing::MatcherInterface; ... class MyWonderfulMatcher : public MatcherInterface<MyType> { public: ... virtual bool Matches(MyType value) const { // Returns true if value matches. return value.GetFoo() > 5; } ... };新写法:
using ::testing::MatcherInterface; using ::testing::MatchResultListener; ... class MyWonderfulMatcher : public MatcherInterface<MyType> { public: ... virtual bool MatchAndExplain(MyType value, MatchResultListener* listener) const { // Returns true if value matches. return value.GetFoo() > 5; } ... };也就是:把Matches()重命名为MatchAndExplain(),并新增一个MatchResultListener*类型的第二参数。
3.2 迁移规则二:ExplainMatchResultTo()的逻辑并入MatchAndExplain()
如果你曾经用ExplainMatchResultTo()增强失败消息:
using ::testing::MatcherInterface; ... class MyWonderfulMatcher : public MatcherInterface<MyType> { public: ... virtual bool Matches(MyType value) const { return value.GetFoo() > 5; } virtual void ExplainMatchResultTo(MyType value, ::std::ostream* os) const { // Prints some helpful information to os to help // a user understand why value matches (or doesn't match). *os << "the Foo property is " << value.GetFoo(); } ... };迁移时,把ExplainMatchResultTo()的逻辑搬进MatchAndExplain(),原来写入::std::ostream*的内容改写入MatchResultListener:
using ::testing::MatcherInterface; using ::testing::MatchResultListener; ... class MyWonderfulMatcher : public MatcherInterface<MyType> { public: ... virtual bool MatchAndExplain(MyType value, MatchResultListener* listener) const { // Returns true if value matches. *listener << "the Foo property is " << value.GetFoo(); return value.GetFoo() > 5; } ... };3.3 迁移规则三:MakePolymorphicMatcher()的同步改造
用MakePolymorphicMatcher()定义的多态 Matcher面临同样的迁移。旧代码:
using ::testing::MakePolymorphicMatcher; ... class MyGreatMatcher { public: ... bool Matches(MyType value) const { return value.GetBar() < 42; } ... }; ... MakePolymorphicMatcher(MyGreatMatcher()) ...需要把Matches()改为MatchAndExplain()并加上MatchResultListener*参数:
using ::testing::MakePolymorphicMatcher; using ::testing::MatchResultListener; ... class MyGreatMatcher { public: ... bool MatchAndExplain(MyType value, MatchResultListener* listener) const { return value.GetBar() < 42; } ... }; ... MakePolymorphicMatcher(MyGreatMatcher()) ...若多态 Matcher 还用独立的ExplainMatchResultTo()函数(注意这里是自由函数,参数是const MyGreatMatcher&):
void ExplainMatchResultTo(const MyGreatMatcher& matcher, MyType value, ::std::ostream* os) { *os << "the Bar property is " << value.GetBar(); }同样要把逻辑收进MatchAndExplain():
class MyGreatMatcher { public: ... bool MatchAndExplain(MyType value, MatchResultListener* listener) const { *listener << "the Bar property is " << value.GetBar(); return value.GetBar() < 42; } ... };3.4 新接口在源码中的真实定义
FAQ 描述的这套接口,正是仓库 gmock-matchers.h 中的实现。MatchResultListener是一个抽象类,其<<运算符用于把解释内容写入底层流;关键点是它提供了IsInterested()方法:
// Returns true iff the listener is interested in an explanation of // the match result. A matcher's MatchAndExplain() method can use // this information to avoid generating the explanation when no one // intends to hear it. bool IsInterested() const { return stream_ != NULL; }而MatcherInterface<T>的核心纯虚函数就是新 API:
virtual bool MatchAndExplain(T x, MatchResultListener* listener) const = 0;源码注释还给出了编写MatchAndExplain()的重要约定:只有当你能提供打印值之外、非显而易见的信息时才写入解释,且解释内容应写成"非限制性定语从句"风格(如Pointee(...)生成 "which points to ...");匹配成功与否不是决定是否解释的因素(例如Not()内部在匹配成功时也可能需要打印失败信息)。这解释了为什么新 API 让 gmock 能更高效地按需生成诊断消息——正是 FAQ 所说 1.4.0 变更的初衷。
四、必须用 Google Test 吗?——与其他测试框架的集成
FAQ 明确回答:Google Mock 开箱即用地与 Google Test 配合,但很容易配置成与任何测试框架协作。做法是:在测试中不再调用TEST/TEST_F等 gtest 宏,而是把 mock 的初始化(InitGoogleMock())和期望验证逻辑(测试结束时的验证)接到你自己框架的对应钩子上。具体步骤记载在 ForDummies.md 的 "Using Google Mock with Any Testing Framework" 一节。若沿用 gtest,则直接使用仓库中随带的 gmock 即可(README.md 特别提醒:使用 Google Mock 时必须使用其捆绑版本的 Google Test)。
五、被 gcc 模板报错淹没?用 Google Mock Doctor 翻译成人话
gcc 面对 gmock 宏展开出的深模板嵌套时,报错信息可读性极差。FAQ 推荐先使用Google Mock Doctor工具:它扫描 stdin 中的 gcc 报错,输出对问题(官方称之为"疾病")的通俗诊断。
"安装"即定义一个别名(指向仓库内真实存在的脚本 gmock_doctor.py):
alias gmd='<path to googlemock>/scripts/gmock_doctor.py'使用方式是把编译输出管道给它:
<your-favorite-build-command> <your-test> 2>&1 | gmd例如:
make my_test 2>&1 | gmd也可以直接运行gmd,然后把 gcc 的报错粘贴进去。
从脚本源码看,它的内部机制是一个诊断器(Diagnoser)列表:_DIAGNOSERS包含 13 种疾病识别器,覆盖"Incomplete By Reference Argument"(按引用传参不完整)、"Mock Object Pointer"(忘了取 mock 对象指针)、"Need To Return Something"、"Wrong MOCK_METHODn Macro"(用了参数个数错误的宏)、"Wrong Parenthesis Position"(ON_CALL/EXPECT_CALL右括号位置错误,应为EXPECT_CALL(my_mock, Foo(_)).method(...)而非EXPECT_CALL(my_mock, Foo(_).method(...)))等常见错误模式。Diagnose()会先剥离 ANSI 颜色转义码,再对每条报错逐一匹配所有诊断器,输出类似[WMM - Wrong MOCK_METHODn Macro]的带标签诊断。脚本自身版本号为 1.0.3,运行时会打印 "Google Mock Doctor v1.0.3 - diagnoses problems in code using Google Mock."。
六、能不能 mock 可变参数函数?——不能直接 mock,但可以教它
FAQ 明确:无法在 gmock 中直接 mock 带省略号(...)的可变参数函数。原因很本质:mock 对象无法知道调用方传了几个参数、各是什么类型——只有基类作者才清楚协议。
因此要 mock 这类函数,用户必须教会 mock 对象如何推算参数个数与类型,一个可行的做法是提供该函数的重载版本。FAQ 同时给出工程建议:省略号参数是从 C 继承来的,并非真正的 C++ 特性,它不安全,且不适用于带构造/析构函数的参数类型,在 C++ 中应尽量避免。
七、MSVC 的 C4301 / C4373 警告:编译器自身的 bug 与正确规避
用 MSVC 2005 SP1 编译如下代码:
class Foo { ... virtual void Bar(const int i) = 0; }; class MockFoo : public Foo { ... MOCK_METHOD1(Bar, void(const int i)); };可能触发:
warning C4301: 'MockFoo::Bar': overriding virtual function only differs from 'Foo::Bar' by const/volatile qualifier这是MSVC 的 bug(同样的代码 gcc 编译毫无问题)。如果用 VS 2008 SP1,则会得到变体:
warning C4373: 'MockFoo::Bar': virtual function overrides 'Foo::Bar', previous versions of the compiler did not override when parameters only differed by const/volatile qualifiersFAQ 给出了根本解释:在 C++ 中,函数声明里的顶层const形参修饰符会被忽略,因此:
class Foo { ... virtual void Bar(int i) = 0; // int or const int? Makes no difference. };你可以用int声明Bar()、用const int定义它,编译器照样匹配。既然声明层面的const形参毫无意义,建议在Foo和MockFoo中同时去掉它,即可绕过 VC 的这个 bug。
⚠️ 注意区分:这里只讨论顶层const。当参数是指针或引用时,限定其所指对象(pointee/referee)的const依然有意义,以下两个声明并不等价:
void Bar(int* p); // Neither p nor *p is const. void Bar(const int* p); // p is not const, but *p is.八、编译超大 mock 类时 MSVC 内存耗尽:避开/clr
FAQ 观察到:使用/clr编译标志时,Visual C++ 编译 mock 类会消耗5~6 倍的内存。结论直白:编译原生 C++ mock 时避免使用/clr。如果你的工程以/clr模式构建且 mock 类庞大,可以考虑为含 mock 的源文件单独关闭/clr或用纯原生配置编译测试。
九、期望总是不满足?用--gmock_verbose=info看调用轨迹
当搞不清为什么 gmock 认为期望没被满足时,FAQ 建议用:
--gmock_verbose=info这个标志让 gmock打印接收到的每一次 mock 函数调用轨迹。通过研读轨迹,你就能发现期望不满足的真正原因(例如参数不匹配、调用次数不够、调用顺序不符等)。该标志同样用于控制输出噪音——FAQ 后面提到,调试时若觉得输出太吵,可以选择更低的 verbosity 级别。
十、断言一个函数"绝不能被调用":.Times(0)
最直接的写法:
EXPECT_CALL(foo, Bar(_)) .Times(0);FAQ 特别指出:如果一个本不该被调用的函数被调用了,而你又没写EXPECT_CALL(foo, Bar()).Times(0),gmock 会打印 "Uninteresting function call" 提示(详见下文第十四节)——这是 gmock "宁可多提示、不愿放过 bug" 的设计选择。
十一、同一个期望被报告两次失败,是冗余吗?——不是,它们代表不同时刻
当 gmock 检测到失败时,它会打印相关信息(mock 函数参数、相关期望的状态等)。若随后又检测到另一个失败,gmock 会再次打印,包括期望状态。有时两次失败之间期望状态没有变化,于是你看到相同描述出现两次。
FAQ 澄清:这两条信息并不冗余,因为它们对应的是不同的时间点;状态相同这一事实本身就是有价值的调试信息(说明该期望在这段时间内一直没有被满足)。
十二、mock 对象触发堆检查失败:先检查虚析构函数
如果使用 mock 对象时出现堆检查(heap check)失败,而用真实对象却正常,FAQ 首先追问:你 mock 的类(理想情况下是纯接口)有虚析构函数吗?
从基类派生子类时,基类析构函数必须是 virtual,否则会出事:
class Base { public: // Not virtual, but should be. ~Base() { ... } ... }; class Derived : public Base { public: ... private: std::string value_; }; ... Base* p = new Derived; ... delete p; // Surprise! ~Base() will be called, but ~Derived() will not // - value_ is leaked.把~Base()改为 virtual 后,delete p会正确调用~Derived(),堆检查器也就满意了。这条规则对"被 mock 的接口"尤其关键——如果接口没有虚析构,gmock 生成的派生类析构同样不会被调用。
十三、"新期望覆盖旧期望"让人别扭?其实是你没选对表达方式
当人们抱怨这条规则时,通常指的是这种代码:
// foo.Bar() 应被调用两次,第一次返回 1,第二次返回 2。 // 然而期望必须倒着写,太痛苦了!!! EXPECT_CALL(foo, Bar()) .WillOnce(Return(2)) .RetiresOnSaturation(); EXPECT_CALL(foo, Bar()) .WillOnce(Return(1)) .RetiresOnSaturation();FAQ 指出:问题在于没有选择最佳的意图表达方式。默认情况下期望不要求按任何特定顺序匹配;想要排序必须显式声明——这是 gmock(以及 jMock)的根本哲学:测试很容易被过度指定(over-specify),我们要让它难一点。
13.1 方式一:用InSequence把期望放入序列
// foo.Bar() 应被调用两次,第一次返回 1,第二次返回 2。 { InSequence s; EXPECT_CALL(foo, Bar()) .WillOnce(Return(1)) .RetiresOnSaturation(); EXPECT_CALL(foo, Bar()) .WillOnce(Return(2)) .RetiresOnSaturation(); }这样就能按自然顺序书写。
13.2 方式二:把动作序列放进同一条期望
// foo.Bar() 应被调用两次,第一次返回 1,第二次返回 2。 EXPECT_CALL(foo, Bar()) .WillOnce(Return(1)) .WillOnce(Return(2)) .RetiresOnSaturation();13.3 为什么是从后往前搜索?
回到本源问题:gmock 为什么从后往前搜索期望(和ON_CALL)?因为这样允许用户先为常见情况设置默认行为(例如在 mock 构造函数或测试夹具的 setup 阶段),稍后再用更具体的规则覆盖。如果从前往后搜索,这个"默认值 + 特例覆盖"的经典模式就不可能实现。这是 gmock 期望匹配顺序的设计依据。
十四、没有EXPECT_CALL却用ON_CALL设了行为,调用时为何还警告?
FAQ 的回答是:在"整洁"与"安全"之间,gmock 选择安全。人们常在 mock 构造函数或SetUp()里写ON_CALL(默认行为很少随测试变化),然后在测试体内写各不相同的EXPECT_CALL。但设置了ON_CALL不代表调用是被期望的——如果没有EXPECT_CALL而方法被调用,那可能是错误;若 gmock 悄悄放行而不通知,bug 就会在不知不觉中潜入。
如果你确认这些调用没问题,可以改用:
EXPECT_CALL(foo, Bar(_)) .WillRepeatedly(...);替代:
ON_CALL(foo, Bar(_)) .WillByDefault(...);这样 gmock 就知道你确实期望这些调用,不再打印警告。此外还可以用--gmock_verbose标志调节输出详细程度。
十五、想在 action 里删除 mock 函数的参数?自定义 action 或 Invoke 都行
当需要执行 gmock 内置 action 不支持的操作时,FAQ 给出三条路:
- 用
MakeAction()定义自己的 action; - 用
MakePolymorphicAction()定义多态 action; - 写一个 stub 函数,用
Invoke()调用它。
三条路径的详细配方见 CookBook.md 的 "Writing New Actions"、"Writing New Polymorphic Actions" 与 "Using Functions Methods Functors" 三节。
十六、MOCK_METHODn()第二个参数为什么长得那么怪?
FAQ 首先承认这是一个观点问题("What?! I think it's beautiful. :-)"),然后列出了这一语法的多个实际优势。
16.1 参数类型被括号保护,天然规避逗号歧义
尝试 mock 一个带 map 参数的函数:
virtual int GetSize(const map<int, std::string>& m);如果采用MOCK_METHODn(Method, return_type, arg_1, ..., arg_n)的提案式语法:
MOCK_METHOD1(GetSize, int, const map<int, std::string>& m);编译器会认为const map<int, std::string>& m是两个参数而不是一个(因为模板参数中的逗号),直接编译失败。虽然可以用typedef绕开,但那很碍事。gmock 的语法把参数类型保护在括号内:
// This compiles fine. MOCK_METHOD1(GetSize, int(const map<int, std::string>& m));只有当返回类型包含未受保护的逗号时才需要typedef,而这种情形罕见得多。
16.2 其他三大优势
MOCK_METHOD1(Foo, int, bool)会让读者困惑方法到底返回int还是bool,gmock 的语法则不会产生这种歧义;- gmock 描述函数类型的方式并非新发明——C 语言早已使用,
tr1的function库也大量采用;既然 tr1 将进入新版 STL,与它保持一致是稳妥的选择; - 函数类型语法同样用于 gmock 其他 API(如 action 接口),用户为了使用高级特性反正都要学,不如在
MOCK_METHOD*中统一。
十七、能 mock 静态/全局函数吗?——能,但你需要改代码
FAQ 的回答:可以,但需要做一些改动。而且它给出的工程判断很尖锐:如果你发现自己需要 mock 一个静态函数,这通常是模块耦合过紧的信号(灵活性、可复用性、可测试性都差)。更好的做法是定义一个小接口,通过该接口调用函数,这样就能轻松 mock 了——初期多花点功夫,但通常很快回本。
十八、mock 对象要做复杂的事很痛苦,gmock 很烂?
FAQ 免费回答了这个"不是问题的问题"。它区分了两种测试范式:
- 基于状态的测试(state-based testing):不用 mock,直接执行代码并断言返回正确值或系统处于期望状态;
- 基于交互的测试(interaction-based testing):mock 对象验证自己是否被以正确方式调用,并在错误一出现时就报告,从而精确定位错误触发上下文——这往往比状态测试更有效、更经济。
结论:如果你做的是状态测试,只是需要一个替身来模拟真实对象,那应该用 fake(假实现)而不是 mock。用 mock 执行复杂动作会痛苦,因为那本就不是 mock 的强项。觉得 gmock 烂,往往是用错了工具——或者正在解决错误的问题。
十九、看到 "Uninteresting function call encountered - default action taken" 要恐慌吗?
FAQ 的回答斩钉截铁:绝对不用!它只是一条 FYI(仅供参考)。
它的含义是:某个 mock 函数没有设置任何期望(按 gmock 规则,这表示你不关心该函数的调用,它可以被调用任意次数),而现在它被调用了。这没关系——你并没有说过不允许调用它!
但如果你本意是禁止该函数被调用、只是忘了写EXPECT_CALL(foo, Bar()).Times(0)呢?虽然可以争辩这是用户的责任,gmock 还是好心打印提示。所以当你看到这条消息并认为不该有"无趣调用"时,应该去调查发生了什么。为了便于排查,gmock 在遇到无趣调用时会打印函数名和参数。
二十、自定义 action:用Invoke()还是实现 action 接口?
FAQ 的回答是:两者都行,选更顺手的那一个。通常:
- 如果 action 只服务于特定函数类型,用
Invoke()更简单; - 如果 action 可用于不同类型的函数(例如
Return(value)这样的通用动作),MakePolymorphicAction()最省事; - 当你想精确控制action 能用于哪些函数类型时,实现
ActionInterface是正解。
仓库中 gmock-actions.h 里的Return()实现(class ReturnAction)就是实现ActionInterface的范例,需要时可直接参考。
二十一、SetArgPointee报 "conflicting return type specified"?
这个错误的根源是:gmock 不知道 mock 方法被调用时应返回什么值。SetArgPointee()只描述了副作用(设置指针指向的内容),却没说返回值。解决方案是把SetArgPointee()和Return()用DoAll()链接起来:
// 示例结构(完整配方见 CookBook 的 Mocking Side Effects 一节) EXPECT_CALL(foo, Bar(_)) .WillOnce(DoAll(SetArgPointee<0>(value), Return(some_value)));详细示例可查阅 CookBook.md 的 "Mocking Side Effects" 一节。
二十二、FAQ 之外:如何继续获得帮助
如果这里找不到答案,FAQ 给出的资源路径是:
- 阅读 gmock 的其他 wiki 页面(同目录下的 ForDummies.md、CookBook.md、CheatSheet.md 均已随仓库分发);
- 搜索 googlemock 邮件列表归档;
- 在 googlemock 讨论组提问。
FAQ 同时提醒:不要在 issue tracker 里提问,因为那里由极少数人低频监控。提问时应尽量提供:gmock 版本(或 SVN revision)、操作系统、编译器名称与版本、完整的编译命令行参数、完整的编译错误信息,以及实际代码(理想情况下是一个最小但完整的程序)——这正是让诊断高效的关键信息。
二十三、FAQ 速查表
| 问题 | 一句话答案 |
|---|---|
| mock 方法不生效 | 方法必须是virtual,否则无法被重写分派 |
| 自定义 Matcher 编译失败 | Matches()→MatchAndExplain(value, listener),ExplainMatchResultTo()逻辑并入其中 |
| 必须用 gtest 吗 | 不必,gmock 可配置接入任意测试框架 |
| 模板报错看不懂 | alias gmd='<gmock>/scripts/gmock_doctor.py'后make test 2>&1 \| gmd |
| 能 mock 可变参数函数吗 | 不能直接 mock,可提供重载版本并"教会"mock 对象 |
| MSVC C4301/C4373 | 去掉声明中顶层const形参即可规避 |
| 编译大 mock 类内存耗尽 | 避免用/clr编译原生 mock |
| 期望为何不满足 | 用--gmock_verbose=info打印调用轨迹 |
| 断言从不调用 | EXPECT_CALL(foo, Bar(_)).Times(0); |
| 两次相同失败报告 | 不冗余,代表不同时间点 |
| 堆检查失败 | 检查被 mock 的基类是否有 virtual 析构 |
| 新期望覆盖旧期望 | 用InSequence或单条期望串多个WillOnce |
| 未设 EXPECT_CALL 也警告 | gmock 倾向安全;确认无误可用WillRepeatedly |
| 静态/全局函数能 mock 吗 | 能,但建议改为接口注入以降低耦合 |
| "Uninteresting call" 警告 | 仅供参考;若本应禁止调用则补Times(0) |
| 自定义 action 用哪个 | 特定类型用Invoke(),通用用MakePolymorphicAction(),精确控制用ActionInterface |
| SetArgPointee 报错 | 用DoAll(SetArgPointee(...), Return(...))补齐返回值 |
这份 FAQ 连同其配套文档、gmock 源码与测试脚本一起内置于 miniblink49 仓库的v8_6_7/testing/目录中,是排查 gmock 使用问题的第一手权威资料;遇到上述任何症状,直接对照本表的解决方案即可快速定位。
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考