Rust中如何为serde_json::Value数组去重并实现自定义排序
serde_json::Value数组去重与自定义排序最优实现 serde_json::Value本身未实现Ord,且受孤儿规则限制无法直接为外部类型实现外部trait,最高效、最符合Rust规范的实现方案如下,无额外运行时开销,无规则冲突。
推荐生产方案:零成本新类型包装(性能最优)
该方案完全符合Rust孤儿规则:SortableValue是当前crate内定义的本地类型,为其实现Ord trait不存在合规性问题,属于Rust官方推荐的标准实现路径;新类型为单字段引用包装,编译后内存布局与原类型完全一致,无拷贝、无装箱、无动态分发开销。
实现步骤
- 定义引用版新类型,避免不必要的值拷贝
#[derive(PartialEq, Eq)] struct SortableValue<'a>(&'a serde_json::Value);
- 为新类型实现全序比较逻辑,可根据业务需求自定义排序规则,以下示例遵循通用JSON排序约定:类型优先级为
Null < Bool < Number < String < Array < Object,同类型递归比较内部值
impl<'a> PartialOrd for SortableValue<'a> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) } } impl<'a> Ord for SortableValue<'a> { fn cmp(&self, other: &Self) -> std::cmp::Ordering { use serde_json::Value::*; match (self.0, other.0) { (Null, Null) => std::cmp::Ordering::Equal, (Null, _) => std::cmp::Ordering::Less, (_, Null) => std::cmp::Ordering::Greater, (Bool(a), Bool(b)) => a.cmp(b), (Bool(_), _) => std::cmp::Ordering::Less, (_, Bool(_)) => std::cmp::Ordering::Greater, (Number(a), Number(b)) => a.as_f64().unwrap().total_cmp(&b.as_f64().unwrap()), (Number(_), _) => std::cmp::Ordering::Less, (_, Number(_)) => std::cmp::Ordering::Greater, (String(a), String(b)) => a.cmp(b), (String(_), _) => std::cmp::Ordering::Less, (_, String(_)) => std::cmp::Ordering::Greater, (Array(a), Array(b)) => { for (va, vb) in a.iter().zip(b.iter()) { match SortableValue(va).cmp(&SortableValue(vb)) { std::cmp::Ordering::Equal => continue, res => return res } } a.len().cmp(&b.len()) } (Array(_), _) => std::cmp::Ordering::Less, (_, Array(_)) => std::cmp::Ordering::Greater, (Object(a), Object(b)) => { let mut a_entries: Vec<_> = a.iter().collect(); let mut b_entries: Vec<_> = b.iter().collect(); a_entries.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); b_entries.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); for ((ka, va), (kb, vb)) in a_entries.iter().zip(b_entries.iter()) { match ka.cmp(kb) { std::cmp::Ordering::Equal => { match SortableValue(va).cmp(&SortableValue(vb)) { std::cmp::Ordering::Equal => continue, res => return res } } res => return res } } a.len().cmp(&b.len()) } } } }
- 就地排序+去重,允许修改原数组时性能最高,整体时间复杂度为O(n log n),无额外大内存分配
fn dedup_and_sort(arr: &mut Vec<serde_json::Value>) { arr.sort_by(|a, b| SortableValue(a).cmp(&SortableValue(b))); arr.dedup_by(|a, b| SortableValue(a) == SortableValue(b)); }
如果不允许修改原数组,可先遍历原数组收集元素引用,排序去重后再克隆对应值,重复率越高该方式比全量克隆后处理的性能优势越明显。
临时场景方案:闭包直接比较
如果仅需单次处理小数据量数组,无需复用排序逻辑,可直接在sort_by、dedup_by的闭包内编写比较规则,省略新类型定义。该方式无性能损失,但复用性差,多场景使用时容易出现比较逻辑不一致的bug。
避坑说明
- 不要用
HashSet去重:serde_json::Value虽实现了Hashtrait,但浮点数类型的Number存在NaN、正负零等哈希值与逻辑值不匹配的边缘问题;且HashSet本身无序,去重后仍需单独排序,内存占用远高于就地排序去重方案,整体性能更差。 - 不要用动态分发包装:不要为了实现
Ord使用dyn Ord之类的动态分发封装,会引入堆分配和虚函数调用开销,破坏零成本抽象的性能优势。 - 浮点数比较必须使用
f64::total_cmp,保证全序关系符合Ordtrait的契约,避免排序过程出现未定义行为。 - 大JSON结构排序可根据业务需求简化比较逻辑,比如数组/对象直接按长度、序列化后字节长度比较,可大幅提升排序速度。
内容的提问来源于stack exchange,提问作者Marko Seidenglanz
相关产品推荐
相关产品推荐

