dev

News in C#7 – Pattern Matching in Deep

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).

settings
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.

 

 

 

 

 

Advertisements