Hello
everyone. Today I want to show you a new feature in C# 8 called Tuple patterns. After more than two-year
break, I have decided that it’s high time to resurrect my blog. So, let’s talk
what is the tuple pattern and what the problem this pattern solving. Tuple
patterns ⎼
some algorithms depend on multiple inputs. Tuple patterns allow you to switch
based on multiple values expressed as a tuple. The following code shows a
switch expression for the game rock, paper, scissors (I’ve grabbed this code
from What's
new in C# 8.0 documentation):
public static string
RockPaperScissors(string first, string second)
=> (first, second) switch
{
("rock", "paper") => "rock
is covered by paper. Paper wins.",
("rock", "scissors") => "rock
breaks scissors. Rock wins.",
("paper", "rock") => "paper
covers rock. Paper wins.",
("paper", "scissors") => "paper
is cut by scissors. Scissors wins.",
("scissors", "rock") => "scissors
is broken by rock. Rock wins.",
("scissors", "paper") => "scissors
cuts paper. Scissors wins.",
(_, _) => "tie"
};
If
you see in existing code, we used tuple pattern to compare a pair of strings
and return string as a result. For example, for strings “rock” and “paper” we
will return a new string “rock is covered by paper. Paper wins.” So, let’s take
a look how it works under the hood. (Below you will find IL code with further
explanation how this code works)
.method public hidebysig static
string RockPaperScissors (
string first,
string second
) cil managed
{
// Method begins at RVA 0x2078
// Code size 206 (0xce)
.maxstack 2
.locals init (
[0] string,
[1] string
)
IL_0000: ldarg.0
IL_0001: brfalse IL_00c2
IL_0006: ldarg.0
IL_0007: ldstr "rock"
IL_000c: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0011: brtrue.s IL_0032
IL_0013: ldarg.0
IL_0014: ldstr "paper"
IL_0019: call bool [mscorlib]System.String::op_Equality(string, string)
IL_001e: brtrue.s IL_0054
IL_0020: ldarg.0
IL_0021: ldstr "scissors"
IL_0026: call bool [mscorlib]System.String::op_Equality(string, string)
IL_002b: brtrue.s IL_0073
IL_002d: br IL_00c2
IL_0032: ldarg.1
IL_0033: brfalse IL_00c2
IL_0038: ldarg.1
IL_0039: ldstr "paper"
IL_003e: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0043: brtrue.s IL_0092
IL_0045: ldarg.1
IL_0046: ldstr "scissors"
IL_004b: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0050: brtrue.s IL_009a
IL_0052: br.s IL_00c2
IL_0054: ldarg.1
IL_0055: brfalse.s IL_00c2
IL_0057: ldarg.1
IL_0058: ldstr "rock"
IL_005d: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0062: brtrue.s IL_00a2
IL_0064: ldarg.1
IL_0065: ldstr "scissors"
IL_006a: call bool [mscorlib]System.String::op_Equality(string, string)
IL_006f: brtrue.s IL_00aa
IL_0071: br.s IL_00c2
IL_0073: ldarg.1
IL_0074: brfalse.s IL_00c2
IL_0076: ldarg.1
IL_0077: ldstr "rock"
IL_007c: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0081: brtrue.s IL_00b2
IL_0083: ldarg.1
IL_0084: ldstr "paper"
IL_0089: call bool [mscorlib]System.String::op_Equality(string, string)
IL_008e: brtrue.s IL_00ba
IL_0090: br.s IL_00c2
IL_0092: ldstr "rock is covered by paper. Paper wins."
IL_0097: stloc.0
IL_0098: br.s IL_00ca
IL_009a: ldstr "rock breaks scissors. Rock wins."
IL_009f: stloc.0
IL_00a0: br.s IL_00ca
IL_00a2: ldstr "paper covers rock. Paper wins."
IL_00a7: stloc.0
IL_00a8: br.s IL_00ca
IL_00aa: ldstr "paper is cut by scissors. Scissors wins."
IL_00af: stloc.0
IL_00b0: br.s IL_00ca
IL_00b2: ldstr "scissors is broken by rock. Rock wins."
IL_00b7: stloc.0
IL_00b8: br.s IL_00ca
IL_00ba: ldstr "scissors cuts paper. Scissors wins."
IL_00bf: stloc.0
IL_00c0: br.s IL_00ca
IL_00c2: ldstr "tie"
IL_00c7: stloc.0
IL_00c8: br.s IL_00ca
IL_00ca: ldloc.0
IL_00cb: stloc.1
IL_00cc: ldloc.1
IL_00cd: ret
} // end of method Program::RockPaperScissors
string RockPaperScissors (
string first,
string second
) cil managed
{
// Method begins at RVA 0x2078
// Code size 206 (0xce)
.maxstack 2
.locals init (
[0] string,
[1] string
)
IL_0000: ldarg.0
IL_0001: brfalse IL_00c2
IL_0006: ldarg.0
IL_0007: ldstr "rock"
IL_000c: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0011: brtrue.s IL_0032
IL_0013: ldarg.0
IL_0014: ldstr "paper"
IL_0019: call bool [mscorlib]System.String::op_Equality(string, string)
IL_001e: brtrue.s IL_0054
IL_0020: ldarg.0
IL_0021: ldstr "scissors"
IL_0026: call bool [mscorlib]System.String::op_Equality(string, string)
IL_002b: brtrue.s IL_0073
IL_002d: br IL_00c2
IL_0032: ldarg.1
IL_0033: brfalse IL_00c2
IL_0038: ldarg.1
IL_0039: ldstr "paper"
IL_003e: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0043: brtrue.s IL_0092
IL_0045: ldarg.1
IL_0046: ldstr "scissors"
IL_004b: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0050: brtrue.s IL_009a
IL_0052: br.s IL_00c2
IL_0054: ldarg.1
IL_0055: brfalse.s IL_00c2
IL_0057: ldarg.1
IL_0058: ldstr "rock"
IL_005d: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0062: brtrue.s IL_00a2
IL_0064: ldarg.1
IL_0065: ldstr "scissors"
IL_006a: call bool [mscorlib]System.String::op_Equality(string, string)
IL_006f: brtrue.s IL_00aa
IL_0071: br.s IL_00c2
IL_0073: ldarg.1
IL_0074: brfalse.s IL_00c2
IL_0076: ldarg.1
IL_0077: ldstr "rock"
IL_007c: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0081: brtrue.s IL_00b2
IL_0083: ldarg.1
IL_0084: ldstr "paper"
IL_0089: call bool [mscorlib]System.String::op_Equality(string, string)
IL_008e: brtrue.s IL_00ba
IL_0090: br.s IL_00c2
IL_0092: ldstr "rock is covered by paper. Paper wins."
IL_0097: stloc.0
IL_0098: br.s IL_00ca
IL_009a: ldstr "rock breaks scissors. Rock wins."
IL_009f: stloc.0
IL_00a0: br.s IL_00ca
IL_00a2: ldstr "paper covers rock. Paper wins."
IL_00a7: stloc.0
IL_00a8: br.s IL_00ca
IL_00aa: ldstr "paper is cut by scissors. Scissors wins."
IL_00af: stloc.0
IL_00b0: br.s IL_00ca
IL_00b2: ldstr "scissors is broken by rock. Rock wins."
IL_00b7: stloc.0
IL_00b8: br.s IL_00ca
IL_00ba: ldstr "scissors cuts paper. Scissors wins."
IL_00bf: stloc.0
IL_00c0: br.s IL_00ca
IL_00c2: ldstr "tie"
IL_00c7: stloc.0
IL_00c8: br.s IL_00ca
IL_00ca: ldloc.0
IL_00cb: stloc.1
IL_00cc: ldloc.1
IL_00cd: ret
} // end of method Program::RockPaperScissors
On
the surface, there is a lot of IL code here which is hard to understand. But we
can take a look more deeply at this code and split this code to the simple
logical blocks.
IL_0000: ldarg.0
IL_0001: brfalse IL_00c2
IL_0006: ldarg.0
IL_0007: ldstr "rock"
IL_000c: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0011: brtrue.s IL_0032
IL_0013: ldarg.0
IL_0014: ldstr "paper"
IL_0019: call bool [mscorlib]System.String::op_Equality(string, string)
IL_001e: brtrue.s IL_0054
IL_0020: ldarg.0
IL_0021: ldstr "scissors"
IL_0026: call bool [mscorlib]System.String::op_Equality(string, string)
IL_002b: brtrue.s IL_0073
IL_002d: br IL_00c2
IL_0001: brfalse IL_00c2
IL_0006: ldarg.0
IL_0007: ldstr "rock"
IL_000c: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0011: brtrue.s IL_0032
IL_0013: ldarg.0
IL_0014: ldstr "paper"
IL_0019: call bool [mscorlib]System.String::op_Equality(string, string)
IL_001e: brtrue.s IL_0054
IL_0020: ldarg.0
IL_0021: ldstr "scissors"
IL_0026: call bool [mscorlib]System.String::op_Equality(string, string)
IL_002b: brtrue.s IL_0073
IL_002d: br IL_00c2
The first line of code loads the argument
at index 0 into the evaluation stack. The next line allows to transfer control
to a target instruction if value is false, a null reference or zero. If this
check instruction returns false for us, then compiler navigates to IL_00c2.
Below you will find how this label looks like.
IL_00c2: ldstr "tie"
IL_00c7: stloc.0
IL_00c8: br.s IL_00ca
From development perspective this IL instruction works like goto statement.
IL_00c7: stloc.0
IL_00c8: br.s IL_00ca
From development perspective this IL instruction works like goto statement.
public static string
RockPaperScissors(string first, string second)
{
if (first == null)
goto NotFound;
NotFound:
return "tie";
}
If
the first instruction does not return null, the compiler loads the arguments at
index 0. This instruction will check the first argument “rock” using equality
operator
IL_0006: ldarg.0
IL_0007: ldstr "rock"
IL_000c: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0011: brtrue.s IL_0032
IL_0007: ldstr "rock"
IL_000c: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0011: brtrue.s IL_0032
If
the first argument (in our case the parameter first) called “rock” compiler creates an instruction to navigate to
label IL_0032. I will post bellow the information about IL_0032 label.
IL_0032:
ldarg.1
IL_0033: brfalse IL_00c2
IL_0038: ldarg.1
IL_0039: ldstr "paper"
IL_003e: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0043: brtrue.s IL_0092
IL_0045: ldarg.1
IL_0046: ldstr "scissors"
IL_004b: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0050: brtrue.s IL_009a
IL_0052: br.s IL_00c2
IL_0033: brfalse IL_00c2
IL_0038: ldarg.1
IL_0039: ldstr "paper"
IL_003e: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0043: brtrue.s IL_0092
IL_0045: ldarg.1
IL_0046: ldstr "scissors"
IL_004b: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0050: brtrue.s IL_009a
IL_0052: br.s IL_00c2
Here
at the first line the compiler downloads the second argument from stack and checks
this argument to null value. If this value is equal to null, we will
jump to IL_00c2 label and return “tie” result. In example below I will try to
show how this result will look in C# code.
public static string
RockPaperScissors(string first, string second)
{
if (first == null)
goto NotFound;
if(first.Equals("rock"))
goto IL_0032;
IL_0032:
if (second == null)
goto NotFound;
NotFound:
return "tie";
}
In
the second operation, we are checking equality to value “paper”. If the
second argument is equal to “paper”, the compiler creates an instruction to navigate
to IL_0092
label and returns string “rock is covered by paper. Paper wins.”
IL_0092: ldstr
"rock is covered by paper. Paper wins."
IL_0097: stloc.0
IL_0098: br.s IL_00ca
IL_0097: stloc.0
IL_0098: br.s IL_00ca
If
the previous check returns false, the compiler checks the second
instruction from stack and compares this argument with “scissors”
IL_0045: ldarg.1
IL_0046: ldstr "scissors"
IL_004b: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0050: brtrue.s IL_009a
IL_0046: ldstr "scissors"
IL_004b: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0050: brtrue.s IL_009a
If
this checking result returnes true, we will jump to label IL_009a and
return value “rock breaks scissors. Rock wins.”
IL_009a: ldstr
"rock breaks scissors. Rock wins."
IL_009f: stloc.0
IL_00a0: br.s IL_00ca
IL_009f: stloc.0
IL_00a0: br.s IL_00ca
Otherwise,
the next call IL_0052: br.s IL_00c2 will jump us to exit and return “tie” from
method. Ok, I will propose to see how this code will be mapped to C#.
public static string
RockPaperScissors(string first, string second)
{
if (first == null)
goto NotFound;
if(first.Equals("rock"))
goto IL_0032;
IL_0032:
if (second == null)
goto NotFound;
if (second.Equals("paper"))
goto IL_0092;
if (second.Equals("scissors"))
goto IL_009a;
IL_0092:
return "rock
is covered by paper. Paper wins.";
IL_009a:
return "rock
breaks scissors. Rock wins.";
NotFound:
return "tie";
}
You
see, that is something like a jump table or simple state machine.
Other checking for “rock” and “scissors” just adds additional labels for
jumping. I would suggest taking a look at the C# projection for our IL code
with updating label names to a readable format.
public static string
RockPaperScissors(string first, string second)
{
if (first == null)
goto NotFound;
if(first.Equals("rock"))
goto ROCK;
if (first.Equals("paper"))
goto PAPER;
if (first.Equals("scissors"))
goto SCISSORS;
ROCK:
if (second == null)
goto NotFound;
if (second.Equals("paper"))
goto
rock_is_covered_by_paper;
if (second.Equals("scissors"))
goto
rock_breaks_scissors;
PAPER:
if (second == null)
goto NotFound;
if (second.Equals("rock"))
goto
paper_covers_rock;
if (second.Equals("scissors"))
goto
paper_is_cut_by_scissors;
SCISSORS:
if (second == null)
goto NotFound;
if (second.Equals("rock"))
goto scissors_is_broken_by_rock;
if (second.Equals("paper"))
goto
scissors_cuts_paper;
rock_is_covered_by_paper:
return "rock
is covered by paper. Paper wins.";
rock_breaks_scissors:
return "rock
breaks scissors. Rock wins.";
paper_covers_rock:
return "paper
covers rock. Paper wins.";
paper_is_cut_by_scissors:
return "paper
is cut by scissors. Scissors wins.";
scissors_is_broken_by_rock:
return "scissors
is broken by rock. Rock wins.";
scissors_cuts_paper:
return "scissors
cuts paper. Scissors wins.";
NotFound:
return "tie";
}
When
we look more precisely at this C #code, we can split this code into four states
(state machine).
In
state 0 we have the basic logic for checking the first argument. The State
number one shows how state machine works when we check “rock” parameter.
The
state number two does the same for “paper” validation
And
the last state 3, where we will check “scissors” validation.
So,
as you see, as much parameters you will add, as more complex state machine will
be created. I find this feature very useful and it will help to develop a new
functionality in less time using C# language.
No comments:
Post a Comment