能否用DispatchProxy拦截IEnumerable<T>扩展方法调用以延续缓存?
我正在用DispatchProxy创建带缓存功能的IEnumerable<T>,它会在首次枚举时计算元素并缓存,供后续枚举复用。因为预期元素量大且需要延迟计算(Collection不适用),同时存在多次对计算成本高的对象进行部分枚举的场景,所以采用动态代理,希望让缓存行为延续到所有从已缓存Enumerable派生的IEnumerable<T>。
我的问题是:能不能通过DispatchProxy拦截扩展方法调用?尤其是像Append这类会返回新增元素的新Enumerable的扩展方法,以便把结果包装到我的代理类实例中?我知道这可能实现不了,但还是想确认。
public class CachedEnumerable<TElement> : DispatchProxy { public static IEnumerable<TElement> Create(IEnumerable<TElement> elements) { var proxy = Create<IEnumerable<TElement>, CachedEnumerable<TElement>>(); var cached = proxy as CachedEnumerable<TElement>; cached._enumerator = elements.GetEnumerator(); return proxy; } private IEnumerable<TElement> Target { get { var enumerator = GetEnumeratorImpl(); while (enumerator.MoveNext()) { yield return enumerator.Current; } } } private readonly List<TElement> _cache = []; private IEnumerator<TElement>? _enumerator; protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) { if (targetMethod?.Name == nameof(IEnumerable.GetEnumerator)) { return GetEnumeratorImpl(); } if (targetMethod?.ReturnType == typeof(IEnumerable<TElement>)) { // Extension methods would ideally be dispatched to this code. return Make(targetMethod.Invoke(Target, args) as IEnumerable<TElement>); } return targetMethod?.Invoke(Target, args); } private IEnumerator<TElement> GetEnumeratorImpl() { foreach (var element in _cache) { yield return element; } while (_enumerator.MoveNext()) { _cache.Add(_enumerator.Current); yield return _enumerator.Current; } } }
目前GetEnumerator调用可以正常转发到GetEnumeratorImpl,但扩展方法调用只会执行默认实现。
DispatchProxy无法直接拦截扩展方法调用,核心原因是:扩展方法本质是静态方法,编译器在编译时会把扩展方法的调用转换为对静态类方法的直接调用,而不是调用目标对象的实例方法。你的代理类的Invoke方法根本接收不到扩展方法的调用请求,自然无法拦截或包装其返回结果。
针对你需要延续缓存行为的需求,提供两种可行的替代方案:
方案1:手动封装常用扩展方法
创建自定义静态工具类,封装你需要用到的扩展方法(比如Append、Where、Select等),在方法内部把返回的新IEnumerable<T>重新包装进CachedEnumerable<TElement>:
public static class CachedEnumerableExtensions { public static IEnumerable<T> Append<T>(this IEnumerable<T> source, T element) { var appended = Enumerable.Append(source, element); return CachedEnumerable<T>.Create(appended); } // 按需封装其他扩展方法,比如Where、Select等 }
这种方式需要你主动使用自定义封装的方法,而非.NET原生扩展方法,优点是实现简单,能完全控制缓存行为的延续。
方案2:自定义IEnumerable实现类(替代DispatchProxy)
放弃使用DispatchProxy,直接实现IEnumerable<T>接口,在类内部处理缓存逻辑,同时手动封装返回新序列的方法,确保新返回的序列也带有缓存功能:
public class CachedEnumerable<T> : IEnumerable<T> { private readonly IEnumerable<T> _source; private readonly List<T> _cache = []; private IEnumerator<T>? _enumerator; private CachedEnumerable(IEnumerable<T> source) { _source = source; } public static CachedEnumerable<T> Create(IEnumerable<T> source) { return new CachedEnumerable<T>(source); } public IEnumerator<T> GetEnumerator() { foreach (var item in _cache) { yield return item; } _enumerator ??= _source.GetEnumerator(); while (_enumerator.MoveNext()) { var item = _enumerator.Current; _cache.Add(item); yield return item; } } // 自定义Append方法,返回带缓存的新序列 public CachedEnumerable<T> Append(T element) { var appended = Enumerable.Append(this, element); return Create(appended); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
这种方式灵活性更高,能完全掌控所有返回序列的缓存行为,但需要你手动封装所需方法,或者添加隐式转换让原生扩展方法的返回结果自动被包装。
额外优化提示
你现有代码中的Target属性使用yield return会导致每次访问都触发重新枚举,这可能引发不必要的重复计算(虽然缓存逻辑在GetEnumeratorImpl中,但这个设计存在隐患),建议调整Target的实现,直接返回已缓存的序列,或者确保枚举逻辑能正确复用缓存数据。
内容的提问来源于stack exchange,提问作者jeff.w

