通用类型扩展器






4.83/5 (30投票s)
通过将任何引用类型扩展到任何其他类型来模拟扩展属性。
目录
引言
我们都知道没有扩展属性。有扩展方法,但没有扩展属性。如果这已经困扰您,那么本文可能是一个解决方案。
当然,如果您需要新属性,您应该优先选择“干净”的方式来扩展您的类型。但这并非总是可能的——.NET Framework 或第三方程序集中存在密封类。或者,仅仅为了在一种情况下使用该属性而扩展一个类型,似乎是一种开销。简而言之,我不会讨论扩展属性的优缺点——您可以随意使用它们。
但我会给您一个例子:我必须实现一个模块,其中各种类型都会传入——也许一次,也许两次,不知道传入多少次。但我必须在第二次接收到实例时识别它。所以我想给任何实例添加一个“票证”。像下面这样会很好
theIncomingReference.TicketID = new Guid();
theIncomingReference.TicketTime = DateTime.Now;
所以我可以这样做
if(theIncomingReference.TicketID != null) {return;}
当然,`theIncomingReference` 没有实现“`TicketID`”或“`TicketTime`”作为属性!
万一我能从别人的工作中受益,我在网上搜索了扩展属性。没有找到直接支持。我偶然发现了Mixin。但那不是我想要的。您必须使用接口和工厂方法——不够通用。对我来说确实没有解决方案。所以我按照我自己的方式做了(老派弗兰克),并想出了 **通用类型扩展器**。
最后,我的解决方案看起来像这样
theIncomingReference.Extended().Dynamically().TicketID = new Guid();
theIncomingReference.Extended().Dynamically().TicketTime = DateTime.Now;
或者通过使用具体类型作为扩展
theIncomingReference.Extended().With<Ticket>().TicketID = new Guid();
theIncomingReference.Extended().With<Ticket>().TicketTime = DateTime.Now;
当然,以后您可以读取属性
Guid ticketID = (Guid) theIncomingReference.Extended().Dynamically().TicketID;
或者更类型安全
Guid ticketID = theIncomingReference.Extended().With<Ticket>().TicketID;
就是这样(老派迈克尔)——在所有引用类型上都能工作,无需进行任何设置——没有接口,没有继承,没有工厂!
想知道它是如何工作的吗?
概述
再次,开箱即用的扩展属性没有支持。所以我们必须模拟它。这是通过将一个类型的引用(例如 `Ticket`)“绑定”到已扩展的对象(例如 `theIncomingReference`)来完成的。这种“绑定”被存储起来,并在每次请求已扩展对象时使用。所以,实际上您使用的是“绑定”对象的属性,而不是已扩展对象的属性。诀窍是,您总是为同一个已扩展对象获得同一个“绑定”对象实例。听起来很简单?是的,但当然也有一些事情需要注意——比如垃圾回收。我们很快就会看看这个问题。但首先,一张显示 `UniversalTypeExtender` 组件的类图
您可以在类图中看到“绑定”对象。它被称为 `RepositoryItem`,并且作为嵌套类存在于其相关的 `ExtensionRepository` 中。 `RepositoryItem` 持有对已扩展对象的引用以及该对象的扩展。 `ExtensionRepository` 跟踪这些 `RepositoryItem`。 `UniversalTypeExtender` 本身只提供一个扩展方法——`Extended<T>()`。此方法返回一个 `Extender` 对象,用于扩展对象。 `Extender` 进而管理扩展对象的不同方式——目前有 `Dynamically()` 和 `With<TE>()`。 `Dynamically()` 返回一个真正的动态对象(`DynamicPropertyExtension`)——这就是为什么您可以调用任何属性而无需进行任何其他设置。 `With<TE>()` 返回指定类型的实例——这使您可以进行类型安全的编码。
好了,这是简要概述。让我们更深入地了解这些组件。
通用类型扩展器
如前所述,要扩展对象,您所要做的就是调用扩展方法 `Extended<T>()`。这非常明显,因为扩展方法是我们向任何类型添加自己功能而无需修改代码的唯一方法。通过使用泛型类型 `T`,我们可以控制扩展仅在引用类型上工作——这意味着类。对于值类型来说会很麻烦,因为它们主要通过值传递。但扩展器的概念需要引用,您稍后就会看到。 `Extended<T>()` 返回一个 `Extender`,它获取对已扩展对象的引用。所以代码看起来非常简单
public static Extender<T> Extended<T>(this T extendee) where T : class {
Contract.Requires<ArgumentNullException>(extendee != null);
return new Extender<T>(extendee);
}
此外,`UniversalTypeExtender` 通过实现单例模式来持有 `ExtensionRepository` 的实例
private static volatile ExtensionRepository mRepositoryShouldOnlyBeCalledFromProperty;
private static readonly object SyncRoot = new object();
private static ExtensionRepository Repository {
get {
if (mRepositoryShouldOnlyBeCalledFromProperty == null) {
lock (SyncRoot) {
if (mRepositoryShouldOnlyBeCalledFromProperty == null) {
mRepositoryShouldOnlyBeCalledFromProperty =
new ExtensionRepository();
}
}
}
return mRepositoryShouldOnlyBeCalledFromProperty;
}
}
有关此实现的更多信息,请参阅此处或此处。它只是确保只创建一个 `ExtensionRepository` 实例。
扩展器
同样,这是一段非常不起眼的 C# 代码。`Extender` 仅持有对要扩展对象的引用,并提供一组方法来实现扩展此对象的不同方式
public class Extender<T> where T:class {
private readonly T mExtendee;
internal Extender(T extendee) {
Contract.Requires<ArgumentNullException>(extendee != null);
mExtendee = extendee;
}
public dynamic Dynamically() {
return With<DynamicPropertyExtension>();
}
public TE With<TE>() where TE:class, new() {
return Repository.GetExtension<TE>(mExtendee);
}
}
正如您所看到的,实际工作已委托给存储库。为此,`With<TE>` 强制 `TE` 提供一个无参构造函数,因为存储库必须在第一次请求时创建一个新的 `TE` 实例。此外,您可以看到 `Dynamically()` 只是调用 `With<TE>` 并传入一个预定义的类型——`DynamicPropertyExtension`,我们稍后会再讨论。
您可能会想,为什么我要引入 `Extender` 而不是直接在 `UniversalTypeExtender` 上实现适当的扩展方法。我希望 `Extended()` 扩展方法仅限于引用类型。因此,我必须定义 `Extended<T>()`,其中 `T` 仅限于类。您不必考虑 `T`——只需键入 `Extended()`。但如果我也必须定义 `TE`(来自 `With<TE>()`)——例如 `Extended<T, TE>()`——那么您将在每次调用时都必须指定 `T`。我不喜欢这样,仅此而已 。将 `Extender` 视为一个微型流畅接口的一部分。
但让我们继续讨论 `ExtensionRepository`...
扩展存储库
存储库持有一个 `RepositoryItem` 的字典。如上所示,`Extender` 调用 `GetExtension<T>()`
public T GetExtension<T>(object forInstance) where T : class, new() {
return Get(forInstance).GetExtension<T>();
}
该方法又调用 `Get<T>()`
public RepositoryItem Get<T>(T forInstance) where T:class {
lock (mSyncRoot) {
return GetOrNew(forInstance);
}
}
此方法锁定存储库,以确保只有一个线程可以访问用于调用 `GetOrNew` 方法的内部项字典。
private readonly Dictionary<RepositoryItemTargetKey, RepositoryItem> mItems = new
Dictionary<RepositoryItemTargetKey,RepositoryItem>();
private RepositoryItem GetOrNew(object forInstance) {
RepositoryItem item;
if (mItems.TryGetValue(new RepositoryItemTargetKey(forInstance), out item)) {
return item;
}
return NewItem(forInstance);
}
private RepositoryItem NewItem(object forInstance) {
RepositoryItem item = new RepositoryItem(forInstance);
mItems.Add(item.Key, item);
StartGCTimer();
return item;
}
使用锁定是因为存储库必须确保调用获得项字典的“安全”视图。想象一下,一个调用会创建一个新项,因为请求的项还不存在。同时——在将新项添加到字典之前——另一个调用会做同样的事情。结果将是同一个对象的两个扩展项!但这将绕过我们的概念——所以我们锁定。
`GetOrNew` 查找属于请求实例的项。如果找不到项,则会创建它。否则,它将被返回。
垃圾回收
在我们检查 `RepositoryItem` 之前,我们将看一下垃圾回收。这是 .NET 中释放未使用的对象内存的方式。如果您以前从未听说过,您可以在此处获取更多信息。重要的是要知道它会删除“未使用的”对象。这意味着不再引用的对象。现在,再次看看我们的存储库:所有创建的项都存储在内部项字典中。这意味着这个字典会越来越大。因为所有这些项至少有一个引用——至少来自字典本身——框架不会自动进行垃圾回收。所以我们必须自己处理这个问题。当然,我们做到了!如果您深入研究 `ExtensionRepository` 的代码,您会注意到有一个计时器会不时地进行垃圾回收
public void CollectGarbage() {
lock (mSyncRoot) {
List<KeyValuePair<RepositoryItemTargetKey,
RepositoryItem>< itemsToRemove = mItems.Where(item =>
item.Value.CouldBeCollectedByGC).ToList();
foreach (var item in itemsToRemove) {
mItems.Remove(item.Key);
}
if (mItems.Count == 0) {
StopGCTimer();
}
}
}
此方法会询问每个项是否可以被收集(项本身如何知道这一点稍后会说明),如果可以,则将其删除。再次,注意锁定!您可以自己弄清楚计时器何时开始和停止。您可以通过设置 `UniversalTypeExtender` 的 `CollectGarbageIntervalInMilliseconds` 属性来定义垃圾回收的时间间隔。默认值为 15,000 毫秒。
垃圾回收似乎只是本文的一小部分,但它确实是 `UniversalTypeExtender` 的重要组成部分。
存储库项
现在我们来检查 `RepositoryItem`。它持有对已扩展对象的引用以及为此对象请求的所有扩展的引用。正如您所看到的,对已扩展对象的引用被存储为弱引用
private readonly WeakReference mTargetReference;
public RepositoryItem(object target) {
Contract.Requires<argumentnullexception>(target != null);
mTargetReference = new WeakReference(target);
}
如果它被存储为“普通”字段——这意味着强引用——如上所述,它永远不会被垃圾回收。但是 `WeakReference` 允许您持有引用,同时允许在没有其他强引用指向它时对其进行收集。所以,您可以检查引用是否仍然有效
public bool CouldBeCollectedByGC {
get { return !mTargetReference.IsAlive; }
}
很好!当请求扩展时,几乎需要做与存储库中描述的相同的事情:锁定、在列表中搜索、创建实例等等。
private readonly object mSyncRoot = new object();
private readonly List<object> mExtensions = new List<object>();
public T GetExtension<T>() where T : class, new() {
lock (mSyncRoot) {
return GetExtensionOrNew<T>();
}
}
private T GetExtensionOrNew<T>() where T : class, new() {
object extension;
for (int i = 0; i < mExtensions.Count; i++) {
extension = mExtensions[i];
if (extension is T) {
return (T) extension;
}
}
return NewExtension<T>();
}
private T NewExtension<T>() where T : class, new() {
T newExtension = new T();
mExtensions.Insert(0, newExtension);
return newExtension;
}
存储库项目标键
`RepositoryItemTargetKey` 用作存储库内部字典的键。对于弱引用生成正确的键是必需的,这是 `UniversalTypeExtender` 的基本功能。在我最初的版本中,有一个简单的列表保存了这些项。然后,Mike Marynowski 向我指出了字典,并给了我如何使用它进行弱引用的想法。这给我的实现带来了巨大的性能提升。谢谢你,Mike!简而言之,`RepositoryItemTargetKey` 将 `GetHashCode` 和 `Equals` 方法委托给弱引用。有关更多详细信息,我建议您查看代码并阅读本文下方的 Mike 的评论。
动态属性扩展
如开头所示,您可以通过调用 `theIncomingReference.Extended().Dynamically()` 来扩展对象。然后,您可以输入任何您想要的属性名称,并将其设置为任何您喜欢的任何值。这是奇迹吗(老派弗莱迪)?如果您了解 .NET 4 中的`DynamicObject`,您已经知道答案,当然是:不是!`DynamicObject` 允许您实现动态属性访问。有一些特殊方法可以重写,构建属性的 getter 和 setter。在这种情况下,值存储在一个私有列表中——当然带有它们的属性名。
private readonly Dictionary<string, object> mProperties = new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result) {
return TryGetPropertyValue(binder.Name, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value) {
SetPropertyValue(binder.Name, value);
return true;
}
private bool TryGetPropertyValue(string propertyName, out object result) {
return mProperties.TryGetValue(propertyName, out result);
}
private void SetPropertyValue(string propertyName, object value) {
mProperties[propertyName] = value;
}
您应该意识到这里只处理一种对象类型——实际上不是类型安全的!它也支持使用索引作为 getter 和 setter。但这几乎是相同的。所以,如果您愿意,可以查看源代码。就这样!您已经完成了整个故事!
使用代码
该代码使用了 Code Contracts 和 FluentAssertions 进行测试。因此,您必须同时安装它们——都是免费的——才能编译和测试源代码。但是最终的 DLL 可以在 `bin` 文件夹中找到——准备好使用。只需引用它并将其与您的最终二进制文件一起分发。 `UniversalTypeExtender` 是一个扩展方法。您必须引用 `TB.ComponentModel` 命名空间才能使用它。
局限性
- 如前所述,它仅适用于引用类型。
- 您想通过 `With<TE>()` 方法使用的扩展必须实现一个无参默认构造函数。
- 正如您所见,对象不会以任何方式被修改。扩展只是存储在一个静态列表中。这意味着,例如,序列化将不起作用。因此,将已扩展对象放入 ASPX 页面的 ViewState 中——希望在回发时取回扩展——当然行不通。而将对象存储在 Session 中应该没问题。
性能
仅供参考,我做了一个小设置来测试性能。此测试是在 2.67 GHz 的 Core Duo 处理器、3 GB RAM 上进行的。在测试期间,创建了 10,000 个对象,每个对象都被扩展。然后再次循环这 10,000 个对象,并读取每个对象的扩展。 `UniversalTypeExtender` 的垃圾回收在测试期间被调用。测试的平均持续时间约为 0.8 秒——总共 20,000 次调用!仅作比较:我最初的实现——使用列表而不是字典——在同一测试中花费了 8.5 秒。
结论
如果框架不支持某些内容,您将不得不自己处理 。
值得关注的点
在使用 WeakReferences 时,我不得不在我的单元测试中处理它们。这意味着我必须控制它们被垃圾回收,以便验证我代码中的某些操作。通常,您不能——也不应该!——自己控制收集时间。您可以信任运行时——它最清楚何时执行此操作。但为了测试,您必须控制它。因此,您可以看到我在某些测试中调用 `GC.Collect()`。
Sacha Barbar 指出了 `ConditionalWeakTable` 类,它是一种字典,实现了我需要的功能。这意味着,将对象存储为弱引用并进行垃圾回收。这似乎是最好的解决方案,也许我将来会更改它。感谢 Sacha 提供如此棒的信息。