通用,编译时安全延迟加载方法的方法

Aar*_*ght 9 c# generics lazy-loading

假设我创建了一个如下所示的包装类:

public class Foo : IFoo
{
    private readonly IFoo innerFoo;

    public Foo(IFoo innerFoo)
    {
        this.innerFoo = innerFoo;
    }

    public int? Bar { get; set; }
    public int? Baz { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

这里的想法是,innerFoo可能包装数据访问方法或同样昂贵的东西,我只希望它GetBarGetBaz方法被调用一次.所以我想在它周围创建另一个包装器,它将保存第一次运行时获得的值.

当然,做到这一点很简单:

int IFoo.GetBar()
{
    if ((Bar == null) && (innerFoo != null))
        Bar = innerFoo.GetBar();
    return Bar ?? 0;
}

int IFoo.GetBaz()
{
    if ((Baz == null) && (innerFoo != null))
        Baz = innerFoo.GetBaz();
    return Baz ?? 0;
}
Run Code Online (Sandbox Code Playgroud)

但是如果我用10种不同的属性和30种不同的包装器来做这件事,它会变得非常重复.所以我想,嘿,让我们做这个通用:

T LazyLoad<T>(ref T prop, Func<IFoo, T> loader)
{
    if ((prop == null) && (innerFoo != null))
        prop = loader(innerFoo);
    return prop;
}
Run Code Online (Sandbox Code Playgroud)

几乎让我得到了我想要的地方,但并不完全,因为你不能ref拥有自动财产(或任何财产).换句话说,我不能这样写:

int IFoo.GetBar()
{
    return LazyLoad(ref Bar, f => f.GetBar());  // <--- Won't compile
}
Run Code Online (Sandbox Code Playgroud)

相反,我必须改为Bar拥有一个明确的支持字段并编写显式的getter和setter.哪个好,除了我最终编写的冗余代码比我最初写的更多.

然后我考虑了使用表达式树的可能性:

T LazyLoad<T>(Expression<Func<T>> propExpr, Func<IFoo, T> loader)
{
    var memberExpression = propExpr.Body as MemberExpression;
    if (memberExpression != null)
    {
        // Use Reflection to inspect/set the property
    }
}
Run Code Online (Sandbox Code Playgroud)

这对于重构非常有用 - 如果我这样做,它会很好用:

return LazyLoad(f => f.Bar, f => f.GetBar());
Run Code Online (Sandbox Code Playgroud)

但它实际上并不安全,因为有些人不那么聪明(也就是说,我从现在起3天内不可避免地忘记了如何在内部实施)可能会决定写这个:

return LazyLoad(f => 3, f => f.GetBar());
Run Code Online (Sandbox Code Playgroud)

哪个要么崩溃要么导致意外/未定义的行为,这取决于我编写LazyLoad方法的防御程度.所以我也不喜欢这种方法,因为它会导致运行时错误的可能性,这在第一次尝试时就已经被阻止了.它也依赖于Reflection,虽然这个代码对性能不敏感,但在这里感觉有点脏.

现在我可以决定全力以赴并使用DynamicProxy进行方法拦截而不必编写任何代码,实际上我已经在某些应用程序中执行此操作.但是这个代码驻留在许多其他程序集所依赖的核心库中,并且在如此低的级别上引入这种复杂性似乎是非常错误的.IFoo通过将基于拦截器的实现放入其自己的程序集中将其与界面分离起来并没有多大帮助; 事实是,这个类仍然会在所有地方使用,必须使用,所以这不是那些可以用一点DI魔术轻易解决的问题之一.

我已经想到的最后一个选项是使用如下方法:

 T LazyLoad<T>(Func<T> getter, Action<T> setter, Func<IFoo, T> loader) { ... }
Run Code Online (Sandbox Code Playgroud)

这个选项也非常"meh" - 它避免了Reflection,但仍然容易出错,而且它并没有真正减少那么多的重复.它几乎和必须为每个属性编写显式的getter和setter一样糟糕.

也许我只是非常挑剔,但这个应用程序还处于早期阶段,而且随着时间的推移它会大幅增长,我真的希望保持代码干净利落.

一句话:我陷入僵局,寻找其他想法.

题:

有没有办法清理顶部的延迟加载代码,这样实现将:

  • 保证编译时的安全性,如ref版本;
  • 实际上减少了代码重复的数量,比如Expression版本; 和
  • 不承担任何重要的额外依赖?

换句话说,有没有办法只使用常规的C#语言功能和可能的一些小助手类?或者我只是要接受在这里进行权衡并从列表中获得上述要求之一?

Aar*_*ght 1

我最终实现了一些与 .NET 4 中的类有点相似的东西Lazy,但更多地针对“缓存”的特定概念而不是“延迟加载”进行了定制。

准惰性类如下所示:

public class CachedValue<T>
{
    private Func<T> initializer;
    private bool isValueCreated;
    private T value;

    public CachedValue(Func<T> initializer)
    {
        if (initializer == null)
            throw new ArgumentNullException("initializer");
        this.initializer = initializer;
    }

    public CachedValue(T value)
    {
        this.value = value;
        this.isValueCreated = true;
    }

    public static implicit operator T(CachedValue<T> lazy)
    {
        return (lazy != null) ? lazy.Value : default(T);
    }

    public static implicit operator CachedValue<T>(T value)
    {
        return new CachedValue<T>(value);
    }

    public bool IsValueCreated
    {
        get { return isValueCreated; }
    }

    public T Value
    {
        get
        {
            if (!isValueCreated)
            {
                value = initializer();
                isValueCreated = true;
            }
            return value;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这个想法是,与类不同,它也可以从特定值Lazy<T>初始化。我还实现了一些隐式转换运算符,以便可以直接将值分配给属性,就好像它们只是. 我没有实现线程安全功能- 这些实例不是为了传递而设计的。CachedValue<T>TLazy<T>

然后,由于实例化这些东西非常冗长,我利用一些泛型类型推断功能来创建更紧凑的惰性初始化语法:

public static class Deferred
{
    public static CachedValue<T> From<TSource, T>(TSource source, 
        Func<TSource, T> selector)
    {
        Func<T> initializer = () =>
            (source != null) ? selector(source) : default(T);
        return new CachedValue<T>(initializer);
    }
}
Run Code Online (Sandbox Code Playgroud)

归根结底,这给我带来的是一个几乎 POCO 类,使用自动属性,这些属性在构造函数中使用延迟加载器进行初始化(从 中进行空合并Deferred):

public class CachedFoo : IFoo
{
    public CachedFoo(IFoo innerFoo)
    {
        Bar = Deferred.From(innerFoo, f => f.GetBar());
        Baz = Deferred.From(innerFoo, f => f.GetBaz());
    }

    int IFoo.GetBar()
    {
        return Bar;
    }

    int IFoo.GetBaz()
    {
        return Baz;
    }

    public CachedValue<int> Bar { get; set; }
    public CachedValue<int> Baz { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我对此并不十分兴奋,但我对此感到非常满意。好处是,它还允许外部人员以与实现无关的方式填充属性,这在我想要覆盖延迟加载行为(即从大型 SQL 查询中预加载一堆记录)时非常有用:

CachedFoo foo = new CachedFoo(myFoo);
foo.Bar = 42;
foo.Baz = 86;
Run Code Online (Sandbox Code Playgroud)

我现在坚持这个。用这种方法似乎很难搞砸包装类。由于使用了隐式转换运算符,因此对于null实例来说它甚至是安全的。

它仍然有一种有点黑客的感觉,而且我仍然对更好的想法持开放态度。