Generics

Generics provide a way for you to make a kind of “template” out of your code that works the same across a variety of different data types. While it could be considered a more advanced topic, there are a few important benefits to using them early on. In this lesson I will introduce generic lists and dictionaries, and show how generics are used for specific Unity functionality like getting components and loading resources. If you’re feeling adventurous, feel free to check out some quick examples of custom generic methods and classes at the end.

Generic List

If you have done any experimenting with arrays, you might have run into a few wish-list features. For example, you might have been disappointed to see that they are “immutable” – that means you can’t re-size it by adding or removing objects dynamically. Many of the features you are hoping for are excluded for the sake of performance. If you are writing highly optimized code, such as a complex artificial intelligence for playing Chess, then you will want that extra speed. In most other cases, the benefits of slightly less performant, but more robust options will win.

The Generic List is very much like an array. Much of the syntax for reading and writing will look the same too. However, a list can add or remove items dynamically, tell you the index of an item it contains, be sorted, etc. Let’s see what that looks like in code:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ListExamples : MonoBehaviour {
	public List<int> indices = new List<int>();

	void Start () {
		for (int i = 0; i < 10; ++i) {
			int index = UnityEngine.Random.Range(0, 10);
			Debug.Log("Adding Entry: " + index);
			indices.Add( index );
		}

		if (indices.Contains(3)) {
			Debug.Log("Removing entry");
			indices.Remove(3);
		}

		indices.Sort();
		Debug.Log("Sorted entries... Now:");

		for (int i = 0; i < indices.Count; ++i) {
			int index = indices[i];
			Debug.Log( string.Format("i: " + index) );
		}
	}
}

First, note that on line 3, I have specified that I am using a new namespace, “System.Collections.Generic”. This allows us to declare and use generics without fully specifying them. For example, without the using statement, line 7 would have looked like this instead:

public System.Collections.Generic.List<int> indices = new System.Collections.Generic.List<int>();

We declared our list on line 7, and initialized it as an empty list. This is great to show that you don’t need to know how large your list will be, or what specific elements it will contain. The “generic” part of the line is seen between the “<” and “>” where we specified a data type – in this example we used an “int”. This means that this particular list can only hold int values. Because all elements of the list are forced to be the same data type, the code will run more safely and quickly. We call it generic because we can pass any kind of DataType (sometimes there are exceptions to this rule) in the declaration. A list of float, string, or Transform would be similarly declared:

List<float> list1 = new List<float>();
List<string> list2 = new List<string>();
List<Transform> list3 = new List<Transform>();

In the Start method, I ran a variety of tasks on our list and print messages to the Console explaining what is happening. First, I use a for loop to generate and add 10 random numbers to our list. I made them random to show that the list can hold duplicate values, and to help illustrate the fact that I can add values out of order and sort them later.

Next, on line 21, I check to see if we added any entries of the number 3. If we did, we remove that value. (Note that in this example I only remove the first occurrence that I find, but there could be more).

On line 27, I tell the list to sort itself – by default it will sort its elements in ascending order. There are several other ways to sort elements which you can research later.

Finally on line 30 I make another loop to print the elements in order as they appear after having been sorted. You should hopefully recognize the bracket syntax for reading an element at an index, because it is the same as working with an array.

Copy the example code and then attach your script to an object in scene. Run the scene and check out the output in the console window. On one sample run I saw that I added the following indices: (1, 1, 7, 8, 3, 6, 1, 5, 0, 2), then I saw that an entry was removed, and then I saw the entries sorted as (0, 1, 1, 1, 2, 5, 6, 7, 8).

Tip:
I only covered a small set of features of the Generic List. Check out the reference for more.

Generic Dictionary

A dictionary is another kind of data collection used by programmers. Unlike an array or list, it is considered unordered, and is accessed by a “key” rather than by an “index”. Certain scenarios might make this feel more natural, because referencing an object by index doesn’t necessarily mean anything. For example, if I have a list of GameObject, then I don’t intuitively know that the GameObject at index 0 represents a minion and the GameObject at index 1 represents a boss. However, by associating a value to a key, a relationship becomes more obvious and readable in your code.

Imagine you were making an RPG with a large assortment of shop items. Certain shops might only show certain items at any given time, and the objects they show may not be based on any particular order. If you managed your items as a list or as an array, every time you wanted to get a reference to an item, you would have to “loop” through each item in the list until you found the one that matched. With a dictionary, you can essentially say, “give me the item named ‘Dagger’” and immediately get its reference. Let’s see what that looks like in code:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class DictionaryExamples : MonoBehaviour {
	public Dictionary<string, int> stats = new Dictionary<string, int>();

	void Start () {
		stats.Add("HP", 10);
		stats.Add("MP", 3);
		stats.Remove("MP");
		if (stats.ContainsKey("HP"))
			Debug.Log( string.Format("You have {0} hit points remaining", stats["HP"]) );
	}
}

On line 7, we declare our dictionary. Note that we declared that we are using the “System.Collections.Generic” namespace to use the shorter specification. Dictionaries have a “Key” and a “Value”, each of which can be a different data type. I illustrate that here by making the “Key” a type of string, and the “Value” a type of “int”.

In the start method, I use a few of the methods available to dictionaries. First I add a Key and Value for a stat called “HP”. Next I add a different stat called “MP”. In addition to adding key-value pairs, you can remove them, although in that case you only specify the key. I remove the “MP” stat on line 13. When working with dictionaries it is a good idea to check that a dictionary has a key before you try to read it. I do that on line 14 before reading the value with bracket syntax (again, similar to reading an array) on the end of line 15.

There are a few gotchas you might run into when using dictionaries that you should be aware of:

  1. Unity does not “recognize” Dictionaries, so even if you mark it as public, it will not be serialized or appear in the inspector. Note that this does not prevent you from using them in your scripts, but it does make editor based initialization an issue.
  2. if you try to use the Add syntax (.Add(key, value)) to a dictionary which already contains that key, you will get an error: “ArgumentException: An element with the same key already exists in the dictionary.” You can change a value by the indexer such as (dictionary[key] = value).
  3. if you try to write a dictionary value by indexer, and the key does not exist, it will add the key and assign the value automatically.
  4. if you try to read a dictionary value by indexer, and the key does not exist, you will get an error: “KeyNotFoundException: The given key was not present in the dictionary.”
  5. If you try to set a dictionary value to the wrong data type, the compiler will complain at you: “error CS1502: The best overloaded method match for `System.Collections.Generic.Dictionary.Add(string, int)’ has some invalid arguments”
Tip:
I only covered a small set of features of the Generic Dictionary. Check out the reference for more.

Generics And Unity

Although the examples of generics I have given so far applied specifically to instances of a class (our list and dictionary variables), you should know that it can also apply to method declarations as well. Because of the benefits of generics, you will find them scattered throughout Unity’s functionality. The first place you might notice is with the ability to “Add” or “Get” components on a GameObject:

Foo foo = gameObject.AddComponent<Foo>();
Bar bar = gameObject.GetComponent<Bar>();

These two lines show how the Generic markers are used on the methods AddComponent and GetComponent to specify what kind of component will be added or retrieved. Note that in the example, “Foo” and “Bar” are example names of classes which you would have needed to create scripts for.

By using the first line, you can add an instance of a script to a GameObject while your game is actually running. For example, you might add a Death script to an object whenever its hit points are reduced to zero. You could also build complex objects at run time instead of at edit time to make sure that the order they are added allows you to correctly configure all of an object’s dependencies. For example, if Foo needs a reference to Bar, then you want to make sure Bar was added to the object first.

The second line returns an instance of a script on a GameObject (or null if it can’t find one). You can use this to test whether or not a reference to an object has a component you are looking for, and if so, take some kind of action on it (refer to its public properties or utilize its public methods).

You will also see similar syntax when loading resources:

TextAsset data = Resources.Load<TextAsset>("Level_0");

Note that for that line to work, you will need a text asset of the correct name in your project within a folder named “Resources”. It is possible to have multiple different kinds of assets sharing the same name, such as a prefab and sprite, but when you specify the kind of object to load as the Generic argument, it will return the correct item.

Custom Generic Methods

By now you might be wondering how to create your own generic method. Consider the following examples:

void ToggleComponent<T> () where T : MonoBehaviour {
	T t = gameObject.GetComponent<T>();
	if (t)
		t.enabled = !t.enabled;
}

T FindOrAdd<T> () where T : MonoBehaviour {
	T t = gameObject.GetComponent<T>();
	if (t == null)
		t = gameObject.AddComponent<T>();
	return t;
}

A generic method declaration begins much like any other method you have declared, however the generic “<“ and “>” type marker is inserted just before the parenthesis. The value you put inside the generic marker is similar to a parameter name, but is used to refer to the “Data Type” that will be used within the method. Note that you can re-use that same identifier as the data type for the method’s return type, or as a data type on a parameter which the method accepts.

After the parenthesis is something called a “constraint” – an optional bit of code which applies to the data types that we allow this method to operate upon. In this example, we specify that the methods can only work with data types that are, or derive from, the MonoBehaviour base class. Because of our constraint we can write any code within the method that would apply to any MonoBehaviour – such as the ability to toggle whether or not it is enabled. Without the constraint, the compiler wouldn’t know any details of the object and wouldn’t be able to write such a specific implementation.

Tip:
There are several other constraints I have not shown here. See the reference for more.

Custom Generic Classes

You can also make a class Generic. In fact this is how the List and Dictionary which we used earlier would be created:

using System;

public class InfoEventArgs<T> : EventArgs {
	public T info;

	public InfoEventArgs() {
		info = default(T);
	}

	public InfoEventArgs (T info) {
		this.info = info;
	}
}

This class becomes a sort of template for a subclass of EventArgs which contains a single property, but that property can be of any type. This example shows two “constructors” which are a special type of method which creates an instance of a class. When we initialized our dictionary using “new Dictionary” we were calling a constructor. You can tell a method is a constructor because it does not have a return type in its signature and because the name of the method is the same name as the name of the class.

The first constructor is a default constructor – it doesn’t take a parameter. Since I didn’t use any constraints on this class, the Data Type could be a value type (like an int) or a reference type (like a GameObject), each of which have different and incompatible default values – I can’t assign a “GameObject” a value of “0” and I can’t assign an “int” a value of “null”. The “default” keyword solves this problem for me and initiates a generic type for me automatically.

On the second constructor, I initiate the “info” variable to whatever value the user passes along. I used a special keyword “this” which is a way for a script to refer to itself. Using the dot notation on the “this” keyword allows me to differentiate between the instance’s variable “info” and the constructor’s parameter “info” since they share the same name. Note that I could also have differentiated them by giving the parameter a different name.

Summary

In this lesson we introduced a new language feature called generics. Generics can be applied to methods and classes as a way to help make code reusable across a variety of DataTypes in a safe and quick way. I introduced the generic list and dictionary, showed how Unity uses generics in its own libraries, and finally showed how to create custom generic methods and classes, with or without constraints.

2 thoughts on “Generics

  1. Hello! Thank you for the great tutorials. Looks like lines numbers in the text and in the code snippets do not correspond anymore for some reasons.

Leave a Reply

Your email address will not be published. Required fields are marked *