Here's a bit of code.

Value value;
try
{
  value = dictionary[key];
}
catch (KeyNotFoundException ex)
{
  return null;
}

return DoThingWithValue(value);

Index access is a function that will, depending on the key you try, either give you back a value or throw an exception.

Let's quickly establish whether this is an input or output problem. A particular input might throw an exception, which suggests we need to strengthen the type of our input — except that's not quite true. The result of a dictionary lookup should probably be viewed as dependent on the dictionary itself, and not on the key we try: the dictionary could hold a value for every key possible under the type constraint, but in the case where it doesn't, that's the fault of the dictionary and not the key we're trying. That means it's more appropriate to fix this by altering its return type instead of its key type.

C#'s standard dictionary gives a few helpful functions for safely analysing contents; by using the Contains function we can declare a block in which an already typed key value is further typed (semantically) as 'existing inside the given dictionary' for the duration of the block.

What on Earth does that mean? It means this.

Value value;
if (dictionary.ContainsKey(key))
{
  value = dictionary[key];
}
else
{
  return null;
}

return DoThingWithValue(value);

Inside the if block, we've declared a scope in which we can safely do a lookup on a dictionary, knowing that the call won't fail because of a missing key. Outside that scope we can't safely do the lookup because it may throw an exception. Readers will probably notice that this 'alternative' code block is effectively equivalent to what's done above; the only difference is that we're now avoiding throwing unnecessary exceptions.

This is still hideous of course, so C# offers something cleaner.

if (!dictionary.TryGetValue(key, out var value))
{    
    return null;
}

return DoThingWithValue(value);

Blocks of code like these pop up frequently in code I maintain. I hate them with a passion. One could argue this addresses potential threading concerns, but overall this reconstruction has done nothing to solve the problems raised above whilst introducing extra problems on top.

Syntactic issues include the declaration of a variable inside a function call that's actually declared outside the scope of the if block; said variable's being accessible even when TryGetValue returns false (and who knows what value it holds then); the requirement to craft and throw your own exceptions if you can't sensibly default value's value.

The higher level issues with the out-var construct are more subtle. Sure, the construct allows an obvious place for recovery, but the code doesn't require you to handle the error inside the if block, so the barely conscious developer might just use the block for logging, and then carry on with whatever value they've pulled out, even if the function returned false (with no compiler error in sight). Sure, the block is nicely self-contained and encourages you to make use of value in the space below the if, but if you have a number of dictionary lookups to do where the handling code is equivalent, you're duplicating a lot of code.

These issues alone are enough for me to conclude that I never want to use the out var construct in code again, ever.


A colleague who worked at Google in a previous life (developers, in my experience, seem to have a habit of considering their employment histories as 'past lives') said that a running joke amongst developers there was the decision by Google of all companies to make a language whose name made it hard to look up online. One must substitue 'golang' for 'go' in a search for 'dictionary lookup go' in order to find the following example code:

if val, ok := dict[key]; ok {
    //do something here
}

This piece of code is surprisingly strongly typed. val exists only in the scope of the block where ok is true, meaning val is appropriately populated. But that's just luck: I could just as easily have done the dictionary assignment outside the if condition and accessed val even when ok was false.

Go tutorials have the nasty habit of praising Go's apparent clarity when it comes to errors. As part of the return type (and with linters encouraging developers not to ignore error results) code often looks like this:

result, err := foo()
if err != nil {
  // handle
}

// ""happy path""

And tutorials will see this as an example of Go's error handling's working. The most offensive claim is that which states that developers are now forced to handle errors because they are part of the return type. There is no compiler demand to actually do this however, so there's no warning if a developer decides to access a variable that shouldn't be set because an error is set instead.

Of course, C# is a capable language, so we can emulate this Go code with an out variable.

if (foo(out var result) is var err && err != null)
{
  // handle
}

// ""happy path""

The precise issue is with the fact that the return types contain multiple utterly independent results. result and err are free to hold whatever values they like, and it's just convention that states that result is undefined iff err is defined. These issues are less obvious when seen in the target language because languages have conventions that encourage developers to do the right thing (and those conventions have definitely served Go well), but when the same behaviour is seen in C# it exposes somewhat how dependent we are on convention to not make mistakes.

The flaw in Go's error approach is most obvious when dealing with multiple dangerous interactions with an object. A scanner for example may run into problems when Text() is called, but checking for an error every time would be both slow and difficult to reason about, so a function Err() is exposed that one can check when done to see if anything did in fact go wrong.

scanner := bufio.NewScanner(input)
for scanner.Scan() {
    token := scanner.Text()
    // process token
}
if err := scanner.Err(); err != nil {
    // process the error
}

This terrifies me. The convention is very clear (call scanner.Err()) but that's all that stands between the barely conscious developer and a very silent failure of the call to scanner.Text().

Personally, I would rather those conventions exist as compile requirements, so that I never even have the chance to access a variable in an inappropriate context.