From AS3 to C#, Part 9: Even More Special Functions
Last week’s article continued the discussion of special types of functions in C#’s class system, including variable numbers of arguments (“var args”), indexers, and conversion operators. Today’s article should finish up the topic of special functions. Read on to learn about the built-in support for delegates, events, and object initializers!
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
Before we get to events we need to cover delegates. A delegate is a definition of a function’s signature, but not actually the definition a particular function. It’s like the relationship between an interface and the class that implements it. You need an actual function to “implement” the delegate. Here’s how that looks:
public class FunctionalUtilities { // Declare the type of function that Every() will take as a parameter. // It will be called to check each element of the array. public delegate bool EveryCallback(int a); // The second parameter can be any function that fits the signature // specified by EveryCallback. It must take one int and return a bool. public static bool Every(int[] integers, EveryCallback callback) { for (int i = 0; i < integers.Length; ++i) { // Call the delegate like any other function if (callback(integers[i]) == false) { return false; } } return true; } } public class TestEvery { // This function satisfies the delegate by taking one int and // returning a bool public bool IsEven(int val) { return (val % 2) == 0; } public void Foo() { int[] integers = { 2, 4, 6 }; // Pass the IsEven function as the second parameter. The Every() // function will call it. bool allEven = FunctionalUtilities.Every(integers, IsEven); Debug.Log("all integers are even? " + allEven); } }
Delegates essentially give us a way to strongly-type AS3’s Function
. With Function
, you can pass any function at all or even null
. It gives no hint to the caller as to what the expected parameters or return type are. Instead, a runtime error will occur if there is any mismatch. Here’s how the equivalent AS3 would look:
public class FunctionalUtilities { // The second parameter can be any function or even null public static function every(integers:Vector.<int>, callback:Function): Boolean { for (var i:int = 0; i < integers.length; ++i) { // Call the callback function like any other function if (callback(integers[i]) == false) { return false; } } return true; } } public class TestEvery { public function isEven(val:int): Boolean { return (val % 2) == 0; } public function foo(): void { var integers:Vector.<int> = new <int>[ 2, 4, 6 ]; // Pass the IsEven function as the second parameter. The Every() // function will call it. var allEven:Boolean = FunctionalUtilities.every(integers, isEven); trace("all integers are even? " + allEven); } }
These strongly-typed delegate functions form the basis of C#’s built-in event system. We simply use the event
keyword to declare a new event like so:
public class PushButton { // First, declare a delegate specifying the signature of // functions the event calls public delegate void ClickHandler(PushButton button); // Next, declare an event with the delegate type public event ClickHandler OnClick; public void Click() { // Check if anyone has subscribed to the event if (OnClick != null) { // Call the event like a function to call all // of the listeners OnClick(this); } } } public class MyUI { private PushButton okButton; public MyUI() { okButton = new PushButton(); // Use the += operator to add a listener to the event okButton.OnClick += HandleOKButtonClicked; // Clicking the button calls the listener okButton.Click(); } // Declare a function with the type specified by the delegate. // This will allow it to be used as an event listener. private void HandleOKButtonClicked(PushButton button) { Debug.Log("OK button clicked"); // Use the -= operator to remove a listener from the event okButton.OnClick -= HandleOKButtonClicked; } }
The way we declared the event is akin to the shortcut syntax we used with properties to automatically generate the “backing variable”, getter, and setter:
// Shortcut version public class Person { public String Name { get; set; } } // Long version public class Person { // "backing variable" private String name; public String Name { // custom getter get { return name; } // custom setter set { name = value; } } }
With events, it’s generating code like this for us:
// Shortcut version public class PushButton { public delegate void ClickHandler(PushButton button); public event ClickHandler OnClick; } // Long version public class PushButton { public delegate void ClickHandler(PushButton button); // "backing delegate" private ClickHandler onClick; public event ClickHandler OnClick { // custom "adder" add { onClick += value; } // custom "remover" remove { onClick -= value; } } }
This means we can create custom “adder” and “remover” methods. You usually won’t do this, but it does allow you a convenient place to add in addition code. For example, you might optimize by only performing expensive operations when there is at least one listener. Otherwise, no one cares and the operation might be skipped:
public class Enemy { public delegate void MoveHandler(PushButton button); // "backing delegate" private MoveHandler onMove; private int numMoveListeners; public event MoveHandler OnMove { // custom "adder" add { onMove += value; numMoveListeners++; } // custom "remover" remove { onMove -= value; numMoveListeners--; } } public void Update() { if (numMoveListeners > 0) { // ... perform expensive physics calculations } } }
EDIT
: See this comment below for more on the differences between delegates and events.
In AS3 we are given the flash.events.Event
class to derive from to define the contents of our event instead of callback parameters. We are also given the flash.events.EventDispatcher
class to derive from if we want to dispatch events:
// First, extend Event to create the event type public class ClickEvent extends Event { private _button:PushButton; public function get button(): PushButton { return _button; } // Create a constructor to forward the necessary parameters and take any additions public ClickEvent( type:String, button:PushButton, bubbles:Boolean = false, cancelable:Boolean = false ) { super(type, bubbles, cancelable); _button = button; } // Override clone() to create copies public override function clone(): Event { return new ClickEvent(type, _button, bubbles, cancelable); } } // Now extend EventDispatcher public class PushButton extends EventDispatcher { // Declare an event type String public static const CLICK:String = "click"; public function click(): void { // Create an Event object with the event type string var clickEvent:ClickEvent = new ClickEvent(CLICK, this); // Call dispatchEvent to call all the listeners dispatchEvent(clickEvent); } } public class MyUI { private var okButton:PushButton; public function MyUI() { okButton = new PushButton(); // Use the addEventListener function with the event type string // to add a listener to the event okButton.addEventListener(PushButton.CLICK, handleOKButtonClicked); // Clicking the button calls the listener okButton.click(); } // Declare a function with any signature. If it doesn't take only one // parameter-- an Event-- an Error will be thrown at runtime when the // event is dispatched. private function handleOKButtonClicked(event:ClickEvent): void { trace("OK button clicked"); // Use the removeEventListener function with the event type string // to remove a listener from the event okButton.removeEventListener(PushButton.CLIK, handleOKButtonClicked); } }
This system requires quite a bit more code, doesn’t enforce type safety on the listener function, uses an arbitrary event type string rather than variables, requires you to derive from EventDispatcher
, and is horrifyingly slow. It’s the reason programmers like me have created alternatives like TurboSignals, but they have their own problems that are off-topic for this article.
Lastly for today, let’s discuss a simpler topic: object initializers. It’s common to set several variables on a newly-instantiated class, so C# has a shortcut available:
public class Vector3 { public double x; public double y; public double z; } // Long version Vector3 vec = new Vector3(); vec.x = 1; vec.y = 2; vec.z = 3; // Shortcut version using object initializer Vector3 vec = new Vector3() { x = 1, y = 2, z = 3 }; // When calling the default constructor and using an object initializer, // the parentheses are optional too Vector3 vec = new Vector3 { x = 1, y = 2, z = 3 };
This is like a strongly-typed version of AS3’s with
blocks that can only be used at object initialization time. Here’s how it’d look in AS3:
public class Vector3 { public var x:Number; public var y:Number; public var z:Number; } // Long version var vec:Vector3 = new Vector3(); vec.x = 1; vec.y = 2; vec.z = 3; // Shortcut version using 'with' block var vec:Vector3 = new Vector3(); with (vec) { x = 1, y = 2, z = 3 };
Unfortunately, with
blocks in AS3 are painfully slow as they entail far more than just syntax sugar, as in C#.
Let’s wrap up for today with a quick comparison summarizing this article’s topics: delegates, events, and object initializers:
//////// // C# // //////// public class IntVector2 { public int x; public int y; } public class PushButton { // Delegate public delegate void ClickHandler(PushButton button, IntVector2 location); // Event public event ClickHandler OnClick; public void Click(int x, int y) { // Object initializer IntVector2 location = new IntVector2 { x = x, y = y }; // Dispatch event OnClick(this, location); } }
///////// // AS3 // ///////// public class IntVector2 { public var x:int; public var y:int; } // Event - parameters require extending Event class public class ClickEvent extends Event { private _button:PushButton; private _location:IntVector2; public function get button(): PushButton { return _button; } public function get location(): IntVector2 { return _location; } public ClickEvent( type:String, button:PushButton, location:IntVector2, bubbles:Boolean = false, cancelable:Boolean = false ) { super(type, bubbles, cancelable); _button = button; _location = location; } public override function clone(): Event { return new ClickEvent(type, _button, _location, bubbles, cancelable); } } // Event - dispatching requires extending EventDispatcher class public class PushButton extends EventDispatcher { // Delegate // {impossible, use type-unsafe Function instead} public static const CLICK:String = "click"; public function click(x:int, y:int): void { // Object initializer - requires with block var location:IntVector2 = new IntVector2(); with (location) { x = x, y = y }; // Dispatch event var clickEvent:ClickEvent = new ClickEvent(CLICK, this, location); dispatchEvent(clickEvent); } }
Next week we’ll start covering exciting new concepts like structures, enumerations, and generics. Stay tuned!
Spot a bug? Have a question or suggestion? Post a comment!
#1 by Shawn on September 25th, 2014 ·
Thanks Jackson! Also worth noting that you can shorthand this further using actions:
The ‘event’ keyword itself is quite interesting, it’s not strictly necessary, and seems like it locks the handler to only using += or -=, enforcing a ‘best practice’ that does not allow the ability to overwrite existing listeners…
So far in my code I’ve just been using straight Actions, not defined as event, and it feels very similar to AS3 Signals. I definitely like to keep the ability to clean all listeners in a destroy() function.
#2 by Shawn on September 25th, 2014 ·
Sorry, it ate my angle brackets.
#3 by jackson on September 26th, 2014 ·
I put them back in on the
Action
lines.Good point about
Action
as an alternative. I intentionally left it out of the article since I’m focusing on the language itself right now, not any APIs from even the .NET library, let alone Unity. In this case, theAction
isn’t getting you much. You could still have just used the delegate like this:That would allow you to use
=
on the delegate instance:Or call the event from outside the class:
Events are mostly useful when you want to:
add
andremove
behaviorI’ll discuss
Action
in a future article. It,Func
, andPredicate
are very useful in a variety of related cases.