内联表达式引用在应用生命周期内是否一致?缓存实现相关疑问
问题描述
我有两个疑问,同时附上了自己的代码实现,希望得到解答:
- 如果使用内联表达式,该表达式的所有引用在应用程序的生命周期内是否始终相同?
- 单个匿名函数在应用生命周期内是否能保证引用一致?若可以,我打算将逻辑相同的表达式抽象为命名函数,实现可缓存查询的集中式缓存(目前各调用方自行实现缓存,效果参差不齐)。
现有代码实现
初始方法:通过主键筛选Foo并应用转换
public T GetFooByIdAndApplyTransformation<T>(int fooID, Func<Foo,T> transformation) { return dbContext.Foo .Where(x => x.id == fooID) .Select(x => transformation(x)) .FirstOrDefault(); }
调用示例
var fooBarId = GetFooByIdAndApplyTransformation(fooId, (x) => x.BarId);
带缓存的修改版本
public T GetFooByIdAndApplyTransformation<T>(int fooID, Func<Foo,T> transformation, bool Cacheable = false) { T result; var cacheKey = new Tuple<int, Func<Foo,T>>(fooId, transformation); if (Cacheable && Cache.TryGet(cacheKey, out var cacheResult)) { result = (cacheResult as T) ?? throw new InvalidOperationException($"Cached value cannot be cast to type {typeof(T)}"); } else { result = dbContext.Foo .Where(x => x.id == fooID) .Select(x => transformation(x)) .FirstOrDefault(); if (Cacheable) { cache.Set(cacheKey, result); } } return result; }
已知局限性
- 并非所有查询都适合缓存,因此添加了可选的缓存启用参数;
- 即使代码中两处使用功能完全相同的表达式,它们仍是不同的引用,可能导致缓存中存在指向同一值的不同键。
解答
关于内联表达式/匿名函数的引用一致性
内联匿名函数的引用不会自动保持一致
每次编译器遇到内联的匿名函数(比如(x) => x.BarId),都会生成一个新的委托实例——即使逻辑完全相同,不同位置的内联匿名函数在内存中的引用也是不同的。也就是说,代码里两个一模一样的内联匿名函数,GetHashCode()结果不同,作为缓存键时会被视为不同的键,导致缓存失效或重复缓存。单个匿名函数的引用仅在特定场景下一致
如果匿名函数是在同一作用域内定义的单个实例(比如在类的构造函数中赋值给类级别的委托字段),这个委托实例的引用在应用生命周期内是固定的。但如果是在方法内部每次调用时创建的内联匿名函数,每次调用都会生成新的引用,无法保证一致性。
优化缓存方案的建议
针对你的集中式缓存需求,推荐两种可行方案:
方案1:使用命名委托/静态函数统一引用
将常用的转换逻辑定义为静态命名函数或静态委托实例,确保所有调用方复用同一个委托引用:
// 定义全局统一的转换委托 public static class FooTransformations { public static readonly Func<Foo, int> GetBarId = x => x.BarId; // 其他转换逻辑... } // 调用时使用统一的委托实例 var fooBarId = GetFooByIdAndApplyTransformation(fooId, FooTransformations.GetBarId, Cacheable: true);
这种方式下,FooTransformations.GetBarId是静态只读的委托实例,整个应用生命周期内引用唯一,作为缓存键时能保证相同逻辑对应同一个键。
方案2:基于表达式树生成唯一哈希(支持动态内联表达式)
如果需要支持任意内联表达式,可以将Func<Foo,T>替换为Expression<Func<Foo,T>>,通过解析表达式树结构生成唯一哈希值作为缓存键的一部分,而非直接使用委托引用:
public T GetFooByIdAndApplyTransformation<T>(int fooID, Expression<Func<Foo,T>> transformationExpr, bool Cacheable = false) { T result; // 生成表达式的唯一哈希值 var exprHash = GetExpressionHash(transformationExpr); var cacheKey = new Tuple<int, string>(fooID, exprHash); if (Cacheable && Cache.TryGet(cacheKey, out var cacheResult)) { result = (cacheResult as T) ?? throw new InvalidOperationException($"Cached value cannot be cast to type {typeof(T)}"); } else { // 将表达式编译为委托执行 var transformation = transformationExpr.Compile(); result = dbContext.Foo .Where(x => x.id == fooID) .Select(x => transformation(x)) .FirstOrDefault(); if (Cacheable) { cache.Set(cacheKey, result); } } return result; } // 辅助方法:生成表达式树的唯一哈希 private string GetExpressionHash(Expression expr) { using var stringWriter = new StringWriter(); using var expressionWriter = new ExpressionWriter(stringWriter); expressionWriter.Visit(expr); var exprString = stringWriter.ToString(); // 使用SHA256生成哈希字符串 using var sha256 = SHA256.Create(); var bytes = Encoding.UTF8.GetBytes(exprString); var hashBytes = sha256.ComputeHash(bytes); return Convert.ToBase64String(hashBytes); } // 自定义ExpressionVisitor,遍历表达式树生成唯一序列化字符串 public class ExpressionWriter : ExpressionVisitor { private readonly TextWriter _writer; public ExpressionWriter(TextWriter writer) => _writer = writer; protected override Expression VisitLambda<T>(Expression<T> node) { _writer.Write("Lambda("); Visit(node.Body); _writer.Write(")"); return node; } protected override Expression VisitMember(MemberExpression node) { _writer.Write($"{node.Member.DeclaringType.Name}.{node.Member.Name}"); return node; } // 按需重写其他Visit方法(如Parameter、Constant等),确保表达式结构被完整序列化 }
这种方案支持任意内联表达式,只要逻辑相同,生成的哈希值就相同,避免了必须使用命名委托的限制,但需要额外处理表达式树的序列化和哈希生成。
额外注意事项
- 你的修改代码中有拼写错误:
Cachable应改为Cacheable; - 使用
Tuple作为缓存键时,其默认的Equals和GetHashCode逻辑会比较所有元素的相等性(用委托引用的话就是比较引用是否相同),需符合你的预期; - 缓存时要考虑
Foo数据的更新,需添加缓存失效机制(比如数据更新时删除对应缓存键)。
内容的提问来源于stack exchange,提问作者Sidney
相关产品推荐
相关产品推荐

