资讯动态

mruby-set 完全指南:在 mruby 中实现 Set 集合的构建、运算与源码剖析

发布时间:2026/9/17 19:38:41 来源:尧图企业网站定制
mruby-set 完全指南在 mruby 中实现 Set 集合的构建、运算与源码剖析【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit导读mruby-set 是 mruby 生态中的一个 mrbgem它为轻量级 Rubymruby提供了标准的Set集合类支持元素去重、并集、差集、交集、对称差等常见集合运算。本指南以lib/nghttp2-1.65.0/third-party/mruby/mrbgems/mruby-set/README.md为骨架结合仓库内的 set.rb 实现 与 测试用例系统讲解该 gem 的创建方式、集合运算、破坏性与非破坏性操作、比较与划分以及尚未实现的方法边界。读完本文你将掌握 mruby 环境下Set的全部可用 API 及其底层实现原理。背景说明该 mrbgem 随 nghttp2 以 third-party 方式一并内置在 fluent-bit 仓库的lib/依赖树中nghttp2 的 nghttpx 反向代理通过 mruby 脚本扩展其配置能力Set常被用于处理去重、白名单、标签集合等场景。本文聚焦该 gem 本身仓库中的源码与测试用于印证其行为。一、mruby-set 是什么mruby-set 提供的是Set类——一个不重复元素的集合容器。与 Ruby 标准库中的Set类似它基于哈希表实现因此元素唯一重复插入同一元素不会增加集合大小成员查找、插入、删除的平均时间复杂度接近 O(1)元素顺序不保证与插入顺序一致依赖底层 Hash 的键序。从源码看Set继承自Enumerable见 set.rb内部用一个实例变量hash存储元素每个元素作为键值统一为true。这一点在add、include?、each等方法的实现中体现得淋漓尽致def add(o) hash[o] true # 元素作为 key值为 true self end def include?(o) hash.include?(o) # 复用 Hash#include? end def each(block) return to_enum :each unless block_given? hash.each_key(block) # 只遍历键 self end由于复用了 mruby 内建的HashSet天然继承了哈希表的去重与高效查找能力无需任何 C 语言扩展即可用纯 Ruby 实现这对资源受限的嵌入式环境非常友好。它如何被引入项目gem 元信息位于 mruby-set.gem作者为 yui-knk协议为 MIT构建描述文件 mrbgem.rake 声明了它依赖mruby-hash-ext与mruby-enumerator两个核心 gem这解释了each方法中to_enum能力的来源在 mruby 的 stdlib.gembox 中通过conf.gem :core mruby-set被纳入标准库集合随默认构建一起编译部分专用构建配置如 nintendo_wii.rb也会显式声明conf.gem mrbgems/mruby-set/。二、快速上手创建 Set 的两种方式2.1Set.new—— 从可枚举对象构造Set.new # #Set: {} Set.new(nil) # #Set: {} Set.new([]) # #Set: {} Set.new([1, 2]) # #Set: {1, 2} Set.new(1..3) # #Set: {1, 2, 3}initialize的实现要点见 set.rb参数为nil时直接返回空集合传入可枚举对象时调用merge(enum)若同时传入代码块则先用块转换每个元素再加入集合。s Set.new([1, 2, 3]) { |o| o * 2 } # #Set: {2, 4, 6}注意类型约束参数必须是可枚举对象。__do_with_enum内部检查对象是否响应each否则抛出ArgumentError。测试 test/set.rb 验证了Set.new(false)、Set.new(1)、Set.new(1, 2)都会抛出ArgumentError。2.2Set[...]—— 字面量便捷构造Set[] # #Set: {} Set[1] # #Set: {1} Set[1, 2, 3] # #Set: {1, 2, 3} Set[[a, b]] # #Set: {[a, b]} # 注意数组整体作为一个元素 Set[nil] # #Set: {nil}Set.[]的实现是把传入的参数打包成数组再交给new见 set.rb因此Set[1,2]等价于Set.new([1,2])但Set[[1,2]]会把整个数组当作单个元素存入集合。测试用例 Set.[] 一节 详细印证了这一行为。2.3 去重特性验证ary [2, 4, 6, 4] set Set.new(ary) set.size # 3 # 重复的 4 只保留一份而且构造时是拷贝式的测试中先构造集合再清空原数组集合内容不受影响。三、集合运算并集、差集、交集、对称差这是原 README 给出的核心示例下面逐一拆解运算语义set1 Set.new([1, 2]) set2 Set[1, 2, 3] set3 Set[4] set1 set3 # #Set: {1, 2, 4} set2 - set1 # #Set: {3} set2 set1 # #Set: {1, 2} set1 ^ set2 # #Set: {3}3.1 并集/|/uniondef |(enum) dup.merge(enum) # 基于副本合并不改动自身 end alias | alias union |基于dup实现因此是非破坏性运算原集合不变返回新集合。merge对同类型Set直接hash.merge!对其他可枚举对象逐个add见 set.rb。值得注意dup时initialize_copy会深拷贝内部hash见 set.rb保证副本与原件互不影响——Set#clone、Set#dup的隔离性在 测试 中被专门验证。3.2 差集-/differencedef -(enum) dup.subtract(enum) end alias difference -同样是基于副本的非破坏性操作。subtract对参数逐个执行delete见 set.rb。3.3 交集/intersectiondef (enum) n Set.new __do_with_enum(enum) { |o| n.add(o) if include?(o) } n end遍历参数中的每个元素仅保留也存在于本集合中的元素返回全新集合且与参数类型无关数组、Range 均可。3.4 对称差^def ^(enum) (self | Set.new(enum)) - (self Set.new(enum)) end数学定义(A ∪ B) − (A ∩ B)即只属于其中一个集合的元素。上例中set1 ^ set2得到{3}1、2 同时属于两边被剔除3 只属于 set2。测试 Set#^ 还验证了参数数组中的重复元素5,5只会被当作一个 5 处理。3.5 破坏性合并与剔除如果希望直接修改原集合可以使用set.merge([2, 4, 6]) # 就地并集返回 self set.subtract([2, 4, 6]) # 就地差集返回 self测试 Set#merge 与 Set#subtract 均验证了返回值为selfassert_same语义。四、增删改查元素管理 API 全览方法行为返回值add(o)/加入元素已存在则无操作selfadd?(o)不存在才加入加入成功返回self已存在返回nildelete(o)删除元素不存在也无妨selfdelete?(o)存在才删除删除成功返回self不存在返回nilinclude?(o)/member?/成员判断布尔值size/length元素个数整数empty?是否为空布尔值clear清空所有元素selfreplace(enum)清空后并入新元素selfto_a转成数组内部键的拷贝数组几个关键实现细节add?、delete?的“条件化”语义由 set.rb 实现它们能方便地实现“只在元素缺席时插入”的幂等逻辑replace先clear再merge接受数组、Range、另一个 Set 等任何可枚举对象测试 同时覆盖了数组与 Set 两种参数由于基于 HashSet允许放入nil、false、数组、Range 等任意对象作为元素Set#size 测试 验证了Set[nil]与Set[[]]大小均为 1。五、遍历、过滤与批量变换Set实现了Enumerable接口因此map、select、reject等一切 Enumerable 方法开箱即用同时自身也提供了一批就地destructive版本set.each { |o| ... } # 遍历元素无块时返回 Enumerator set.delete_if { |o| cond } # 就地删除满足条件的元素返回 self set.keep_if { |o| cond } # 就地保留满足条件的元素返回 self set.collect! { |o| x } # 就地变换别名 map!返回 self set.reject! { |o| cond } # 有删除才返回 self无删除返回 nil set.select! { |o| cond } # 有保留才返回 self无保留返回 nil别名 filter!语义要点源码见 set.rbdelete_if/keep_if始终返回self无论是否发生变化reject!/select!遵循“bang 方法约定”无变化时返回nil有变化时返回self便于在代码中判断是否发生了修改each无块调用时返回Enumerator这依赖mruby-enumeratorgem见 mrbgem.rake。测试用例覆盖了上述方法的边界行为例如Set.new(1..10).reject! { |i| i 10 }返回nil而reject! { |i| i % 3 0 }返回去除了 3、6、9 的集合test/set.rb。六、子集与超集关系判断方法别名语义subset?(set)本集合是参数集合的子集含相等proper_subset?(set)本集合是参数集合的真子集superset?(set)本集合是参数集合的超集含相等proper_superset?(set)本集合是参数集合的真超集intersect?(set)—与参数集合存在公共元素disjoint?(set)—与参数集合无任何公共元素实现上这组方法严格要求参数必须是Set传入数组、整数、nil都会抛出ArgumentError见 set.rb。intersect?做了性能优化始终遍历元素较少的集合进行成员判断def intersect?(set) raise ArgumentError, value must be a set unless set.is_a?(Set) if size set.size any? { |o| set.include?(o) } else set.any? { |o| include?(o) } end end边界语义在 测试 中被完整覆盖空集合是任何集合的子集Set[].subset?(Set[])为真但Set[].proper_subset?(Set[])为假Set[].intersect?(Set[])为假。七、嵌套集合与 flatten 展平Set的元素可以是另一个Set从而形成嵌套集合。mruby-set 为此提供了展平能力set Set[1, Set[2, Set[3]], Set[4]] set.flatten # #Set: {1, 2, 3, 4} # 返回新集合 set.flatten! # #Set: {1, 2, 3, 4} # 就地展平若本无嵌套则返回 nil实现原理见 set.rbflatten_merge维护一个seen集合记录已访问的Set的object_id递归展开嵌套 Set 时若再次遇到同一对象抛出ArgumentError: tried to flatten recursive Set从而防御循环引用导致的无限递归flatten!通过detect先判断是否存在嵌套 Set若无嵌套则返回nil否则就地replace(flatten())。测试用例 对多层嵌套、集合内同一 Set 多次出现、以及set1 Set[1, set2]; set2.add(set1)这种自引用场景都做了验证自引用场景断言抛出ArgumentError。八、classify 与 divide集合划分8.1classify—— 按块结果分桶set Set.new(1..10) set.classify { |i| i % 3 } # {0#Set: {3, 6, 9}, 1#Set: {1, 4, 7, 10}, 2#Set: {2, 5, 8}}classify返回的是Hash键为块的返回值值为对应元素组成的子集见 set.rb无块时返回Enumerator。测试验证了返回值类型为Hash、每个值为Set且所有子集大小之和等于原集合大小test/set.rb。8.2divide—— 返回子集组成的集合set Set.new(1..10) ret set.divide { |i| i % 3 } # #Set: {#Set: {3, 6, 9}, #Set: {1, 4, 7, 10}, #Set: {2, 5, 8}}divide内部复用classify把分类结果的值子集收集进一个新的 Set 返回见 set.rb。测试断言ret.flatten等于原集合、ret类型为Settest/set.rb。⚠️限制divide只支持一元arity 1代码块。若传入二元块如{ |a, b| (a - b).abs 1 }实现会直接抛出NotImplementedError: Set#divide with 2 arity block is not implemented.这一点在 README 的 Limitations 中有明确声明也是原版 RubySet#divide所支持的二元划分形式在此 gem 中尚未实现的部分。九、比较、哈希与字符串化9.1与eql?Set[2, 3, 1] Set[1, 2, 3] # true # 集合相等与顺序无关 Set[1] [1] # false # 类型不同不相等的实现见 set.rb分三种情况同一对象直接相等同类实例比较内部hash子类实例逐个成员比对。eql?则要求两边都是Set且内部哈希eql?。hash方法委托给hash.hash因此两个相等的集合拥有相同的哈希值可以作为 Hash 的键或放入其他集合。9.2飞船运算符依据子集/超集关系返回-1、0、1若两者不可比无包含关系则返回nil见 set.rb这使得Set可参与Comparable风格的排序逻辑。9.3inspect与to_sSet[1, 2, 3].inspect # #Set: {1, 2, 3} Set[].inspect # #Set: {}inspect输出#Set: {元素1, 元素2}的格式见 set.rbto_s是它的别名对递归嵌套的集合会输出#Set: {...}防止无限展开。Set#inspect 测试 印证了输出格式。另有join(separator)方法把元素数组拼接为字符串。9.4reset—— 元素哈希键变更后的重哈希set.reset当集合中的元素如可变对象的哈希值在其加入后被修改可能导致成员查找失效此时调用reset会执行hash.rehash恢复一致性若集合已被冻结则抛出FrozenError见 set.rb。十、已知限制尚未实现的方法原 README 的 Limitations 明确列出了以下未实现项使用时需要规避freeze源码中冻结逻辑被整体注释掉set.rb对应的 freeze 测试也以注释形式保留test/set.rb。因此当前版本不能将 Set 冻结不过reset内部仍保留了frozen?判断说明冻结能力被预留为未来扩展点。to_set实现同样被注释set.rb。由于Set本身已是集合to_set缺失的影响有限——需要将其他对象转为 Set 时直接用Set.new(obj)或Set[obj]即可。divide的二元块形式Set#divide仅支持一元块传入二元块会抛出NotImplementedError见上文第八节。实际测试中这三点对应场景分别被“不测试”或“断言抛异常”的方式处理边界行为有明确预期不会产生未定义行为。十一、在本仓库中的定位与使用建议依赖关系mruby-set 作为 mruby 的标准 mrbgem被 stdlib.gembox 收录随 mruby 默认构建编译它依赖mruby-hash-ext提供Hash#each_key、Hash#merge!等扩展与mruby-enumerator提供to_enum。源码位置核心实现集中在 mrblib/set.rb约 325 行纯 Ruby行为契约由 test/set.rb30 余组断言固定是研究 mruby 集合语义的第一手材料。许可MIT 协议版权归 yui-knk2016见 LICENSE。典型使用场景建议在 mruby 脚本中做去重如收集唯一用户 ID、唯一标签、白名单/黑名单成员判断include?、多来源数据的并集/交集归并|、、按规则分组classify等。需要注意两点一是元素查找依赖其hash值可变对象作为元素时要留意哈希一致性必要时调用reset二是集合操作的非破坏性版本、-、、^都会产生新集合高频循环中使用时建议改用merge、subtract等就地版本以减少分配。【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价