Behind the sc#enes

C# and .NET celebrate their 15th birthday this year. To mark the occasion a new version (C# 7.0) will be released in a few weeks after two years have passed since the last update. Over the years, a lot of very powerful new features like lambda expressions and LINQ (C# 3.0) or async/await (C#5.0) have been added to the language. If we take a closer look at these additions, we see that many of them are actually extensions of the C# compiler. They enable us to express complex code in a very compact way using special keywords. This compact code is then translated into a less sophisticated but more verbose version by the compiler.

Let’s take a look at an example: A new feature in C# 7.0 is called “Pattern Matching”. It greatly enhances the switch statement which previously could only be used with compile-time constant values. Consider the following class hierarchy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Teacher : Person
{
    public string Subject { get; set; }
}

public class Student : Person
{
    public int Grade { get; set; }
}

Now imagine we want to write a method that takes a person object and returns a different description for teachers and students:

1
2
3
4
5
6
7
8
9
switch (person)
{
    case Teacher t:
        return "Teacher (" + t.Subject + ")";
    case Student s:
        return "Student";
    default:
        throw new ArgumentException();
}

We can now switch on the runtime type of an object! The compiler will generate code that checks for the type and then branches to the correct case. We can even add a variable name (after the type) that the result of the type cast will be assigned to and that can be used in the case block. But it doesn’t end there: It’s also possible to add additional conditions using the when keyword:

1
2
3
4
5
6
7
8
9
10
11
switch (person)
{
    case Teacher t:
        return "Teacher (" + t.Subject + ")";
    case Student s when s.Grade == 1:
        return "Exceptional student";
    case Student s:
        return "Student";
    default:
        throw new ArgumentException();
}

The open source tool ILSpy allows us to view and analyze the generated IL code. It can even translate that IL code back into C#. Let’s take a look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
if (person != null)
{
    Teacher teacher;
    string result;
    if ((teacher = (person as Teacher)) != null)
    {
        Teacher t = teacher;
        result = "Teacher (" + t.Subject + ")";
    }
    else
    {
        Student student;
        if ((student = (person as Student)) != null)
        {
            Student s = student;
            if (s.Grade == 1)
            {
                result = "Exceptional student";
                return result;
            }
        }
        Student student2;
        if ((student2 = (person as Student)) == null)
        {
            goto IL_34;
        }
        result = "Student";
    }
    return result;
}
IL_34:
throw new ArgumentException();

Behind the scenes, the compiler just generates the more complicated if-else-cascade that we previously would have needed to write ourselves. As stated earlier, this applies to most of the language features that were added to C# over the years. Even complex ones like LINQ queries, yield or async/await are implemented in a similar way. The generated code might be more complex than in this example and sometimes involves multiple private classes, but the main point still holds true: the original code is not compiled into IL directly, but transformed into a more “basic” version of C# without the fancy new keywords in a first step. And we could have written that code ourselves, but as we all know: developers are lazy and love syntactic sugar. ;)