C# 通用的、编译时安全的延迟加载方法
假设我创建了一个包装器类,如下所示:C# 通用的、编译时安全的延迟加载方法,c#,generics,lazy-loading,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可能包装数
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;
}
相反,我必须更改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
}
}
但它实际上并不安全,因为一些不太聪明的人(即我自己,从现在起3天内,我不可避免地忘记了这是如何在内部实现的)可能会决定写以下内容:
return LazyLoad(f => 3, f => f.GetBar());
这将导致崩溃或导致意外/未定义的行为,具体取决于我编写LazyLoad
方法的防御方式。所以我也不太喜欢这种方法,因为它可能导致运行时错误,而这种错误在第一次尝试时就可以避免。它还依赖于反射,在这里感觉有点脏,尽管这段代码对性能不敏感
现在我也可以决定全力以赴,使用DynamicProxy进行方法拦截,而不必编写任何代码,事实上,我已经在一些应用程序中这样做了。但这段代码驻留在许多其他程序集所依赖的核心库中,以如此低的级别引入这种复杂性似乎是错误的。将基于拦截器的实现与IFoo
接口分离,并将其放入自己的程序集中,这并没有真正的帮助;事实上,这个类仍然会被广泛使用,必须被使用,所以这不是一个可以用一点DI魔法简单解决的问题
我已经想到的最后一个选择是采用如下方法:
T LazyLoad<T>(Func<T> getter, Action<T> setter, Func<IFoo, T> loader) { ... }
T懒散加载(Func getter、Action setter、Func loader){…}
这个选项也是非常“meh”的——它避免了反射,但仍然容易出错,而且它并没有真正减少那么多重复。这几乎和必须为每个属性编写显式getter和setter一样糟糕
也许我只是在吹毛求疵,但这个应用程序仍处于早期阶段,随着时间的推移,它将大幅增长,我真的希望保持代码整洁
一句话:我陷入了僵局,正在寻找其他想法
问题:
有没有办法清理顶部的延迟加载代码,以便实现:
- 保证编译时的安全性,如
版本李>ref
- 实际上减少代码重复的数量,比如
版本;及表达式
- 不承担任何重要的附加依赖项
换句话说,有没有一种方法可以通过使用常规的C#语言特性和一些可能的小助手类来实现这一点?还是我必须接受这里有一个折衷方案,并从列表中删除上述要求之一?如果您可以使用.NET 4,您应该使用 它提供了您需要的功能,并且是完全线程安全的
如果您不能使用.NET4,我仍然建议您查看它,并“窃取”它的设计和API。它使延迟实例化变得非常容易。如果您所要做的只是避免将此代码复制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
}
int-IFoo.GetBar()
{
return LazyLoad(ref Bar,f=>f.GetBar());//我最终实现了一个类似于.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;
}
}
}
最终,我得到的是一个几乎是POCO的类,使用构造函数中的延迟加载程序初始化的自动属性(从deferred
合并为null):
我现在坚持这一点。使用这种方法似乎很难搞乱包装器类。由于使用了隐式转换运算符,它甚至可以安全地抵抗null
实例
它仍然有点黑客的感觉,我仍然愿意接受更好的想法。我确实研究过这个类(仍然在.NET 3.5上,但我可以自己实现)-但我无法确定如何将其应用到设计中,请记住,innerFoo
实际上可能是null
,并且外部类应该能够更改属性值(因此是公共setter)。也许我只是没有看到树的森林-你可以举一个例子,说明使用Lazy
,这会是什么样子吗?你真的需要一个公共可读写属性,可以填充也可以不填充,以及一个公共GetX()设置属性并返回值?@Jeffrey:长话短说,是的,因为这个类的部分原理是,属性可能会被急切地加载。例如,如果对象包装了几个数据库调用,则更高级别的组件可能会决定从一个大记录集中加载其中几个,以避免重复的
int IFoo.GetBar()
{
return LazyLoad(ref Bar, f => f.GetBar()); // <--- Won't compile
}
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;
}
}
}
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);
}
}
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; }
}
CachedFoo foo = new CachedFoo(myFoo);
foo.Bar = 42;
foo.Baz = 86;