C# 简易扩展属性
(v2) 如何让您的C#对象实时携带扩展属性,而无需修改其代码。V2包含一个可处置属性的收集器。
引言
众所周知,C# 4.0确实支持所谓的“扩展方法”。它们本质上是编译器支持的语法糖,它在语法上帮助您相信,您已经通过更多方法“扩展”了实例的类型,而无需从其继承,即使该类型本身是密封的。
但就目前而言,尚不支持“扩展属性”。在许多场景中,拥有这样的功能将非常有用,基本上在任何您需要某些对象携带运行时生成的信息的情况下——无论它们的类型如何,或者它们是否密封,甚至您是否可以访问其源代码。
到目前为止,这些情况通过创建某种从对象类型继承的包装器,使用代理工厂(AOP/IoC风格)、混合(mixins)、反射和发出(emit)、动态(dynamics)等方式解决。这些都是极其强大的替代方案,互联网上有大量信息可以帮助您构建自己的库,或者使用任何可用的库。但这些通常也很复杂和重量级,并且它们往往会对性能产生实质性影响。当您需要处理未知数量的不同类型(可能很大)时,那么尝试在运行时使用反射和发出生成这些包装器将是一个问题……
现在,我一直坚信已经存在一种为实例即时添加属性的机制,而无需创建包装器来扩展类型和实例化代理:您只需使用可视化设计器即可看到,几乎对于它使用的任何控件,它都显示了比原始控件更多的“属性”。
它确实存在:它基于TypeDescriptor
类。除了许多其他优点之外,它还允许我们在运行时将属性附加到任何类实例——请注意,这并不意味着它会以任何方式修改类型的结构:它只是将这个新属性添加到实例携带的内部属性列表。
利用这一事实,我们将使用自定义属性来存储所需信息。与标准属性不同,标准属性在编写(或动态创建)时固定在类型的元数据中,我们将这些扩展属性定义为键/值对,您可以在运行时将其附加到任何给定对象,以便以后可以像处理任何对象的标准属性一样,访问它们以设置或检索它们存储的值。这些键/值对将存储在我们按需附加到对象实例的自定义属性实例中。然后我们将构建一些方法,使我们能够轻松访问这些内容。请注意,该解决方案只是模拟,因为它无法完全模仿原生属性的语法——这就是为什么我更喜欢称它们为“元属性”。
使用解决方案
在深入了解解决方案的细节之前,让我们先看看如何使用它。假设我们有一个对象实例,我们想用一个名为“MyNewProperty
”的新元属性来扩展它,并且我们想用一个string
值来设置它,然后检索它。所有这些魔法都像以下操作一样简单
using MB.Tools;
···
// Instance is of whatever type, as far it is a class, and not a struct or enum
instance.SetMetaProperty( "MyNewProperty", "James Bond" );
···
var value = instance.GetMetaProperty( "MyNewProperty" );
Console.WriteLine( "Value: {0}", value );
···
- 扩展方法
SetMetaProperty(name, value)
用于通过使用名称和要存储的值将元属性附加到调用它的宿主实例来创建元属性。如果元属性已经存在,其旧值将替换为新值,并且如果可能,此旧值将被处置(稍后会详细介绍)。 - 第二个主要的扩展方法是
GetMetaProperty(name)
。它返回存储在给定名称的元属性中的值,如果该元属性不存在则抛出异常。
请注意,属性名称只是普通的string
s,我选择拦截的唯一限制是它们不能为null
或空。但与标准属性不同,它们接受任何string
作为其名称——在内部,这些string
s用作定位元属性的键。另请注意,在任何情况下,它们都被认为是区分大小写的。
更多有用方法
还有一些其他扩展方法(与上述方法一样,它们也扩展了System.Object
类型)
bool TryGetMetaProperty(name, out value)
:如预期那样,仅当元属性存在时返回true
,并将其值设置到out
参数中。如果元属性不存在,它只返回false
而不抛出异常。bool HasMetaProperty(name)
:如果调用它的对象带有给定名称的元属性,则返回true
,否则返回false
。IMetaProperty RemoveMetaProperty(name)
:用于从调用它的对象中移除元属性,如果未找到则返回null
,或者返回一个IMetaProperty
实例,该实例可用于访问其内容和状态。其自身的属性有AutoDispose
、PropertyName
和PropertyValue
。void ClearMetaProperties()
:用于移除调用它的对象可能携带的所有元属性,并可能处置它们。List<string> ListMetaProperties()</string>
:返回一个列表(可能为空),其中包含调用它的对象可能携带的元属性的名称。
以下不是扩展方法,而是static
类MetaPropertyExtender
的static
方法
IMetaPropertiesHolder GetMetaPropertiesHolder(obj, bool create)
:返回附加到宿主对象的属性实例,形式为实现IMetaPropertiesHolder
接口的对象。此接口提供IEnumerable<IMetaProperty> MetaProperties
属性,允许您迭代宿主对象可能携带的元属性。
工作原理(基础)
如引言中所述,我们将使用自定义属性,并根据需要使用TypeDescriptor
机制将其附加到宿主对象。此自定义属性是一个内部类,实现了IMetaPropertiesHolder
接口,请注意,如前所述,GetMetaPropertiesHolder()
方法允许您在需要时获取宿主实例携带的该属性。此自定义属性将提供托管支持元属性所需的键/值对的能力。您可以使用其MetaProperties
属性直接访问它们。
当需要时,通常是因为您正在使用SetMetaProperty()
设置一个元属性,将创建一个自定义属性类的实例并使用TypeDescriptor
的AddAttributes()
方法将其附加到宿主对象。在这种情况下,始终会创建一个名为“TypeDescriptionProvider
”的第一个元属性,用于存储添加自定义属性时返回的描述符提供者。它在该解决方案中没有用,但将来可能会有用。
当检索存储在元属性中的值时,通常通过使用GetMetaProperty()
或TryGetMetaProperty()
,通过TypeDescriptor
的GetAttributes()
方法获取宿主实例的扩展属性列表,并定位第一个是自定义属性类实例的属性。如果没有,第一个Get()
方法将抛出异常,而第二个TryGet()
方法只是返回false
。
一旦您检索到这个持有者,剩下的就是操作其内部的键/值对存储,以提供创建拥有扩展元属性幻觉的扩展方法。
显然,实际代码更复杂,因为它拦截了许多错误并抛出了必要的异常,并且为了简化,许多方法都进行了重构。此外,在此V2版本中,代码已修改以考虑下一节中解释的内容。
注意事项
前面已经提到过,但我想强调的是,此解决方案只能扩展类的实例。它不适用于struct
和enum
。
好的一面是,它适用于**任何**类的**任何**实例,而无需您做任何特殊的事情。一旦设置了对namespace
的引用,只需使用扩展方法即可。
使用提供的示例
如果您不想看到跟踪和调试信息,请取消注释“#undef DEBUG
”指令。
版本2的新功能:可处置场景
当宿主实例最终确定时,其元数据也随之最终确定。但是,如果不使用代理或拦截器,则无法保证我们会在给定实例被处置时收到通知。在这种情况下,标准、最简单和最安全的规则是不将需要处置的对象存储在扩展元属性中。
这是一个强有力的限制吗?嗯,这确实取决于您的场景。例如,与其存储一个Connection
对象,为什么不尝试存储它的ConnectionString
属性呢?它不是我们需要在不再需要时处置的非托管资源。如果这是您的场景,您可以使用现有解决方案,而无需使用下面描述的可选机制。
那么,如果您别无选择,只能将以后需要处置的对象存储在元属性中呢?通常,当其宿主对象被处置时。
MetaPropertyExtender
的第二版实现了一个可选机制来帮助您实现类似的目标。它主要包括
- 一个
WeakReference
列表,用于跟踪那些已被扩展且同时实现了IDisposable
的对象,以及 - 一个计时器,它会引发一个事件,用于验证这些对象中哪些仍然存活。如果它们不存活,则会处理它们的元属性,以处置那些实现了
IDisposable
且其AutoDispose
属性设置为True
的值。
注意:此AutoDispose
属性设置为SetMetaProperty()
方法的第三个可选参数,默认值为True
。如果您不希望您的值自动处置,即使它实现了IDisposable
,则将此AutoDispose
属性设置为false
。
它是一个可选机制。它只在您使用MetaPropertyExtender
的StartCollector()
方法时开始跟踪。它接受一个可选参数,让您可以调整事件引发的间隔(以毫秒为单位),以适应您的特定需求、性能限制或您的个人偏好。您可以多次调用此方法,其效果是修改计时器的间隔。
您还可以使用StopCollector()
方法停止收集器——请注意,在这种情况下,内部跟踪对象列表中可能保留的任何对象都不会被处理……但也不会从该列表中移除。因此,如果您再次启动收集器,它们将存在并准备好被处理。
是的,我同意,它不像在那些存储在元属性中的值上调用Dispose()
那样具有确定性(*),但是如果您决定不手动检查宿主实例上是否存在元属性,或者您可能无法更改它们,这是一个可能有所帮助的后备机制。
(*) 首先,计时器滴答存在延迟。但也要注意,即使您在对象上调用了Dispose()
,无论出于何种原因,它在弱引用中被识别为已处置(alive 为 false)之前,似乎长时间保持活动状态。这似乎与GC运行的节奏有关,但我不能完全保证。所以,就目前而言,没有办法立即知道宿主对象上是否已调用Dispose()
。至少,据我所知(或想实现的方式),没有。
历史
- [v1, 2012年5月]:初始版本
- [v1, 2012年5月]:版本2 - 包含一个可选收集器,可在需要时处置存储在元属性中的值