简体   繁体   中英

Handling mutable collection keys in C#

Let's say I have a simple class Cat in C#, with a Name property of type string . Now I need a collection class for my cats, so I decide to wrap a Dictionary<string, Cat> in a custom collection class. Basically, this class holds a private dictionary variable and adds or removes collection members as necessary, as well as indexing cats by their name:

class Cats
{
    private Dictionary<string, Cat> m_dic = new Dictionary<string,Cat>();

    public void Add(Cat c)
    {
        m_dic.Add(c.Name, c);
    }

    public void Remove(string name)
    {
        m_dic.Remove(name);
    }

    public Cat this[string name]
    {
        get
        {
            return m_dic[name];
        }
    }
}

Now I can create a collection and cats to it, like this:

Cats cs = new Cats();
cs.Add(new Cat("Valentina"));
cs.Add(new Cat("Sophie"));
cs.Add(new Cat("Tomboy"));

And I can retrieve a cat from its name:

Cat c1 = cs["Sophie"];

All this is very good. The problem is, when I change the name of a cat, like this:

c1.Name = "Sofia";

...the collection key for the object referred to by c1 is not updated, evidently. So, if I try to retrieve the same item using its new name, I get an exception:

Cat c2 = cs["Sofia"]; //KeyNotFoundException is thrown here.

This is correct and obvious behavior by the runtime. My question is: can you suggest an elegant and reliable method to alter the collection keys whenever the name property of an element changes?

My objective is to be able to retrieve, at any time, an item from its name, as you can imagine. I have approached the problem by having the setter of the Name property raise an event so that any collection holding that object can update the corresponding key. This method is quite cumbersome and not very efficient, though.

Can you think of anything better? Thank you.

How large is your collection going to be, and how important is being able to retrieve an item through an index?

If it's going to be relatively small (several hundred, as opposed to thousands), you might be better off using a List<Cat> , and access them using the new LINQ extension methods, like:

public Cat this[string name]{
    get{
        //Will return the first Cat in the list (or null if none is found)
        return m_List.Where(c => c.Name == name).FirstOrDefault();
    }
}

Adding (and deleting) is also trivial:

public void Add(Cat c){
    m_List.Add(c);
}

Let me know if this will not work for you. Hope this helps!

...by having the setter of the Name property raise an event...

Do you mean something like this?

c1.Name = "Sofia";
NameChangedEventHandler handler = NameChangedEvent;
if (handler != null)
{
    handler(c1, new NameChangedEventArgs("Sophie", "Sophia"));
}

Is this what you mean by having the setter raise an event? If so, then I would suggest moving this to the Name property setter of the Cat class. There's no reason to require the setters to raise the event like this. It should be done implicitly when the name of the Cat changes through the public property.

To me, this is an elegant solution; it just doesn't conform to the way Dictionary collections work. I don't know that that's a problem per se, but it does tightly couple the Cats collection to the Cat class.

Keep in mind that you'll probably want to implement many of the same interfaces that the generic Dictionary class does. Otherwise, the Cats collection will resemble a Dictionary in some ways, but not fully.

EDIT: This is in response to your comment. I hope that I can more clearly convey my thoughts. My intent is to improve your design.

I agree that, in general , events do provide a looser level of coupling. However, in this case, the Cats collection is still tightly coupled with the Cat class because the collection is registering with a specific type of event exposed by a specific type of class.

So how can this be improved?

A straightforward way to improve this is to have the Cat class implement an event that is defined in an interface. .NET provides such an interface for this express purpose - the INotifyPropertyChanged interface in the System.ComponentModel namespace. By implementing this interface in the Cat class, this would allow the Cats collection wrapper to be defined like this:

class Cats
{
    private Dictionary<string, INotifyPropertyChanged> m_dic =
        new Dictionary<string, INotifyPropertyChanged>();
    public void Add(INotifyPropertyChanged obj)
    {
        m_dic.Add(obj.Name, obj);
    }
    public void Remove(string name)
    {
        m_dic.Remove(name);
    }
    public INotifyPropertyChanged this[string name]
    {
        get { return m_dic[name]; }
    }
}

See the improvement? The collection is more flexible now. It can hold any type that implements the INotifyPropertyChanged interface. In other words, it is not tied to the Cat class anymore.

However, it still has the requirement that whatever value is stored in the dictionary implement a Name property (see the Add() method), so there is still some work to be done.

Ultimately, you are wanting the collection to hold objects that provide a string property to be used as the key value. The solution is to define this as an interface as well.

public interface INotificationKey : INotifyPropertyChanged
{
    string Key { get; set; }
}

Notice that the INotificationKey interface inherits from the INotifyPropertyChanged interface, which allows the collection wrapper to be defined like this:

class NotificationDictionary
{
    private Dictionary<string, INotificationKey> m_dic =
        new Dictionary<string, INotificationKey>();
    public void Add(INotificationKey obj)
    {
        m_dic.Add(obj.Key, obj);
    }
    public void Remove(string key)
    {
        m_dic.Remove(key);
    }
    public INotificationKey this[string key]
    {
        get { return m_dic[key]; }
    }
}

This is a substantially more flexible solution. But it still falls short because it does not fully act like a Dictionary should. For example, as defined, the NotificationDictionary class cannot be used in a foreach iteration since it does not implement the IEnumerable<> interface.

To qualify as a truly elegant solution, the collection should behave like a Dictionary . This will require a little more effort on the front end, but on the back end, you'd have a solution that would be flexible enough to adapt to a variety of situations.

你也可以在Cat上进行回调,这样当它的Name属性发生变化时,你的收藏就会得到通知

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM