How can additional data be associated with existing objects using extension methods?

依然范特西╮ 提交于 2019-12-05 06:41:57

The System.Runtime.CompilerServices.ConditionalWeakTable class seems to be just what the doctor ordered, and doesn't seem to entail the sort of memory leak worries that other approaches might raise. Following is my first simple wrapper around the use of ConditionalWeakTable. I will hide them a bit better (make them internal and more obscurely named) and put other methods in front of them, but this works and is a big relief and help to me.

(Thanks to svick, Jeppe Stig Nielsen, Tormod, and user2246674 for helping me think this through.)

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

(EDIT: The original answer follows, for historical purposes.)

The System.ComponentModel.TypeDescriptor.GetAttributes(object) method exposes a collection of System.Attribute objects that have been added to the specified object. Thus if an attribute were added to an object (but not a struct or enum), capable of storing key-value pairs, those pairs could be accessed via extension methods, hiding the storage mechanism from calling code. This is not quite as clean as extension properties would be, because of the unavoidable method pcall syntax, but is still useful in certain programming scenarios.

Since the object storing the data must inherit from System.Attribute, and it is unknown in advance what type of data will need to be stored, a straightforward solution is to create a class that both inherits from System.Attribute and implements IDictionary. Easy-to-use extension methods can then be made to wrap the use of this class, simplifying further the storage and retrieval of extension data.

Following is code for one such implementation:

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

... and some generic extension methods to wrap it up a bit further:

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

}

... and, finally, two examples of custom extension methods to use this idea in real-world code. One uses the ExtensionDataAttribute class directly (and is thus a little more nuts-and-bolts), the other uses the generic extension methods provided above:

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

I hope that these ideas have been useful. One doesn't always have the luxury of extending a class, and in some situations it may make the design of extension methods cleaner if one does not have to assemble and pass in extra information with each method call, on an object which may not have been created in the developer's codebase at all.

Sure there is. All you need is a Dictionary that is accessible to whoever interacts with the data. Whenever you need the data you call TheDataStore[theObject] and get back the value object containing whatever properties you desire.

At the abstract level, such a property store is how attached properties work in WPF.

I was thinking something like:

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

This is just a sketch, of course. If YourType (or any class derived from it) overrides Equals and GetHashCode you might want to give a custom equality comparer to the fooValues data store. The Dictionary<,> is not thread-safe, so if parallelism is needed use another collection or use locks.

It's probably a problem that each instance "extended" like this will never be garbage collected. You could try to use weak references instead, somehow. See comment by svick below.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!