Exploring C# 7
Expressiveness Redefined & #notasugly
Intro
Since we have all been actively celebrating the 20th anniversary of Visual Studio, it felt appropriate to post about C# 7! In this post we will explore the features that make C# 7 so promising. I’ve put together a demonstration C# 7 project, that is available here .
This post contains examples and details on five of the nine new C# 7 features.
- Pattern matching
out
variables- Tuples
- Local functions
throw
expressions
These are the remaining features, that I do not cover in this post.
ref
locals and returns- More expression-bodied members
- Generalized
async
return types - Numeric literal syntax improvements
Pattern Matching
With C# 7 we welcomed the concept of “patterns”. This concept allows for the extraction of information when a variable is tested for a certain “shape” and matches a specified pattern. We’re able to leverage the “shape” from which we matched on as a declared variable in scope, consuming it as we deem necessary. This is referred to as “dynamic” (or “method”) dispatch.
Dynamic dispatch is nothing new to C#, and has been around forever. C# 7 exposes this functionality via constant and type patterns.
Constant Patterns
Constant pattern null
, similar to (obj == null)
.
public void IsExpression(object obj)
{
if (obj is null) // Constant pattern "obj is null"
{
return;
}
}
Type Patterns
Look closely at this syntax. This is where we start mixing metaphors. Prior to C# 7 we could use the “is” expression to do simple type assertions
obj is [type]
. Additionally, we all know how to declare a variable int i
. This new syntax merges these concepts together and is more compound and
expressive.
public void IsExpression(object obj)
{
// (obj is int i)
// "obj is int" // type assertion "typically evaluates type compatibility at run time"
// "int i" // declaration
if (obj is int i) // Type pattern "obj is int i"
{
// We can then use the "i" (integer) variable
}
// Note, the variable "i" is also available in this scope.
// This is in fact by design, more on that out the "out variable" section
}
The when
keyword has also been extended, now it not only applies to the catch
statement but also the case
labels within a switch
statement.
Consider the following classes:
class Shape
{
protected internal double Height { get; }
protected internal double Length { get; }
protected Shape(double height, double length)
{
Height = height;
Length = length;
}
}
class Circle : Shape
{
internal double Radius => Height / 2;
internal double Diameter => Radius * 2;
internal double Circumference => 2 * Math.PI * Radius;
internal Circle(double height, double length)
: base(height, length) { }
}
class Rectangle : Shape
{
internal bool IsSquare => Height == Length;
internal Rectangle(double height, double length)
: base(height, length) { }
}
Now imagine that we have a collection of these Shape
objects, and we want to print out their various details - we could use “pattern matching” as such:
static void OutputShapes(IEnumerable<Shape> shapes)
{
foreach (var shape in shapes)
{
// Previously, this was not permitted. Case labels had to be concrete
// such as enums, numerics, bools, strings, etc.
switch (shape)
{
case Circle c:
WriteLine($"circle with circumference {c.Circumference}");
break;
case Rectangle s when (s.IsSquare):
WriteLine($"{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
default:
WriteLine("This is not a shape that we're familiar with...");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
}
}
As you can see, we are able to more easily reason about the specific shape
in context. For example, with each iteration of our collection we switch
on the shape
. If the shape
is an instance of the Circle
subclass, we’ll execute the case
label “Circle” and we get the instance declared as
its type in the variable c
. Likewise, if the shape
is a Rectangle
and that rectangle s
just so happens to also be a square when (s.IsSquare)
evaluates to true
- we will then execute the square case
label. If the shape
is an instance of a Rectangle
but not a square, we execute the
“Rectangle” case
label. Notice we still have default
fall-thru. Finally, we can also have a “null” case
label.
out
variables
.NET
developers are more than familiar with the Try*
pattern, but as a refresher this is what it looks like. Imagine we are trying to parse
a System.String
input value as a System.Int32
. Imagine that the consumer doesn’t really care if it is parsed, they’re fine with a default(int)
if it fails.
public int ToInt32(string input)
{
int result;
if (int.TryParse(input, out result))
{
return result;
}
return default(int);
}
Let’s quickly recap this. First, we declare a variable namely result
. We then invoke the int.TryParse
which returns a bool
whether or not the
parse was successful. If true
then the declare result
variable is not equal to the parsed int
value. If the input
was "12"
, then result
would be 12
. If the input
was "Pickles"
, then the return from the invocation to the ToInt32
would be 0
as int.TryParse
would return false
.
Now with C# 7 we can declare our out
variable inline as follows:
public int ToInt32(string input)
{
// Note: the declaration is inline with the out keyword
if (int.TryParse(input, out int result))
{
return result;
}
return default(int);
}
The scope of the result
variable is identical to the previous example, as it actually “leaks” out to the if
statement. We can re-write this even
more expressively:
public int ToInt32(string input) => int.TryParse(input, out var result) ? result : result;
A few things you might notice. First, this is now a single line as we can express this with the lambda operator. We leverage the ternary operator as
well. Additionally, we can use the var
keyword for our declaration. And since the result
variable is in scope we can use it as both return cases.
If unable to be parsed, it is in fact a default(int)
anyways.
Tuples
Most developers are familiar with System.Tuple<T[,T1...]>
. This class
has served us well all the while it has been around.
One of the advantages is that it exposes readonly
fields - from the values that it is instantiated with. This
was also great for equality comparisons and even using the tuple as a dictionary key.
In C# 7 we have a new syntax for expressing tuples. Enter the ValueTuple
, and as the name implies - this is a struct
instead of a class
. There
are obvious performance gains from using a light-weight value-type over the allocation of a class
.
void LegacyTuple()
{
var letters = new Tuple<char, char>('a', 'b');
// Values were accessible via these Item* fields.
var a = letters.Item1;
var b = letters.Item2;
}
This wasn’t overly exciting from an API perspective, as the field names do not really imply anything about their intention.
void ValueTuple()
{
var letters = ('a', 'b');
var a = letters.Item1;
var b = letters.Item2;
// Note: ToTuple extension method
var systemTuple = letters.ToTuple();
var c = systemTuple.Item1;
var d = systemTuple.Item2;
}
You might notice that the syntactic sugar is pouring over this new feature. This is referred to as a “tuple literal”. We dropped the entire new
keyword
usage, as well as specifying the types. They are all inferred and in fact known, IntelliSense proves this immediately. But we still have the issue of
these tuples not being very API friendly. Let’s explore how we can give them custom names.
void MoreValueTuples()
{
var lessonDetails =
(Subject: "C# Language Semantics", Category: Categories.Programming, Level: 300);
// Note: IntelliSense now hides Item1, Item2 and Item3
// Instead we are provided with the following:
var subject = lessonDetails.Subject; // string
var category = lessonDetails.Category; // Categories [enum]
var level = lessonDetails.Level; // int
}
Deconstruction
Now that we see how we can instantiate a ValueTuple
, let’s take a look at how we can declare one for usage.
void DeconstructionExamples()
{
var lessonDetails =
(Subject: "C# Language Semantics", Category: Categories.Programming, Level: 300);
// We can deconstruct in three various ways
// First, the fully qualified type
(string subject, Categories category, int level) = lessonDetails;
// Next using the var keyword per named declaration
(var subject, var category, var level) = lessonDetails;
// Finally, omitting any type declaration and using var wholeheartedly
var (subject, category, level) = lessonDetails;
}
There are often questions about how deconstruction is implemented, and whether or not it is ordinal based. For the ValueTuple
it is in fact
ordinal based. However, note that deconstruction is not actually limited to tuples. With C# 7 any object
that defines a public void Deconstruct
method can be deconstructed. Consider the following:
class Person
{
private readonly (string First, string Middle, string Last) _name;
private readonly DateTime DateOfBirth _dateOfBirth;
public Person((string f, string m, string l) name, DateTime dob)
{
_name = name;
_dateOfBirth = dob;
}
public void Deconstruct(out double age,
out string firstName,
out string middleName,
out string lastName)
{
age = (DateTime.Now - _dateOfBirth).TotalYears;
firstName = _name.First;
middleName = _name.Middle;
lastName = _name.Last;
}
}
Now that the Person
is defined with this Deconstruct
method, we can deconstruct it following the same ordinal based semantics.
void DeconstructNonTuple()
{
var person = new Person(("David", "Michael", "Pine"), new DateTime(1984, 7, 7));
(int age, string first, string middle, string last) = person;
// Note: to partially deconstruct you can ignore a specific ordinal by using the _
// This does not actually naming the ordinal variable, but truly ignoring it.
var (_, _, _, _) = person; // Ignore all, not very useful
var (_, firstName, _, _) = person; // Cherry-pick first name
}
Comparing Anonymous object
vs. ValueTuple
At first glance tuples look almost like anonymous objects. They are in fact very different. An anonymous object is actually a reference type whereas
a ValueTuple
is a struct
- value type. Also, you can only return an anonymous object from a method as an object
which isn’t very API friendly.
Within a fluent LINQ
chained method anonymous objects are great and will still be normal for projection.
Local Functions
At first glance, local functions seem a bit odd. I’ve heard people say, “this method is starting to look like a class”. At first, I was one of these people too. Once you get used to the idea and see the benefits it really does make sense. Here is a quick comparison of the two, note the benefits of local functions as they compare to lambdas.
Lambda(s) | Local Function(s) | Details | |
---|---|---|---|
Generics | Local functions allow for the use of generics | ||
Iterators | The yield keyword is valid within local functions |
||
Recursion | Local functions support recursion | ||
Allocatey | Delegates require an object allocation |
||
Potential Variable Lifting | Implicitly captured closure is non-existent |
It is vital to understand that local functions are not a replacement for Action<T[,T1...]>
or Func<T[,T1...]>
. These delegate declarations are still
needed as parameters to enable lambda expression arguments. If you see the #notasugly hashtag, this was coined by Mads Torgersen.
More efficient
When using local functions, there is no object
created - unlike delegates that require an object for it to be used. Likewise, local functions
help to alleviate another issue with lambdas in that they do not need to implicitly capture a variable longer than it is potentially needed.
In C# lambdas capture values by reference, meaning that garbage collection may not be able to correctly clean up code that is “allocatey”.
Declaration
With local functions, the declaration of the local function can actually occur after the return
statement - as long as it is within the method body
in which it is consumed. If you’re familiar with some of the implementations of the LINQ
extension methods on IEnumerable<T>
, you would know a lot
of the methods are defined with argument validation followed by the instantiation of “Iterator” classes, where these classes do the actual work.
Because of deferred execution, iterators do not actually execute validation logic until they are iterated - for example invoking .ToList()
, .ToArray()
,
or simply using them in a foreach
statement. Ideally, we would like our iterators to “fail-fast” in the event of being given invalid arguments. Let’s
imagine that the .Select
extension method was implemented as follows:
public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source,
Func<T, TResult> selector)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (selector == null) throw new ArgumentNullException(nameof(selector));
foreach (var item in source)
{
yield return selector(item);
}
}
Since this method is written as an iterator, the validation is skipped until it’s iterated. With C# 7 we can use local function to get both “fail-fast” validation and the iterator together.
public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source,
Func<T, TResult> selector)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (selector == null) throw new ArgumentNullException(nameof(selector));
return iterator();
IEnumerable<TResult> iterator()
{
foreach (var item in source)
{
yield return selector(item);
}
}
}
throw
expressions
Leveraging some pre-existing C# functionality - null
coalescing, we can now throw
when a value is evaluated as null
. A common validation
mechanism is to throw
if an argument is null
. Consider the following:
class LegacyService : IService
{
private readonly IContextProvider _provider;
public LegacyService(IContextProvider provider)
{
if (provider == null)
{
throw new ArgumentNullException(nameof(provider));
}
_provider = provider;
}
}
With C# 7 we can simplify this with the throw
expression.
class ModernService : IService
{
private readonly IContextProvider _provider;
public ModernService(IContextProvider provider)
{
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}
}
If the given provider
argument is null
we’ll coalesce over to the throw
expression.
From C# 6 to C# 7, then and now
I have a presentation that I have been fortunate enough to give at some regional conferences. One of these occasions was recorded, and I felt it made sense to share it here - Enjoy!!
Further Reading
Share this post
Sponsor
Twitter
Facebook
Reddit
LinkedIn
StumbleUpon
Email