Use Lambda expression to create a generic KeyedCollection

The .NET framework ships with an abstract KeyedCollection<TKey, TItem> class.  Basically, all you have to do is implement the GetKeyForItem() method you can could look up any item either by ordinal or a customized key. For example, suppose you had a Person class like this:

   1:  public class Person
   2:  {
   3:      public Guid ID { get; set; }
   4:      public string FirstName { get; set; }
   5:      public string LastName { get; set; }
   6:  }

And suppose you wanted to be able to lookup a Person in the collection either by index or by the FirstName property (yes, a contrived example). You could sub-class the KeyedCollection class like this:

   1:  public class PersonKeyedCollection : KeyedCollection<string, Person>
   2:  {
   3:      protected override string GetKeyForItem(Person item)
   4:      {
   5:          return item.FirstName;
   6:      }
   7:  }

Which would allow you the get a person from the list either by index or by key (in this case the FirstName is my little demo example) like this:

   1:  var list = new PersonKeyedCollection();
   2:  list.Add(new Person() { FirstName = "Steve" });
   3:  list.Add(new Person() { FirstName = "Bob" });
   4:  list.Add(new Person() { FirstName = "Jim" });
   6:  Console.WriteLine("Result by key  : " + list["Steve"].ToString());
   7:  Console.WriteLine("Result by index: " + list[0].ToString());

This is all well and good but it becomes a pain when you want to do this for different types and you end up sub-classing the KeyedCollection class a bunch of times just to be able to specify the property you want to use for the key.  You also don’t want a solution that relies on Reflection by specifying the name of the property and invoking it on each call.  Alternatively, you can create a more re-usable solution utilizing generics and lambdas so you can write your code like this:

   1:  var list = new GenericKeyedCollection<string, Person>(p => p.FirstName);

Now you won’t have to sub-class KeyedCollection ever again because you’ve created a re-usable GenericKeyedCollection class.  Here is the complete code implementation:

   1:  public class GenericKeyedCollection<TKey, TItem> : KeyedCollection<TKey, TItem>
   2:  {
   3:      private Func<TItem, TKey> getKeyFunc;
   5:      protected override TKey GetKeyForItem(TItem item)
   6:      {
   7:          return getKeyFunc(item);
   8:      }
  10:      public GenericKeyedCollection(Func<TItem, TKey> getKeyFunc)
  11:      {
  12:          this.getKeyFunc = getKeyFunc;
  13:      }
  14:  }

There are a couple of key points.  First, we simply utilize the generic System.Func<T, TResult> class that was introduced in .NET 3.5. We use a lambda expression to pass a delegate into the constructor that will be used on each GetKeyForItem() call.  Then inside GetKeyForItem() we simply invoke the delegate passing in the current item.

posted on Friday, January 23, 2009 4:14 PM Print
# re: Use Lambda expression to create a generic KeyedCollection
Luke Sandell
1/25/2009 8:20 AM
Nice job... I have done the exact same thing.

I also becoming a fan of the ToDictionary() and ToLookup() extension methods on IEnumerable for creating custom dictionaries on the fly.
# re: Use Lambda expression to create a generic KeyedCollection
1/27/2009 5:25 PM
Very cool! However, without a null constructor, the collection cannot be serialized using XmlSerializer.
# re: Use Lambda expression to create a generic KeyedCollection
1/27/2009 9:11 PM
Yep, that's true. If you need a default constructor you could do this:
public class Person2KeyedCollection : GenericKeyedCollection<string, Person>
public Person2KeyedCollection() : base(p => p.FirstName) {}

but at that point, there's not a whole lot of difference between that and the "normal" implementation because you're sub-classing either way.
# re: Use Lambda expression to create a generic KeyedCollection
Nelson P. Varghese
12/16/2009 2:47 AM
Nice one. As the KeyedCollection<TKey, TItem> class stores the key in the lookup dictionary internally, the key should be immutable.
# Generic KeyedCollection in VB.NET
Brett Riester
11/18/2010 9:46 AM
Imports System.Runtime.CompilerServices

Public Module modKeyedCollection

<ExtensionAttribute()> _
Public Function ToMyKeyedCollection(Of TKey, TItem)(ByVal lst As IEnumerable(Of TItem), ByVal getKeyForItem As Func(Of TItem, TKey)) _
As MyKeyedCollection(Of TKey, TItem)
Dim col = New MyKeyedCollection(Of TKey, TItem)(getKeyForItem)
For Each item In lst
Return col
End Function

End Module

Public Class MyKeyedCollection(Of TKey, TItem)
Inherits Collections.ObjectModel.KeyedCollection(Of TKey, TItem)

Protected _GetKeyForItem As Func(Of TItem, TKey)
Public Sub New(ByVal getKeyForItem As Func(Of TItem, TKey))
_GetKeyForItem = getKeyForItem
End Sub

Protected Overrides Function GetKeyForItem(ByVal item As TItem) As TKey
Return _GetKeyForItem(item)
End Function

End Class

Post Comment

Title *
Name *
Comment *  

View Steve Michelotti's profile on LinkedIn

profile for Steve Michelotti at Stack Overflow, Q&A for professional and enthusiast programmers

Google My Blog

Tag Cloud