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

New Features of C# 8

0.00/5 (No votes)
7 Nov 2019 1  
This article will give you a brief idea about what's new in C# 7.1, 7.2, 7.3, 8.

Image 1

Image 2 Introduction

C# - A well known name in the programming world. The heart of the .NET development people, C# is a powerful type-safe, sophisticated, object-oriented language. It enables developers to develop Windows Apps, Web Apps, database apps, Web services, XML Apps, backend services and many more.

Basically, C# was developed in 1999 by Microsoft and named 'C-Like Object Oriented Language', Microsoft called this language as COOL. Later in July 2000, the language was renamed as C#. The first public version of C# (1.0) was released in Jan 2002 with Visual Studio 2002. Till now, there have been many changes made in this language. C# is getting more powerful with each version. In May 2018, Microsoft released C# 7.3 with Visual Studio 2017. Prior to that, C# 7.1 and 7.2 were released in Aug 2017 and Nov 2017 respectively. The latest version of C# (8.0) was released recently in September 2019 with the compatibility to .NET Framework 4.8 and Visual Studio 2019. If you want to see what's new in version C# V7, you can refer to my previous article here.

In this article, we will see new features included in C# 7.1, 7.2, 7.3 and 8.0. So let's get started.

Image 3 What's New in C#7.1

Async Main

This is the big change in main method compilation technique, now C# can accept Async main method, it helps developers to write await in your main method, see the below sample:

static string Main()
{
    await AsyncMethod1();
}

Before this feature, if you want to put await for your async method, then you need to use GetAwaiter() method. If your main method returns Task, then it can include async modifiers. see the sample given below:

static async Task Main()
{
    await AsyncMethod1();
}

Change in Default Literal

Default literals produce default values to type, there is change in syntax for the default literal, see the sample below:

//previously you wrote
Func<int, string> whereClause = default(Func<int, string>);

Now, the right-hand side part is eliminated, and you just need to type default keyword, see the below sample:

Func<int, string> whereClause = default;

Change in Tuple Syntax

There is change in tuple syntax also, tuple was introduced in C#7, there you need to define the name of the tuple that you are going to use it in the program, see the sample given below:

int count = 3;
string colors = "colors of the flag";
var tupleCol = (count: count, colors: colors);

Here, we need to define the count: count and colors: colors names, but in C#7.1, they have changed the syntax as below:

int count = 3;
string colors = "colors of the flag";
var tupleCol = (count,colors); // here syntax gets trimmed

Image 4 What's New in C#7.2

After C#7.1 version, C#7.2 was released with some advanced features, this build had more focus on efficiency and performance. Let's trace out some new features of C#7.2.

Leading Underscore in Numeric Literals

C#7.0 has features of digit separators in literals but it does not accept _ (underscore) as the characters of the value but in C#7.2, you can use binary and Hex numeric literals to start with underscore, see the following sample:

int val_bi = 0b_0110_01;

Private Protected Access Modifiers

New access modifier was added in C#7.2 named as 'private protected', basically it was the combination of private access modifiers and protected access modifiers where, protected access modifiers limit the member access to only the derived classes and private access modifiers limit the members to only same class. So finally, we got a modifier which will be accessed by containing class or derived but for the same assembly only.

Conditional 'ref'

As the name suggests, we can assign 'ref' result depending on the conditional expression, see the following example:

ref var finalVal = ref (value1 != null ? ref val1_arr[0] : ref val2_arr[0]);

In the above sample, we are assigning value to finalVal variable but this assignment is dependant upon the with the conditional operator, so either the value of val1_arr or value of var2_arr will be assigned.

Changes in Named Arguments

We have seen about functional named and optional arguments, in C#7.2, if the order of the argument is correct, then you don't need to assign a name to the arguments, see the sample given below:

//suppose I have written a function and used below named arguments
EmpDetails(EmpID: 3, firstName: "Manavya", City: "Daswel");

//here in C#7.2, I can write the above code line as
EmpDetails(EmpID: 3, "Manavya", City: "Daswel");

//if you observed that firstName: "Manavya" is replaced by only "manavya" 
// and it is the beauty of the C#7.2

Image 5 What's New in C#7.3

This build is more focused on efficient safe coding to reduce the memory consumption, avoid errors, buffer over tray and making the existing features more powerful, let's trace out the features one by one.

Improvement in stackalloc Operator

We know that stackalloc allocates a block, the good thing of the operator is that, it releases the memory when method return exits the control, so there is no need of garbage collection.

Uptil now, with the help of stackalloc, you will be able to initialize the values as below:

int* Array1 = stackalloc int[3] {5, 6, 7};

but from now onwards, array also can be a part of stackalloc as below:

Span<int> Array = stackalloc [] {10, 20, 30};

Fixed Statement Supports More Types

Now onwards, fixed statements support more types (fixed statements are the statements that prevent the garbage collector from cleaning the movable variables, you can use fixed keyword to create these statements), so from C#7.3 onwards, fixed statements support any types that have GetPinnableReference() methods, which return ref T which is fixed.

Unmanaged Constraints

You can use the unmanaged constraints to show the type parameter must be unmanaged type with non-pointer (a member is an unmanaged type if it is of byte, short, sbyte, int, long, char, bool type.)

unsafe public static byte[] convert_to_byte<T>(this T argument) where T : unmanaged
{
    var size = sizeof(T);
    var result = new Byte[size];
    return result1;
}

The above sample is unmanaged and unsafe.

You can also use System.Enum and System.Delegate as base class constraints.

!= and = is Now Supported by Tuples

From C#7.3 version, tuples are now supporting equal to and not equal to operator.

As we know, this equal to and not equal to operator compare left side with right side values, see the sample below:

var exp1 = (val1: 100, val2: 20);
var exp2 = (val1: 100, val2: 20);
exp1 == exp2; //it will return displays as 'true'

Here, we will get output as true, because both the operands are true.

Changes in 'in' Method for Overloading

Do you know in method is a good replacement for 'out' or 'ref' keyword, 'in' arguments cannot be modified by calling function but 'out' or 'ref' arguments can be modified by calling methods. Now here comes the issue, when we give the same name to methods for 'by reference' and 'by value' methods, then it throws an ambiguity exception, to avoid this error, C#7.3 has used 'in' method, see the below sample:

static void calculateArea(var1 arg);
static void calculateArea(in var1 arg);

Extending Out Parameter Boundaries

C#7.3 has extended out parameters boundaries and now you can define the out parameter in constructor initializers, property initializers also, see the below sample for more details.

//here, we have defined the out parameter in constructor
public class parent
{
   public parent(int Input, out int Output)
   {
      Output = Input;
   }
}

//now let's use the above class in the following class
public class Child : parent
{
   public Child(int i) : base(Input, out var Output)
   {
      //"The value of 'Output' is " + Output
   }
}

Multiple Compiler Options

C#7.3 provides you with multiple compiler options using that you can:

  1. sign assembly using public key
  2. replace path from build environment

Sign Assembly Using Public Key

If you want to sign any assembly with the public key, then you can use this option. This assembly will be identified as signed but with the public key, this option is useful when you want to sign an assembly of open source code. The syntax is a bit simple, you just need to add parameter as -public sign.

Replace Path From Build Environment

This option enables you to replace the source path from the build with earlier mapped source path, in short, it will help you to map source path with physical path, the syntax is as below:

-pathmap:path1=sourcePath1,path2=sourcePath2

Here, path1 is the full path of source file and path2 is the alternate path that we need to replace with path1. Basically, these paths are found in PDB file.

Image 6 What's New in C# 8

C# 8 comes with very strong features like enhancement in pattern matching, read-only members, static function (local), nullable reference, stackalloc in nested loop, changes in indices and ranges.

Let's trace them one by one.

Using Declarations

A using declaration is nothing but a technique where a variable is declared before using keyword, it also tells the compiler that the variable should be disposed before the end of the method (where that variable is used), see the below sample to understand the using declaration:

var ReadFile(string szpath)
{
 using StreamReader StrFile = new StreamReader(szpath);
 string iCount;
 string szLine;
 while ((szLine = StrFile.ReadLine()) != null) 
  { 
  Console.WriteLine(szLine); 
  iCount++; 
  } 
 // file is disposed here
}

In the above sample, you can see using statement can be used as declaration statement. The StrFile variable is disposed as soon as the closing brace of the method is reached.

You can write down the same code with earlier regular coding steps:

var ReadFile(string szpath)
{
using (StreamReader StrFile = new StreamReader(szpath))
 {
  string iCount;
  string szLine;
  while ((szLine = StrFile.ReadLine()) != null) 
  { 
  Console.WriteLine(szLine); 
  iCount++; 
  } 
 
 } //file disposed here
}

In the earlier code, we can see StrFile object is disposed after the using scope comes to an end.

Consuming Asynchronous Streams

Do you want to consume asynchronous streams? Then this functionality is useful to you. This task is carried out by a function which return asynchronous streams with IAsyncEnumerable<T> enumerator. This method is declared as anync modifier, this function also contains yield return statement (which is used to return asynchronous stream). To consume anync steam, we need to use await keyword before it actually returns the value. Go through the below sample, where we have return anync stream.

public async System.Collections.Generic.IAsyncEnumerable<int> asyncStream()
{
    for (int iCount = 0; iCount < 10; iCount++)
    {
        await Task.Delay(10);
        yield return iCount;
    }
}
//call above method in mail method
await foreach (var num in asyncStream())
{
    Console.WriteLine(num);
}

In the above sample, we have return number up to 10 with async stream.

New Type Indices and Ranges

C#8 comes with two new types that are used to fetch the items from list/array. System.Index and System.Range are used to fetch those types. ^ (caret) denotes the indices sign and .. denotes the range sign. Let's trace them a bit to understand the concept.

Elaborating Indices (^)

Generally, when we fetch the items from array, then the index starts with 0, but when we use this new type, its indices are started from end and go in upward direction.

See the below sample:

var simArray = new string[]
{                 
    "one",         // 0      ^4
    "two",         // 1      ^3
    "three",       // 2      ^2
    "four",        // 3      ^1
};

In the above sample, we have four items in array, in general, index starts with 0 and ends with 3, but in case of indices, it starts with ^4 (end) to ^1, so it's reserve indexing.

Now if I am trying to fetch any index, then the output will be as below:

Console.WriteLine("fetch using simple array " + simArray[1]);
//above code gives output as "two"
Console.WriteLine("fetch using indices caret operator " + simArray[^1]);
//above code gives output as "four"

Elaborating range(..)

This operator fetches the start and end of its operands.

So if we consider the above example and try to fetch values from 'simArray', then we will get the output as below:

Console.WriteLine("fetch using range operator" + simArray[..]);
//above code gives output as "one" to "four"
Console.WriteLine("fetch using range operator" + simArray[..3]);
//above code gives output as "one" to "three"
Console.WriteLine("fetch using range operator" + simArray[2..]);
//above code gives output as "two" to "four"

In the above sample:

  • [..] indicates, fetch all items of list
  • [..3] indicates. fetch all items up to index 3
  • [1..] indicates, fetch all items starting from index 1

stackalloc as an Expression

We know that stackalloc operator allocates a block memory on the stack and releases when the method call exits, so it does not need garbage collection. The syntax of the stackalloc is:

stackalloc T[E]

where T is the unmanaged type and E is expression of type int.

In C#8, you can use stackalloc as an expression, this will be applicable if the return result System.Span<T>. See the below sample:

Span<int> num = stackalloc[] { 20, 30, 40 };
var index = num.IndexOfAny(stackalloc[] { 20 });
Console.WriteLine(index);  // output: 0

In the above sample, we have used stackalloc as collection of int data and we pass stackalloc as expression to IndexOfAny method, which will return the result as 0.

Enhancement in Interpolated String

C# 8 has introduced some enhancement in interpolated string.

Do you know what is interpolated string?

The $ is used to identify the string as an interpolated string, this string contains interpolation expression and further that are replaced by the actual result. See the below sample:

string szName = "ABC";
int iCount = 15;

// String interpolation:
Console.WriteLine($"Hello, {szName}! You have {iCount} apples");

If we run above code, the output will be "Hello, ABC! You have 15 apples", so we can say that variable in curly braces are replaced with the actual string. (Here, if you have observed that you can see string starts with $, this string is called as interpolated string.

Now in C#8, $@"" or @$"" are valid means you can start the string either by @ or $. In the earlier version of C#, @ is allowed only after $.

Make the Struct Member as Read-Only

If you want to give read only property to any struct member, then it is possible in C#8, where instead of giving whole struct as read-only, you can now make the struct member as read-only. The readonly attribute indicates that it does not modify state, see the below sample snippet, this is the general code practice that we follow to make the whole struct as readonly:

public readonly struct getSqure
{
    public int InputA { get; set; }
    public int InputB { get; set; }
    public int output => Math.Pow(A,B);

    public override string ToString() =>
        $"The answer is : {output} ";
}

In the above sample, we have used Math.Pow method to calculate the square of the input number, instead of that in C#8, we can make the struct member as readonly, see below snippet, where we have made some of the struct members as readonly.

public struct getSqure
{
    public int InputA { get; set; }
    public int InputB { get; set; }
    public readonly int output => Math.Pow(A,B);

    public override string ToString() =>
        $"The answer is : {output} ";
}

 

Default Interface Methods

This is a good features of C#8, which alllows you to add member to the interface (like methods) even in later versions without violating the existing implementation, because here, the existing implementation inherits default implementation.

Local Static Functions

In C# 8, you can make a local function as static that will help to certify that local function does not hold variable from inner enclosing loop, see in the below sample a local function trying to access local variable.

int Add()
{
    int A;
    Add();
    return Sum;

    void LocalFunction() => A + 3;
}

Now to avoid such issues, we can use local static function, in which we can re-write same code with method:

int Add()
{
    int A = 5;
    return Sum(A);

    static int Sum(int val) => val + 3;
}

null-coalescing Operator

C# 8 introduced the operator called as null-coalescing operator. This operator is denoted as ??=. Basically, this operator is used to assign right side expression value to right side operand side, but this assignment is possible when the left side operand value is evaluated as null. In simple words, this operator returns the value of left operand if it is not null otherwise it returns the right side operand value, see the below snippet for more detail:

List<int> lstNum = null;
int? a = null;

(lstNum ??= new List<int>()).Add(100);
Console.WriteLine(string.Join(" ", lstNum)); 

// the output would be : 100

lstNum.Add(a ??= 0);
Console.WriteLine(string.Join(" ", numbers));  // output: 100 0
// the output would be : 100 0

Switch Expression Changes

There is change in switch expression and its syntax for pattern matching, see the sample given below.

Suppose we are having an enum with some random numbers as below:

public enum RandomNum
{
    One,
    Three,
    Five,
    Seven,
    Six,
}

Now, you can use switch expression in the following ways:

public static string getRandomNum(RandomNum iNum) =>
    iNum switch
    {
        RandomNum.One => return "1",
        RandomNum.Three => return "3",
        RandomNum.Five => return "5",
        RandomNum.Seven => return "7",
        RandomNum.Six => return "6",
        _              => throw new Exception("invalid number value"),
    };

In the above switch expression sample, you will find syntax level changes as below:

  • In the original syntax, case keyword is used with switch statement, but here case keyword is replaced by =>, which is more self explanatory.
  • In the original syntax, default keyword is used for default case but here, default keyword is replaced by _ (underscore) sign.
  • In the original syntax, switch keyword is used before variable, but here, it comes after variable.

Disposable ref structs

A struct which is declared as ref has no rights to implement any interface so we are not able to dispose its objects as it has no rights to implement IDisposable interface, so we have to make the disposable ref structure we need to access void Dispose() method. In the same way, we can dispose to readonly ref structure also.

Image 7 Summed up

In this article, we have seen different features of C# 7.1, 7.2, 7.3 and 8.0, each version has its unique ability to make C# more stronger than the previous version. In later versions of this article, we will focus on each feature in detail, till then, you can enjoy this article.

Suggestions and queries are always welcome.

Happy coding!

History

  • 8th November, 2019: Initial version

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