One technique to make your code easier to read, and work with, is to use “early exits” in your functions. Here is how I used early exits to clean up a function I wrote for a game.
Let’s say we have an online game and want the players to be able to change their character’s name. Here’s the code for our simple Player class, with an Id and Name property.
namespace Workbench.Models; public class Player { public int Id { get; } public string Name { get; set; } }
The function to rename the character needs to do several checks to prevent the user from renaming to a bad name, for example: an empty string, a name that another user already has, swear words, etc.
One way to check these conditions is to write a series of “if…else” statements. However, with many conditions, this can lead to deep indentation levels – making the code difficult to read.
public string RenamePlayer_BAD(int playerId, string newName) { string response = ""; if (string.IsNullOrWhiteSpace(newName)) { response = "The new name must contain at least one character"; } else { if (_players.All(p => p.Id != playerId)) { response = $"There is no player with an Id of {playerId}"; } else { if (_players.Any(p => p.Id != playerId && p.Name == newName)) { response = $"There is already a player named: {newName}"; } else { if (ContainsBannedWords(newName)) { response = "Named contains banned words. Choose a different name."; } else { // Name change is valid Player player = _players.First(p => p.Id == playerId); player.Name = newName; response = $"Name changed to: {newName}"; } } } } return response; }
With “early exits”, you can eliminate most levels of indentation, making the code easier to read.
Starting at the top of the function, you do simple “if” checks for values that are bad. If a value is bad, do whatever processing is needed for that situation and do a “return” to stop executing the rest of the code in the function.
This way, the “if” statements only go one level deep. We’ve also eliminated the “else” statements in this function.
The function is a few lines longer vertically, with the addition of the “return” lines. However, it’s much easier to follow the path through the function.
public string RenamePlayer_BETTER(int playerId, string newName) { if (string.IsNullOrWhiteSpace(newName)) { return "The new name must contain at least one character"; } if (_players.All(p => p.Id != playerId)) { return $"There is no player with an Id of {playerId}"; } if (_players.Any(p => p.Id != playerId && p.Name == newName)) { return $"There is already a player named: {newName}"; } if (ContainsBannedWords(newName)) { return "Named contains banned words. Choose a different name."; } // Name change is valid Player player = _players.First(p => p.Id == playerId); player.Name = newName; return $"Name changed to: {newName}"; }
This technique is commonly used to validate input parameters to functions. If the parameter has a bad value, throw an exception at the beginning of the function, instead of constantly checking throughout the function that the value is valid.
private void FunctionThatNeedsGoodParameterValues(int id, string value) { if (id < 1) { throw new ArgumentOutOfRangeException(nameof(id), "Must be a positive non-zero value"); } if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException("Cannot be an empty string", nameof(value)); } // Do the work the function was designed for, // knowing we have valid parameter values. }