news 2026/10/20 3:08:04

UE5 TSet成员函数详解:从增删查改到性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UE5 TSet成员函数详解:从增删查改到性能优化

老读者都知道,咱们这个系列讲容器的时候,我最喜欢按“底层结构 -> 常用函数 -> 踩坑现场”这个路子来。前两篇把 TArray 和 TMap 聊完之后,今天轮到 TSet。说白了,TSet 就是一个哈希集合,底层哈希表,只存元素本身,不存键值对,元素唯一,查找平均 O(1)。很多人一听“集合”就以为它是个“自动去重的数组”,真上手之后才发现:这玩意既不能用 [] 按下标取,也不保证插入顺序,甚至连 Sort 都不是直接调的。这几点你要是没提前搞明白,项目里写起来真的是一个坑接一个坑。

这篇我打算把 TSet 的常用成员函数从头到尾捋一遍:add、emplace、remove、empty、num、contains、find、Array()、sort、=赋值、[]、reserve,一个一个拆开讲清楚它们的行为、底层逻辑和适用场景,顺带把那些文档里不会写的细节也补上。无论你是刚接触 UE5 C++ 的新手,还是已经写过一段时间容器但没细抠过 TSet 的人,这篇都能拿来当参考手册用。

1. TSet 是什么,先搞清楚它和 TArray、TMap 的本质区别

1.1 哈希集合的核心语义

TSet 在 Unreal Engine 里对应的是 C++ 标准库里的 unordered_set。它的内部不是连续内存,而是一张哈希表。你往里面塞一个元素,UE 会先对这个元素算出一个哈希值,再根据哈希值决定它落在哪个桶里。

因为这个结构,TSet 天然具备三个特点:元素唯一、顺序不保证、查找极快。往同一个 TSet 里添加两个值相同的元素,第二个不会真的加进去,集合里永远只有一个。

我用一个生活化的类比来解释:TArray 就像书架上一排按顺序摆好的书,你要找某一本,得一本本看过去(线性查找);TSet 就像一堆散放在收纳箱里的球,每个球上都贴了一个标签,你想知道某个标签的球在不在箱子里,不用翻,直接按标签去对应区域摸就行。

这个“直接去对应区域摸”就是哈希查找,平均时间复杂度 O(1),而 TArray 的线性查找是 O(n)。数据量小的时候差别不明显,一旦元素数量上千上万,性能差距就非常可观了。

1.2 什么时候该用 TSet,什么时候不该用

结合我自己做项目的经验,TSet 最典型的四个使用场景是:

  • 去重:比如从配置表里读取大量 ID,重复出现多次只需要保留一份。
  • 存在性判断:比如判断一个技能是否为玩家已学习,或者判断一个关卡是否已经被通关。
  • 集合运算:求交集、并集、差集,比如筛选玩家同时拥有的道具和任务奖励道具。
  • 标记系统:比如在寻路算法里标记已经访问过的节点,或者记录本帧已经处理过的 Actor。

但如果你需要保持插入顺序、按下标随机访问、频繁按索引排序并输出,那 TSet 就不是好选择,老老实实回去用 TArray。很多人犯的错就是拿 TSet 当“能自动去重的 TArray”用,结果发现顺序乱了、下标没了,一脸懵。

举个例子,一个技能系统里,你想让玩家技能栏按学习顺序显示,那应该用 TArray 记录顺序,再用 TSet 做快速查重和存在判断,两者配合,各干各的。不要试图让一个容器包办所有事。

容器底层结构元素唯一性顺序保证随机访问查找复杂度
TArray连续数组不保证保证支持 []线性 O(n)
TSet哈希表保证唯一不保证不支持平均 O(1)
TMap哈希表键唯一不保证通过键访问平均 O(1)

2. 增删查改:最常用的成员函数逐个拆解

2.1 Add 与 Emplace:都是添加元素,差别在哪

先看 Add。TSet 的 Add 签名一般是:

TSElementType* Add(const TSElementType& Element);

注意,它返回的是元素指针,这一点比 TArray 的设计更适合做“是否新增”的判断。如果元素已经存在,Add 不会重复插入,而是返回集合中已有元素的指针。

TSet<int32> IdSet; IdSet.Add(10); IdSet.Add(10); // 不会插两次,集合里还是只有 10 int32* Found = IdSet.Add(20); // 如果之前没有 20,现在有了;如果已经有了,返回已有元素的指针

那 Emplace 呢?它长这样:

template <typename... ArgsType> TSElementType* Emplace(ArgsType&&... Args);

Emplace 的作用是把参数直接转发给元素的构造函数,在集合内部原地构造对象,省掉一次临时对象创建和拷贝/移动的过程。对于 FString、FName 这种有堆分配的类,能少一次拷贝就有一次的性能收益。

TSet<FString> NameSet; // Add 方式:先构造一个临时 FString,再拷贝或移动进集合 FString Name = TEXT("PlayerOne"); NameSet.Add(Name); // Emplace 方式:直接用字符串字面量原地构造 FString,跳过临时变量 NameSet.Emplace(TEXT("PlayerTwo"));

我个人的习惯是:插入临时值、插入字面量、插入构造参数较多的对象时,优先用 Emplace。但如果你手里已经有了一个现成的对象,而且后续还要用它做其他操作,那直接 Add 也没问题。Add 和 Emplace 返回值都是一个指针,你可以通过它访问集合中对应的元素。

还有一个容易被忽略的点:Add 和 Emplace 的插入操作可能会触发哈希表的扩容。扩容意味着重新分配内存、重新计算所有已有元素的哈希值,这是一个相对昂贵的操作。这个问题我们后面讲 Reserve 的时候会专门说。

2.2 Remove 与 Empty:删除操作没你想的那么简单

TSet 的删除函数是 Remove:

int32 Remove(const TSElementType& Element);

它按“值”删除,返回实际移除的元素个数。因为 TSet 元素唯一,这个返回值要么是 0(没找到),要么是 1(删掉了)。注意,这里不是按索引删,也没有 RemoveAt 这种函数,因为 TSet 压根没有稳定索引的概念。

TSet<int32> NumberSet; NumberSet.Add(1); NumberSet.Add(2); int32 RemovedCount = NumberSet.Remove(2); // RemovedCount 为 1 int32 RemovedAgain = NumberSet.Remove(2); // RemovedAgain 为 0,已经不存在了

我踩过一个很典型的坑:在 range-based for 循环里直接调用 Remove。比如:

for (int32 Value : NumberSet) { if (Value == 2) { NumberSet.Remove(Value); // 危险操作,迭代器已经失效 } }

这样写的问题是,Remove 会引起哈希表结构变化,当前循环的迭代器可能失效,轻则漏删,重则崩溃。正确做法是使用迭代器遍历并配合 RemoveCurrent:

for (auto It = NumberSet.CreateIterator(); It; ++It) { if (*It == 2) { It.RemoveCurrent(); } }

CreateIterator 返回的迭代器支持 RemoveCurrent,删除当前元素之后仍然可以安全地继续向下走。如果你需要做条件批量删除,推荐用这种方式。

再看 Empty。Empty 是清空 TSet 的,签名:

void Empty(int32 ExpectedNumElements = 0);

这个函数的隐藏细节在参数上。ExpectedNumElements 表示清空后希望预留的元素容量。如果你马上要往这个 Set 里再塞大量数据,你可以指定一个预期值,让底层的哈希表一次性分配好空间,避免清空后又反复扩容。

还有一个容易混淆的是 Reset 和 Empty 的区别:

  • Empty:释放全部元素,并且默认把内部桶也释放掉。
  • Reset:也是清空元素,但保留容量,后续添加元素时不需要重新分配哈希桶。

如果你要重复使用同一个 TSet 做批量数据的收集和清空,用 Reset 会更省性能。比如每帧都要往 Set 里塞当前帧可见的 Actor,帧末清空,那 Reset 就是比 Empty 更合理的选择。

2.3 Contains 与 Find:判断存在和获取元素

Contains 是最常用的存在性检查函数:

bool Contains(const TSElementType& Element) const;

它内部就是算一次哈希,然后去对应桶里比较,平均 O(1)。别在数据量大的循环里用 TArray 的 Contains(其实是线性查找),换成 TSet 会让你整个系统的帧时间明显改善。

Find 则是更进阶的查询函数:

const TSElementType* Find(const TSElementType& Element) const; TSElementType* Find(const TSElementType& Element);

返回的是元素的指针,而不是副本。如果找不到,返回 nullptr。所以判空是必须的:

TSet<FName> UnlockedLevels; UnlockedLevels.Add(TEXT("Level_01")); FName TargetLevel = TEXT("Level_02"); if (FName* FoundLevel = UnlockedLevels.Find(TargetLevel)) { // 找到了,FoundLevel 指向集合中对应的元素 } else { // 没找到 }

这里必须重点提醒一个坑:不要通过 Find 返回的指针直接修改元素内容。因为 TSet 的元素在哈希表里的位置,是根据这个元素自身的哈希值算出来的。如果你改了元素内容导致哈希值变了,哈希表里就找不到它了。轻则 Contains 判断失效,重则同一个“看起来一样”的元素能重复插入,去重彻底失效。

我见过一个线上问题:TSet 里存了自定义结构体,里面有个字段是动态刷新的,有人直接通过 Find 指针改了这个字段,结果后面 Add 另一个字段值相同的元素时,两个元素居然同时存在于集合中,因为它们的哈希值已经不同了,集合再也无法正确去重。

正确的做法是:先移除旧元素,再添加修改后的新元素。如果你需要频繁修改元素里不影响哈希的字段,那可以把这些字段排除在 GetTypeHash 计算之外,这个问题我们放到后面自定义类型那节细讲。

2.4 Num、Array()、=赋值:数量、转换和拷贝

Num() 返回集合中的元素数量,类型是 int32,签名:

int32 Num() const;

这个函数没什么好说的,O(1) 获取,几乎成了我调试日志里的常客。

Array() 这个函数比较特殊,它返回的是一个 TArray,里面包含了 TSet 的所有元素:

TArray<TSElementType> Array() const;

注意,这是拷贝,不是引用,复杂度是 O(n)。而且因为 TSet 本身无序,返回的数组顺序也是不保证的,同一次运行里每次调用结果都可能不同。

如果你只是想遍历一下,完全没必要调 Array(),直接用 range-based for 就行。Array() 真正的价值在于:把集合转换成数组之后,就能使用 TArray 那一套排序、下标访问、二分查找、按索引删除等能力。这个我们在讲排序的时候会详细展开。

= 赋值运算符,TSet 默认支持。它的行为是把另一个 TSet 的所有元素复制到当前集合:

TSet<int32> SourceSet; SourceSet.Add(1); SourceSet.Add(2); TSet<int32> TargetSet; TargetSet = SourceSet; // 复制所有元素

使用赋值时,需要注意两点。第一,TSet 的拷贝是深拷贝,元素对象会被完整复制一份,不是共享底层内存。对于包含指针成员的元素结构,你得想清楚浅拷贝和深拷贝的语义区别。第二,如果你确定不再需要源集合,用 MoveTemp 做移动赋值会比拷贝高效:

TargetSet = MoveTemp(SourceSet);

移动之后 SourceSet 处于有效但未定义的状态,通常为空,不要再对它的内容做任何假设。

关于 [] 下标操作,TSet 明确不支持。我见过不止一个从 TArray 转过来的人下意识写:

TSet<int32> MySet; MySet[0]; // 编译报错

这个必须说清楚:TSet 没有 [] 运算符。它的设计目标就是高效存在性查找,不是随机访问。如果你真的需要按顺序取第几个元素,就先调 Array() 转成数组再按索引访问。当然,这个顺序是不可靠的,只适合临时输出或一次性遍历,不能作为业务逻辑上的稳定顺序来依赖。

3. 排序、遍历、与 TArray 互转的完整姿势

3.1 TSet 本身没有 Sort,排序前先转数组

标题里写了 sort,我得先澄清一下:TSet 类本身并没有 Sort 成员函数。你不可能直接调用 MySet.Sort(),我在网上还见过有人这么写然后编译报错,以为是自己漏了头文件,实际上是根本没这个 API。

那我们要排序时怎么办?绕一步:先用 Array() 把元素拷出来,得到一个 TArray,然后对这个数组调用 Sort。

TSet<int32> ScoreSet; ScoreSet.Add(30); ScoreSet.Add(10); ScoreSet.Add(20); TArray<int32> SortedScores = ScoreSet.Array(); SortedScores.Sort(); // 默认升序

这里 TArray::Sort 的默认行为是升序,内部实现是快速排序(或类似的分治排序),不是稳定排序。如果你关心排序稳定性,可以用 Sort 的稳定版本或者额外带上索引。

TArray 支持自定义排序规则,你可以传一个二元谓词。比如对 FString 按长度从短到长排:

TSet<FString> NameSet; NameSet.Add(TEXT("LongName")); NameSet.Add(TEXT("Short")); TArray<FString> SortedNames = NameSet.Array(); SortedNames.Sort([](const FString& A, const FString& B) { return A.Len() < B.Len(); });

这个顺序跟 TSet 内部本来就有的哈希顺序没有任何关系,排序结果完全取决于你的比较函数。用场景举例,一个成就系统里,玩家完成的成就有好多,你要在 UI 上按稀有度排序展示,那就把 TSet 里的成就 ID 转成数组,再对应稀有度字段排序,逻辑链条非常清晰。

3.2 遍历 TSet 的三种方式

遍历 TSet 我常用的有三种,各有适用场景。

第一种是 range-based for,最简单,适合只读遍历:

for (const int32& Value : MySet) { // 处理 Value }

但是前面说了,这种遍历方式不能在循环里删除元素。

第二种是使用迭代器,适合需要遍历时删除或者做结构性修改:

for (auto It = MySet.CreateIterator(); It; ++It) { int32 Value = *It; if (Value < 0) { It.RemoveCurrent(); } }

迭代器的 RemoveCurrent 是安全的,如果你既想删当前元素,又想继续遍历后续元素,这是最推荐的方式。

第三种是把集合转成 TArray 再遍历:

TArray<int32> TempArray = MySet.Array(); for (int32 Value : TempArray) { MySet.Remove(Value); // 这时候删的是原集合,但遍历的是数组副本,安全 }

这种方式的本质是“先快照,再修改”。当你的删除逻辑比较复杂,或者想在遍历过程中多次调用其他修改函数,这种快照法最省心。代价是额外的一次 O(n) 拷贝,但对于一般的游戏数据规模完全可接受。

遍历的时候还要记住:TSet 的遍历顺序没有任何保证。哈希表的桶序号、元素插入历史、扩容记录都会影响遍历顺序。在单元测试或者输出日志里,不要假设 TSet 遍历结果是可预测的。如果需要稳定顺序,先排序再输出。

3.3 什么情况下能对 Array() 的结果放心用下标

拿到 TSet.Array() 返回的 TArray 之后,你就可以对它做所有 TArray 支持的操作,包括 [] 下标访问。

比如你想快速看看“某个 TSet 前 3 个元素”是什么,可以:

TArray<int32> ElementArray = MySet.Array(); if (ElementArray.Num() >= 3) { int32 First = ElementArray[0]; int32 Second = ElementArray[1]; int32 Third = ElementArray[2]; }

但这只能用于临时观测或者调试,不能作为业务逻辑里“第几个元素是哪一个”的依据。因为 TSet 的无序性,任何一次转数组的顺序都不可控,不同帧之间可能都不一样。如果你的游戏逻辑需要稳定次序,比如“玩家背包里的第 3 个道具是某某”,那就不能只用 TSet 存储数据,你需要额外引入一个 TArray 或 TMap 来维护顺序,TSet 只用来查重。

我做过一个模拟背包系统,一开始图省事只用 TSet 存道具 ID,结果做 UI 显示的时候发现每次打开背包道具顺序都随机,才意识到 TSet 根本不保证顺序。后来改成 TArray 存顺序,TSet 存唯一性校验,问题立刻解决。这个教训值得记下来。

4. Reserve 等空间管理函数与底层细节

4.1 Reserve 的作用和参数该传多少

Reserve 的签名如下:

void Reserve(int32 Number);

它的作用是预分配哈希桶的数量,让 TSet 可以容纳至少 Number 个元素而不触发扩容。当你提前知道要往里塞多少数据的时候,先 Reserve 一次,能省掉大量因为扩容而重新哈希的开销。

扩容的逻辑是这样的:TSet 内部哈希表的桶数量不是刚好等于元素数量的,它会有一个负载因子(加载因子),通常是 0.5 左右。当元素数量超过桶数乘以负载因子时,哈希表就会扩容,一般是桶数翻倍,然后所有已有元素重新计算哈希并搬到新桶里。

这一步的代价非常可观。假设你要插入 10000 个元素,如果没有 Reserve,哈希表可能经历了好几次扩容,每次扩容都是全量重新哈希,累计的开销可能让一次批量加载卡顿好几帧。如果你在批量插入前调用:

TSet<int32> EntityIdSet; EntityIdSet.Reserve(10000); for (int32 i = 0; i < 10000; ++i) { EntityIdSet.Add(i); }

那么哈希表一上来就把容量备足,整个循环里的 Add 几乎都是 O(1) 且无扩容,性能提升非常明显。

我实测过在模拟项目里批量加载一万个整数到 TSet,不 Reserve 的耗时大约是 Reserve 之后的四到五倍,尤其是在 Debug 环境下更明显。所以凡是“一次性批量加载大量数据到 Set”的场景,Reserve 几乎是必写的。

还有一种用法是在构造函数或者初始化函数里预留一个合理容量,比如某个系统运行时会频繁添加元素,但你也说不准最终数量,那可以先预留一个经验值。Reserve 传少了也只是后续多扩容几次,不会出错;传太多会浪费内存,因为桶数一旦分配就占用着。所以这个数填“你预期的大致最大元素数”最合适。

4.2 底层哈希表与自定义类型的哈希要求

TSet 之所以能做到平均 O(1) 查找,核心是哈希函数。UE 的内置类型大多已经实现了合适的 GetTypeHash,比如 int32、float、FString、FName、FVector 等等。

但如果你想往 TSet 里塞自定义结构体,就必须自己提供哈希函数和相等比较函数。TSet 默认使用 TDefaultMapHashableKeyFuncs,它内部依靠 GetTypeHash 和 operator== 来完成元素的哈希和去重判断。

看一个最小实现示例:

struct FItemData { int32 ItemId; FString ItemName; friend uint32 GetTypeHash(const FItemData& Item) { return HashCombine(GetTypeHash(Item.ItemId), GetTypeHash(Item.ItemName)); } friend bool operator==(const FItemData& A, const FItemData& B) { return A.ItemId == B.ItemId && A.ItemName == B.ItemName; } };

注意这里的 GetTypeHash 和 operator== 必须基于参与哈希计算的字段保持一致。如果两个元素 operator== 返回 true,GetTypeHash 必须返回相同的值,否则集合的去重和查找就会乱套。

我遇到过一种情况:结构体里有一个 Id 字段和一个动态变化的 Hp 字段。如果 GetTypeHash 同时包含 Id 和 Hp,那 Hp 变化后整个元素在哈希表里的位置就变了。如果我又通过 Find 拿到指针改了 Hp,这个集合就彻底坏了。正确设计是 GetTypeHash 和 operator== 都只用 Id,Hp 只是附属数据,这样 Find 之后改 Hp 不会影响哈希定位。

friend uint32 GetTypeHash(const FItemData& Item) { // 只用 Id 做哈希,Hp 可以随便改 return GetTypeHash(Item.ItemId); } friend bool operator==(const FItemData& A, const FItemData& B) { return A.ItemId == B.ItemId; }

这种设计非常实用,既能利用 TSet 去重,又不限制你修改元素里的可变字段。

4.3 空间开销:为什么 TSet 比 TArray 吃内存

TSet 底层是哈希桶数组,桶里存储元素的槽位通常还带着一些额外的控制数据,比如是否被占用的标记。同样的元素数量,TSet 的内存占用明显大于 TArray。

这就带来一个工程上的权衡:TSet 用更大的内存换来了更快的查找速度。如果某个数据集合体量非常大(几十万甚至上百万),但平时只做遍历、不需要高效查找,那用 TSet 纯属浪费内存。反过来,如果查找是这个集合的核心操作,那多消耗的内存就是合理代价。

我自己在优化一个碰撞分区的数据结构时做过对比:十万个元素,TArray 线性查找一次最坏要跑好几毫秒,TSet 平均不到零点几毫秒。内存上 TSet 大约是 TArray 的两倍出头。对于“查找”这种高频操作来说,这内存花得值。

5. 日常最容易踩的坑和最终选择建议

5.1 通过 Find 修改元素的坑(必踩)

这个坑我前面提过,但因为它太典型,值得单独再说一遍。

看下面这段代码:

TSet<FName> ProcessingIds; FName TargetId = TEXT("Char_001"); FName* IdPtr = ProcessingIds.Find(TargetId); if (IdPtr) { // 千万别尝试在这里做类似 *IdPtr = TEXT("Char_002") 的操作 }

FName 这类元素还好说,因为 FName 本身是不可变的,你没法通过指针改内容。但如果是自定义结构体,里面含有可变字段,很容易写出这种危险代码:

FItemData* Item = ItemSet.Find(TargetItemId); if (Item) { Item->Hp = -1; // 危险:如果 Hp 参与哈希计算,会造成哈希表脏数据 }

最安全的设计是:哈希相关字段尽量设计成 const 或只读,可变字段不参与哈希。如果没法避免,那就先移除再插入。这一段代码写出来不到两秒,排查这个 bug 可能要两天。

5.2 线程安全和其他注意事项

TSet 不是线程安全的。多个线程同时读写同一个 TSet,会产生未定义行为,常见的表现是崩溃或者数据错乱。如果一定要跨线程共享 TSet,需要自己加锁:

FCriticalSection SetLock; TSet<int32> SharedSet; { FScopeLock Lock(&SetLock); SharedSet.Add(NewId); }

另外有两个细节要提醒。第一,TSet 的序列化(比如存档)顺序是不稳定的,不要依赖它去生成确定性输出。如果你需要确定性数据流,先把元素排序再序列化。第二,当 TSet 里的元素是指针时,去重和查找比较的是指针值本身,也就是地址,不是指针指向的对象内容。如果你想让两个内容相同的对象被视为重复,必须存对象本身或者专门提供自定义哈希。

5.3 TArray / TSet / TMap 的选择思路

我总结一个自己的速记方式:

  • 要顺序、要下标、要频繁按索引遍历,选 TArray。
  • 只要查重、判断存在、做集合运算,选 TSet。
  • 需要键值映射、按键查值,选 TMap。

如果在开发中发现自己在 TSet 里反复做 Array() 转数组然后排序,还抱怨顺序不稳定,那大概率是数据结构选错了。正确的做法是增加一个 TArray 维护顺序,TSet 负责查重和存在性判断,各司其职。

实际开发里我见过一个挺典型的搭配:一个任务系统维护了一个 TSet 用于快速判断任务是否已接取,同时有一个 TArray 用于按接取顺序展示任务列表。新任务接取时,先查 TSet 是否已存在,不存在就 Add 进 TSet,同时 Append 到 TArray 末尾。删除任务时从两个容器里同步移除。这套组合既保证了 O(1) 的存在性判断,又保持了 UI 列表的顺序稳定。

5.4 一次性速查:常用成员函数名字和含义

最后整理一个我把常用函数放在一起的速查表,方便你写代码的时候随手翻:

函数作用注意事项
Add添加元素存在时不会重复添加,返回元素指针
Emplace原地构造添加比 Add 少一次拷贝,参数直传构造函数
Remove按值删除返回删除数量,遍历中不要直接调用
Empty清空集合可以带预留容量参数
Reset清空元素但保留容量高频复用场景比 Empty 更省性能
Num返回元素数量O(1)
Contains判断是否存在平均 O(1)
Find查找并返回元素指针找不到返回 nullptr
Array拷贝转成 TArrayO(n),顺序不保证
Sort无此函数,需先转 TArray转数组后调用 TArray::Sort
=拷贝赋值/移动赋值深拷贝,MoveTemp 可移动
[]不支持存在性判断用 Contains
Reserve预分配哈希桶批量插入前调用,参数填预期最大数量

如果你现在接手一个项目,看到里面 TSet 用得乱糟糟,最简单的整改动作就是:先按这个表把所有不合理的调用方式标出来,再逐个修正。比如把遍历删除改成迭代器 RemoveCurrent,把批量插入前补上 Reserve,把依赖顺序的业务逻辑从 TSet 迁到 TArray。做完这三步,你会发现整个系统的稳定性和性能都有肉眼可见的提升。

我在模拟项目 X 里就是按这个思路重做了技能缓存系统,原来每次上线加载技能配置都要卡一下,排查后发现一个一万多条的 TSet 没有被 Reserve,反复扩容导致耗时翻了好几倍,加上一行 Reserve 之后加载直接流畅了。这种问题在项目里不显山不露水,但真优化起来效果立竿见影。希望这篇把 TSet 成员函数的底细说清楚之后,你也能少走这些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 3:06:46

STM8S硬件I2C读写EEPROM:从软件模拟到产品级驱动实践

简介&#xff1a;面向STM8S系列嵌入式开发者的I2C读写EEPROM硬件例程包&#xff0c;针对数据存储中常见的24C02、24C04等EEPROM读写需求&#xff0c;提供一套可直接在IAR环境下编译运行的完整工程。代码基于STM8S的硬件I2C外设实现&#xff0c;不依赖软件模拟时序&#xff0c;通…

作者头像 李华
网站建设 2026/10/20 3:06:04

Claude Code实战指南:终端AI智能体与代码重构

前两天有位读者私信我&#xff0c;说看了一堆AI编程工具的安利文&#xff0c;结果代码还是得自己复制粘贴&#xff0c;问“到底有没有能真正上手干活的东西”。我的回答是&#xff1a;有&#xff0c;但你要先理解它的玩法。这里说的就是 Claude Code——一个跑在终端里的 AI 智…

作者头像 李华
网站建设 2026/10/20 3:05:34

本地离线OCR工具实战:基于C++与Qt6的桌面截图文字识别方案

别跟我提云OCR。前年做内部工具时&#xff0c;我一开始就是从云OCR入手的&#xff0c;后面被延迟、费用和数据隐私三件事联合劝退了。当时的需求很朴素&#xff1a;在桌面上随手截图&#xff0c;把屏幕里的文字变成可编辑文本&#xff0c;能复制、能搜索、能存档。可一旦图像要…

作者头像 李华
网站建设 2026/10/20 3:05:32

打印机驱动不可用怎么办?四条系统修复路径与AI助手实操指南

打印机驱动不可用这类报错&#xff0c;我一年里至少会碰上七八次。每次朋友发来截图&#xff0c;设备属性里都是一条醒目的黄色提示&#xff0c;要么打印任务卡在队列里一动不动&#xff0c;要么后台服务停了怎么点都没反应。我最早也只会重新装一遍&#xff0c;后来才发现真正…

作者头像 李华
网站建设 2026/10/20 3:04:59

强化学习入门:5×5网格世界中的策略评估与策略改进实战

如果你刚开始接触强化学习&#xff0c;大概率会在各种教材和课程里撞见同一个经典练习&#xff1a;在一个网格世界里&#xff0c;给定某个策略&#xff0c;先把它的价值函数算出来&#xff0c;再根据价值函数改进这个策略&#xff0c;循环往复直到收敛。标题里的“&#xff08;…

作者头像 李华
网站建设 2026/10/20 3:04:09

基于Springboot在线租房系统【附源码+文档】

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华