Using Generics
The Crash Course
- Generics let you create type-safe classes without actually needing to commit to any specific data type.
- When you create an instance of a class that uses generics, you'll need to indicate which type (or types) you are using it for in any particular instance using the angle brackets (< and >): List<string> listOfStrings = new List<string>();
- This tutorial also covers some of the details of using the List class and the Dictionary class, both of which use generics.
Introduction
In this tutorial, we'll take a look at a powerful feature in C# called generics.
We'll start by looking at why generics exist in the first place. The problem they solve. We'll then look at two different generic classes available in all C# programs that are extremely useful. (There are many more than these two, but these are the ones we'll cover.)
In the next tutorial, we'll look at how to actually create your own class that uses generics.
The Motivation for Generics
Before we can really discuss generics, we need to discuss why they exist in the first place. So I'm going to start by trying to get you to think about the underlying problem that generics will address.
Let's talk about arrays for a moment. Arrays can't change sizes, so adding and removing items is… inconvenient. It usually demands that you be willing to make a new array, copy the parts over that you want, and either leave out the deleted items or insert the new items into a slightly bigger array. The Array class gives you some tools for doing that, but it still leaves things in an uncomfortable state.
However, with your knowledge of classes, you might be tempted to say, "Hm. Maybe I can make a class that handles this. Give it a field with an array, and add methods for adding and removing items. Then the painful parts are shoved into a class that I can use!" Indeed, that's a good idea! Here's a version of that, which is wrapped around an int array:
public class ListOfNumbers { private int[] _numbers; public ListOfNumbers() { _numbers = new int[0]; } public AddNumber(int newNumber) { // Make a new array, slightly larger than the current one. int[] newNumbers = new int[_numbers.Length + 1]; // Copy everything over to this new array. Array.Copy(_numbers, newNumbers, _numbers.Length); // In the last slot, put the new number. newNumbers[newNumbers.Length - 1] = newNumber; // Update the array of numbers this object is tracking with the new, longer array. // The old one will be cleaned up by the garbage collector eventually. _numbers = newNumbers. } public int GetNumber(int index) { return _numbers[index]; } }
This isn't complete but shows the gist.
It is actually a workable solution!
But there's a problem. It only works for int values. What if we needed a similar thing for string or long or float?
We could make a similar ListOfStrings class and ListOfLongs class. We'd have to copy and paste to make them because inheritance won't buy us much here. Everything is built around that int[] field, and we don't have tools to allow that to change with each of these ListOfWhatevers types.
An alternative is to use a base class. In fact, to maximize generalization, we could turn this into an object[] instead of an int[]. Then it could store anything! However, this has its own problem. The class itself can't limit usage to only a specific type. You could make a list intended to store int values, but it could inadvertently be used to drop a string or a long in there. Besides, you'd need to cast any time you pull values out of such a class (like when you call the GetItem(int index) method). So that's not such a great solution either.
This is the place where generic types come into play.
Much like how a method with a parameter gives you flexibility, some types can be programmed in a way that allows you to put in a placeholder for some other type used within the class and allow you to indicate which type to actually use when you go to use it. That sentence probably didn't make a ton of sense, so let's just look at an example.
The List<T> Class
The List<T> class is the official version of what we were just trying to make above.
It is a generic type, which is evident by the <T> part of the name. This signifies that it has a placeholder to be filled in later, when you go to use it.
For example, we'll be able to make a List<int> (usually read as "list of int") or a List<string>:
List<int> numbers = new List<int>(); numbers.Add(10); numbers.Add(20); Console.WriteLine(numbers[0]); numbers.RemoveAt(0); // Removes at index 0, which was the 10. Console.WriteLine(numbers[0]); Console.WriteLine(numbers.Count);
This code shows how to work with a generic class like List<T>. When the List<T> class was defined, it left a placeholder, which it referred to as T. When you go to use it, you fill in the type with what you need it to be. In this case, int.
Using a generic class like this is not too hard to do. There's definitely some overhead compared to a normal, non-generic class, and List<int> is a bit more awkward to write out than int[]. But the flexibility is immensely valuable for those situations where a class would otherwise have a bunch of variations that only differ by the types of some of its fields, properties, method parameters, and method return types.
The code above also shows us the most essential tools when working with the List<T> class. I hope you can see from that code just how useful List<T> really is. We're able to add and remove items without much pain. We can access the elements with square brackets, almost as though they were an array. (We'll see how to do that ourselves later.) We can also use its Count property to see how many items are currently in the list. (Note that it's different from Length, which arrays provide. That difference is… awkward and annoying, but something you just learn to deal with.)
Indeed, now that we know the basics of generic types and can see how to use lists, you will almost always prefer a list to an array! (Arrays are simpler and faster, as long as you're doing things arrays naturally do. But most times, when you have collections of items, you want to add and remove things from them, and lists are better in those cases.)
We've made a List<int>, but let's look at another example: List<string>. The List<T> class can be used with any type, so we can also do this:
List<string> words = new List<string>(); words.Add("apple"); words.Add("banana"); words.Add("cherry"); foreach (string word in words) { Console.WriteLine(word); } words.Remove("cherry"); words.Clear(); // Removes everything.
This shows usage with strings and a few other things you can do with lists.
But we can fill in anything for T. We could make a List<bool> or a List<Player> or a List<IFileReader> or even a List<List<int>> to make a list of lists of numbers.
I should point out that it is possible for a generic type to put some limits on what types can be used with it. That isn't too common, and the List<T> class does not have any limitations. So I'm going to skip the details on that for now. Just be aware that it is possible for a generic type to do something like saying, "This can be any struct, but not a class," or "The type must implement this specific interface," etc. You will encounter that someday, but it is not very common.
Using the Dictionary Class
The other generic class that we'll look at here is the Dictionary<K, V> class. This generic class has two generic type parameters.
A dictionary is useful when you want to be able to look up one set of things from another. The English language usage of the word dictionary hints at that, since we use dictionaries to look up a definition from a word. But dictionaries can be used to look up any data type from any other data type. We could look up strings based on numbers, numbers based on enumeration values, players based on their string name, etc.
Some other programming languages use the word map, hashtable, or associative array for the same concept.
To use a dictionary, you pick your two types and fill them in:
Dictionary<int, string> englishWords = new Dictionary<int, string>(); englishWords[0] = "zero"; englishWords[1] = "one"; englishWords[10] = "ten"; englishWords[100] = "one hundred"; Console.WriteLine("The word for 100 is " + englishWords[100]); englishWords.Remove(10); Console.WriteLine("I know " + englishWords.Count + " words for numbers.");
You'll use lists more than dictionaries, but nothing else will do when you need a dictionary. They're extremely useful.
What's Next?
Generics are very powerful and allow us to make container or collection classes that can be flexible with the types they work with. We've looked at both the List class and the Dictionary class, both of which will prove to be very useful as we make software.
Now that we know how generics work, in terms of using them, let's go ahead and see how we could actually create generics in our own classes.
public object GetNumber(int index)
{
return numbers[index];
}
What is the object here?
The object? You mean what does the object keyword accomplish on the first line? It's the return type of the method. You've seen it in other places throughout the C# Crash Course. It's often void, which means it doesn't return anything. It's sometimes a more specific type, like int or string. In those cases, you know that the result will be a number, or text.
When we use object for the return type, it means the method could return literally anything (except void, but it could still return null which is about the same thing).
The problem here is that what's actually getting returned is probably not just an object. It's probably something else. Without generics, you'll have to know what the type actually is and cast to the correct type. That's painful, and a big part of the motivation for generics in the first place.
Post preview:
Close preview