假设我创建了一个如下所示的包装类:
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
可能包装数据访问方法或同样昂贵的东西,我只想调用它的GetBar
和GetBaz
方法一次。所以我想围绕它创建另一个包装器,它将保存第一次运行时获得的值。
当然,这很简单:
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#语言功能和可能的一些小助手类?或者我只是要接受在这里进行权衡并从列表中获得上述要求之一?
答案 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
}
制作Bar
,Baz
和朋友公共字段而不是属性。这应该是你正在寻找的。 p>
但是,再说一遍,不要告诉任何人你这样做了!
答案 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的感觉,我仍然愿意接受更好的想法。