C# has been declared the Programming Language of 2023 so it is the perfect time to explore some features of the language and of .Net. In this post which serves to kick-off a series titled C# Alchemy, we'll take a look at KeyedCollection
, an abstract base class from the Collections framework. KeyedCollection
is useful when you need to map a key to value but the key itself is embedded inside the value, for example, a library catalog where the ISBN of each book serves as the key.
The Basics
To illustrate the usefulness of KeyedCollection
, let's build a Pokédex—one of the most important advancements in modern technology! First we define our Pokemon class:
public class Pokemon {
public Pokemon(string name, int hp, string type) {
Name=name;
Type=type;
HP=hp;
}
public readonly string Name;
public readonly int HP;
public readonly string Type;
}
Since each species of Pokémon has a unique name, we'll use it as the key for our Pokédex. To create the Pokédex, we will extend KeyedCollection
. Notice from the class definition of KeyedCollection
that it has two type parameters, one for the data type of the key and one for the data type of the data itself:
public abstract class KeyedCollection<TKey,TItem> :
System.Collections.ObjectModel.Collection<TItem>
Assuming you are familiar with using Dictionary
for key to value mappings, the immediate benefit of KeyedCollection
when the key is a member of the value should be clear. To define the Pokédex, extend KeyedCollection
using string
and Pokemon
as the type parameters:
using System.Collections.ObjectModel;
public class Pokedex : KeyedCollection<string,Pokemon> {
override protected string GetKeyForItem(Pokemon pokemon) {
return pokemon.Name;
}
}
As I am neither a Pokémon Master nor Trainer, I deferred to my kids for a starter pack of some Pokémon to add to our Pokédex:
List<Pokemon> myPokemon = new() {
new Pokemon("Eevee", 55, "Normal"),
new Pokemon("Pikachu", 35, "Electric"),
new Pokemon("Vaporeon", 130, "Water"),
new Pokemon("Lilligant", 70, "Grass"),
new Pokemon("Floragato", 61, "Grass")
};
Pokedex pokedex = new();
myPokemon.ForEach(
pokemon => pokedex.Add(pokemon));
Console.Out.WriteLine(pokedex.Count());
5
Lets add more:
pokedex.Add(new Pokemon("Sandaconda", 72, "Ground"));
pokedex.Add(new Pokemon("Eevee", 55 , "Normal"));
Since Eevee is already in the Pokédex and KeyedCollection
follows the conventions for a Collection, an exception is thrown when trying to add a Pokémon that is already in the Pokédex:
Error: System.ArgumentException: An item with the same key has already been added. Key: Eevee
Use the Contains
method to check if the Pokémon is recorded in the Pokédex:
bool havePikachu = pokedex.Contains("Pikachu");
Console.Out.WriteLine(havePikachu);
True
Remove a Pokémon from the Pokédex with Remove
:
pokedex.Remove("Pikachu");
bool havePikachu = pokedex.Contains("Pikachu");
Console.Out.WriteLine(havePikachu);
False
Pokémon can be accessed by name. Lets add an extension method to the Pokemon
class for printing it and demonstrate retrieval. You may also iterate over every Pokémon in the Pokédex:
public static void Print(this Pokemon pokemon) {
Console.Out.WriteLine($"{pokemon.Name}");
Console.Out.WriteLine($"HP: {pokemon.HP}");
Console.Out.WriteLine($"{pokemon.Type} Type");
}
Pokemon eevee = pokedex["Eevee"];
eevee.Print();
Eevee
HP: 55
Normal Type
foreach(Pokemon pokemon in pokedex) {
Console.Out.WriteLine("---");
pokemon.Print();
}
---
Eevee
HP: 55
Normal Type
---
Vaporeon
HP: 130
Water Type
---
Lilligant
HP: 70
Grass Type
---
Floragato
HP: 61
Grass Type
---
Sandaconda
HP: 72
Ground Type
LINQ operations may also be used on the Pokédex to easily filter, query, and transform data:
Count the number of Pokémon with HP larger than 70:
pokedex.Where(pokemon=>pokemon.HP > 70).
Count()
2
Return the value of the highest HP in the Pokédex:
pokedex.Max(pokemon=>pokemon.HP)
130
Using Generics
The type parameters may also take advantage of generics to extend capabilities. Here, we'll create an interface to encapsulate our Pokémon and use it to specialize based on the type of the Pokémon species. The Pokédex will still use the name of a Pokémon as its key, but the data type of the Pokémon entry is deferred until the Pokédex is created. We'll constrain the type parameter of the Pokémon using the interface type:
public interface IPokemon {
string Name {get;}
int HP {get;}
string Type {get;}
}
public class WaterPokemon : IPokemon {
private readonly string _name;
private readonly int _hp;
public WaterPokemon(string name, int hp) {
_name=name;
_hp = hp;
}
public string Name => _name;
public int HP => _hp;
public string Type => "Water";
}
public class GrassPokemon : IPokemon {
private readonly string _name;
private readonly int _hp;
public GrassPokemon(string name, int hp) {
_name=name;
_hp = hp;
}
public string Name => _name;
public int HP => _hp;
public string Type => "Grass";
}
public class Pokedex<TPokemon> : KeyedCollection<string,TPokemon>
where TPokemon : IPokemon {
override protected string GetKeyForItem(TPokemon pokemon) {
return pokemon.Name;
}
}
WaterPokemon vaporeon = new WaterPokemon("Vaporeon", 130);
GrassPokemon lilligant = new GrassPokemon("Lilligant", 70);
GrassPokemon florogato = new GrassPokemon("Floragato", 61);
Pokedex<WaterPokemon> waterPokemon = new();
Pokedex<GrassPokemon> grassPokemon = new();
Pokedex<IPokemon> allPokemon = new();
waterPokemon.Add(vaporeon);
grassPokemon.Add(lilligant);
grassPokemon.Add(florogato);
allPokemon.Add(vaporeon);
allPokemon.Add(lilligant);
allPokemon.Add(florogato);
Final Thoughts and Caveats
KeyedCollection
provides a simple, expressive way to do a key to value mapping when the key is a member of the value itself. Operations on the collection are constant time and uniqueness is guaranteed and enforced by the API. The unique key is a natural identifier for the data, allowing for cleaner, more readable and intuitive code. It doesn't come with any major drawbacks but if you were paying attention to the code then you might have noticed the use of the readonly
modifier. KeyedCollection
does come with one big caveat - immutability. When using KeyedCollection
you want to make sure that your keys are immutable.
Now go make some KeyedCollection
magic. You can find a Notebook with the code here.
Top comments (0)