Search code examples
c#linqcollections

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.


Solution

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