Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# collection indexed by property?

A problem I often run into is needing to store a collection of objects in such a way that I can retrieve them by a particular field/property that is a unique "index" for that object. For example, I have a Person object for which the name field is a unique identifier, and I want to be able to retrieve from some collection of Person objects the Person whose name="Sax Russell". In Java I usually accomplish this by using a Map where I actually want a Set, and always using the "index" field of the object as its key in the map, i.e. peopleMap.add(myPerson.getName(), myPerson). I was thinking of doing the same thing in C# with Dictionarys, like this:

class Person {
    public string Name {get; set;}
    public int Age {get; set;}
    //...
}

Dictionary<string, Person> PersonProducerMethod() {
    Dictionary<string, Person> people = new Dictionary<string, Person>();
    //somehow produce Person instances...
    people.add(myPerson.Name, myPerson);
    //...
    return people;
}

void PersonConsumerMethod(Dictionary<string, Person> people, List<string> names) {
    foreach(var name in names) {
        person = people[name];
        //process person somehow...
    }
}

However, this seems clumsy, and introduces a rather loose coupling between the keys of the Dictionary and its values; I implicitly depend on every producer of Person dictionaries using the Name property as the key under which to store each Person. I have no guarantee that the element at people["Sax Russell"] is actually a Person with Name="Sax Russell" unless I double-check every time I access the dictionary.

Might there be some way to explicitly ensure that my collection of Person objects is indexed by name, using custom equality comparers and/or LINQ queries? It's important that lookup stay constant-time, which is why I can't just use List.Find or Enumerable.Where. I've tried using a HashSet and constructing it with an equality comparer that compares just the Name field of the objects it's given, but there doesn't seem to be any way to then retrieve the Person objects using just their name.

like image 485
Edward Avatar asked Jul 09 '13 00:07

Edward


2 Answers

I'm not sure if there's anything built-in that does what you want, but there's nothing stopping you from wrapping a dictionary specifying the key yourself, and implementing IList<Person>. The key here (no pun intended) is the consumer does not has access to the underlying dictionary so you can be assured the keys are accurate.

Part of the implementation might look like the following, note the custom indexer as well:

public partial class PersonCollection : IList<Person>
{

    //the underlying dictionary
    private Dictionary<string, Person> _dictionary;

    public PersonCollection()
    {
        _dictionary = new Dictionary<string, Person>();
    }

    public void Add(Person p)
    {
        _dictionary.Add(p.Name, p);
    }

    public Person this[string name]
    {
        get
        {
            return _dictionary[name];
        }
    }

}

As a side bonus, you are also free to change the implementation later without having to change the consuming code.

like image 149
lc. Avatar answered Oct 21 '22 15:10

lc.


You can build your own collection backed by a dictionary to accomplish this task. The idea is to store a delegate that takes a Person and returns a string by reading the Name property.

Here is a skeletal solution of such a collection:

public class PropertyMap<K,V> : ICollection<V> {
    private readonly IDictionary<K,V> dict = new Dictionary<K,V>();
    private readonly Func<V,K> key;
    public PropertyMap(Func<V,K> key) {
        this.key = key;
    }
    public void Add(V v) {
        dict.Add(key(v));
    }
    // Implement other methods of ICollection
    public this[K k] {
        get { return dict[k]; }
        set { dict[k] = value; }
    }
}

Here is how to use it:

PropertyMap<string,Person> mp = new PropertyMap<string,Person>(
    p => p.Name
);
mp.Add(p1);
mp.Add(p2);
like image 23
Sergey Kalinichenko Avatar answered Oct 21 '22 14:10

Sergey Kalinichenko