From AS3 to C#, Part 11: Generic Classes, Interfaces, Methods, and Delegates
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
- From AS3 to C#, Part 1: Class Basics
- From AS3 to C#, Part 2: Extending Classes and Implementing Interfaces
- From AS3 to C#, Part 3: AS3 Class Parity
- From AS3 to C#, Part 4: Abstract Classes and Functions
- From AS3 to C#, Part 5: Static Classes, Destructors, and Constructor Tricks
- From AS3 to C#, Part 6: Extension Methods and Virtual Functions
- From AS3 to C#, Part 7: Special Functions
- From AS3 to C#, Part 8: More Special Functions
- From AS3 to C#, Part 9: Even More Special Functions
- From AS3 to C#, Part 10: Alternatives to Classes
- From AS3 to C#, Part 11: Generic Classes, Interfaces, Methods, and Delegates
- From AS3 to C#, Part 12: Generics Wrapup and Annotations
- From AS3 to C#, Part 13: Where Everything Goes
- From AS3 to C#, Part 14: Built-in Types and Variables
- From AS3 to C#, Part 15: Loops, Casts, and Operators
- From AS3 to C#, Part 16: Lambdas and Delegates
- From AS3 to C#, Part 17: Conditionals, Exceptions, and Iterators
- From AS3 to C#, Part 18: Resource Allocation and Cleanup
- From AS3 to C#, Part 19: SQL-Style Queries With LINQ
- From AS3 to C#, Part 20: Preprocessor Directives
- From AS3 to C#, Part 21: Unsafe Code
- From AS3 to C#, Part 22: Multi-Threading and Miscellany
- From AS3 to C#, Part 23: Conclusion
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 : struct —
T
must be a structure - where T : class —
T
must be a class - where T : new() —
T
must have a constructor that takes no parameters - where T : MyClass —
T
must derive fromMyClass
- where T : IInterface —
T
must implement theIInterface
interface - where T : U —
T
must beU
(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!
Spot a bug? Have a question or suggestion? Post a comment!
#1 by henke37 on September 29th, 2014 ·
Note that generics should not be confused with templates from C++. They are similar, but very different.
#2 by Zeh on September 29th, 2014 ·
A great article as always. I’ve been using generics for a while but there were a lot of small features I did not know about until reading this.
#3 by Bernd on July 28th, 2015 ·
I have a question regarding Generics. The following case:
1. A class Wrapper Where T : Fruit
2. a Base Class Fruit.
3. several classes derived from Fruit: Apple, Banana, etc.
4. now I have a collection with several instances of Wrapper all wrapping different Fruits.
5. At one point I want to check items in a collection if they are Wrapper Objects. How to do it best?
6.
if (object is Wrapper<Fruit>)
does not work7.
if (object is Wrapper)
does also not work8. I want to avoid
if (object is Wrapper<Apple> || object is Wrapper<Banana> ... etc)
#4 by jackson on July 28th, 2015 ·
Sorry for the HTML stripping. I think angle brackets work if you use
&lt;
and&gt;
.As to your question, you can do this with a bit of reflection. Here’s an example
The output looks like this:
The key is really just to use reflection to get the generic
Type
for the object you want to inspect. I don’t know of a way to do it without reflection, but it’d sure be nice. You’re correct that a simpleobj is Wrapper<Fruit>
doesn’t work.#5 by Bernd on July 29th, 2015 ·
Thanks for taking the time to answer this. You are awesome. I find something valuable in your blog everyday.
#6 by Bernd on July 28th, 2015 ·
it seems that using the code tag on my snippets removed the generics from the above post. :-(
it should be
6. if (object is Wrapper)
8. if (object is Wrapper || object is Wrapper … etc)
#7 by Bernd on July 28th, 2015 ·
arrow brackets dont work at all :-( I use square brackets for Generics now.
6. if (object is Wrapper [Fruit] )
8. if (object is Wrapper [Apple] || object is Wrapper[Banana] … etc)
#8 by slava on February 1st, 2016 ·
Debug.Log(honorRoll.CompareTo(john)); // output: false – why false? 3.5 > 3.1
Debug.Log(honorRoll.CompareTo(paul)); // output: true – …3.5 > 3.6?
#9 by jackson on February 1st, 2016 ·
Good catch! I’ve updated the article to fix the typo. The
>
has been changed to<
. Thanks for letting me know.#10 by slava on February 2nd, 2016 ·
I so proud, because I such C# lame..
omg f#$kin adobe! :)
#11 by Stan on October 23rd, 2017 ·
I think
private const defaultValue* = null;
misses :
#12 by jackson on October 23rd, 2017 ·
Thanks for letting me know about this typo. I’ve updated the article with the fix.