Overview
Since news in C# 7.0 were announced, there are various papers and post describing these new features:
There is a simple list (quite a lot):
- Tuple Value Types
- Pattern Matching
- Ref Returns
- Out Var
- Local Functions
- Binary Literals
- Partial Class Enhancements (replace)
Pattern Matching
So when I saw the pattern matching constructs, I was interested not only interester how to use it but how it is interpreted in the Intermediate Language (IL) internally.
First of all, for the tests I have used Visual Studio 15 Preview 2 (It is NOT Visual Studio 2015, that is in reality Visual Studio 14).
Next, in the project properties -> build tab, add __DEMO__,__DEMO_EXPERIMENTAL__ into Conditionals compilation symbols (see Figure 1).

Test Code
Lets start with a simple example. As you can see, there is defined interface IMember inherited by two classes – User and Admin.
public interface IMember { int Id { get; set; } string Name { get; set; } } public class User : IMember { public int Id { get; set; } public string Name { get; set; } public void Login() { } } public class Admin : IMember { public int Id { get; set; } public string Name { get; set; } public void PrintListOfUsers() { } }
Next, we have a common method DoAction(IMember member) that checks the real type and do action based on this type (for our purpose, we omit possibility to have a particular method on the IMember implemented in the inherited classes).
There are various ways how to check and use the type:
- Using is operator and casting
- Using as operator
- Using pattern matching
I have built all these three possibilities separately and decompile generated assemblies via dotPeek.
Using is operator and casting
It is the worst possibility because you are casting the the variable two times – first while using is and second type, using direct casting.
C# Code
public static void DoAction(IMember member) { // with is if (member is User) { var userWithIs = (User)member; userWithIs.Login(); } }
Decompiled Code:
public static void DoAction(Program.IMember member) { if (!(member is Program.User)) return; ((Program.User) member).Login(); }
IL Code:
.method public hidebysig static void DoAction( class C7News.Program/IMember member ) cil managed { .maxstack 2 .locals init ( [0] bool V_0, [1] class C7News.Program/User V_1 ) IL_0000: nop // [20 7 - 20 37] IL_0001: ldarg.0 // member IL_0002: isinst C7News.Program/User IL_0007: ldnull IL_0008: cgt.un IL_000a: stloc.0 // V_0 IL_000b: ldloc.0 // V_0 IL_000c: brfalse.s IL_001e IL_000e: nop // [22 7 - 22 38] IL_000f: ldarg.0 // member IL_0010: castclass C7News.Program/User IL_0015: stloc.1 // V_1 IL_0016: ldloc.1 // V_1 IL_0017: callvirt instance void C7News.Program/User::Login() IL_001c: nop IL_001d: nop IL_001e: ret } // end of method Program::DoAction
Using as operator
Better way is to use as operator and check for the null value (in this case via null propagation).
C# Code
public static void DoAction(IMember member) { // with as var userWithAs = member as User; userWithAs?.Login(); }
Decompiled Code:
public static void DoAction(Program.IMember member) { Program.User user = member as Program.User; if (user == null) return; user.Login(); }
IL Code:
.method public hidebysig static void DoAction( class C7News.Program/IMember member ) cil managed { .maxstack 1 .locals init ( [0] class C7News.Program/User user ) IL_0000: nop // [20 7 - 20 49] IL_0001: ldarg.0 // member IL_0002: isinst C7News.Program/User IL_0007: stloc.0 // user // [21 7 - 21 24] IL_0008: ldloc.0 // user IL_0009: brtrue.s IL_000d // [22 9 - 22 16] IL_000b: br.s IL_0014 // [23 7 - 23 19] IL_000d: ldloc.0 // user IL_000e: call instance void C7News.Program/User::Login() IL_0013: nop IL_0014: ret } // end of method Program::DoAction
Patter Matching Construct
With the pattern matching, it can be written in the following form. If the is operator returns true, variable userPM is assigned with the member value, cast to User type.
C# Code
public static void DoAction(IMember member) if (member is User userPM) { userPM.Login(); }
Decompiled Code:
public static void DoAction(Program.IMember member) { Program.User user = member as Program.User; if (user == null) return; user.Login(); }
IL Code:
.method public hidebysig static void DoAction( class C7News.Program/IMember member ) cil managed { .maxstack 2 .locals init ( [0] class C7News.Program/User user, [1] bool V_1 ) IL_0000: nop // [20 7 - 20 49] IL_0001: ldarg.0 // member IL_0002: isinst C7News.Program/User IL_0007: stloc.0 // user // [21 7 - 21 24] IL_0008: ldloc.0 // user IL_0009: ldnull IL_000a: cgt.un IL_000c: stloc.1 // V_1 IL_000d: ldloc.1 // V_1 IL_000e: brfalse.s IL_0019 IL_0010: nop // [23 7 - 23 19] IL_0011: ldloc.0 // user IL_0012: callvirt instance void C7News.Program/User::Login() IL_0017: nop IL_0018: nop IL_0019: ret } // end of method Program::DoAction
Results
From the code, you can see the difference between all three expressions. Interesting is the code decompiled in the third case which is the same as in the seconds case. But the IL code is different.
Using is operator and casting
It is nothing new that this way is the worst option. The code checks the type of the variable (isinst) and that cast it to the proper type (castclass).
Using as operator
In this case, the IL code is the most compact. It just calls isinst to check if the variable has a proper type and call the method if true.
Using pattern matching
In this, most important case for us, it calls isinst once too. But except variable for the cast variable (userPM, user in IL), it has to store a bool value for the if branch evaluation (V_1 in IL).
So it can look like the as operator is still the most optimized code. Yes, in this simple example. But in the most complicated situations like using pattern matching in switch/case, it brings a lot of advantages for more readable code.