Basic

August 25 2020

0 comments

Throwing Exceptions as Expressions in Unity — The C# 7 Way

By Rubén Torres Bonet

August 25, 2020


__CONFIG_colors_palette__{"active_palette":0,"config":{"colors":{"6c4de":{"name":"Main Accent","parent":-1},"67ed2":{"name":"Accent Dark","parent":"6c4de","lock":{"saturation":1}}},"gradients":[]},"palettes":[{"name":"Default","value":{"colors":{"6c4de":{"val":"var(--tcb-skin-color-0)"},"67ed2":{"val":"rgb(59, 65, 63)","hsl_parent_dependency":{"h":160,"l":-0.05,"s":0.04}}},"gradients":[]},"original":{"colors":{"6c4de":{"val":"rgb(51, 190, 127)","hsl":{"h":152,"s":0.57,"l":0.47}},"67ed2":{"val":"rgb(59, 65, 63)","hsl_parent_dependency":{"h":160,"s":0.04,"l":0.24}}},"gradients":[]}}]}__CONFIG_colors_palette__

Things have evolved in C# 7, but many programmers still haven't. Today, let's talk about throwing exceptions in Unity — the C# 7 way —.

Indeed, exceptions are a tricky topic. If you mention them, devs will run to you and demand that you lower your voice.

Game developers generally dislike exceptions. After all, misusing them ends up doing more harm than good.

But still, there are cases where you might want to use them.

And the day you do, you will be happy to know that you can throw exceptions just as if they were expressions in C# 7.

I know this sounds abstract, so I do what I do best...

I prepared a cheesy C# example. Your favorite.

Let's go.

TL;DR:

  • You can use throw as an expression, not only as statements.
  • Use cases: ternary operator, expression-bodied members, null-coalescing operator.
Unity C# Throw Expressions Thumbnail

What About C# Exceptions?

You probably have been throwing exceptions all your life the traditional way. That means, you used the throw keyword as a statement.

But statements come with restrictions. Like, you cannot use them in places where C# expects an expression.

As we will see, this limitation leads to semantic constructs that make your code more verbose.

But what if I told you we can upgrade our exception-throwing mechanics to use expressions instead?

Now, that might sound like quantum mechanics to you.

After all, you don't need to know the theoretical differences between statements and expressions to be a successful programmer.

But it pays off to dedicate a few lines to explain this so we all speak the same language (C#).

No worries, it's easy.

Expression vs Statement (Source: therevisionist.org)

First Things First

What's an Exception?

You are programming your next C# function as usual.

But you dont trust yourself. Or more precisely, you don't trust your colleagues.

After all, your colleagues have been misusing your functions lately and funny enough, they complain when things break.

"How dare they blame me for their sh**?" You ask yourself, often aloud.

That no more.

You are going to implement exceptions in your new system so the code explodes in their face before they even try to blame you when using your system the wrong way.

You use exceptions to abort the execution of your code when something goes wrong, e.g. when someone passes incorrect input to your function.

So you detect that something is off and what do you do about it?

Exactly. You throw an exception.

And whoever invoked your function has two options:

  • Deal with the exception explicitly: try + catch + finally.
  • Ignore it and deal with the consequences of Unity handling it for you.

Here's how throwing an exception looks like:

throw new Exception("I broke for good");
throw new Exception("I broke for good");
throw new Exception("I broke for good");

Here, we used the throw statement to throw an exception. And statements have limitations... that we are about to demolish.

But first...

What's a Statement?

Statements are nothing more than the actions you do in your code. Declaring a variable, conditionals, loops, etc. are each a statement.

if (...) { ... }
int a;
while (...) { ...}
if (...) { ... } int a; while (...) { ...}
if (...) { ... }
int a;
while (...) { ...} 

So, what's the problem with statements?

Well, you cannot use statements in places where the language expect a expression.

Which brings me to the next point..

What's an Expression?

Expressions are everything that produces a result. A number, a string, a boolean... or any operation that generates this type of values.

Certain C# features require expressions and do not accept statements, such as expression-bodied members and the ternary operator:

public int GetHitPoints(int level) => 10 + 5 * level
int experiencePerLevel = (level < 10) ? 3000 : 5000
public int GetHitPoints(int level) => 10 + 5 * level int experiencePerLevel = (level < 10) ? 3000 : 5000
public int GetHitPoints(int level) => 10 + 5 * level
int experiencePerLevel = (level < 10) ? 3000 : 5000

The expressions are the right part of these two lines.

Traditionally we can't throw exceptions in these places, because the throw keyword is just a statement.

So what we do is to wrap these throw statements into conditionals, like this:

string[] args = commandLine.Split(' ');
if (args.Length == 0)
{
throw new ArgumentException("No command present", nameof(commandLine));
}
string[] args = commandLine.Split(' '); if (args.Length == 0) { throw new ArgumentException("No command present", nameof(commandLine)); }
string[] args = commandLine.Split(' ');
if (args.Length == 0)
{
    throw new ArgumentException("No command present", nameof(commandLine));
}

But since C# 7, we have the option to use the throw keyword as/in expressions...

Which lets you produce more compact code and use it in places where it was forbidden.

How exactly?

Unity C# Exceptions: Throw as Expressions

Let's assume you have an in-game console for you to cheat. You surely want to quickly iterate over your functionality and such.

If you want to create a character, you could waste your time navigating through all the menus...

Or you can just type in a command like this:

createcharacter duncan
createcharacter duncan
createcharacter duncan

The problem?

We make mistakes when typing commands, so you need to protect your code against input that corrupts your database.

If you break the game, you won't get rid of your boss breathing down your neck while you work.

So you can make your code safer and explicit by using exceptions.

Certainly, you have more options to code safely... but exceptions is the chosen one for today's post.

So here's a rudimentary example of command parsing routine:

void ParseCheatCommand(string commandLine)
{
string[] args = commandLine.Split(' ');
if (args.Length == 0)
{
throw new ArgumentException("No command present", nameof(commandLine));
}
var command = args[0];
if (command.Equals("createcharacter"))
{
if (args.Length != 2)
{
throw new ArgumentException("Usage: createcharacter name", nameof(commandLine));
}
var characterName = args[1];
var newCharacter = new Character
{
Name = characterName
};
newCharacter.Save();
}
else if (command.Equals("quit"))
{
// ...
}
}
void ParseCheatCommand(string commandLine) { string[] args = commandLine.Split(' '); if (args.Length == 0) { throw new ArgumentException("No command present", nameof(commandLine)); } var command = args[0]; if (command.Equals("createcharacter")) { if (args.Length != 2) { throw new ArgumentException("Usage: createcharacter name", nameof(commandLine)); } var characterName = args[1]; var newCharacter = new Character { Name = characterName }; newCharacter.Save(); } else if (command.Equals("quit")) { // ... } }
void ParseCheatCommand(string commandLine)
{
  string[] args = commandLine.Split(' ');
  if (args.Length == 0)
  {
    throw new ArgumentException("No command present", nameof(commandLine));
  }
  var command = args[0];
    
  if (command.Equals("createcharacter"))
  {
    if (args.Length != 2)
    {
      throw new ArgumentException("Usage: createcharacter name", nameof(commandLine));
    }
    var characterName = args[1];
    var newCharacter = new Character
    {
      Name = characterName
    };
    newCharacter.Save();
  }
  else if (command.Equals("quit"))
  {
    // ...
  }
}

We always expect at least one word: the command. If that's not present, we throw an exception (line 6) to warn the user to back off.

If we want to create a character, then we check that we passed two arguments: the createcharacter command and the character name. If that's not the case, we also throw an exception.

In both cases, we used the throw statement within conditional blocks, which adds a few extra lines (and that's okay).

But how would this look if we used the throw keyword as an expression?

You could, for instance, transform the code to use ternary operators.

Take a look. Don't be shy.

void ParseCheatCommandV2(string commandLine)
{
string[] args = commandLine.Split(' ');
var command = args.Length > 0 ? args[0] : throw new ArgumentException("No command present", nameof(commandLine));
if (command.Equals("createcharacter"))
{
var characterName = (args.Length == 2) ? args[1] : throw new ArgumentException("Usage: createcharacter name", nameof(commandLine));
var newCharacter = new Character
{
Name = characterName
};
newCharacter.Save();
}
else if (command.Equals("quit"))
{
// ...
}
}
void ParseCheatCommandV2(string commandLine) { string[] args = commandLine.Split(' '); var command = args.Length > 0 ? args[0] : throw new ArgumentException("No command present", nameof(commandLine)); if (command.Equals("createcharacter")) { var characterName = (args.Length == 2) ? args[1] : throw new ArgumentException("Usage: createcharacter name", nameof(commandLine)); var newCharacter = new Character { Name = characterName }; newCharacter.Save(); } else if (command.Equals("quit")) { // ... } }
void ParseCheatCommandV2(string commandLine)
{
  string[] args = commandLine.Split(' ');
  var command = args.Length > 0 ? args[0] : throw new ArgumentException("No command present", nameof(commandLine));
    
  if (command.Equals("createcharacter"))
  {
    var characterName = (args.Length == 2) ? args[1] : throw new ArgumentException("Usage: createcharacter name", nameof(commandLine));
    var newCharacter = new Character
    {
      Name = characterName
    };
    newCharacter.Save();
  }
  else if (command.Equals("quit"))
  {
    // ...
  }
}

That's how you throw an exception within a ternary operator.

If we have arguments, then we take the first one as our command. Otherwise, we throw an exception.

The same for the character name.

We can go further and protect our characters from developers who try to set their names to an empty value:

private class Character
{
public string Name
{
get => _name;
set => _name = string.IsNullOrEmpty(value) ? value : throw new ArgumentException("Name can't be empty");
}
private string _name;
}
private class Character { public string Name { get => _name; set => _name = string.IsNullOrEmpty(value) ? value : throw new ArgumentException("Name can't be empty"); } private string _name; }
private class Character
{
  public string Name
  {
    get => _name;
    set => _name = string.IsNullOrEmpty(value) ? value : throw new ArgumentException("Name can't be empty");
  }
  private string _name;
}

See that setter?

That setter is expecting an expression, so that's what we are giving it by using a throw expression.

No more secrets to it. I promised I was going to be easy on you.

What's Next?

Okay, I hope this article gave you a new angle about exceptions.

It's useful to have this tool in certain cases, but personally I won't go crazy about changing my existing code.

After all, a solid if block is more readable than a long ternary operator (but that's me).

If you missed my other resources on modern C# for Unity game development, you can start from the beginning with Unity C#: Out Parameters.

~ Ruben

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Rubén Torres Bonet

About the author

Born Game Developer, now ready to help you develop better games. Primary programmer on Star Trek Bridge Crew (Oculus Quest), Diamond Dash. Programmer on Time Stall, Catan Universe, Anne Frank House VR, Jelly Splash, Blackguards Definitive Edition. I also worked in minor XR experiences for HoloLens and Vive for clients such as Audi and Volkswagen.

You might also like

The Bright Side of Ray-Traced Global Illumination in Unity
>