facebook

Blog

Stay updated

Let’s discover together what’s new in C# 8
New features of C# 8: part one
Wednesday, February 05, 2020

C# is the reference language of the Microsoft .NET world and is updated periodically. In Visual Studio 2019, .Net Core 3.x and .Net standard 2.1, we find support for version 8, which is enriched with many new features that are not only compiler functions applicable to any version of the .NET Framework or .NET Core, but there’s an explicit dependency on .NET Core 3. In this article, I will show you the first part of the new features introduced in this version.

using Declarations

using statement tells the compiler that it must Dispose the variable at the end of the inclusion scope.
This keyword is used when we deal with resources that implement the IDisposable interface. It’s a best practice to make the call to Dispose on these objects to immediately release the resources and use of using ensures that this always happens correctly. With C# 8 we can omit round brackets and braces, and declare a variable simply by placing using before it:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines.txt");
    // Notice how we declare skippedLines after the using statement.
    int skippedLines = 0;
    foreach (string line in lines)
    {
    //Some code here...
    }
    // Notice how skippedLines is in scope here.
    return skippedLines;
    // File is disposed here
}

In this example, the file Dispose is executed when the closing brace for the method is reached. Similarly the skippedLines variable is visible until the end of the method. If we wanted to close the scope before the end of the stack, as usual, C# allows us to outline the scope of different stack variables by enclosing everything between braces.
Let’s see what our code with the syntax previous to the new version of C# would have been like, and where the Dispose would have been executed:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    // We must declare the variable outside of the using block
    // so that it is in scope to be returned.
    int skippedLines = 0;
    using (var file = new System.IO.StreamWriter("WriteLines.txt"))
    {
         foreach (string line in lines)
         {
         // Some code here...
         }
    } // File is disposed here
    return skippedLines;
}

In this case, the file Dispose is executed when the closing brace associated with the using instruction is reached and we must declare the skippedLines variable to the outside of the using block otherwise it would not be in scope to be returned.

Local static functions

Since version 7.0, C# supports local functions: these are private methods nested in another member and can only be called by its container member and not outside of it:

public int Method()
{
    int num;
    LocalFunction();
    return num;
 
    void LocalFunction() => num = 4;
}

In C# 8, static local functions are an extension of the local functions, you can now add the static modifier to make sure that this local function does not refer to variables in the scope of inclusion.
In the previous code, the local function LocalFunction accesses the num variable, declared in the scope of inclusion (the method Method). We cannot therefore declare LocalFunction with the static modifier.
In the following code instead, the local function RectangleArea can be static because it does not access any variable in the scope of inclusion:

double RectangleAreaCalculation()
{
    double x = 10;
    double y = 3;
    return RectangleArea(x, y);
 
    static double RectangleArea(double length, double width) =>
    length + width;
}

Asynchronous streams

Let’s immediately give a simple example of how we could manage a data stream coming from an iterator until now:

public async Task<List<int>> GenerateSequence()
{
    List<int> list = new List<int>() ;
    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(100);
        list.Add(i);
    }
    return list;
}

It is clear that, with this approach, if we wanted to print the items of the list in the console, we should wait to have the complete list available and then iterate on each item:

public async void PrintSequence()
{
    List<int> list = await GenerateSequence();
    foreach (int num in list)
    {
        Console.WriteLine(num);
    }
}

A support to asynchronous flows been introduced with C# 8. This feature leads the async/await pattern, which I mentioned in my previous article, to manage iterators. In practice, async streams are async methods that return an IAsyncEnumerable<T>, and that have a return expressed with a yeld (yeld return) to return the next items in the asynchronous stream as soon as they are available:

public async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
    for (int i = 0; i > 20; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

Ultimately, we can use await to wait for an asynchronous call and in turn make asynchronous the GenerateSequence() function. The important difference is that the function does not return a Task<IEnumerable<int>>, but an IAsyncEnumerable<int>.
This interface contains asynchronous members to scroll the stream. Actually, it is not the IEnumerable object that must be returned in asynchronous, but the stream of the elements.
To use an asynchronous flow, you need to add the keyword await before foreach when enumerating the stream elements:

await foreach (var number in GenerateSequence())
{
    Console.WriteLine(number);
}

In this case, as we have an element available, it will be “printed” to console, without waiting to have the whole list available.

Let’s take a look at the definition of the interface IAsyncEnumerator:

public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
    T Current { get; }
 
    ValueTask<bool> MoveNextAsync();
}

You can notice the presence of the interface IAsyncDisposable that allows us to browse and to carry out the dispose of all the resources, all completely in asynchronous way.

Struct ref Disposable

A struct declared with the ref modifier cannot implement any interface and therefore cannot implement the IDisposable interface.
From now on, we can enable a ref struct for deletion if we add a public void Dispose method within it. This method will be automatically consumed by the instruction using:

class Program
{
   static void Main(string[] args)
   {
      using (var message = new Message())
      {
       Console.WriteLine("This message will self-destruct!");
      }
   }
}
 
ref struct Message : IDisposable
{
   public void Dispose()
   {
   }
}

We can also apply this feature to readonly ref struct statements.

Unmanaged constructed types

In C# 7.3 and earlier, a constructed type (a type that includes at least one type argument) cannot be an unmanaged type.
With C# 8, a constructed type is unmanaged if it contains only unmanaged fields of types.

For example, given the following generic type of Person<T>:

public struct Person<T>
{
    public T Age;
    public T NumberOfSons;
}

The Person<int> type is an unmanaged type in C# 8 (because it contains only properties of type int: int is an unmanaged type). As with any unmanaged type, you can create a pointer to a variable of this type or allocate a memory block in the stack for instances of this type:

Span<Person<int>> statisticalResearch = stackalloc[ ]
{
    new Person<int> { Age = 30, NumberOfSons = 1 },
    new Person<int> { Age = 45, NumberOfSons = 3 },
    new Person<int> { Age = 36, NumberOfSons = 0 }
};

Members of reading only

You can apply the readonly modifier to any member of a struct, indicating in fact that it will not change its state. Compared to the application of the modifier to the entire structure, this option allows for more granular control.
Within a method marked as readonly it is not possible to change the content of a field because this would generate a compilation error.

public struct Employee
{
    public string FullName { get; set; }
    public double DaysWorked { get; set; }
    public double DailyWages{ get; set; }
    public double Salary => DailyWages * DaysWorked;
 
    public readonly override string ToString() 
    {
        // Compiling error
    DaysWorked = 30;
 
        // Salary is not readonly and generates a warning
         return $"Salary for employee {Surname} {Name} is: {Salary}$";
        }
}

The ToString() method does not change the state. This condition may be indicated by adding the readonly modifier to the ToString() statement.
The previous change generates a compiler warning, because ToString accesses the Salary property, which is not marked as readonly:

warning CS8656: Call to non-readonly member 'Employee.Salary.get' from a 'readonly' member results in an implicit copy of 'this'

The compiler generates a warning when creating a defensive copy. The Salary property does not change the status, so you can correct the warning by adding the readonly modifier to the statement:

public readonly double Salary => DailyWages * DaysWorked;

Attention: the readonly modifier is required, the compiler does not assume that the access functions get do not change the status; you need to declare readonly explicitly. The automatically implemented properties are an exception
The compiler will in fact consider all the automatically implemented Getters as readonly.

Interfaces

This is a novelty that I found very interesting: we can now add members to the interfaces and provide an implementation for those members. This feature also supports C# interoperability with Android (Java) or Swift APIs, which already have similar implementations. This language functionality is one of those that needs a specific runtime to be supported and allows API authors to add methods to an interface without compromising the origin or compatibility with existing implementations of that interface. Existing implementations inherit the default implementation.

Suppose you have an interface like the following:

public interface IPrintable
{
    void Print();
}

And a class that implements it:

public class EBook : IPrintable
{
    public void Print()
    {
        Console.WriteLine("Print");
    }
}

If we need to add a new feature, we will usually add a new member to the interface, such as the Download method:

public interface IPrintable
{
    void Print();
    void Download(string path);
}

The EBook class would return a compilation error similar to: ‘EBook’ does not implement interface member ‘IPrintable.Download(string)’, as it no longer correctly implements the interface.
C# 8 introduces the default interface methods with which we can have a default implementation of a member directly in the interface:

public interface IPrintable
{
    int NumberOfPages { get; set; }
    double PricePerPage { get; set; }
    //Default Get
    double Price { get => NumberOfPages * PricePerPage; } 
    //Short default Get 
    //double Price => NumberOfPages * PricePerPage;
    void Print();
    void Download(string path);
    {
        Console.WriteLine($”Download in {path}”);
    }
}

The class that implements the interface is obliged to implement, as usual, the method without body, and can optionally implement and overwrite the default methods provided by the interface.
Default methods are only visible if we use an interface variable and are not visible through the class implementing it. On the contrary, only the members implemented on it are visible.
Interfaces can now include static members (including fields and methods) and virtual (but in this case a class implementing the interface cannot override a virtual method, only an interface can do that).
C# 8 also allows us to specify access modifiers for members of an interface, which in previous versions were implicitly public. Any modifier is now permitted for interface members.

public interface IPrintable
{
    void Print();
    private static string copyright = "© 2005-2011 Blexin s.r.l. All Rights Reserved";
    public virtual void PrintCopyright()
    {        
        Console.WriteLine(copyright);
    }
}

That’s all for now, in the next article we will see what this new version of C# set aside for us.

See you soon!

Discover more from Blexin

Subscribe now to keep reading and get access to the full archive.

Continue reading