多语言中的集合操作:筛选、分组、映射、排序与去重

Table of Contents

多语言中的集合操作:筛选、分组、映射、排序与去重

集合操作是业务开发里最常见的一类代码。查列表、做筛选、做映射、按字段排序、按条件分组,几乎每天都在写。

但不同语言处理集合的风格差异非常明显。有的语言自带很强的链式表达能力,有的语言更偏显式循环,有的语言虽然写起来更长,但性能和控制感更强。

这篇文章用同一个 Prod 列表作为例子,对比 C#、C++、Python、Go、JavaScript、Rust 里几种最常见集合操作的写法。

在线执行环境 https://rextester.com

为什么值得单独看集合操作

很多时候,判断一个人是否真正熟悉某门语言,不是看他会不会定义类,而是看他处理集合时是否顺手。

因为集合操作最能暴露语言习惯:

  • C# 擅长 LINQ 风格的声明式表达。
  • Python 简洁灵活,列表推导式很强。
  • JavaScript 在前端场景里非常自然。
  • Go 常常回到显式循环,代码朴素但直接。
  • Rust 写法更严格,但可组合性很强。
  • C++ 可以非常灵活,但风格差异也最大。

统一示例

假设有数据模型 Prod,字段有 NamePriceIsActive

筛选

C# var filtered = lst.Where(t => t.IsActive)

Python filtered = [p for p in lst if p.isActive]

Rust let filtered = lst.into_iter().filter(|x| x.is_active).collect::<Vec<_>>();

C++

// 拷贝到 filtered 的 inserter 迭代器
auto filtered1 = vector<Prod>();
filtered1.reserve(lst.size()); // 可选:预先分配空间提高效率
std::copy_if(lst.begin(), lst.end(), 
    std::inserter(filtered1, filtered1.begin()), 	// back_inserter 也常见 
    [](const Prod& p) { return p.GetIsActive(); });

auto filtered2 = vector<Prod>();
for (const auto& p : lst) { // 基于范围的for循环更易读
    if (p.GetIsActive()) {
        filtered2.push_back(p);
    }
}

Js let lst2 = lst.filter(item => item.isActive)

Go

var newlst []Prod
for _, v := range lst {
    if v.IsActive {
        newlst = append(newlst, v)
    }
}

分组

C# var gp = lst.GroupBy(t => t.IsActive)

Python

gp = {}
for p in lst:
    if not p.isActive in gp:
        gp[p.isActive] = []
    gp[p.isActive].append(p)

Rust

let mut mp: HashMap<bool, Vec<Prod>> = HashMap::new();
for item in lst.iter() {
    mp.entry(item.is_active).or_default().push(item.clone());
}

C++

auto mp = map<bool, vector<Prod>>();
for (const auto& p : lst) {
	// operator[] 会自动为不存在的键创建空 vector
	mp[p.GetIsActive()].push_back(p);
}

也可以在创建之前判断 key 是否存在不存在添加默认的键值对,再修改

auto mp = map<bool, vector<Prod>>();
for (const Prod& p : lst) {
	if (mp.find(p.GetIsActive()) == mp.end()) {
		mp.insert(make_pair(p.GetIsActive(), vector<Prod>()));
	}
	mp[p.GetIsActive()].push_back(p);
}

Js

let gp = {}
for (let i = 0; i < lst.length; i++) {
  if (!gp[lst[i].isActive]) {
    gp[lst[i].isActive] = []
  }
  gp[lst[i].isActive].push(lst[i])
}

Go

mp := make(map[bool][]Prod)
for _, v := range lst {
    mp[v.IsActive] = append(mp[v.IsActive], v)
}

映射

C# var newlst = lst.Select(t => t.Price * 0.8);

Python newlst = [p.Price * 0.8 for p in lst]

Rust let newlst :Vec<f64> = lst.iter().map(|x| (x.price as f64) * 0.8).collect();

C++

auto newlst = vector<double>();
newlst.reserve(lst.size());
std::transform(lst.cbegin(), lst.cend(), 
    back_inserter(newlst), 
    [](const Prod& p) { return p.GetPrice() * 0.8;});

Js let newlst = lst.map(t => t.price * 0.8)

Go

var newlst []float64
for _, v := range lst {
    newlst = append(newlst, float64(v.Price)*0.8)
}

排序

C# var od = lst.OrderBy(t => t.Price);

Python lst.sort(key=lambda p: p.Price, reverse=True)

Rust

let mut od = lst.clone();
od.sort_by(|a, b| a.price.cmp(&b.price));

C++

sort(lst.begin(), lst.end(), 
	[](const Prod& left, const Prod& right) {
		return  left.GetPrice() < right.GetPrice();
	});

Js lst.sort((a, b) => a.price - b.price)

Go

sort.Slice(lst, func(i, j int) bool {
    return lst[i].Price < lst[j].Price
})

lst.sort! 的宏会自动补全样板代码,注意 i 和 j 是下标

去重

C# lst.Distinct();

Python dist = list(set(lst))

Rust

let dist: Vec<String> = lst
        .iter()
        .cloned()
        .collect::<HashSet<String>>()
        .into_iter()
        .collect();

C++

std::sort(lst.begin(), lst.end());
auto unique_end = std::unique(lst.begin(), lst.end());
lst.erase(unique_end, lst.end());

C++ 的去重要先排序,后去重。如果是复杂数据结构的类型:

std::sort(lst.begin(), lst.end(), [](const Prod& left, const Prod& right) {
	return left.GetName() < right.GetName();
	});
auto unique_end = std::unique(lst.begin(), lst.end(),
	[](const Prod& left, const Prod& right) {
		return left.GetName() == right.GetName();
	});
lst.erase(unique_end, lst.end());

Js let dist = [...new Set(lst)] … 是 js 的展开语法,将集合展开为数组

Go

var dist []Prod
mp := make(map[string]Prod)
for _, item := range lst {
    s := fmt.Sprintf("%s %d %t", item.Name, item.Price, item.IsActive)
    if _, isExist := mp[s]; !isExist {
        mp[s] = item
    }
}
for _, item := range mp {
    dist = append(dist, item)
}

并集

C# var unions = lst.Union(lst2);

Python unions = list(set(lst + lst2))

Rust

let set1: HashSet<_> = lst1.iter().cloned().collect();
let set2: HashSet<_> = lst2.iter().cloned().collect();
let lst: Vec<String> = set1.union(&set2).cloned().collect();

C++

// 自定义比较器
auto compareProd = [](const Prod& left, const Prod& right) {
	string ls = left.GetName() + to_string(left.GetPrice()) + to_string(left.GetIsActive());
	string rs = right.GetName() + to_string(right.GetPrice()) + to_string(right.GetIsActive());
	return ls < rs;
};
sort(lst1.begin(), lst1.end(), compareProd);
sort(lst2.begin(), lst2.end(), compareProd);

// 使用back_inserter方法
auto unions = vector<Prod>();
unions.reserve(lst1.size() + lst2.size());
std::set_union(
	lst1.begin(), lst1.end(),
	lst2.begin(), lst2.end(),
	std::back_inserter(unions),
	compareProd
);

C++ 的去重要求有序,求并集也一样

Js let unions = [...new Set([...lt, ...lt2])]

Go

unions := []Prod{}
keys := make(map[string]struct{})

append2unions := func(l []Prod) {
    for _, v := range l {
        s := fmt.Sprintf("%s %d %t", v.Name, v.Price, v.IsActive)
        if _, isExist := keys[s]; !isExist {
            keys[s] = struct{}{}
            unions = append(unions, v)
        }
    }
}
append2unions(lst1)
append2unions(lst2)
fmt.Println("并集: ", unions)

keys := make(map[string]struct{}) 类似于字符串的 set,只有键,没有值。也可以存一个 bool 值

这段代码就是遍历元素,构造字符串的键 s,将没有存储到字典的键存到 unions,已有的就跳过

差集

C# var excepts = lst.Except(lst2);

Python excepts = list(set(lst2) - set(lt))

Rust

let set1: HashSet<_> = lst1.iter().cloned().collect();
let set2: HashSet<_> = lst2.iter().cloned().collect();
let lst: Vec<String> = set1.difference(&set2).cloned().collect();

C++

auto compareProd = [](const Prod& left, const Prod& right) {
	string ls = left.GetName() + to_string(left.GetPrice()) + to_string(left.GetIsActive());
	string rs = right.GetName() + to_string(right.GetPrice()) + to_string(right.GetIsActive());
	return ls < rs;
};
sort(lst1.begin(), lst1.end(), compareProd);
sort(lst2.begin(), lst2.end(), compareProd);

auto diff = vector<Prod>();
diff.reserve(lst1.size() + lst2.size());
std::set_difference(
	lst1.begin(), lst1.end(),
	lst2.begin(), lst2.end(),
	std::back_inserter(diff),
	compareProd
);

C++ 的交集叫 set_intersection

Js let excepts = [...(new Set([...lt, ...lt2]).difference(new Set(lt)))]

new Set([...lt, ...lt2]) 是并集

new Set([...lt, ...lt2]).difference(new Set(lt)) 通过 difference 制造差集

再将结果展开成数组

Go

differences := []Prod{}
keys := make(map[string]int)

for idx, v := range lst1 {
    s := fmt.Sprintf("%s %d %t", v.Name, v.Price, v.IsActive)
    if _, isExist := keys[s]; !isExist {
        keys[s] = idx
    }
}
for _, v := range lst2 {
    s := fmt.Sprintf("%s %d %t", v.Name, v.Price, v.IsActive)
    delete(keys, s)
}
for _, idx := range keys {
    differences = append(differences, lst1[idx])
}
fmt.Println("差集: ", differences)

Go 求差集的方式,就是遍历 lst1 得到 key 的集合,再遍历 lst2 删除对应的 key,剩下来的就是差集

实际开发里怎么选

如果你写的是业务代码,我自己的倾向是优先写“团队最容易一眼读懂”的版本,而不是最花哨的版本。

  • C# 优先用清晰的 LINQ。
  • Python 优先用推导式和内置容器。
  • JavaScript 优先用 mapfilter 这类常见组合。
  • Go 在数据规模不大时,普通循环通常就够了。
  • C++ 和 Rust 更适合在你明确需要控制性能、所有权或底层行为时再写得更复杂。

总结

跨语言看集合操作,最重要的不是记住每种语法,而是看清“这一门语言鼓励你怎样处理数据流”。

一旦把筛选、分组、映射、排序、去重这些基础操作都对上号,再去读真实项目代码,会轻松很多。

Related Posts

多语言中的变量与函数定义:从语法差异到理解方式

多语言中的变量与函数定义:从语法差异到理解方式 接触的编程语言越多,越会发现一个很现实的问题:概念看起来差不多,但落到具体语法时,每门语言都有自己的表达习惯。

Read More

多语言中的对象声明:同一个数据模型在 6 种语言里怎么写

多语言中的对象声明:同一个数据模型在 6 种语言里怎么写 当我们从一门语言切换到另一门语言时,最先遇到的问题通常不是算法,而是“这个对象到底应该怎么定义”。

Read More

C++ 面向对象程序设计:继承、虚函数和动态绑定到底在解决什么问题

C++ 面向对象程序设计:继承、虚函数和动态绑定到底在解决什么问题 很多人学 C++ 面向对象时,会背下数据抽象、继承、动态绑定这些术语,但一旦落到代码里,还是容易停留在“语法知道,场景不清楚”的阶段。

Read More