ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

lo 集合运算全解析:Intersect 类别下 21 个交集、并集、差集与包含判定 Helper 的使用与源码实现

lo 集合运算全解析:Intersect 类别下 21 个交集、并集、差集与包含判定 Helper 的使用与源码实现 lo 集合运算全解析Intersect 类别下 21 个交集、并集、差集与包含判定 Helper 的使用与源码实现【免费下载链接】lo A Lodash-style Go library based on Go 1.18 Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lolo 是一套基于 Go 1.18 泛型、风格对标 Lodash 的工具库其核心包中的intersect子类别集中了所有与集合关系相关的 Helper从单元素的包含判断到子集、交集、并集、差集再到排除、去重与集合等价比较。本文以 core/intersect.md 文档页为骨架结合 intersect.go 的完整源码与 intersect_test.go 的测试用例逐函数讲解用法、边界语义与底层实现帮助你精确选择最合适的 Helper 并理解其性能取舍。一、Intersect 帮助函数总览在 lo 的文档站点中core/intersect.md 是核心包core下intersect子类别的索引页。该页面本身是一个聚合页通过HelperList categorycore subCategoryintersect /组件动态渲染 docs/data 目录下所有标记了category: core、subCategory: intersect的 Helper 卡片渲染逻辑见 HelperList.tsx 与 HelperCard.tsx。全部 21 个 Helper 按文档中的排列顺序可分为五组分组函数位置position存在性判断Contains、ContainsBy0、10子集/部分满足Every、EveryBy、Some、SomeBy、None、NoneBy20~70交、并、差Intersect、IntersectBy、Difference、Union、UnionBy、UnionByErr80~111排除与删除Without、WithoutBy、WithoutByErr、WithoutEmpty、WithoutNth110~140集合等价比较ElementsMatch、ElementsMatchBy150、160所有实现均位于仓库根目录的 intersect.go 中。在 Go 代码里这些函数直接属于lo包使用时通过lo.前缀调用例如lo.Intersect(...)、lo.Union(...)。二、存在性判断Contains 与 ContainsByContains是最基础的存在性判断返回元素是否出现在集合中。present : lo.Contains([]int{0, 1, 2, 3, 4, 5}, 5) // true函数签名与实现都非常直观intersect.gofunc ContainsT comparable bool { for i : range collection { if collection[i] element { return true } } return false }注意类型约束是comparable即元素必须是可比较类型基本类型、指针、可比较的结构体等因此无法直接用于函数、切片、map 等不可比较类型。ContainsBy则是把相等判断替换为谓词函数从而突破comparable的限制可以自定义匹配规则exists : lo.ContainsBy( []int{0, 1, 2, 3}, func(x int) bool { return x 3 }, ) // true其实现intersect.go对集合做线性扫描只要谓词对任一元素返回true即提前返回func ContainsByT any bool) bool { for i : range collection { if predicate(collection[i]) { return true } } return false }ContainsBy的类型约束放宽为T any因此也可以作用于结构体切片、指针切片等场景。对应测试见 intersect_test.go。三、子集与部分满足判定Every / Some / None 及其 By 变体这一组函数回答子集中的元素与集合的关系全部在Every、至少一个在Some、一个都不在None。它们都接收(collection, subset)两个切片参数并有对应的谓词版*By。Every子集是否全部包含Every返回true当且仅当subset的所有元素都出现在collection中若subset为空返回true数学上空集是任何集合的子集。ok : lo.Every([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}) // trueEveryBy则是集合内所有元素都满足谓词若集合为空也返回trueok : lo.EveryBy( []int{1, 2, 3, 4}, func(x int) bool { return x 5 }, ) // trueSome是否至少有一个命中Some返回true当且仅当subset中至少有一个元素出现在collection中subset为空时返回falseok : lo.Some([]int{0, 1, 2, 3, 4, 5}, []int{0, 6}) // trueSomeBy返回true当集合中至少一个元素满足谓词集合为空时返回falseok : lo.SomeBy( []int{1, 2, 3, 4}, func(x int) bool { return x 3 }, ) // trueNone是否一个都不命中None返回true当subset中没有任何元素出现在collection中subset为空时返回trueok : lo.None([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) // trueNoneBy返回true当集合中没有任何元素满足谓词集合为空时返回trueok : lo.NoneBy( []int{1, 2, 3, 4}, func(x int) bool { return x 0 }, ) // true注意这三组函数在空输入上的语义差异是常见的边界陷阱Every(空子集)true、Some(空子集)false、None(空子集)true。源码实现小数据线性扫描、大数据哈希集合Some与None的实现intersect.go将subset通过Keyify构建哈希集合再线性扫描collection查询整体为 O(nm)。Every则藏着一个性能优化intersect.go当subset长度不超过 8常量everySmallSubset 8时走无分配的嵌套线性扫描everySmall超过 8 才构建collection的哈希集合走everyLarge。源码注释解释了原因对少量子集元素map 分配和对通常大得多的集合做哈希的开销反而高于几次线性比较。func EveryT comparable bool { if len(subset) 0 { return true } if len(subset) everySmallSubset { return everySmall(collection, subset) } return everyLarge(collection, subset) }实现中的边界语义这几个函数使用相等性作为判定因此在处理浮点数时与 map 键语义一致NaN永不相等因此NaN永远不会被匹配到。这是与 map 路径保持一致的有意设计注释见 intersect.go。四、交、并、差Intersect / Union / Difference 及其变体这是intersect子类别中名字最直接的一组也是文档页核心主题在两个或多个集合之间寻找相似元素的直接体现。Intersect多集合交集Intersect返回所有传入切片中共同出现的元素去重后的交集lo.Intersect([]int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0}) // []int{3}签名是func Intersect[T comparable, Slice ~[]T](lists ...Slice) Sliceintersect.go。两个值得注意的点泛型参数Slice ~[]T允许传入命名切片类型如type myStrings []string并原样返回该类型测试用例 type preserved 专门验证了这一点intersect_test.go传入 0 个切片时返回空切片传入 1 个切片时等价于对自身做去重Intersect([]int{0,1,1})返回[]int{0,1}见 intersect_test.go。IntersectBy基于键函数的交集当需要按自定义键而非元素本身求交集时使用IntersectBy例如两个结构体切片按ID求交集transform : func(v int) string { return strconv.Itoa(v) } lo.IntersectBy(transform, []int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0}) // []int{3}签名是func IntersectBy[T any, K comparable, Slice ~[]T](transform func(T) K, lists ...Slice) Sliceintersect.go。它先用transform把每个元素投影为可比较键K再按键做交集返回的是第一个切片中命中的原元素。Difference两个集合的差集Difference返回两个切片之间的差异返回值是二元组第一个切片出现在list1但不在list2中的元素第二个切片出现在list2但不在list1中的元素。left, right : lo.Difference([]int{0, 1, 2, 3, 4, 5}, []int{0, 2, 6}) // []int{1, 3, 4, 5}, []int{6}实现intersect.go同样做了双路径优化两侧长度都不超过 8differenceSmallThreshold 8时走无分配的嵌套扫描differenceSmall否则用Keyify构建两个哈希集合走differenceLarge。Union多集合并集保序去重Union返回所有传入切片的去重并集且保持元素相对顺序不变lo.Union([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}, []int{0, 10}) // []int{0, 1, 2, 3, 4, 5, 10}实现intersect.go按元素总数区分两条路径总元素数不超过 8unionSmallThreshold 8时通过扫描已构建的 result 去重unionSmall避免维护 seen 集合超过 8 时用map[T]struct{}去重unionLarge总体为 O(总元素数)。UnionBy / UnionByErr按键去重的并集UnionBy用 iteratee 提取的键做去重结果值取首次出现的原元素lo.UnionBy(func(i int) int { return i / 2 }, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}) // []int{0, 2, 4, 10} lo.UnionBy(func(s string) string { return s[:1] }, []string{foo, bar}, []string{baz}) // []string{foo, baz}UnionByErr允许 iteratee 返回错误一旦出错立即终止并返回第一个错误intersect.golo.UnionByErr(func(i int) (int, error) { if i 42 { return 0, errors.New(invalid value) } return i / 2, nil }, []int{0, 1, 2}, []int{42}) // []int{0, 1}, error(invalid value)对应测试见 intersect_test.go。五、排除与删除Without 家族这组函数用于从集合中剔除指定元素与交并差配合可以完成更灵活的集合操作。Without按值排除Without返回排除了指定值后的新切片原切片不变lo.Without([]int{0, 2, 10}, 2) // []int{0, 10}签名是func Without[T comparable, Slice ~[]T](collection Slice, exclude ...T) Slice。实现intersect.go同样做了小排除列表优化排除项不超过 4 个withoutSmallExcludeThreshold 4时走线性扫描withoutSmall否则构建哈希集合withoutLarge。源码注释指出变参Without的调用绝大多数只传 1~4 个排除值此时 map 分配与哈希开销高于几次比较。WithoutBy / WithoutByErr按键排除WithoutBy按 iteratee 提取的键排除元素非常适合按结构体字段如ID过滤type User struct { ID int Name string } users : []User{ {1, Alice}, {2, Bob}, {3, Charlie}, } filtered : lo.WithoutBy(users, func(u User) int { return u.ID }, 2, 3) // []User{{1, Alice}}WithoutByErr增加错误传播能力iteratee 出错时立即返回nil切片和错误filtered, err : lo.WithoutByErr(users, func(u User) (int, error) { if u.ID 2 { return 0, fmt.Errorf(Bob not allowed) } return u.ID, nil }, 2, 3) // []User(nil), error(Bob not allowed)WithoutEmpty / WithoutNth排除零值与按索引排除WithoutEmpty返回排除零值0、、nil等后的切片。注意它已标记为废弃Deprecated源码直接委托给lo.Compactintersect.go文档明确建议改用lo.Compactlo.WithoutEmpty([]int{0, 2, 10}) // []int{2, 10}WithoutNth按索引从 0 开始排除指定位置的元素lo.WithoutNth([]int{-2, -1, 0, 1, 2}, 3) // []int{-2, -1, 0, 2}实现intersect.go把nths构造成索引集合一次线性扫描完成过滤。六、集合等价比较ElementsMatch 与 ElementsMatchByElementsMatch判断两个切片是否包含相同的元素集合不关心顺序但重复元素的出现次数必须一致即多重集合相等空集与空集视为相等lo.ElementsMatch([]int{1, 1, 2}, []int{2, 1, 1}) // true这与Every/Contains的集合式判定不同ElementsMatch是带重数的比较。实现intersect.go直接委托给ElementsMatchByiteratee 为恒等函数。ElementsMatchBy允许按自定义键比较intersect.go算法是计数器归零法长度不等直接返回false否则用 map 对list1的键计数1、对list2的键计数-1最终所有计数为 0 才返回truetype Item struct { ID string } lo.ElementsMatchBy( []Item{{a}, {b}}, []Item{{b}, {a}}, func(i Item) string { return i.ID }, ) // true对应测试见 intersect_test.go。七、源码级的双路径优化小输入扫描、大输入哈希纵观 intersect.goEvery、Intersect、Difference、Union、Without五组函数都内置了阈值分发的双路径实现。这是当前仓库实现中最值得关注的工程细节函数阈值常量值小路径策略大路径策略EveryeverySmallSubset8子集元素逐个Contains线性扫描无分配对 collection 构建哈希集合IntersectintersectSmallProduct64两列表len(a)*len(b) ≤ 64时嵌套扫描无 map 分配对最后一个列表构建 map从后往前逐列表收缩DifferencedifferenceSmallThreshold8双侧长度均 ≤ 8 时嵌套扫描双侧构建Keyify哈希集合UnionunionSmallThreshold8总元素数 ≤ 8 时扫描 result 去重维护seen集合Without/WithoutBywithoutSmallExcludeThreshold4排除项 ≤ 4 时线性扫描构建excludeMap哈希集合核心思路统一map 的堆分配与哈希开销在小输入下会超过 O(n·m) 嵌套扫描因此对常见的小规模调用多数业务场景选择零分配或低分配的扫描路径只有在输入规模大到让哈希查找O(nm)明显占优时才切换到 map 路径。注释中的设计意图intersect.go、intersect.go、intersect.go、intersect.go、intersect.go与 benchmark 目录下的 core_intersect_bench_test.go 一一对应后者为上述每个函数都提供了基准测试BenchmarkContains、BenchmarkEvery、BenchmarkIntersect、BenchmarkUnion、BenchmarkWithout等。测试方面intersect_test.go 特意为大小两条路径分别编写了用例如TestIntersect/TestIntersectBy、TestDifference_smallScan/TestDifference_large并在注释中说明小路径用例刻意控制数据规模以触发小路径而大路径用例用 9 元素以上的列表强制走 map 路径保证两条分支都被覆盖验证。八、实战选型建议围绕集合关系这一定位可以把 21 个 Helper 归纳为四类使用场景单元素/谓词查询判断某个东西在不在用Contains/ContainsBy。子集关系判断这些都在吗 / 有在的吗 / 都不在吗用Every/Some/None需要自定义规则时用对应的*By变体。集合代数求公共元素用Intersect/IntersectBy求合并去重用Union/UnionBy/UnionByErr求差异用Difference。过滤排除与等价比较剔除指定值用Without家族按键剔除用WithoutBy需要错误传播用WithoutByErr剔除零值用lo.Compact代替已废弃的WithoutEmpty断言两个集合元素相同且重数一致用ElementsMatch/ElementsMatchBy。所有函数都遵循函数式风格不修改传入切片返回新切片测试中type preserved用例也验证了命名切片类型会原样保留。结合 intersect.go 中双路径实现与 benchmark/core_intersect_bench_test.go 的基准覆盖你可以在大规模数据场景下放心使用 map 路径在小规模高频调用场景下享受扫描路径的低分配优势。【免费下载链接】lo A Lodash-style Go library based on Go 1.18 Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表