Changing a Type within a Class when you Instantiate: An Intro to Generics
Have you ever run into a problem that you couldn’t describe well enough to even ask about it?
“I want to change a specific type to another type when I instantiate a class to make it more general and reusable…”
“Is this even possible?“
Say you’re designing a multidimensional Dictionary
that holds geographic AreaObjects
public class AreaObjectCollection {
private Dictionary<int, Dictionary<int, List<AreaObject>>> _map =
new Dictionary<int, Dictionary<int, List<AreaObject>>>();
public void Add(AreaId id, AreaObject obj) {
int x = id.X;
int y = id.Y;
if (!_map.ContainsKey(x)) {
_map.Add(x, new Dictionary<int, List<AreaObject>>());
}
if (!_map[x].ContainsKey(y)) {
_map[x].Add(y, new List<AreaObject>());
}
_map[x][y].Add(obj);
}
...
}
public class AreaId {
public int X { get; set; }
public int Y { get; set; }
}
public class AreaObject {
public string AreaId { get; set; }
}
What if you wanted to be able to swap out AreaObject
with a different type on instantiation?
AreaObjectCollection<SomeType> x = new AreaObjectCollection<SomeType>();
You would need all the AreaObject
declarations in the code to become SomeType
when you instantiate the AreaObjectCollection
class.
Generics
We can use Generics for this. I spent a long time never writing my own classes that used Generic syntax. This was fine, for the most part… then, one of those problems came up that I couldn’t describe very well. As a beginner, I didn’t know that a language feature like Generics was built just for me!
“Generics introduce to the .NET Framework the concept of type parameters, which make it possible to design classes and methods that defer the specification of one or more types until the class or method is declared and instantiated by client code”
So there you go! I wanted to: *change a specific type to another type when I instantiate a class to make it more general and reusable…*
Let’s see what we need to change with our original code to make this happen…
Quick Refactor
To make our AreaObject
declarations swappable when we instantiate AreaObjectCollection
, we give the class a generic type parameter:
public class GenericAreaObjectCollection<T> where T : IAreaObject {
public Dictionary<int, Dictionary<int, List<T>>> map =
new Dictionary<int, Dictionary<int, List<T>>>();
public void Add(AreaId id, T obj) {
int x = id.X;
int y = id.Y;
if (!map.ContainsKey(x)) {
map.Add(x, new Dictionary<int, List<T>>());
}
if (!map[x].ContainsKey(y)) {
map[x].Add(y, new List<T>());
}
map[x][y].Add(obj);
}
}
public class AreaObject : IAreaObject {
public string AreaId { get; set; }
}
public interface IAreaObject {
string AreaId { get; set; }
}
We’ve done two Generic things here:
- On line 1, we added a generic type parameter with
GenericAreaObjectCollection<T>
. This allows us to pass in the type that we want to substitute for<T>
- On line 1, we added a generic type parameter constraint with
where T : IAreaObject
. This makes sure we can only pass in a type that implementsIAreaObject
interface. On line 22, we make sureAreaObject
implementsIAreaObject
We can now replace AreaObject
with a different class that implements IAreaObject
[Fact]
public void GenericAreaObjectCollection_Add_enhancedAreaObject() {
var areaObjectCollection = new GenericAreaObjectCollection<EnhancedAreaObject>();
areaObjectCollection.Add(
new AreaId() { X = 1, Y = 1 },
new EnhancedAreaObject() {
AreaId = "My Area",
SomeCoolAttribute = "I'm a better AreaObject"
});
_output.WriteLine(areaObjectCollection.ToJson());
/*
"map": {
"1": {
"1": [
{
"AreaId": "My Area",
"SomeCoolAttribute": "I'm a better AreaObject"
}
]
}
*/
}
Summary
We had a problem that wasn’t easy to connect a solution to. We simply wanted design a class that operated on a specific type declared within it. We wanted to extend the class so that we could also operate on other types - swapping out that specific type and all of its declarations when we instantiate it.
This is a job for Generics. By designing the class with a generic type parameter <T>
for the swappable type, we are able to instantiate the class with any type that satisfies the generic type parameter constraint specified - where T :
.
Generics are probably not new to you, but applying Generics when you run into a prime situation for them might be. Generics is a deep subject, e.g. covariance and contravariance. I hope to get back to it soon, so be sure to sign up for my newsletter, leave a comment, or just get in touch somehow!
Sources
- MSDN. Generics (C# Programming Guide)
- Skeet, Jon. C# in Depth, Third Edition. Manning Publications, September 2013.
- MSDN. Covariance and Contravariance in Generics