避免在map/unordered_map中进行多次查找
哈哈,这个问题我太有共鸣了——谁没为了减少一次map查找跟代码较劲过呢!毕竟那个高开销函数多跑一次都是浪费,而重复的查找逻辑要么冗余要么写得别扭。我来给你梳理几种不同语言下的最优解,总有一款适合你:
先聊聊「两次查找」的典型写法(为什么我们想避开它)
先举个Go的例子,这是大家最容易想到的写法,但确实存在两次map操作:
func getValue(s string) int { // 第一次查找:检查是否存在 if val, ok := cache[s]; ok { return val } // 计算后第二次操作:插入map val := expensiveFunc(s) cache[s] = val return val }
语义上是两次查找/插入操作,虽然有些语言的底层可能会优化,但我们总希望能在代码层面做到更高效、更简洁。
「一次查找但笨拙」的常见写法
比如在C++里,我们会用迭代器来复用查找结果,但代码确实多了点“仪式感”:
int getValue(const std::string& s) { auto it = cache.find(s); // 仅一次查找 if (it != cache.end()) { return it->second; } // 利用迭代器hint插入,避免再次查找 int val = expensiveFunc(s); cache.insert(it, {s, val}); return val; }
逻辑没问题,但每次都要写迭代器、判断,确实不够清爽。
更优的实现方式:分语言看技巧
1. Python:直接用标准库的缓存装饰器
Python的functools.lru_cache简直是这种场景的天选工具,一行代码搞定所有缓存逻辑,完全不用自己维护map:
from functools import lru_cache # 直接给高开销函数加缓存,或者给包装函数加 @lru_cache(maxsize=None) # maxsize=None表示无限制缓存 def get_value(s): return expensive_func(s)
不仅自动帮你做一次查找判断,还处理了缓存的存储、过期(如果需要可以设置maxsize),甚至单线程下完全不用操心线程安全(多线程场景可以加个简单锁,或者用cachetools库的线程安全缓存)。
如果不想用装饰器,也可以用更简洁的写法:
def get_value(s): val = cache.get(s) if val is None: val = expensive_func(s) cache[s] = val return val
这个写法看起来是两次操作,但Python的dict.get和赋值的底层开销已经很低,代码也足够简洁。
2. Go:封装泛型工具函数(Go 1.18+)
Go 1.21+虽然有maps.LoadOrStore,但它的第三个参数是直接传值——这意味着不管key存不存在,都会先调用expensiveFunc,反而浪费性能。所以我们可以自己封装一个泛型函数,把逻辑抽离:
import "sync" // 假设cache是线程安全的,用sync.Map或者加锁的map var cache = make(map[string]int) var mu sync.Mutex // 泛型函数:加载缓存,不存在则计算并存储 func loadOrCompute[K comparable, V any](m map[K]V, mu *sync.Mutex, key K, compute func() V) V { mu.Lock() defer mu.Unlock() val, ok := m[key] if !ok { val = compute() m[key] = val } return val } // 业务代码就变得超级简洁 func getValue(s string) int { return loadOrCompute(cache, &mu, s, func() int { return expensiveFunc(s) }) }
这样一来,所有缓存逻辑都封装在loadOrCompute里,业务代码只需要关心调用高开销函数,既保证了一次查找的效率,又摆脱了笨拙的重复代码。
3. C++:用try_emplace避免冗余操作
C++17引入的try_emplace是个神器,它可以在key不存在时插入,并且返回迭代器和是否插入的标识。但要注意:不要直接把expensiveFunc作为参数传进去(否则不管key存不存在都会调用),应该先判断:
#include <unordered_map> #include <string> std::unordered_map<std::string, int> cache; int getValue(const std::string& s) { auto it = cache.find(s); if (it != cache.end()) { return it->second; } // 已经确定key不存在,直接emplace插入,避免再次查找 auto [newIt, _] = cache.emplace(s, expensiveFunc(s)); return newIt->second; }
如果是C++20及以上,还可以用结构化绑定让代码更简洁:
int getValue(const std::string& s) { auto [it, inserted] = cache.try_emplace(s); if (inserted) { it->second = expensiveFunc(s); } return it->second; }
这个写法既保证了仅一次查找(try_emplace内部也是一次查找+插入),代码也比之前的迭代器写法清爽很多。
总结
其实核心思路就是两个:
- 优先用语言标准库提供的缓存工具,比如Python的
lru_cache,这些工具已经帮你优化了所有细节,代码最简洁 - 如果需要自己实现缓存,封装通用的缓存逻辑函数,把查找、计算、存储的逻辑抽离,让业务代码干净清爽,也避免重复造轮子
内容的提问来源于stack exchange,提问作者Felix Dombek

