Introduction To Csharp 8
Introduction To Csharp 8
1
Contents
Introduction to C# 8 ....................................................................................... Error! Bookmark not defined.
Introduction .............................................................................................................................................. 5
Nullable reference types ........................................................................................................................... 6
Enable Nullable Reference Types.......................................................................................................... 6
Examples ............................................................................................................................................... 7
Summary ............................................................................................................................................... 8
Default Interface Methods........................................................................................................................ 8
Modifiers in Interfaces ........................................................................................................................ 10
Diamond Problem ............................................................................................................................... 12
Using “This” Keyword.......................................................................................................................... 15
The ILogger Example ........................................................................................................................... 16
The Player Game ................................................................................................................................. 17
Limitations........................................................................................................................................... 18
Summary ............................................................................................................................................. 20
Introduction to Asynchronous Streams .................................................................................................. 21
Pull Programming Model vs. Push Programming Model .................................................................... 22
Motivation and Background ............................................................................................................... 23
Asynchronous pull with Client/Server ................................................................................................ 29
Async Streams ..................................................................................................................................... 32
Syntax .................................................................................................................................................. 33
Cancellation ........................................................................................................................................ 34
Summary ............................................................................................................................................. 35
Indices and Ranges .................................................................................................................................. 36
Index.................................................................................................................................................... 36
Range .................................................................................................................................................. 36
Summary ............................................................................................................................................. 40
Pattern Matching .................................................................................................................................... 41
Switch expressions .............................................................................................................................. 43
Tuple Pattern ...................................................................................................................................... 43
Positional Pattern................................................................................................................................ 44
Property Pattern ................................................................................................................................. 44
2
Recursive Patterns .............................................................................................................................. 45
More about Recursive Patterns .......................................................................................................... 46
Summary ............................................................................................................................................. 52
Using declarations ................................................................................................................................... 53
Enhancement of interpolated verbatim strings ...................................................................................... 53
Null-coalescing assignment..................................................................................................................... 53
Unmanaged constructed types ............................................................................................................... 53
Static local functions ............................................................................................................................... 54
Readonly-Member .................................................................................................................................. 54
Stackalloc in nested expressions............................................................................................................. 55
Disposable ref structs.............................................................................................................................. 55
3
This icon shows the positive influence of the feature.
This icon shows the .NET community opinion about the feature.
This icon explains which technology is used behind the scenes by the feature.
This icon means that the feature has a bug, or it is still not done.
4
Introduction
C# 8 is moving aggressively into the innovation world to keep C# growing. Because of this strategy,
Microsoft left many developers in a confused state. Currently, the developers have a very controversial
opinion on the C# 8 new features. In this book, you can have an overview of the new features of C# 8
language. I have described the important features separately and demonstrate them with examples; the
other less essential features I have handled them briefly. Besides, I have written about the pros and cons
of each important feature. It is also worth to mention that the main features concertation in C# 7 was to
add safe, efficient code to C#, and in C# 8, we have seen more big language features and the preparing
for the Records, which planned to be released with C# 9. After reading this book, you will have a greater
understanding of C# 8, and hopefully, you will be better prepared to use C# 8, and for the new C#
challenges you will meet in the future.
Figure -1- C# 7 focused on data consumption, code simplification, and performance and C# 8 more focus
on data consumption
5
Nullable reference types
C# 8.0 introduces nullable reference types. This feature is another way to specify that a given
parameter, variable, or return value can be null or not. In C# 8, the compiler emits a warning or error if a
variable that must not be null is assigned to null. Those warnings can help you to find and fix most of
your null exception bugs before they blow up at runtime.
<Nullable>enable</Nullable>
Figure -2- shows you a demo project where the Nullable Reference Types feature is enabled
If you put #nullable enable on the file head, that should allow the nullability check for the whole file, as
shown below in the image.
6
Figure -3- Enabling Nullable Reference Types partially
#nullable restore
Examples
Example 1
7
Example 2
#nullable enable
class Person
{
public string Name { get; set; } // Warning normalString is null!
public string? NullableName { get; set; }
// Enable the below code then the warning above will be disappeared
//public Person(string name)
//{
// Name = name;
//}
}
The first property Name is a reference type, and it is null for this reason the compiler warning you.
The Second property is NullableName is a nullable reference type that why the compiler is not warning
because the NullableName can be null, you have defined as nullable.
Summary
Nullable Reference Types help you to eliminate the NullReferenceException (the Billion Dollar Mistake,
ALGOL60, Tony). Moreover, it helps you to solve the problems in code like a pyramid of doom.
However, in complicated scenarios, this feature may bring some confusion in regards to reference types
and using ‘?’ character. For more information, please read Jon Skeet blog’s at:
https://codeblog.jonskeet.uk/2019/02/10/nullableattribute-and-c-8/
The main benefit that default methods allow you to add new functionality to the interfaces of your
libraries and ensure the backward compatibility with code written for older versions of those interfaces.
The C# syntax for an interface in the .NET compiler is extended to accept the new keywords in the
interfaces, which are listed below. For example, you can write a private method in the interface, and
the code still compiles and work.
8
Allowed in the interface:
Not allowed:
Example
Consider this simple example that illustrates how this feature works.
interface IDefaultInterfaceMethod
{
public void DefaultMethod()
{
Console.WriteLine("I am a default method in the interface!");
}
}
class AnyClass : IDefaultInterfaceMethod
{
}
class Program
{
static void Main()
{
IDefaultInterfaceMethod anyClass = new AnyClass();
anyClass.DefaultMethod();
}
}
Output:
If you look at the code above, you can see that the interface has a default method, and the implementer
class contains neither any knowledge of this default method nor the implementation of that interface
method.
9
The above code will produce the compile-time error: AnyClass does not contain any member of the
Default Interface Method.
That is proof that the inherited class does not know anything about the default method.
Figure -4- Compile-Error in the Main, because the AnyClass does not have any information about the
Default Interface Method
Modifiers in Interfaces
The C# syntax for an interface is extended to accept the following keywords: protected, internal, public,
and virtual. By default, the default interface methods are virtual unless the sealed or private modifier is
used. Similarly, abstract is the default on interface members without bodies.
Example
interface IDefaultInterfaceMethod
{
// By default, this method is virtual. The “virtual” keyword is unnecessary!
virtual void DefaultMethod()
{
Console.WriteLine("I am a default method in the interface!");
}
// By default, this method will be abstract, and the abstract keyword can
be here used
abstract void Sum();
}
10
}
}
class Program
{
static void Main()
{
IDefaultInterfaceMethod anyClass = new AnyClass();
anyClass.DefaultMethod();
Output:
https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-02-
27.md#collision-of-lookup-rules-and-decisions-for-base
The keywords virtual and abstract are redundant, and they can be removed from the interface;
however, removing them does not have any effect on the compiled code.
Example
11
The above code produces a compile-time error: The modifier public is not valid for this item.
Diamond Problem
An ambiguity error that can arise because of allowing multiple inheritance. It is a big problem for
languages (like C++) that allow multiple inheritance of state. In C#, however, multiple inheritance is
unallowed for classes, but rather only for interfaces in a limited way, so that does not contain state.
12
Consider the following situation:
interface B : A
{
void A.m()
{
System.Console.WriteLine("interface B");
}
}
interface C : A
{
void A.m()
{
System.Console.WriteLine("interface C");
}
}
class D : B, C
{
static void Main()
{
C c = new D();
c.m();
}
}
The above code will produce the compile-time error, which shown below in Figure -7-:
13
Figure -7- Diamond Problem error message
The .NET development team has decided to solve the Diamond Problem by taking the most specific
override at runtime.
If you want to know more about this problem, you can find more information in the proposal: “Default
Interface Methods and C# Language Design Notes for Apr 19, 2017”.
Back to our example, the problem is that the most specific override cannot be inferred from the
compiler. However, you can add the method “m” in the class “D” as shown below, and now the compiler
uses the class implementation to solve the diamond problem.
class D : B, C
{
// Now the compiler uses the most specific override, which is defined in the
class “D”.
void A.m()
{
System.Console.WriteLine("I am in class D");
}
static void Main()
{
A a = new D();
a.m();
}
14
}
Output:
> I am in class D
void CallDefaultThis(int x)
{
this[0] = x;
}
}
Console.WriteLine(defaultMethodWithThis[0]);
defaultMethodWithThis.CallDefaultThis(0);
Output:
0
SetX
15
The ILogger Example
The logger interface is an good example to explain the Default methods technique. I have defined below
one abstract method named WriteCore. All other methods have a default implementation.
ConsoleLogger and TraceLogger classes are implementing the interface. If you look at the code below,
you can see that the code is compact. In the past, it was mandatory to implement all the methods in a
class that implements an interface unless that class is declared as an abstract, and this might make your
code DRY.
enum LogLevel
{
Information,
Warning,
Error
}
interface ILogger
{
void WriteCore(LogLevel level, string message);
case LogLevel.Warning:
16
Trace.TraceWarning(message);
break;
case LogLevel.Error:
Trace.TraceError(message);
break;
}
}
}
17
public class WeakPlayer : ILimitedPlayer
{
}
As you see in the code example above, the default implementation is in the IPowerPlayer interface and
the ILimitedPlayer interface. The limited player is doing less damage. If we define a new class, for
example, SuperDuperPlayer, which inherited from the class StrongPlayer, then the new class
automatically receives the default power attack behavior of the interface, as shown in the example
below.
Limitations
There are some limitations and considerations that you first need to understand when using modifier
keywords in the interfaces. In most cases, the compiler protects you against the wrong usage of this
feature by detecting a lot of common errors, such as those listed below.
interface IAbstractInterface
{
abstract void M1() {}
abstract private void M2() {}
abstract static void M3() {}
static extern void M4() {}
18
}
The error CS0500, means the default method IAbstractInterface.M3() cannot be abstract, and at the
same time, it has a body. The error CS0621, says the method cannot be private and, at the same time,
abstract.
19
Figure -8- Compilation Errors for the wrong modifiers in the Default Interface Methods
Summary
This language feature allows you to add members to the interfaces and provide an implementation for
those members. It enables API authors to add methods to an interface in later versions without breaking
the source or binary compatibility with existing implementations of that interface.
You can add new functionality to the interface without breaking the compatibility with the older
versions of those interfaces.
20
Please use this feature carefully. Otherwise, it can easily lead to violating the single responsibility
principles.
It is a very arguable feature, and it left many open discussions among the .NET community. I have
discussed this theme in-depth on Reddit:
https://www.reddit.com/r/csharp/comments/8xg2kx/default_interface_methods_in_c_8/
It is based on the Trait technique. Traits—Programming is a proven and powerful technique in OOP.
After many tries by Microsoft to simplify asynchronous operations, the async/await pattern has gained a
good acceptance among developers thanks to its easy-to-understand approach.
One of the significant limitations of the existing Async Methods is the requirement that it must have a
scalar return result (one value). Let us consider the following method async Task<int>
DoAnythingAsync(). The result of DoAnythingAsync is an integer (One value).
Because of this limitation, you cannot use the yield inside Async Methods, or you cannot return an async
enumeration, for example, public async Task<IEnumerable<T>> DoAnythingAsync <T>().
If it were to be possible to combine async/awaiting feature with a yielding operator that can lead to a
powerful programming model, which is known as asynchronous data pull or also known as pull-based
enumeration and in F# this is called async sequence.
Async Streams in C# 8 removes the scalar result limitation and allows the async method to return
multiple results.
This change makes the async pattern more flexible so that you can load data from the database
asynchronously and show data that are partially loaded immediately, or you can download data from in
an asynchronous sequence that returns the data in chunks when they become available.
A simple definition: Async Streams, allows you to use async keyword to iterate asynchronously over
collections, for example:
21
await foreach (var streamChunck in asyncStreams)
{
Console.WriteLine($“Received data count = {streamChunck.Count}”);
}
Usually, in the Push Programming Model, you do not have to control the Publisher. The data are
asynchronously pushed into a queue, and the Consumers consume the data when the data arrive. Unlike
the Rx, the Async Streams can be called on-demand to generate multiple values until the end of the
enumeration is reached.
Below you can find a comparison between the pull-based model and the push-based model, and you can
see which scenario to which technique is a better fit from the other one. I have used many examples,
and I have added a demo to show you the whole concept and the benefits. Finally, I have explained the
Async Streams feature and demonstrate it.
22
I have used the famous food example Producer/Consumer, but in our scenarios, the Producer will not
generate food; instead of that, he generates data, and the Consumer consumes the generated data, as
shown in Figure -9-. The Pull Model is easy to understand. The Consumer is asking and pulling the data
from the Producer. The other approach is the Push Model. The Producer publishes the data in a queue,
and the Consumer must subscribe to receive the pushed data.
In the faster Producer and slow Consumer use case, the pull model is suitable because the Consumer
pulls its required data from the Producer to avoid the overflow issues. In the slower Producer and faster
Consumer use case, the Push Model is suitable because the Producer pushes more data from the
Producer to Consumer to avoid causing the Consumer unnecessary waiting time.
Rx and Akka Streams (Stream Programming Model) use the Backpressure technique, which is a flow
control mechanism. It uses the Pull or Push Models dynamically to solve the Producer/Consumer
problems mentioned above.
I have used in my example below a slow Consumer to pull the asynchronous sequence of data from a
faster Producer. Once the Consumer processes an element, the Consumer asks the Producer again for
the next element, and so on until the end of the sequence is reached.
I have extended the Console WriteLine/WriteLineAsync method so that it adds the current time and the
current thread Id to the written message as shown below:
// Writes the message with timestamp and the thread Id for the async
methods.
public static async void WriteLineAsync(object message)
{
await Task.Run(() => Console.WriteLine(
$"(Time: {DateTime.Now.ToShortTimeString()},
Thread {Thread.CurrentThread.ManagedThreadId}):
{message} ")
);
}
}
23
{
ConsoleExt.WriteLine("SumFromOneToCount called!");
var sum = 0;
for (var i = 0; i <= count; i++)
{
sum = sum + i;
}
return sum;
}
Method call:
Output:
We can make this method lazy by using the yield operator, as shown below.
24
ConsoleExt.WriteLine("Sum with yield completed.");
ConsoleExt.WriteLine("################################################");
ConsoleExt.WriteLine(Environment.NewLine);
Output:
As you see above in the output window, the result is returned in parts and not as one value. The above
shown accumulated results known as lazy enumeration. However, we still have a problem; the sum
methods are blocking the code. If you look at the threads, then you can see that everything is running in
the main thread.
Now let’s use the magic word async and apply it to the first method SumFromOneToCount (without
yield).
25
Output:
Impressive, we can see that computing is running in another thread, but we still have a problem with
the result. The result is returned as one value!
Imagine that we can combine the lazy enumerations (yield return) with the Async Methods in an
imperative style. This combination is known as Async Streams. It is the new C# 8 feature. The new
feature gives us an excellent technique to solve the Pull Programming Model problems like download
data from a website or to read in records from a file or a database in a modern way.
Let’s try to do that with C# 7. I have added the async keyword to the method SumFromOneToCountYield
as follows: static async Task<IEnumerable<int>> SumFromOneToCountYield(int count)
Figure -10- The error occurs when yield with the async keyword is combined in C# 7
After Adding async to the SumFromOneToCountYield method, we get errors, as shown in Figure -10-.
Let’s try something else. We can put IEnumerable in the Task and remove the yield keyword, as shown
below:
26
{
ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable called!");
var collection = new Collection<int>();
var result = await Task.Run(() =>
{
var sum = 0;
return result;
}
ConsoleExt.WriteLine("SumFromOneToCountAsyncIEnumerable done!");
ConsoleExt.WriteLine("################################################");
ConsoleExt.WriteLine(Environment.NewLine);
Output:
27
As you see in the threads, we calculate everything asynchronously, but we still have a problem. The
results (all results are accumulated in the collection) return as one block. That is not our desired lazy
behavior. If you remember, our goal is combining lazy behavior with an asynchronous computing style.
To achieve the desired behavior, you need to use an external library like Ix (part of the Rx), or you have
to use the new C# 8 feature Async Streams.
Let's get back to our code example. I have used the Async Streams feature from C# 8.
28
var consumingTask = Task.Run(() =>
ConsumeAsyncSumSeqeunc(pullBasedAsyncSequence));
Finally, we have achieved our desired behavior! We can iterate asynchronously over the enumeration.
https://github.com/alugili/IAsyncEnumerableDemo
29
Figure -11- Synchronous Data Pull, the Client, wait until the request is finished
30
Figure -12- Asynchronous Data Pull, the client, can be doing something else while the data are requested
31
Figure -13- Asynchronous Sequence Data Pull (Async Streams). The Client is not blocked!
Async Streams
In C# 8, similar to IEnumerable<T> and IEnumerator<T>; There are two new interfaces,
IAsyncEnumerable<T> and IAsyncEnumerator<T> which are defined as below:
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken
cancellationToken = default);
}
32
}
ValueTask<T> is a struct that wraps Task<T>, and is behaviorally equivalent to Task<T>, except that it
enables more efficient execution when the task completes synchronously (which can often happen
when enumerating a sequence).
Syntax
The foreach syntax has been extended with asynchronous functionality so that making use of the
asynchronous interfaces when the await keyword is used:
As seen above in the example, instead of computing a single value, we can now to potentially compute
many values, sequentially, while also being able to await other asynchronous operations.
Example:
To generate an asynchronous stream, you write a method that combines the principles of iterators and
asynchronous methods. As I mentioned before, your method should include both yield return and await,
and it should return IAsyncEnumerable<T>:
33
{
Console.WriteLine(number); // use the Async sequence
}
}
Output:
0
1
2
3
4
5
6
7
8
9
Consuming an asynchronous stream requires you to add the await keyword before the foreach keyword
when you enumerate the elements of the stream. Adding the await keyword requires the method that
enumerates the asynchronous stream to be declared with the async modifier, and to return a type
allowed for an async method. Typically, that means returning a Task or Task<TResult>. It can also be a
ValueTask or ValueTask<TResult>. A method can both consume and produce an asynchronous stream,
which means it would return an IAsyncEnumerable<T>. The above code generates a sequence from 0 to
9 and waiting 500 milliseconds between each number:
Cancellation
There are many ways to cancel the Async Streams. I have explained below the cancellation token. The
cancellation token is a technique to cancel the async operations, and it can be passed to the extension
method WithCancellation.
I have modified the example above that the asynchronous process should be canceled after one second.
34
await foreach (var number in
CreateSequence(10).WithCancellation(cts.Token))
{
Console.WriteLine(number); // Use the Async sequence
}
Console.WriteLine("Done!");
}
Output:
1
2
Done!
The [EnumeratorCancellation] attribute is very important. Placing this attribute on a parameter tells the
compiler that if a token is passed to the async method, that token should be used instead of the value
initially passed for the parameter.
Summary
Async Streams is an excellent asynchronous pulling technique that can be used to write computations
that generate multiple values asynchronously.
The programming concept behind Async Streams is the async pull model. We can demand the next
element of the sequence, and we eventually get a reply. This technique is different from the push model
of IObservable<T>, which generates values unrelatedly to the consumer’s state. Async Streams provide
an excellent way to represent asynchronous data sources that can be controlled by the consumer, for
example, when the consumer isn't ready to handle more data. Examples include a web application or
reading records from the database.
Async Streams provide an excellent way to represent asynchronous data sources that can be controlled
by the consumer. For example, when downloading data from the web, we would like to create an
asynchronous collection that returns the data in chunks as they become available.
Async Pull Programming Model is known and used for years and already used in many other languages
like Akka Streams.
35
Indices and Ranges
Ranges and indices allow more natural syntax for accessing single items or ranges in a sequence. It
consists of two new operators (Index operator ^ in System.Index) and (Range operator .. in
System.Range), which allow constructing System.Index, and System.Range objects, and using them to
index/slice collections at runtime. The new operators are syntactic sugar and making your code cleaner
and more readable.
Index
Index feature is implemented in System.Index, and it is an excellent way to index a collection from the
ending.
The end index operator ^ (hat operator) specifying that the index is relative to the end of the sequence.
Range
The Range is a more natural syntax for specifying or accessing subranges in a sequence.
The Range easily defines a sequence of data. It is a replacement for Enumerable.Range(), except that the
Range, defines the start and stop points rather than start and count, and it helps you to write more
readable code.
Example:
36
The Range feature is implemented in System.Range and it offer a new way to access ranges or slices of
collections. This new way can help you to avoid LINQ and to make your code compact and more
readable. You can compare this feature with Range in F#.
The range operator has a start and the end parameter, which specifies the start and end of a range as its
operands.
Example:
Range range = 1..4; // 1 is the start parameter and 4 is the end parameter
var slice = array[range];
Console.WriteLine(slice); // output 1, 2, 3
Examples
Array Value Array Index from start Array Index from end
0 0 ^11
1 1 ^10
2 2 ^9
3 3 ^8
37
4 4 ^7
5 5 ^6
6 6 ^5
7 7 ^4
8 8 ^3
9 9 ^2
10 10 ^1
Important Note: the start index is inclusive (included to the slice), and the end index is exclusive
(excluded from the slice).
slice1 will be of type Span<int>. [4..^2] means Skip items from the begin until the index 4 and skip 2
items from the ending.
Bounded Ranges
In the bounded ranges, the lower bound (start index) and the upper bound (end index) are known or
predefined.
38
array[start..end] // Get items from start until end-1
The above Range syntax is inspired by Python. Python supports the following syntax(lower:upper:step),
The step in Python is optional, and it is 1 by default. C# 8 does not support steps, but there are
some wishes in the C# community to use the F# syntax (lower..step..upper).
Output:
5 7 9 11 13 15 17 19
The code above is equal to array.ToList().GetRange(3, 2);.If you compare array.ToList().GetRange(3, 2);
with array[3..5] you can see that the new style is more expressive.
There is a feature request to use the Range in the “if” statement and with the Pattern Matching, for
example, (switch(array) [1..5] ..), but unfortunately, this is still not done yet.
Unbounded Ranges
When the lower bound is omitted, it is interpreted to be zero, or the upper bound is omitted. It is
interpreted to be the length of the receiving collection.
Examples:
Range Chars
var collection = new [] { 'a', 'b', 'c' };
collection[2..]; // Selected chars: c
collection[..2]; // Selected chars: a, b
collection[..]; // Selected chars: a, b, c
39
Range with Strings
Ranges allow creating substrings by using the indexer:
Example:
Summary
The Range feature is very useful to generate sequences of numbers in the form of a collection or a list.
Range and Indexes make the C# syntax simpler and more readable.
An excellent feature and well accepted by many developers and especially for the functional
programming programmers.
40
Nothing is new here. Similar technology already exists in other languages like Python.
Pattern Matching
Pattern matching is one of the powerful constructs, which is available in many functional programming
languages like F#. Furthermore, pattern matching provides the ability to deconstruct matched objects,
giving you access to parts of their data structures. C# offers a rich set of patterns that can be used for
matching.
Pattern matching was initially planned for C# 7, but after a while, the .Net team has found that they
need more time to finish this feature. For this reason, they have divided it into two main parts. Basic
Pattern Matching that is already delivered with C # 7 and the Advanced Patterns Matching delivered
with C# 8. We have seen in C# 7 Const Pattern, Type Pattern, Var Pattern, and the Discard Pattern.
In C# 8, we have seen more patterns like Recursive Pattern, which consist of multiple sub-patterns like
the Positional Pattern, Tuple, Pattern, and Property Pattern.
As I mentioned above, the power of the Pattern matching is to allow you to compare data with a logical
structure or structures, decompose data into constituent parts, or to extract the information from data
in various ways.
The main benefits of Pattern Matching: Pattern Matching provides a flexible and powerful way of testing
data against a series of conditions (like if-else) and performing some computations based on the
condition met.
To have a deep understanding of Pattern Matching and Recursive Patterns, we need many examples.
I have defined two classes Employee and Company, and I have used them to demonstrate the Pattern
Matching concepts:
41
set;
}
public void Deconstruct(out string name, out int age, out Company company)
{
name = Name;
age = Age;
company = Company;
}
}
public void Deconstruct(out string name, out string website, out string
headOfficeAddress)
{
name = Name;
website = Website;
headOfficeAddress = HeadOfficeAddress;
}
}
The Deconstruct method allows you to deconstruct the employ object to a tuple. Deconstruct returns
void, and each value to be deconstructed is indicated by an out parameter in the method signature.
The following Deconstruct method of an Employ class returns the name, age, and company:
public void Deconstruct(out string name, out int age, out Company company)
42
Switch expressions
Most of us are familiar with the original switch statement in C#, which returns one value while the new
switch expression returns an expression. The new switch expressions syntax is a little bit different, but it
is more succinct as compared to the old switch statement. Besides, switch expression allows you to use
Pattern Matching within the switch scope.
switch (value)
{
case pattern guard => Code block to be executed
...
case _ => default code
}
Example:
Console.WriteLine(GetEmployee(bassam));
Console.WriteLine(GetEmployee (thomas));
Console.WriteLine(GetEmployee (obj));
Output:
Bassam Alugili
Thomas Albrecht
Unknown
Tuple Pattern
Tuple patterns allow you to switch based on multiple values expressed as a tuple.
Example:
Console.WriteLine(GetEmployee("Bassam", 42));
Console.WriteLine(GetEmployee("Thomas", 43));
Console.WriteLine(GetEmployee(string.Empty, 0));
43
public static string GetEmployee(string name, int age) => (name, age) switch
{
("Bassam", 42) => $"{name} Alugili",
("Thomas", _) => $"{name} Alugili",
(_, _) => "unknown"
};
Output:
Bassam Alugili
Thomas Albrecht
Unknown
("Bassam", 42) => $"{name} Alugili", the previous code line means: return the following string:$"Bassam
Alugili" only if the first tuple item is “Bassam” and the age is 42.
Positional Pattern
Positional Pattern decomposes a matched type and performs further pattern matching on the values
that are returned from it. The final value of this pattern/expression is true or false, which led to execute
the code block or not.
Output:
In this example, I have used the pattern matching recursively. The first part is the Positional Pattern
employee is Employee(…), and the second part is the sub-patterns within the brackets (_,_, ("Stratec",
_,_)).
The code block after the if statement will only be executed if the conditions in the root Positional
Pattern(employee object must be of type Employee) with its sub-pattern (_,_, ("Stratec",_,_) the
company name must be “Stratec”) are satisfied, and the rest is discarded. To explain it with simple
words, it can be explained as follows: the if statement evaluates true when the company name is
“Stratec” because everything else will be ignored/discarded.
Property Pattern
The property Pattern is straight forward. You can access a type fields and properties and apply a further
pattern matching against them.
44
Old style C# 6:
if (firstEmployee.GetType() == typeof(Employee))
{
var employee = (Employee) firstEmployee;
if (employee.Name == "Bassam Alugili" && employee.Age == 42)
{
Console.WriteLine($"The employee: {employee.Name} , Age {employee.Age}");
}
}
Compare the pattern matching code with C# 6 and look at how the C# 8 code is more clearly. The new
style removes the redundant code and the typecasting or the ugly operators like typeof or as.
Recursive Patterns
Recursive Pattern matching is a very powerful feature, which allows code to be written more elegantly,
mainly when used together with recursion. The Recursive Patterns consist of multiple sub-patterns like
as Positional Patterns, for example, var isBassam = user is Employee ("Bassam",_,_), Property Patterns,
var isMAis= user is Employee {Name : "Mais"}, Discard Pattern (_), and so forth.
MSDN:
“In addition to new patterns in new places, C# 8.0 adds recursive patterns. The result
of any pattern expression is an expression. A recursive pattern is simply a pattern
expression applied to the output of another pattern expression.”
Example:
switch (employee)
45
{
case (_, 10) employeeTmp when (employeeTmp.Name == "Rami Alugili"):
{
Console.WriteLine($"Hi {employeeTmp.Name}, you are now 10!");
}
break;
// If the employee has any other information, then execute the code below.
case (_,_):
Console.WriteLine("any other person!");
break;
}
The case (_, 10) is interpreted as follows. The first part, _, means the Name might be any string, and the
second part, 10 means the Age, must be 10. If the employee tuple contains this pair of information (any
string, 10), then the case block will be executed. Otherwise, the Discard Pattern will be executed case
(_,_).
Output:
If you remove the Deconstruct method from the Company class above, then the following error occurs:
error CS8129: No suitable Deconstruct instance or extension method was found for type Company, with
0 out parameters and a void return type.
I have created two employees and two companies and have mapped each employee to a company:
46
var firstEmployee = new Employee
{
Name = "Bassam Alugili",
Age = 42,
Company = stratec
};
Console.WriteLine(DumpEmployee(firstEmployee));
Console.WriteLine(DumpEmployee(secondEmployee));
Output
In the example above, the case condition is matching any employee with any data; it is a combination of
the Deconstruction Pattern and the Discard Pattern. Now we can go one step further. We need to filter
the Stratec employees.
In fact, there is more than one way to do this with Pattern Matching. We can replace/rewrite the below
case condition from the example above with some different techniques:
The first approach, by using the Recursive Patterns Matching (Deconstruction Pattern) in the switch
statement like the following.
47
Replace the above code with the code below:
Output:
We can also combine Deconstruction Pattern with Var Pattern like the following:
Another approach to filter the data by using Property Pattern recursively as shown below:
One important note I want to mention about using the switch statement with the pattern matching:
48
Look to the following Recursive Patterns matching examples:
The above switch is working fine. If we move one of those switch cases somewhere else up/down. Let’s
say you have moved (_, _, _) employeeTmp => $"The employee: {employeeTmp.Name}", to the
beginning (first case after the switch) as shown below:
error CS8510 : The pattern has already been handled by a previous arm of the switch expression.
error CS8510 : The pattern has already been handled by a previous arm of the switch expression.
error CS8510 : The pattern has already been handled by a previous arm of the switch expression.
49
Figure -14- Error after changing the switch case position in Visual Studio
The compiler knows that the cases below your most generic switch case (_, _ ,_) employeeTmp cannot
be reached (dead code), and the compiler informs you about (you are doing something wrong).
You can test it with Visual studio 2019 or in the web browser: https://sharplab.io
Code:
using System;
namespace PatternMatchingDemo
{
public class Company
{
public string Name
{
get;
set;
}
public void Deconstruct(out string name, out string website, out string
headOfficeAddress)
50
{
name = Name;
website = Website;
headOfficeAddress = HeadOfficeAddress;
}
}
public void Deconstruct(out string name, out int age, out Company
company)
{
name = Name;
age = Age;
company = Company;
}
}
class Program
{
static void Main(string[] args)
{
var stratec = new Company
{
Name = "Stratec",
Website = "wwww.stratec.com",
HeadOfficeAddress = "Birkenfeld",
};
var firstEmployee = new Employee
{
Name = "Bassam Alugili",
Age = 42,
Company = stratec
};
var microsoft = new Company
{
Name = "Microsoft",
Website = "www.microsoft.com",
51
HeadOfficeAddress = "Redmond, Washington",
};
var secondEmployee = new Employee
{
Name = "Satya Nadella",
Age = 52,
Company = microsoft
};
Console.WriteLine(DumpEmployee(firstEmployee));
Console.WriteLine(DumpEmployee(secondEmployee));
}
public static string DumpEmployee(Employee employee) => employee switch
{
{ Name: "Bassam Alugili", Company: Company(_, _, _) } employeeTmp =>
$"The employee: {employeeTmp.Name}! 1",
(_, _, ("Stratec", _, _)) employeeTmp => $"The employee:
{employeeTmp.Name}! 2",
(_, _, Company(_, _, _)) employeeTmp => $"The employee:
{employeeTmp.Name}! 3",
(_, _, _) employeeTmp => $"The employee: {employeeTmp.Name}! 4",
_ => "Other company!"
};
}
}
Summary
Recursive Pattern is the core of the Pattern Matching. Pattern Matching helps you to compare the
runtime data with any data structure and decompose it into constituent parts or extract sub-data from
data in different ways, and the compiler is supporting you to check the logic of your code.
Pattern Matching is an excellent feature. It is giving you the flexibility to testing the data against a
sequence of conditions and performing further computations based on the condition met.
Pattern matching helps you decompose and navigate data structures in a very convenient, compact
syntax. While pattern matching is conceptually similar to a sequence of (if-then) statements, so it can
help you to write the code in a functional style.
Well accepted among .NET Community, and it can be very good used with Records C# 9 feature.
52
Pattern matching is a proven and known technology, which has been used for many years, especially in
functional programming like F#.
Using declarations
It enhances the using operator to use with Patterns and make it more natural.
Public GetFirstItemFromDatabase()
{
using var repository = new Repository();
Console.WriteLine(repository.First());
// repository is disposed here!
}
In the example above, the repository is disposed when the closing brace for the method is reached.
Example.
Null-coalescing assignment
The ??= operator assigns a variable only if it’s null.
You can use it to simplify a common coding pattern where a variable is assigned a value if it is null.
if (variable == null)
{
variable = expression; // C# 1..7
}
53
In C# 7.3 and earlier, a constructed type (a type that includes at least one type of argument) can't be an
unmanaged type. Starting with C# 8.0, a constructed value type is unmanaged if it contains fields of
unmanaged types only.
Bar<int> type is an unmanaged type in C# 8.0. Like for any unmanaged type, you can create a pointer to
a variable of this type or allocate a block of memory on the stack for instances of this type:
// Pointer
var foo = new Foo <int> { Var1 = 0, Var2 = 0 },
var bar = &foo; // C# 8
// Block of memory
Span< Foo<int>> bars = stackalloc[]
{
new Foo <int> { Var1 = 0, Var2 = 0 },
new Foo <int> { Var1 = 0, Var2 = 0 }
};
Notes:
- Constructed value types are now unmanaged if it only contains fields of unmanaged types.
- This feature means that you can do things like allocate instances on the stack.
int AddFiveAndSeven()
{
int y = 1;
int x = 2;
Readonly-Member
54
It allows you to apply the readonly modifier to any member of a struct. It indicates that the member
doesn't modify the state.
var newX = X + 1; // OK
return newX;
}
}
55
C# 8 Feature Cheat Sheet
56
57
58