Continuing once again, today we cover an exciting new topic: generics! Have you ever wished your classes could be parameterized with a type like Vector.<Type> is? With C# generics, you can! Even better, you can parameterize your interfaces, methods, and delegates too. Read on to learn how.

Table of Contents

Generics allow you to parameterize a class instance with a type. In AS3, this only works on Vector via a hack. In C#, this can work on any class and without the hack. Here’s a very simple example:

// Put the type parameter in angle brackets
public class Wrapper<WrappedObjectType>
{
	// Declare fields using the type parameter
	public WrappedObjectType WrappedObject { get; private set; }
 
	// Declare methods that take the type parameter
	public Wrapper(WrappedObjectType wrappedObject)
	{
		WrappedObject = wrappedObject;
	}
}
 
// Put the type parameter in angle brackets, just like Vector
Wrapper<String> stringWrapper = new Wrapper<String>("hello");
Debug.Log(stringWrapper.WrappedObject); // output: hello
 
Wrapper<int> intWrapper = new Wrapper<int>(10);
Debug.Log(intWrapper.WrappedObject); // output: 10

In this example the Wrapper class takes a type parameter called WrapperObjectType. You can name the type parameter anything you’d like, just like function parameters. You can then use it just like any type within the class. This is how the WrappedType property and the wrappedObject parameter to the constructor are allowed to have the WrapperObjectType type when it previously didn’t exist.

To use the generic class, just give it a type in angle brackets like you would with Vector except without a dot (.) after the class name. Unlike Vector, you can design your classes to take multiple type parameters:

public class Tuple<KeyType, ValueType>
{
	public KeyType Key { get; set; }
	public ValueType Value { get; set; }
}
 
Tuple<String, bool> setting = new Tuple<String, bool>();
setting.Key = "auto save";
setting.Value = true;

It’s really just as easy as adding a comma like you would with a function parameter list. You can also put restrictions on which types are allowed by using the where keyword:

public class Tuple<KeyType, ValueType>
	// Each type parameter's constraints go on their own line
	// Each constraint is separated by a comma
	// Format: where TYPEPARAM : CONSTRAINT, CONSTRAINT, ...
	where KeyType : class, new()
	where ValueType : struct
{
	public KeyType Key { get; set; }
	public ValueType Value { get; set; }
}

In this case, KeyType is required to be a class and have a constructor that takes no parameters. ValueType is required to be a structure. Here’s a full list of the constraints you can place on your type parameters:

  • where T : structT must be a structure
  • where T : classT must be a class
  • where T : new()T must have a constructor that takes no parameters
  • where T : MyClassT must derive from MyClass
  • where T : IInterfaceT must implement the IInterface interface
  • where T : UT must be U (another type parameter) or derive from it

You can also make your interfaces generic with the same syntax:

public interface IComparable<T>
{
	bool CompareTo(T other);
}
 
public class Student : IComparable<Student>
{
	public String Name { get; set; }
	public double GPA { get; set; }
 
	public bool CompareTo(Student other)
	{
		return GPA > other.GPA;
	}
}
 
public class MeritLevel : IComparable<Student>
{
	public String Name { get; set; }
	public double GPA { get; set; }
 
	public bool CompareTo(Student other)
	{
		return GPA < other.GPA;
	}
}
 
Student john = new Student { Name = "John", GPA = 3.1 };
Student paul = new Student { Name = "Paul", GPA = 3.6 };
MeritLevel honorRoll = new MeritLevel { Name = "Honor Roll", GPA = 3.5 };
Debug.Log(john.CompareTo(paul)); // output: false
Debug.Log(honorRoll.CompareTo(john)); // output: false
Debug.Log(honorRoll.CompareTo(paul)); // output: true

Here we have the IComparable interface that classes can implement to state that they can be compared to another class. Student and MeritLevel do just that with the Student type parameter.

You can also use generics with class methods in very much the same way:

public T FirstNotNull<T>(params T[] values)
{
	for (int i = 0; i < values.Length; ++i)
	{
		T value = values[i];
		if (value != null)
		{
			return value;
		}
	}
 
	throw new Exception("No non-null value");
}
 
Debug.Log(FirstNotNull<String>(null, null, "hello", "goodbye")); // output: hello

Just like when we used Wrapper<String> to specify the type parameter for the Wrapper class, so do we need to use FirstNotNull<String> to specify the type parameter for the FirstNotNull method.

There are some exceptions to this rule, though. Sometimes the compiler can figure out the type parameter from the parameters you pass. In that case you don’t need to specify the type parameter at all:

public bool AllEqual<T>(params T[] values)
	where T : IComparable
{
	for (int i = 1; i < values.Length; ++i)
	{
		if (values[i].CompareTo(values[0]) != 0)
		{
			return false;
		}
	}
 
	return true;
}
 
Debug.Log(AllEqual(1.1, 1.1, 2.2, 1.1)); // output: false

The last item to make generic are delegates. The syntax is much the same, except that the optional where constraints are just followed by a semicolon since delegates have no body, unlike classes, interfaces, and methods:

public interface IEntity {}
public class Player : IEntity {}
public class Enemy : IEntity {}
 
public delegate void SelectedHandler<T>(T selection) where T : IEntity;
 
public event SelectedHandler<Player> OnPlayerSelected;
public event SelectedHandler<Enemy> OnEnemySelected;

In this case we’ve avoided the need to make two delegates: one for the Player and one for the Enemy. Both events can use the same delegate with a different type parameter.

Lastly, there is one special usage of the default keyword that is useful when using generics. It can be called like a function with a type parameter to get the default value of an arbitrary type. This is useful because you don’t know if the default should be null, 0, or false. Here’s how that looks:

public T FirstOrDefault<T>(params T[] values)
{
	if (values.Length > 0)
	{
		return values[0];
	}
 
	return default(T);
}
 
Debug.Log(FirstOrDefault("hello")); // output: hello
Debug.Log(FirstOrDefault<String>()); // output: null
Debug.Log(FirstOrDefault<bool>()); // output: false

As for how to use generics with AS3, you simply can’t outside of Vector. You end up using Object or * in place of all your type parameters. It’s much slower, type-unsafe, and error-prone. The following summary of today’s generics topics should demonstrate that:

////////
// C# //
////////
 
public class Game
{
	// Define a generic interface
	public interface IComparable<T>
	{
		bool CompareTo(T other);
	}
 
	// Use a generic interface
	public class GameEntity : IComparable<GameEntity>
	{
		public int Level { get; private set; }
 
 
 
		public bool CompareTo(GameEntity other)
		{
			return Level > other.Level;
		}
	}
 
	public class Player : GameEntity
	{
	}
 
	public class Enemy : GameEntity
	{
	}
 
	// Define a generic delegate
	public delegate void SelectedHandler<T>(T selection) where T : GameEntity;
 
	// Use a generic delegate
	public event SelectedHandler<Player> OnPlayerSelected;
 
	public event SelectedHandler<Enemy> OnEnemySelected;
 
}
 
// Define a generic class
public class BaseVector2<ComponentType>
{
	public ComponentType X;
	public ComponentType Y;
 
	// Get default value of a generic type
	private const ComponentType defaultValue = default(ComponentType);
 
	public bool IsZero
	{
		get { X == defaultValue && Y == defaultValue; }
	}
}
 
// Use a generic class
public class Vector2 : BaseVector2<double>
{
}
 
// Define a generic method
public bool AllEqual<T>(params T[] values)
	// Generic method constraint
	where T : IComparable
{
	for (int i = 1; i < values.Length; ++i)
	{
		if (values[i].CompareTo(values[0]) != 0)
		{
			return false;
		}
	}
 
	return true;
}
 
// Call a generic method
Debug.Log(AllEqual(1.1, 1.1, 2.2, 1.1)); // output: false
/////////
// AS3 //
/////////
 
public class Game
{
	// Define a generic interface - impossible, use * instead
	public interface IComparable
	{
		function compareTo(other:*): Boolean;
	}
 
	// Use a generic interface
	public class GameEntity implements IComparable
	{
		private var _level:int;
		public function get level(): int { return _level; }
		public function set level(value:int): void { _level = value; }
 
		public function compareTo(other:*): Boolean
		{
			return level > other.level;
		}
	}
 
	public class Player extends GameEntity
	{
	}
 
	public class Enemy extends GameEntity
	{
	}
 
	// Define a generic delegate
	// {impossible}
 
	// Use a generic delegate - impossible, use Signal library instead
	public var _onPlayerSelected:Signal;
	public function get onPlayerSelected(): Signal { return _onPlayerSelected; }
	public var _onEnemySelected:Signal;
	public function get onEnemySelected(): Signal { return _onEnemySelected; }
}
 
// Define a generic class - impossible, use * instead
public class BaseVector2
{
	public var x:*;
	public var y:*;
 
	// Get default value of a generic type - impossible, convert from null instead
	private const defaultValue:* = null;
 
	public function get isZero(): Boolean
	{
		get { x == defaultValue && y == defaultValue; }
	}
}
 
// Use a generic class - no specialization possible
public class Vector2 extends BaseVector2
{
}
 
// Define a generic method - impossible, use * instead
public function allEqual(...values): Boolean
	// Generic method constraint
	// {impossible}
{
	for (var i:int = 1; i < values.length; ++i)
	{
		if (values[i] != values[0])
		{
			return false;
		}
	}
 
	return true;
}
 
// Call a generic method
trace(allEqual(1.1, 1.1, 2.2, 1.1)); // output: false

That almost wraps up generics. There are a couple of small topics to go with it, but today’s article has covered the bulk. We’re getting close to finishing up C#’s object model (classes, structures, etc.) and are very close to moving on to the remainder of the syntax (types, casts, etc.). Stay tuned!

Continue to Part 12

Spot a bug? Have a question or suggestion? Post a comment!