7

从 .NET Framework 3.5 开始,开发人员已经能够添加可从任何对象类型的实例调用的扩展方法。但是,扩展属性尚未在 C# 中实现。与扩展方法不同,扩展属性将涉及为单个对象存储一些额外的状态信息。

然而,即使对于扩展方法,在某些编程场景中,能够访问调用这些扩展方法的对象的后添加/扩展信息也是非常有用的。

这是最初的问题: 如何在 C# 中添加扩展属性,或者在对象上设置扩展数据?

4

3 回答 3

11

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);
    }
}

我希望这些想法是有用的。一个人并不总是拥有扩展一个类的奢侈,并且在某些情况下,如果一个人不必在每个方法调用中组装和传递额外信息,那么它可能会使扩展方法的设计更清晰,对象可能没有完全是在开发人员的代码库中创建的。

于 2013-06-23T19:30:26.280 回答
0

我在想类似的事情:

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 的评论。

于 2013-06-23T20:29:33.210 回答
0

当然有。您所需要的只是一个可供与数据交互的人访问的字典。每当您需要数据时,您调用 TheDataStore[theObject] 并取回包含您想要的任何属性的值对象。

在抽象级别,这样的属性存储是附加属性在 WPF 中的工作方式。

于 2013-06-23T19:38:49.340 回答