Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

What is New in C#7.1 & C#7.2

0.00/5 (No votes)
7 Feb 2018 1  
C#7.1 and C#7.2 new features in a simple way

Introduction

This article explains new features of C#7.1, C#7.2 and lists some of the planned features of C#8.

Background

Since C# 7.0, Microsoft has created two point releases C#7.1 and C#7.2

In C#7.1 and C#7.2, Microsoft has introduced some small but useful features in the language.

Using the Code

Please find the attached project here or download it from my github page and check out the project. It's simple and easy to understand all the features even without going much into the details.

Note: To check different features in the project, you can change language version as explained in this article.

Index

How to Select C# Language Version in Visual Studio (C#7.1/C#7.2)?

The C# compiler supports C# 7.1 starting with Visual Studio 2017 version 15.3. The most recent version is C# 7.2, which was released in 2017 along with Visual Studio 2017 version 15.5. However, the 7.1 and 7.2 features are turned off by default. To enable these features, we need to change the language version setting for the project.

Following are two methods by which you can change C# language version.

Method 1

  1. First, right-click on the project node in Solution Explorer => Select Properties. It will open project properties.

  2. Select the Build tab => select the Advanced button. It will open advanced build settings window.

  3. In the Language version drop-down, select C# latest minor version (latest), or the specific version C# 7.1/C#7.2, etc. as shown in the image below:

The latest value means you want to use the latest minor version on the current machine. The C# 7.1 means that you want to use C# 7.1, even after newer minor versions are released.

Method 2

You can edit the "csproj" file and add or modify the following lines:

<PropertyGroup>
 <LangVersion>latest</LangVersion>
</PropertyGroup>

Please note, if you use the Visual Studio IDE to update your csproj files, the IDE creates separate nodes for each build configuration. You'll typically set the value the same in all build configurations, but you need to set it explicitly for each build configuration or select "All Configurations" when you modify this setting. You'll see the following in your csproj file:

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
 <LangVersion>latest</LangVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
 <LangVersion>latest</LangVersion>
</PropertyGroup>

Valid settings for the LangVersion element are:

  • ISO-1
  • ISO-2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 7.1
  • default
  • latest

The special strings default and latest resolve to the latest major and minor language versions installed on the build machine, respectively.

New Features in C# 7.1

Now let's consider we have the following class (to make it easy for us to understand the features):

public class Fruit
{
    public int Id { get; set; }
    public string Name { get; set; }

    private static List<Fruit> GetFruits()
    {
        return new List<fruit>
        {
            new Fruit { Id= 1, Name= "Apples" },
            new Fruit { Id= 2, Name= "Apricots" },
            new Fruit { Id= 3, Name= "Avocados" },
            new Fruit { Id= 4, Name= "Bananas" },
            new Fruit { Id= 5, Name= "Boysenberries" },
            new Fruit { Id= 6, Name= "Blueberries" },
            new Fruit { Id= 7, Name= "Bing Cherry" },
            new Fruit { Id= 8, Name= "Cherries" },
            new Fruit { Id= 9, Name= "Cantaloupe" },
            new Fruit { Id= 10, Name= "Crab apples" },
            new Fruit { Id= 11, Name= "Clementine" },
            new Fruit { Id= 12, Name= "Cucumbers" }
        };
    }

    public static async Task<List<Fruit>> GetFruitsAsync
             (CancellationToken cancellationToken = default(CancellationToken))
    {
        return await Task<List<Fruit>>.Factory.StartNew(() =>
        {
            return GetFruits();
        });
    }
}

Following are some of the new features in C# 7.1.

Inferred Tuple Element Names

Before C#7, Tuples were available but without a language support so they were not much efficient. Also, Tuple elements were referenced as item1, item2 and so on, which made that difficult to understand what it is referencing to. In C#7, Microsoft has added language support for Tuple so that it has allowed providing proper names to the Tuple elements (still optional). The following code shows C#7.0 version where Tuple elements have name specified:

//Retrieve fruits (from any data source)
var fruits = Fruit.GetFruits);

//Old way - C#7.0 (Specify names to the Tuple elements)
var tupleExampleQuery = from fruit in fruits
                        select (Id: fruit.Id, Name: fruit.Name);

var firstElementOld = tupleExampleQuery.First(); //(int Id, String Name)
string firstFruitOld = $"First fruit is : Id: {firstElementOld.Id.ToString()},
                       Name: {firstElementOld.Name}";

However, here in the example (and most of the time), tuple names match the name of source elements from where we are referencing, in that kind of scenario, we might want to omit the name. Luckily in C#7.1, we have a feature where we can infer the name of the tuple element from the referencing elements. The following code shows C#7.1 version where tuple elements names are inferred.

//New (Inferred Tuple element names)
var inferredTupleExampleQuery = from fruit in fruits
                                select (fruit.Id, fruit.Name); // Same as above
                                                  // but Id and Name names will be inferred

var firstElementNew = inferredTupleExampleQuery.First(); //(int Id, String Name)
string firstFruitNew = $"First fruit is : Id: {firstElementOld.Id.ToString()},
                       Name: {firstElementOld.Name}";

Note: In order to use this feature, you need to add a NuGet package System.ValueTuple.

Async main

While learning C#, most of the time, we create console apps. In the scenarios, while testing code for the async method, we had to write some additional logic. We normally had to write boilerplate as follows:

public static void Main()
{
    MainAsync().GetAwaiter().GetResult();
}

private static async Task MainAsync()
{
    ... // Main body here
}

In our example, we might need to write something like follows:

//OLD way (before C#7.1)
static int Main(string[] args)
{
    var fruits = Fruit.GetFruitsAsync().GetAwaiter().GetResult();
    //Do Some work

    //return exit code
    return 0;
}

Luckily, in C#7.1, the main method can be async, so in here, our code will now look more similar to the normal async methods as follows:

static async Task Main(string[] args)
{
    var fruits = await Fruit.GetFruitsAsync();

    //Do Some work
}

Following are allowed versions of main():

static void Main()
static void Main(string[])
static int Main()
static int Main(string[])

It has been extended to the following:

static Task Main()
static Task<int>Main()
static Task Main(string[])
static Task<int> Main(string[])

Default Literal Expressions

We can use this feature in default value expressions when the target type can be inferred. To understand this feature, let's continue with the above example. In the above example, fruit method can optionally take CancellationToken as an argument which can be optionally marked as default. So earlier, we used to do as follows:

/*Old way (before C#7.1) - Default literal expressions*/
public static async Task<List<Fruit>> GetFruitsAsync
         (CancellationToken cancellationToken = default(CancellationToken))
{
    return await Task<List<Fruit>>.Factory.StartNew(() =>
    {
        return GetFruits();
    });
}

Here, target type can be inferred in default, so we can omit that and make code a little more better.

/*New way (C#7.1) - Default literal expressions (type in the default is inferred)*/
public static async Task<List<Fruit>> GetFruitsAsync(CancellationToken cancellationToken = default)
{
    return await Task<List<Fruit>>.Factory.StartNew(() =>
    {
        return GetFruits();
    });
}

New Features in C# 7.2

Following are some of the new features in C# 7.2...

Private Protected Access Modifier

Until now, we had Private, Public, Protected, Internal and Internal Protected access modifiers. All of the access specifiers are much discussed and we all know what they do, so I won't go into those details.

The Private Protected access modifier gives the visibility to the member to be accessed for derived classes in the same assembly.

Following are some code examples that help to understand all access modifiers (just read comments in code to understand this feature).

Let's consider we have a private class as follows:

public class Parent
{
    private int private_var { get; set; }
    protected int protected_var { get; set; }
    public int public_var { get; set; }
    internal int internal_var { get; set; }

    internal protected int internal_protected_var { get; set; }
    private protected int private_protected_var { get; set; }
}

A child/derived class within the current assembly:

public class Child_WithinCurrentAssembly : Parent
{
    public Child_WithinCurrentAssembly()
    {
        //this.private_var = 0;             // not accessible = because private members
                                            // are not accessible outside of the class
        this.protected_var = 0;             // accessible - because protected members
                                            // are accessible within derived class
        this.public_var = 0;                // accessible - because public is accessible anywhere
        this.internal_var = 0;              // accessible - because internal is accessible
                                            // within current assembly
        this.internal_protected_var = 0;    // accessible - because internal protected is accessible
                                            // within derived class or within current assembly
        this.private_protected_var = 0;     // accessible - because private protected is accessible
                                            // within derived class and within current assembly
    }
}

A non-child class within current assembly:

 public class NonChild_WithinCurrentAssembly
 {
    public NonChild_WithinCurrentAssembly()
    {
        Parent p = new Parent();
        //p.private_var = 0;                // not accessible = because private members
                                            // are not accessible outside of the class
        //p.protected_var = 0;              // not accessible - because protected members
                                            // are accessible only within derived class
        p.public_var = 0;                   // accessible - because public is accessible anywhere
        p.internal_var = 0;                 // accessible - because internal is accessible
                                            // within current assembly
        p.internal_protected_var = 0;       // accessible - because internal protected is accessible
                                            // within derived class or within current assembly
        //p.private_protected_var = 0;      // not accessible - because private protected
                                            // is accessible
                                            // only within derived class and within current assembly
    }
}

A child/derived class outside current assembly:

public class Child_OutsideCurrentAssembly : Parent
{
    public Child_OutsideCurrentAssembly()
    {
        //this.private_var = 0;                 // not accessible = because private members
                                                // are not accessible out side of the class
        this.protected_var = 0;                 // accessible - because protected members
                                                // are accessible within derived class
        this.public_var = 0;                    // accessible - because public is
                                                // accessible anywhere
        //this.internal_var = 0;                // not accessible - because internal is
                                                // accessible only within current assembly
        this.internal_protected_var = 0;        // accessible - because internal protected is
                                                // accessible within derived class
                                                // or within current assembly
        //this.private_protected_var = 0;       // not accessible - because private protected is
                                                // accessible only within derived and
                                                // within current assembly
    }
}

A non-child class outside current assembly:

public class NonChild_OutsideCurrentAssembly
{
    public NonChild_OutsideCurrentAssembly()
    {
        Parent p = new Parent();
        //base.private_var = 0;                 // not accessible = because private members
                                                // are not accessible out side of the class
        //p.protected_var = 0;                  // not accessible - because protected members
                                                // are accessible only within derived class
        p.public_var = 0;                       // accessible - because public is
                                                // accessible anywhere
        //p.internal_var = 0;                   // not accessible - because internal is
                                                // accessible only within current assembly
        //p.internal_protected_var = 0;         // not accessible - because internal protected is
                                                // accessible only within derived class
                                                // or within current assembly
        //p.private_protected_var = 0;          // not accessible - because private protected
                                                // is accessible
                                                // only within derived class and within
                                                // current assembly
    }
}

Now, this may have led you to a little confusion in the difference between internal protected and private protected. Here is the difference (just remember bold lines):

protected internal modifier means protected OR internal that is - class member is accessible to child class derived from a class (immediate child) and also to any class in the current assembly, even if that class is not a child of class A (so restriction implied by "protected" is relaxed).

private protected means protected AND internal. That is - member is accessible only to child classes which are in the same assembly, but not to child classes which are outside assembly (so restriction implied by "protected" is narrowed - becomes even more restrictive).

Non-trailing Named Arguments

To understand this feature, let's first try to understand Named Arguments and its rules.

Normally, when calling a function, all arguments must be passed in order as specified in the function. Named Arguments allows caller method to send arguments in any order as long as the name of the parameter is specified during the call match the parameter name.

For example, consider the following sample method:

void MethodA(string arg1, int arg2, string arg3)

When called without named arguments, the order should be correct like:

MethodA("arg1_value", 1, "arg3_value")

Now if we want to call it with named argument, we can pass arguments in any order we want.

MethodA(arg2:1, arg3:"arg3_value", arg1:"arg1_value")

Now in case we want to pass only some parameters as named arguments, it is possible, but until C#7.2, the positional arguments were always needed to be placed before the named arguments. So we can call MethodA as follows:

MethodA("arg1_value", 1, arg3:"arg3_value") OR
MethodA("arg1_value", arg2:1, arg3:"arg3_value") OR
MethodA("arg1_value", arg3:"arg3_value", arg2:1)

Now because the rule says the positional arguments always needed to be placed before the named arguments, following method call was not possible before C#7.2, which is now possible.

MethodA(arg1:"arg1_value", 1, arg3:"arg3_value")

Leading Underscores in Numeric Literals

This is a very small but useful feature. In C#7.0, Microsoft has introduced digit separator for readability purposes. So following are valid numbers in C#7.0.

//After C#7 (and before C#7.2)
int i = 123;
int j = 1_2_3;
int k = 0x1_2_3;
int l_binary = 0b101;
int m_binary = 0b1_0_1;

When used in binary and hexadecimal literals, they were not allowed to appear immediately following the 0x or 0b in C#7.0, now with C#7.2 it is allowed. So now, following are also valid numbers in C#7.2.

//After C#7.2
byte n_hex = 0x_1_2;
byte o_hex = 0b_1_0_1;

Reference Semantics with Value Types

There are a lot of changes made within this feature but to cut it short, I will try to explain one interesting change, which I liked, called in parameter.

The in parameter is a complement of the existing ref and out keywords. For instance, see what each of the parameters does.

ref: The ref keyword is used to pass an argument as a reference. This means that when a value of that parameter is changed in the method, it gets reflected in the calling method. An argument that is passed using a ref keyword must be initialized in the calling method before it is passed to the called method.

out: The out keyword is also used to pass an argument like ref keyword, but the argument can be passed without assigning any value to it. An argument that is passed using an out keyword must be initialized in the called method before it returns back to calling a method.

Now take a look at In parameter in: The in keyword is used to pass an argument like ref keyword and an argument that is passed using an in keyword cannot be initialized by the called method.

So out and in are kind of the opposite: in case of out parameters, they are required to be modified by the method whereas in parameters, they are not required to be modified by the method.

The following code will help you to understand the difference and the new in parameter:

class Program
{
    static void Main(string[] args)
    {
        int a;

        a = 1;
        Console.WriteLine($"Inside Main-Method_Passbyval {a} before");
        Method_Passbyval(a);
        Console.WriteLine($"Inside Main-Method_Passbyval {a} after");

        Console.WriteLine(Environment.NewLine);

        a = 10;
        Console.WriteLine($"Inside Main-Method_Passbyref {a} before");
        Method_Passbyref(ref a);
        Console.WriteLine($"Inside Main-Method_Passbyref {a} after");

        Console.WriteLine(Environment.NewLine);

        a = 20;
        Console.WriteLine($"Inside Main-Method_out_parameter {a} before");
        Method_out_parameter(out a);
        Console.WriteLine($"Inside Main-Method_out_parameter {a} after");

        Console.WriteLine(Environment.NewLine);

        a = 30;
        Console.WriteLine($"Inside Main-Method_in_parameter {a} before");
        Method_in_parameter(in a);
        Console.WriteLine($"Inside Main-Method_in_parameter {a} after");

        Console.WriteLine(Environment.NewLine);

        Console.WriteLine($"Press any key to exit.");
        Console.ReadKey();
    }

    /// <summary>
    ///value will be changed in this method but caller will not get updated value for parameter(a)
    /// </summary>
    /// <param name="a"></param>
    private static void Method_Passbyval(int a)
    {
        a++;
        Console.WriteLine($"Inside Method_Passbyval (a++) {a}");
    }

    /// <summary>
    /// value will be changed in this method but called will get updated value for parameter(a)
    /// </summary>
    /// <param name="a"></param>
    private static void Method_Passbyref(ref int a)
    {
        a++;
        Console.WriteLine($"Inside Method_Passbyref (a++) {a}");
    }

    /// <summary>
    /// pass by ref + method it self need to initialize parameter(a)
    /// </summary>
    /// <param name="a"></param>
    private static void Method_out_parameter(out int a)
    {
        a = 100; // need to initialize before use
        a++;
        Console.WriteLine($"Inside Method_out_parameter (a=100 & a++) {a}");
    }

    /// <summary>
    /// pass by ref + method cannot change parameter(a)
    /// </summary>
    /// <param name="a"></param>
    private static void Method_in_parameter(in int a)
    {
        //a++; // modification not allowed
        Console.WriteLine($"Inside Method_in_parameter (no change in a) {a}");
    }
}

The above code will show o/p as follows. The output shows the clear difference between ref, out and in keywords.

Please note: There are more interesting changes added to this section, but I left you to explore that more.

A Preview of C# 8

Following are some features that are being discussed for the upcoming release in C#8.0, some of them have the only prototype designed. But I guess we all are very excited to check those features.

  • Nullable reference types
  • Async Streams
  • Default Interface Implementations
  • Extension Everything

I hope you have enjoyed the reading.

Happy programming!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here