In this post I am looking into the internals of the
is not pattern, which is introduced in C# 9. This pattern allows the code to be more expressive. Assume to have a method with a type of an
object input parameter, which needs validation. The method may only work if the object is a 2 character long
string value. For example, we would like to validate that the
string is a two letter country code.
C# 9 allows
is not patterns. Using this pattern, we can express the above example with the following code:
using System; object o1 = "UK"; if (o1 is not string countryCode || countryCode.Length != 2) Console.WriteLine("Invalid country code"); else Console.WriteLine(countryCode);
Note, that the actual country code is not being validated, we only look at if the string confirms with the format expected.
I used ILSpy to look at the Release build of the above sample. ILSpy is a great tool as it also reflects back the original C# code based on the IL.
// string text = "UK" as string; IL_0000: ldstr "UK" IL_0005: isinst [System.Runtime]System.String IL_000a: stloc.0 // if (text == null || text.Length != 2) IL_000b: ldloc.0 IL_000c: brfalse.s IL_0017 IL_000e: ldloc.0 IL_000f: callvirt instance int32 [System.Runtime]System.String::get_Length() IL_0014: ldc.i4.2 IL_0015: beq.s IL_0022 // Console.WriteLine("Invalid country code"); IL_0017: ldstr "Invalid country code" IL_001c: call void [System.Console]System.Console::WriteLine(string) // } IL_0021: ret // Console.WriteLine(text); IL_0022: ldloc.0 IL_0023: call void [System.Console]System.Console::WriteLine(string) // (no C# code) IL_0028: ret
Based on the IL it visible that the is pattern is still using
isinst IL instruction to determine the type of the object which is on top of the evaluation stack. In this case the
string "UK" is loaded on the top of the evaluation stack by the first instruction. The rest of the code does what a developer would code with the as type-testing operator:
using System; object o1 = "UK"; var countryCode = o1 as string; if (countryCode == null || countryCode.Length != 2) Console.WriteLine("Invalid country code"); else Console.WriteLine(countryCode);
After testing against the
string type, if it is
null, OR it is
string, but not with the length of two, then writes an error message on the console. Otherwise it writes out the country code. It is up to the reader to decide which style is preferred. With the is not pattern I like how an explicit
null check is avoided as well it fits closer to how you would express the intention with natural language. On the other hand, I prefer less how the countryCode variable is declared within the pattern inside the if statement, making it more difficult read (or we just need to train our eyes for this).
Performance wise one could expect that both run with the same performance and it seems so as well. Using BenchmarkDotNet to measure the performance of the two implementations, I do not see a significant difference.
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i5-1035G4 CPU 1.10GHz, 1 CPU, 8 logical and 4 physical cores .NET Core SDK=5.0.101 [Host] : .NET Core 5.0.1 (CoreCLR 22.214.171.124516, CoreFX 126.96.36.199516), X64 RyuJIT DefaultJob : .NET Core 5.0.1 (CoreCLR 188.8.131.52516, CoreFX 184.108.40.206516), X64 RyuJIT | Method | o1 | Mean | Error | StdDev | |---------- |---- |----------:|----------:|----------:| | IsPattern | UK | 0.8238 ns | 0.0327 ns | 0.0305 ns | | AsPattern | UK | 0.8718 ns | 0.0367 ns | 0.0343 ns | | IsPattern | USA | 0.5688 ns | 0.0399 ns | 0.0444 ns | | AsPattern | USA | 0.5442 ns | 0.0416 ns | 0.0446 ns |
One interesting point is that the input with 3 characters runs faster compared to the 2 character version. Based on further measurements the difference is due the jump operation when the else branch of the if statement is hit.