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

时间:2010-04-16 18:39:09

标签: 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; }
}

这里的想法是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;
}

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

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

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

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

相反,我必须更改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
    }
}

这对重构非常有用 - 如果我这样做会很有效:

return LazyLoad(f => f.Bar, f => f.GetBar());

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

return LazyLoad(f => 3, f => f.GetBar());

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

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

我已经想到的最后一个选择是使用类似的方法:

 T LazyLoad<T>(Func<T> getter, Action<T> setter, Func<IFoo, T> loader) { ... }

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

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

结论:我陷入僵局,寻找其他想法。

问题:

有没有办法清理顶部的延迟加载代码,以便实现:

  • 保证编译时安全,例如ref版本;
  • 实际上减少代码重复次数,例如Expression版本;和
  • 不接受任何重要的附加依赖项?

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

3 个答案:

答案 0 :(得分:4)

如果您可以使用.NET 4,则应使用Lazy<T>

它提供了您所需的功能,此外,它是完全线程安全的。

如果您无法使用.NET 4,我仍然建议您查看它,并“窃取”它的设计和API。它使得懒惰的实例化非常容易。

答案 1 :(得分:1)

如果您要做的就是避免复制此代码300次:

private int? bar;
public int Bar
{
    get
    {
        if (bar == null && innerFoo != null)
           bar = innerFoo.GetBar();
        return bar ?? 0;           
    }
    set
    {
        bar = value;
    }
}

然后你总是可以创建一个索引器。

enum FooProperties
{
    Bar,
    Baz,
}

object[] properties = new object[2];

public object this[FooProperties property]
{
    get
    {
        if (properties[property] == null)
        {
           properties[property] = GetProperty(property);
        }
        return properties[property];           
    }
    set
    {
        properties[property] = value;
    }
}

private object GetProperty(FooProperties property)
{
    switch (property)
    {
         case FooProperties.Bar:
              if (innerFoo != null)
                  return innerFoo.GetBar();
              else
                  return (int)0;

         case FooProperties.Baz:
              if (innerFoo != null)
                  return innerFoo.GetBaz();
              else
                  return (int)0;

         default:
              throw new ArgumentOutOfRangeException();
    }
}

这将需要在读取时输出值:

int myBar = (int)myFoo[FooProperties.Bar];

但它避免了大多数其他问题。

编辑添加:

好的,这是你应该做的,但不要告诉任何人你做了,或者我建议了。选择此选项:

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

制作BarBaz和朋友公共字段而不是属性。这应该是你正在寻找的。

但是,再说一遍,不要告诉任何人你这样做了!

答案 2 :(得分:1)

我最终实现了类似于.NET 4中的Lazy类的 kinda sorta ,但更多地针对“缓存”的特定概念进行了自定义,而不是“延迟加载”

准懒惰的类看起来像这样:

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;
        }
    }
}

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

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

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);
    }
}

在一天结束时,我得到的是一个几乎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; }
}

我不是非常激动这个,但我非常高兴与它。有什么好处,它还允许外人以与实现无关的方式填充属性,这在我想要覆盖延迟加载行为时非常有用,即从大SQL查询中预加载一堆记录:

CachedFoo foo = new CachedFoo(myFoo);
foo.Bar = 42;
foo.Baz = 86;

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

它仍有一种有点hackish的感觉,我仍然愿意接受更好的想法。