From AS3 to C#, Part 17: Conditionals, Exceptions, and Iterators

Datetime:2016-08-23 02:54:54          Topic: ActionScript  C#           Share

Continuingthe series on C# syntax, today we’ll look at the differences an AS3 programmer can expect to encounter when using conditionals ( if/else , switch/case/break/goto ) and exceptions ( try/catch/finally/throw ). We’ll also look at iterators, an all-new category for AS3 programmers that empowers us to both iterate however we want and to write coroutines , a kind of lightweight pseudo-thread.

Let’s start simple with what has barely changed at all in C#: if/else conditionals. These are exactly the same in syntax and meaning in both AS3 and C# with one exception. In AS3 there is a whole range of values that are implicitly converted to true or false when put into an if statement. In C#, you can only use a true or false value. For example, the following AS3 code implicitly checks several non- Boolean values:

if (55) { /* ... */ }
if ("hello") { /* ... */ }
if (someObject) { /* ... */ }

All of these would yield compiler errors in C#. Instead, we normally just write expressions that result in a boolean value:

if (3 > 2) { /* ... */ }
if (someBool) { /* ... */ }
if (!isDone) { /* ... */ }

Switch statements are, however, quite a bit more different in C#. In AS3, you’re allowed to “fall through” the case labels to execute the code in the next one. This is a frequent cause of errors, but also a source of flexibility. Here’s how it looks:

var intValue:int = 1;
var result:int;
switch (intValue)
{
	case 0: result = 0;
	case 1: result = 1;
	case 2: result = 2;
}
trace(result); // output: 2

Normally, you prevent this with an explicit break statement in each case :

var intValue:int = 1;
var result:int;
switch (intValue)
{
	case 0: result = 0; break;
	case 1: result = 1; break;
	case 2: result = 2; break;
}
trace(result); // output: 1

In C#, you’re only allowed to “fall through” the cases when there is nothing in them. This is useful for grouping how you handle multiple switch values, but less error prone as the missing break is less likely to be overlooked. In this example, the case is legitimately empty and can omit the break but the others must include it or face a compiler error:

int intValue = 1;
int result;
switch (intValue)
{
	case 0:
	case 1: result = 1; break;
	case 2: result = 2; break;
}
Debug.Log(result); // output: 1

In both languages, break can be replaced by a return or throw statement if you want to exit the whole function or jump to a catch or finally block rather than just skip to the end of the switch . However, C# has another option available to you that allows you to regain some of the lost “fall through” power that AS3 has. Rather than simply omitting the break , you just add a goto case X or goto default statement like this:

int intValue = 1;
int result = -1;
switch (intValue)
{
	case 0: result = 0; goto case 1;
	case 1: result = 1; goto case 2;
	case 2: result = 2; goto default;
	default: break;
}
Debug.Log(result); // output: 2

This example has restored the total “fall through” behavior of the first AS3 example. It’s more verbose and explicit than the AS3 example, too. The disadvantage is that we have to type more code to explicitly fall through, but the advantage is that it’s harder to accidentally omit a break and case runtime errors. Another advantage is that we can “fall” to cases that aren’t necessarily the next one, yielding even more flexibility:

int intValue = 1;
int result = -1;
switch (intValue)
{
	// "fall up"
	case 0: result = 0; goto default;
	case 1: result = 1; goto case 0;
	case 2: result = 2; goto case 1;
	default: ;
}
Debug.Log(result); // output: 0

Next up are exceptions. Like if and else , they too are very similar between the two languages, but with some key differences too. You throw an exception with throw and catch it with try , catch , and finally blocks. Here are the basics in AS3:

try
{
	trace("before");
	throw new Error("boom");
	trace("after");
}
catch (err:Error)
{
	trace("caught it, re-throwing...");
	throw err;
}
finally
{
	trace("cleanup");
}
 
/* output:
before
caught it, re-throwing...
cleanup
*/

C# allows the exact same code with one change. When re-throwing an exception within a catch block, you don’t need to specify its name:

try
{
	Debug.Log("before");
	throw new Error("boom");
	Debug.Log("after");
}
catch (Exception ex)
{
	Debug.Log("caught it, re-throwing...");
	throw;
}
finally
{
	Debug.Log("cleanup");
}
 
/* output:
before
caught it, re-throwing...
cleanup
*/

One final note on exceptions in C#: you have to throw either a System.Exception or an object that derives from it. This is unlike AS3 where you can throw anything you want, including int , Boolean , Vector.<String> , and all manner of other objects.

Lastly for today, let’s talk about an all-new concept in C#: iterators. These are a special type of function that can suspend its operation and be resumed later. When resumed, it’l have its full state restored so it picks up just where it left off. They’re called iterators because they’re often used for that purpose: iterating through some values in a custom way. To illustrate, here’s an iterator function that returns perfect squares:

foreach (var square in PerfetSquares(100))
{
	Debug.Log(square);
}
 
private IEnumerable<int> PerfetSquares(int max)
{
	var cur = 0;
	for (var i = 0; cur < max; ++i)
	{
		cur = i * i;
		yield return cur;
	}
}
 
/* output:
0
1
4
9
16
25
36
49
64
81
100
*/

There are a few parts to the puzzle. First, the function must return either System.Collections.IEnumerable or System.Collections.Generic.IEnumerable<T> . Second, the function must include at least one yield return X statement. This statement suspends the function and returns a value to the caller. Third, the caller must handle the IEnumerable by repeatedly calling it to resume the function and get the next value. The foreach loop does this last part for us, but we could do it manually if we wanted more control:

IEnumerable<int> enumerable = PerfetSquares(100);
IEnumerator<int> enumerator = enumerable.GetEnumerator();
while (enumerator.MoveNext())
{
	Debug.Log(enumerator.Current);
}

Manually calling MoveNext like this allows us more flexibility. We don’t need to simply call the iterator function over and over until it’s done. Instead, we can save off the IEnumerator as a class field variable and call it again elsewhere or later on. If you look at the iterator function as a function that does something in little steps, it becomes a coroutine . These can be useful for all sorts of tasks, such as artificial intelligence actors. Here’s a simple “cat and mouse” game where a “cat” iterator function chases a “mouse” iterator function:

public class CatMouseGame
{
	int catX = 0, catY = 0;
	int mouseX = 4, mouseY = 8;
 
	public CatMouseGame()
	{
		var catCR = Cat().GetEnumerator();
		var mouseCR = Mouse().GetEnumerator();
		while (catCR.MoveNext() && mouseCR.MoveNext())
		{
			Debug.Log(
				"cat: (" + catX + ", " + catY + "), " +
				"mouse: (" + mouseX + ", " + mouseY + ")"
			);
		}
	}
 
	private IEnumerable Cat()
	{
		var speed = 2;
		while (catX != mouseX || catY != mouseY)
		{
			yield return true;
			catX += Math.Min(speed, mouseX - catX);
			catY += Math.Min(speed, mouseY - catY);
		}
		yield return false;
	}
 
	private IEnumerable Mouse()
	{
		var speed = 1;
		while (catX != mouseX || catY != mouseY)
		{
			yield return true;
			mouseX += speed * Math.Sign(mouseX - catX);
			mouseY += speed * Math.Sign(mouseY - catY);
		}
		yield return false;
	}
}
/* output:
cat: (0, 0), mouse: (4, 8)
cat: (2, 2), mouse: (5, 9)
cat: (4, 4), mouse: (6, 10)
cat: (6, 6), mouse: (6, 11)
cat: (6, 8), mouse: (6, 12)
cat: (6, 10), mouse: (6, 13)
cat: (6, 12), mouse: (6, 14)
cat: (6, 14), mouse: (6, 14)
*/

Notice how the iterator functions are called in an interleaved manner: cat, mouse, cat, mouse, etc. We could do this with an arbitrary collection of iterator functions’ enumerators to simulate all the actors in a game, for example. Essentially, we have a tool that allows us to build small, synchronous, lightweight pseudo-threads.

Another thing to notice is that the cat and mouse positions are stored as field variables rather than parameters. It’s certainly possible to take parameters (we did it in the PerfectSquares example), but we aren’t allowed to take any ref or out parameters. This means that the Cat and Mouse iterator functions wouldn’t be able to change the positions via ref parameters, so another means is necessary.

Finally, there is the issue of stopping the iteration. You may have spotted that the PerfectSquares example simply ends with no return statement even though it’s declared as not returning void . The reason this is allowed is that iterators get an exception to the normal rule when they use their first yield return statement. The compiler simply generates a new statement at the end of the function: yield break . This can be used elsewhere in the function like you would use return; in a void -returning function. Here’s how PerfectSquares would look if we used yield break :

private IEnumerable<int> PerfetSquares(int max)
{
	for (var i = 0; true; ++i)
	{
		var cur = i * i;
		if (cur > max)
		{
			yield break;
		}
		else
		{
			yield return cur;
		}
	}
}

Now let’s sum up for today with a side-by-side comparison of how the two languages handle conditionals, exceptions, and iterators:

////////
// C# //
////////
// If-else
if (something) // "something" must be boolean
{
}
else
{
}
 
// Switch
switch (something)
{
	// Fall through
	case 0: // must be empty
 
	// Go to another case
	case 1:
		goto case 2;
 
	// Go to the default case
	case 2:
		goto default;
 
	// Default case
	default;
		// Go to the end of the switch
		break;
}
 
// Execute statements that may throw exceptions
try
{
}
// Catch an exception type
catch (Exception ex)
{
	// Re-throw the exception
	throw;
}
// Execute if exception or not
finally
{
}
 
// Iterator function
IEnumerable<int> Range(int min, int max, int step=1)
{
	for (var i = min; i < max; i += step)
	{
		// Suspend function and return current value
		yield return i;
	}
}
 
// Simple loop over iterator function
foreach (var cur = Range(2, 10))
{
}
 
// Manually call iterator function
var enumerable = Range(2, 10);
var enumerator = enumerable.GetEnumerator();
enumerator.MoveNext();
/////////
// AS3 //
/////////
// If-else
if (something)
{
}
else
{
}
 
// Switch
switch (something)
{
	// Fall through
	case 0:
 
	// Go to another case
	// {impossible in AS3}
 
 
	// Go to the default case
	// {impossible in AS3}
 
 
	// Default case
	default;
		// Go to the end of the switch
		break;
}
 
// Execute statements that may throw exceptions
try
{
}
// Catch an exception type
catch (err:Error)
{
	// Re-throw the exception
	throw err;
}
// Execute if exception or not
finally
{
}
 
// Iterator function
// {impossible in AS3}
 
 
 
 
 
 
 
 
// Simple loop over iterator function
// {impossible in AS3}
 
 
 
// Manually call iterator function
// {impossible in AS3}
//
//

Next time we’ll continue the series by looking at various ways to allocate and clean up resources beyond just new and the garbage collector. Stay tuned!

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





About List