Wednesday, March 18, 2020

C# 7.1 - Everything You Need To Know

Visual Studio 2017.3 brought with it the first minor update to the C# language, C# 7.1. This update adds four new features to C#: async main, target-typed default literals, tuple name inference, and generic support for pattern-matching.
In this post, you'll learn how to enable the new C# 7.1 language features in your projects, everything you need to know to start using all four of the new features, and some gotchas with using C# 7.1 in razor views.

How to Enable C# 7.1

By default, Visual Studio 2017 enables the latest major language version, which is C# 7.0. To enable the C# 7.1 features, you need to tell Visual Studio to use the latest minor language version or to explicitly use C# 7.1.
This is set at the project-level and is stored in the csproj file. So different projects can target different versions of the C# language.
There are 3 different ways to enable C# 7.1:
  1. Project Properties
  2. Edit the csproj File
  3. Lightbulb Code Fix

Method 1 - Project Properties

Right click on the project in solution explorer, go to properties, then select the build tab, select advanced in the bottom right, and then set the language version value.
Enable C# 7.1 through project properties

Method 2 - Edit the csproj File

For projects using the new-style csproj, currently .NET Core, .NET Standard, and older projects that you have upgraded to the new-style csproj:
  • Right click on the project in solution explorer
  • Select Edit [projectname].csproj
For projects using the old-style csproj:
  • Right click on the project in solution explorer
  • Select Unload Project
  • Right click on the project in solution explorer
  • Select Edit [projectname].csproj
You'll then need to add the LangVersion tag to the first PropertyGroup in your projects csproj file:
<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>netcoreapp2.0</TargetFramework>
  <LangVersion>7.1</LangVersion>
</PropertyGroup>
If your csproj includes multiple PropertyGroup tags for different build configurations, for example, debug and release builds, you will need to add the LangVersion tag to each of those tags:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
  <DebugType>full</DebugType>
  <Optimize>false</Optimize>
  <OutputPath>bin\Debug\</OutputPath>
  <LangVersion>7.1</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
  <Optimize>true</Optimize>
  <OutputPath>bin\Release\</OutputPath>
  <LangVersion>7.1</LangVersion>
</PropertyGroup>
These are the values you can use for LangVersion:
  • default
  • latest
  • ISO-1
  • ISO-2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 7.1
default picks the latest major version, currently C# 7.0. When C# 8.0 is available, default will start to use that.
latest picks the latest minor version, currently C# 7.1. When C# 7.2 is available, latest will start to use that.
The explicit version choices will continue to use the selected version, even when newer versions are released. For example, 7.1 will continue using C# 7.1 when C# 8.0 is released.

Method 3 - Lightbulb Code Fix

The final way to enable C# 7.1 is to try and use one of the new language features. For example, you could try and use the new target-typed default literal. You will then get a lightbulb code fix that offers to enable C# 7.1.
The lightbulb lets you upgrade to either latest or 7.1. It also lets you choose to upgrade all C# projects. If you have a lot of projects to upgrade, this is the fastest way to upgrade them all at the same time.
Enable C# 7.1 through lightbulb code fix

New Feature: Async Main

C# 7.1 enables the use of async/await in the Main method. This makes it easier to use async code throughout your entire application.
To use async main, add the async keyword to your main method and make it return either a Task or a Task<int>. Returning a Task corresponds to main methods that currently return void and Task<int> corresponds to main methods that currently return int.
Here is an example of a program using async main. The program waits for two seconds, then prints Hello World.
public class Program
{
    static async Task Main(string[] args)
    {
        await Task.Delay(2000);

        Console.Out.WriteLine("Hello World");
    }
}
This is a very handy feature when you're writing small test applications, such as for bug reports, as it lets you eliminate some boilerplate. Previously, you had to create a separate async method and call that from the Main method.

New Feature: Target-Typed Default Literals

C# 7.1 adds a new target-typed default literal that provides a shortcut for the default(T) operator using type inference.
In earlier versions of C#, to get the default value for a type, you had to specify the type explicitly. For example, default(int) returned 0. C# 7.1 allows you to drop the type and have it inferred automagically.
I predominantly use the default operator with generics, but it is called for in other situations. In the following examples, I show seven different ways you can use the new target-typed default literal. Number seven is my personal favourite.

1. Local Variable Declaration

You can use default when declaring local variables to initialize them.
int i = default;

2. Return Value

You can use default as a return value in a method.
int defaultValue()
{
    return default;
}
You can also use default as the return value in a lambda method.
Func<int> defaultValue = () => default; 

3. Optional Parameter

You can use default to set the default value for an optional parameter on a method.
void DoSomething(int i = default)
{
    Console.Out.WriteLine(i);
}

4. Object or Array Initializer

You can use default inside an object or array initializer as one of the values.
In this example, we see default used inside an object initializer:
void CreateX()
{
    var x = new X
    {
        Y = default,
        Z = default
    };
}

class X
{
    public int Y;
    public int Z;
}
In this example, we see default used inside two different array initializers:
var x = new[] { default, new List<string>() };
Console.Out.WriteLine(x[0] == null); // Prints "True"

var y = new[] { default, 5 };
Console.Out.WriteLine(y[0] == 0); // Prints "True"
In the first example, default takes on the value null, as it gets the default value of List<string>. In the second example, default takes on the value 0, as it gets the default value of int.

5. is operator

You can use default on the right-hand side of the is operator.
int i = 0;
Console.Out.WriteLine(i is default == true); // Prints "True"

Console.Out.WriteLine(default is i == true); // Compile Error

6. Generics

You can use default with generic types. In this example, default creates the default value of generic type T.
public class History<T>
{
    private readonly List<T> history = new List<T>();

    public T Create()
    {
        T value = default;

        this.history.Add(value);

        return value;
    }
}

7. Ternary Operator

You can use default with the ternary operator. This is my favourite use case for the target-typed default literal.
Previously, it was annoying to assign a default value when using the ternary operator. You could not just assign null, you had to explicitly cast null onto the target type.
void method()
{
    int? result = runTest() ? 10 : (int?)null; // OK

    int? result = runTest() ? 10 : null; // Compile Error
}

bool runTest() => true;
If you do not explicitly cast null onto the correct type, you get a compile error. In the previous example, the compile error is:
Type of conditional expression cannot be determined because there is no implicit conversion between 'int' and '<null>'.
The new target-typed default literal makes this a lot cleaner as you no longer need any casting.
void method()
{
    int? result = runTest() ? 10 : default;
}
This might not look like much of an improvement. However, I often see this pattern in cases where the type name is very long and often these types involve multiple generic type parameters. For example, the type might be Dictionary<string, Dictionary<int, List<IDigitalDevice>>>.

New Feature: Tuple Name Inference

Another new feature in C# 7.1 is tuple name inference. This is also known as tuple projection initializers.
This feature allows tuples to infer their element names from the inputs. For example, instead of (x: value.x, y: value.y), you can now write (value.x, value.y).

Behaviour

Tuple name inference works with identifiers (such as local variable x), members (such as a property x.y), and conditional members (such as a field x?.y). In these three cases, the inferred name would be xy, and y respectively.
In other cases, such as the result of a method call, no inference occurs. If a tuple name is not specified in these cases, the value will only be accessible through the default reserved name, e.g. Item3 for the third element of a tuple.
Reserved tuple names such as ItemNRest, and ToString are not inferred. This is to avoid conflicts with the existing usage of these on tuples.
Non-unique names are not inferred. For example, on a tuple declared as (x, t.x), no names will be assigned to either element, as the name x is not unique. Note that this code still compiles, but the variables will only be accessible through Item1 and Item2. This ensures that this new feature is backwards compatible with existing tuple code.

Breaking Change

Despite efforts to preserve backwards compatibility, there is one breaking change in C# 7.1.
In C# 7.0 you might have used extension methods to define new behaviour on tuples; the behaviour of this may change when you upgrade to C# 7.1 due to tuple name inference.

Demonstration

The problem occurs if you have an extension method on tuples and the method name clashes with an inferred tuple name.
Here is a program that demonstrates the breaking change:
public class Program
{
    static void Main(string[] args)
    {
        Action Output = () => Console.Out.WriteLine("Lambda");
        var tuple = (5, Output);
        tuple.Output();
    }
}

public static class Extensions
{
    public static void Output<T1, T2>(this ValueTuple<T1, T2> tuple)
    {
        Console.Out.WriteLine("Extention");
    }
}
In C# 7.0, this program prints Extension, but in C# 7.1 it prints Lambda.

Minor Impact

This breaking change is very unlikely to affect you.
Firstly, as the code must use tuples to be affected, it only affects code written since C# 7.0 was released, which was not very long ago.
Secondly, if you're using the C# 7.1 compiler in Visual Studio 2017.3 to compile C# 7.0 code, you now get a compile error from problematic code. This occurs when you set <LangVersion>7.0</LangVersion>. On the demonstration code, you would get this error:
Error CS8306 Tuple element name 'Output' is inferred. Please use language version 7.1 or greater to access an element by its inferred name.
Thirdly, it is unlikely you have added extension methods to tuples in this way. You may not even have known this was possible.
Finally, you normally want to use names with tuples for readability. You would have to be accessing the tuple values using the reserved names Item1 and Item2 for this to affect you.

How to Check Your Code

If you are worried about this breaking change. Just run the compiler targeting C# 7.0 before upgrading to C# 7.1 to ensure you have not done this anywhere in your code base. If you have, you'll get compile error CS8306 in the places you have done this.

Benefits

Tuple name inference can be quite beneficial in cases where you repeatedly transform, project, and reuse tuples: as is common when writing LINQ queries. It also means that tuples more closely mirror the behaviour of anonymous types.

Simplified LINQ Queries

Tuple name inference makes it a lot nicer to use tuples in lambda expressions and LINQ queries. For example, it lets you transform this query:
items.Select(i => (Name: i.Name, Age: i.Age)).Where(t => t.Age > 21);
into this simpler query:
items.Select(i => (i.Name, i.Age)).Where(t => t.Age > 21);
Since C# 7.0 was released, I've found that my LINQ queries benefit tremendously from tuples. Tuple name inference will improve these queries even further, by making them even more succinct and readable.

Mirrors Anonymous Types

The new tuple name inference behaviour makes the language more symmetric in the sense that tuples now more closely mirror the behaviour on an existing and similar language feature, anonymous types.
Anonymous types infer their names using the same algorithm that is used for tuples in C# 7.1. In this example, we see that tuples and anonymous types look very similar due to name inference behaving similarly:
// Tuples
var t = (value.x, value.y);
Console.Out.WriteLine(t.x == value.x); // Prints "True"

// Anonymous Types
var a = new { value.x, value.y };
Console.Out.WriteLine(a.x == value.x); // Prints "True"

New Feature: Generic Pattern-Matching

C# 7.0 added pattern matching and three kinds of pattern: constant patterns, type patterns, and var patterns. C# 7.0 also enhanced the isexpression and switch statement to use these patterns.
However, in C# 7.0 these patterns fail when the variable being matched is a generic type parameter. For example, both if(t is int i) and switch(t) { case int i: return i; } can fail when t is generic or more specifically, an open type.
C# 7.1 improves the situation by allowing open types to be matched against all types of pattern, rather than just a limited set.

What is an Open Type?

An open type is a type that involves type parameters. On a class that is generic in T, (TT[], and List<T> are all open types). As long as one argument is generic, the type is an open type. Therefore, Dictionary<string, T> is also an open type.
Almost everything else is known as a closed type. The one exception is for unbound generic types, which are generic types with unspecified type arguments. For example, List<> and Dictionary<,> are unbound generic types. You're likely to encounter unbound generic types when using reflection.
For more information on open types, see this stack overflow answer, which precisely defines open types.

Better Generic Pattern-Matching

In C# 7.0, you could match open types against particular patterns, but not all. In C# 7.1, you can match open types against all the patterns you would expect.

Behavior in C# 7.0

In C# 7.0, you could match an open type T against object or against a specific type that was specified in a generic type constraint on T. For example, where T : License, you could match again object or License, but not derivatives of License such as DriversLicense.
This behaviour was counter-intuitive. You would expect and want to be able to match against derivative types and in fact, you can with the asoperator. The problem occurs as there is no type conversion when the specified type is an open type. However, the as operator is more lenient and works with open types.

New Behavior in C# 7.1

C# 7.1 changes pattern matching to work in cases where as works, by changing what types are pattern compatible.
In C# 7.0, static type S and type T are pattern compatible when any of these conversions exist:
  • identity conversion
  • boxing conversion
  • implicit reference conversion
  • explicit reference conversion
  • unboxing conversion from S to T
C# 7.1 additionally considers S and T to be pattern compatible when either:
  • S is an open type, or
  • T is an open type
This means that in C# 7.1 you can pattern match generic types against derivatives such as DriversLicense in is expressions and switchstatements.

Example Code

In the following example, Print is a generic method that uses pattern matching with generic type T. If T is an int, it returns "int", if T is a string, it returns "string", otherwise it returns "unknown".
This code compiles and works as expected in C# 7.1, whereas in C# 7 it gives a compile error.
static string Print<T>(T input)
{
    switch(input)
    {
        case int i: 
          return "int";
        case string s: 
          return "string";
        default: 
          return "unknown";
    }
}

static void Main(string[] args)
{
    string input = "Hello";
    Console.WriteLine(Print(input));
}

C# 7.1 Support in Razor Views

Razor supports C# 7.1. This means you can use the new features within your views. However, these are some gotchas that might affect you if you previously enabled C# 7.0 in your razor views.

Using C# 7.1 in Razor Views

Prior to Visual Studio 2017.3, razor views used C# 6.0 by default. This was true, even when you were using C# 7.0 in your code. If you have never tried to use any C# 7.0 features such as tuples inside a razor view, then you might not have noticed.
To change this, you had to modify Startup.cs and set the razor ParseOptions on IMvcBuilder. You would have done this using code like this:
services.AddMvc().AddRazorOptions(options =>
{
  options.ParseOptions = new CSharpParseOptions(LanguageVersion.CSharp7);
});
This is no longer necessary. The language used by razor views is now determined by the LangVersion tag in the csproj file. So the language available in razor views will always be in sync with the C# language version used for code in an ASP.NET Core project.
If you have upgraded to ASP.NET Core 2.0, you will need to remove this ParseOptions setting from your RazorOptions, as it is no longer necessary nor available on the API.

Razor Models cannot be Tuples

If you previously enabled C# 7.0, you may have found that you could use C# 7's tuples for the model in your razor views. I found this was a convenient way to pass additional strongly-typed variables to a view, without creating a separate ViewModel.
Unfortunately, as of the latest update, this feature is no longer available. You will now get a runtime error and a warning or error inside razor views that use this feature.
The temporary solution is to create separate ViewModels for these views and pass in your parameters that way. You can still use tuples within your razor views, just not for the model.
Fortunately, this situation will only be temporary. Support for tuples on type directive tokens, such as Model, has already been merged into razor. You can track the progress in this issue on GitHub.

Conclusion

There are three ways to enable C# 7.1 in your projects. Of these three methods, the lightbulb code fix provides the fastest and easiest way to upgrade all of your C# projects at the same time.
C# 7.1 adds 4 new language features: async main, target-typed default literals, tuple name inference, and generic support for pattern-matching.
  1. You saw how async main lets you use async/await in your main method.
  2. You saw target-typed default literals used in seven different ways, including my personal favourite #7, which uses default to eliminate redundant casts when using the ternary operator.
  3. You saw how to use tuple name inference, the benefits of it, how it mirrors name inference on anonymous types, how it is a breaking change, and how to detect any resulting issues.
  4. You saw how you can now perform pattern matching between generic types and derivatives in is expressions and switch statements.
If you previously enabled C# 7 in your razor views, you need to remove the razor ParseOptions setting. If you used tuples for any razor view models, you need to temporarily replace those with class based models until support for tuple view models returns.

Discuss

If you've been using any of the new C# 7 or C# 7.1 features in your projects, I would love to hear from you.
Please share your experiences in the comments below.

No comments:

Post a Comment

Free hosting web sites and features -2024

  Interesting  summary about hosting and their offers. I still host my web site https://talash.azurewebsites.net with zero cost on Azure as ...