从 .NET Framework 3.5 开始,开发人员已经能够添加可从任何对象类型的实例调用的扩展方法。但是,扩展属性尚未在 C# 中实现。与扩展方法不同,扩展属性将涉及为单个对象存储一些额外的状态信息。
然而,即使对于扩展方法,在某些编程场景中,能够访问调用这些扩展方法的对象的后添加/扩展信息也是非常有用的。
这是最初的问题: 如何在 C# 中添加扩展属性,或者在对象上设置扩展数据?
从 .NET Framework 3.5 开始,开发人员已经能够添加可从任何对象类型的实例调用的扩展方法。但是,扩展属性尚未在 C# 中实现。与扩展方法不同,扩展属性将涉及为单个对象存储一些额外的状态信息。
然而,即使对于扩展方法,在某些编程场景中,能够访问调用这些扩展方法的对象的后添加/扩展信息也是非常有用的。
这是最初的问题: 如何在 C# 中添加扩展属性,或者在对象上设置扩展数据?
System.Runtime.CompilerServices.ConditionalWeakTable类似乎正是医生所要求的,并且似乎不会引起其他方法可能引发的那种内存泄漏担忧。以下是我使用 ConditionalWeakTable 的第一个简单包装器。我会更好地隐藏它们(使它们在内部并且更隐晦地命名)并将其他方法放在它们前面,但这很有效,对我来说是一个很大的解脱和帮助。
(感谢 svick、Jeppe Stig Nielsen、Tormod 和 user2246674 帮助我思考这个问题。)
public static class ExtensionMethods
{
private static System.Runtime.CompilerServices.ConditionalWeakTable<object, object> extendedData = new System.Runtime.CompilerServices.ConditionalWeakTable<object, object>();
internal static IDictionary<string, object> CreateDictionary(object o) {
return new Dictionary<string, object>();
}
public static void SetExtendedDataValue(this object o, string name, object value) {
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
name = name.Trim();
IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, ExtensionMethods.CreateDictionary);
// if (values == null)
// extendedData.Add(o, values = new Dictionary<string, object>()); // This doesn't seem to be necessary!
if (value != null)
values[name] = value;
else
values.Remove(name);
}
public static T GetExtendedDataValue<T>(this object o, string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
name = name.Trim();
IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, ExtensionMethods.CreateDictionary);
// if (values == null) // ... nor does this!
// return default(T);
// else
if (values.ContainsKey(name))
return (T)values[name];
else
return default(T);
}
internal static object GetExtendedDataValue(this object o, string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
name = name.Trim();
IDictionary<string, object> values = (IDictionary<string, object>)extendedData.GetValue(o, null);
if (values == null)
return null;
else if (values.ContainsKey(name))
return values[name];
else
return null;
}
}
(编辑:出于历史目的,原始答案如下。)
System.ComponentModel.TypeDescriptor.GetAttributes(object) 方法公开了已添加到指定对象的 System.Attribute 对象的集合。因此,如果将属性添加到能够存储键值对的对象(但不是结构或枚举)中,则可以通过扩展方法访问这些对,从而对调用代码隐藏存储机制。由于不可避免的方法 pcall 语法,这不像扩展属性那样干净,但在某些编程场景中仍然有用。
由于存储数据的对象必须从 System.Attribute 继承,并且事先不知道需要存储什么类型的数据,一个直接的解决方案是创建一个既继承自 System.Attribute 又实现 IDictionary 的类。然后可以制作易于使用的扩展方法来包装此类的使用,从而进一步简化扩展数据的存储和检索。
以下是一种此类实现的代码:
/// <summary>
/// A System.Attribute which is also an IDictionary, useful for adding extension data to
/// individual objects, no matter the type
/// </summary>
public class ExtensionDataAttribute : System.Attribute, IDictionary<string, object>
{
// The dictionary wrapped by this collection, which cannot extend by System.Attribute and Dictionary at once
private IDictionary<string, object> data = new Dictionary<string, object>();
/// <summary>
/// Adds this collection of extension data to the specified object; should be called only once
/// </summary>
/// <param name="o">The object to which to add this collection of extension data</param>
public void AddTo(object o) {
System.ComponentModel.TypeDescriptor.AddAttributes(o, this);
}
// Following are encapsulated calls to the wrapped dictionary, which should need no explanation;
// after accessing an ExtensionDataAttribute instance, simply use it as an IDictionary<string, object>
public void Add(string key, object value)
{
data.Add(key, value);
}
public bool ContainsKey(string key)
{
return data.ContainsKey(key);
}
public ICollection<string> Keys
{
get { return data.Keys; }
}
public bool Remove(string key)
{
return data.Remove(key);
}
public bool TryGetValue(string key, out object value)
{
return data.TryGetValue(key, out value);
}
public ICollection<object> Values
{
get { return data.Values; }
}
public object this[string key]
{
get
{
return data[key];
}
set
{
data[key] = value;
}
}
public void Add(KeyValuePair<string, object> item)
{
data.Add(item);
}
public void Clear()
{
data.Clear();
}
public bool Contains(KeyValuePair<string, object> item)
{
return data.Contains(item);
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
data.CopyTo(array, arrayIndex);
}
public int Count
{
get { return data.Count; }
}
public bool IsReadOnly
{
get { return data.IsReadOnly; }
}
public bool Remove(KeyValuePair<string, object> item)
{
return data.Remove(item);
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return data.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return data.GetEnumerator();
}
}
...以及一些通用的扩展方法来进一步包装它:
/// <summary>
/// Extension methods for setting and getting extension data for individual objects, no matter the type
/// </summary>
public static class ExtensionDataAttributeExtensions {
public static void SetExtensionDataAttributeValue(this object o, string name, object value)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(o))
if (a is ExtensionDataAttribute)
{
((ExtensionDataAttribute)a)[name] = value;
return;
}
ExtensionDataAttribute extensionData = new ExtensionDataAttribute();
extensionData[name] = value;
extensionData.AddTo(o);
}
public static T GetExtensionDataAttributeValue<T>(this object o, string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(o))
if (a is ExtensionDataAttribute)
return (T)((ExtensionDataAttribute)a)[name];
return default(T);
}
public static object GetExtensionDataAttributeValue(this object o, string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(o))
if (a is ExtensionDataAttribute)
return ((ExtensionDataAttribute)a)[name];
return null;
}
public static void RemoveExtensionDataAttributeValue(this object o, string name) {
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Invalid name");
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(o))
if (a is ExtensionDataAttribute)
((ExtensionDataAttribute)a).Remove(name);
}
}
...最后,还有两个自定义扩展方法示例,可在实际代码中使用此想法。一个直接使用 ExtensionDataAttribute 类(因此更具体一些),另一个使用上面提供的通用扩展方法:
/// <summary>
/// Extension methods showing samples of using the ExtensionDataAttribute class directly, for use
/// in situations where it is undesirable to include the extension methods provided with that class
/// </summary>
public static class ExtensionMethodsExample1 {
/// <summary>
/// Adds a description to the specified string object
/// </summary>
/// <param name="s">The string to describe</param>
/// <param name="description">The description to set</param>
public static void SetDescription(this string s, string description) {
if (string.IsNullOrWhiteSpace(description))
description = "";
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(s))
if (a is ExtensionDataAttribute) {
((ExtensionDataAttribute)a)["Description"] = description;
return;
}
ExtensionDataAttribute extensionData = new ExtensionDataAttribute();
extensionData["Description"] = description;
extensionData.AddTo(s);
}
/// <summary>
/// Gets the description for the specified string, if it has one;
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static string GetDescription(this string s) {
foreach (Attribute a in System.ComponentModel.TypeDescriptor.GetAttributes(s))
if (a is ExtensionDataAttribute) {
ExtensionDataAttribute eda = (ExtensionDataAttribute)a;
if (eda.ContainsKey("Description"))
return eda["Description"].ToString();
else
return "";
}
return "";
}
}
/// <summary>
/// Extension methods encapsulating calls to extension methods provided with the ExtensionDataAttribute
/// class, demonstrating increased ease of implementing one's own extension data
/// </summary>
public static class ExtensionMethodsExample2 {
public static string GetDescription(this string s)
{
return s.GetExtensionDataAttributeValue<string>("Description");
}
public static void SetDescription(this string s, string description)
{
s.SetExtensionDataAttributeValue("Description", description);
}
}
我希望这些想法是有用的。一个人并不总是拥有扩展一个类的奢侈,并且在某些情况下,如果一个人不必在每个方法调用中组装和传递额外信息,那么它可能会使扩展方法的设计更清晰,对象可能没有完全是在开发人员的代码库中创建的。
我在想类似的事情:
static class Ext
{
static readonly Dictionary<YourType, int> fooValues = new Dictionary<YourType, int>();
public static int GetFoo(this YourType yt)
{
int value;
fooValues.TryGetValue(yt, out value);
return value;
}
public static void SetFoo(this YourType yt, int value)
{
fooValues[yt] = value;
}
}
当然,这只是一个草图。如果YourType
(或从它派生的任何类)覆盖Equals
并且GetHashCode
您可能希望为fooValues
数据存储提供自定义相等比较器。Dictionary<,>
不是线程安全的,因此如果需要并行性,请使用另一个集合或使用锁。
每个像这样“扩展”的实例永远不会被垃圾收集可能是一个问题。您可以尝试以某种方式使用弱引用。请参阅下面 svick 的评论。
当然有。您所需要的只是一个可供与数据交互的人访问的字典。每当您需要数据时,您调用 TheDataStore[theObject] 并取回包含您想要的任何属性的值对象。
在抽象级别,这样的属性存储是附加属性在 WPF 中的工作方式。