C# 8.0 Features

This article covers the new C# 8.0 language feature-set and their C# 7.0 implementation equivalent. Some of the features (links in the article) can be tested already with the Visual Studio 2019 preview.

Interface Default Implementation

Finally! I think there is no explanation required for this feature.

    class Program
    {
        static void Main(string[] args)
        {
            ConsoleLogger consoleLogger = new ConsoleLogger();
            DiagnosticLogger diagnosticLogger = new DiagnosticLogger();

            consoleLogger.Log();
            diagnosticLogger.Log();
        }
    }

    public class ConsoleLogger : Logger
    {

    }

    public class DiagnosticLogger : Logger
    {
        public void Log() => System.Diagnostics.Debug.WriteLine($"Hello from the {nameof(DiagnosticLogger)} implementation");
    }

    public interface Logger
    {
        void Log() => Console.WriteLine($"Hello from the interface implementation");
    }

Output

  • Hello from the DiagnosticLogger implementation
  • Hello from the interface implementation

Async Streams

Lets take a look at following code

        static void Main(string[] args)
        {
            foreach (var s in GetData())
                Console.Write(s);
            Console.ReadLine();
        }

        public static IEnumerable<string> GetData()
        {
            yield return "h";
            yield return "e";
            yield return "l";
            yield return "l";
            yield return "o";
        }

GetData yield returns some static values; but what if the data relies on I/O operations? Hence, the UI responsiveness would suffer. In this case, Async and await operations aren’t supported with C# 7.0 or < in the GetData block.

C# 8.0 does address this issue with IAsyncEnumerable. Return an IAsyncEnumerable and start using awaits in the GetData block.

        static async void Main(string[] args)
        {
            foreach await (var s in GetData())
                Console.Write(s);
            Console.ReadLine();
        }

        public static IAsyncEnumerable<string> GetData()
        {
            yield return "h";
            await Task.Delay(500);
            yield return "e";
            await Task.Delay(500);
            yield return "l";
            await Task.Delay(500);
            yield return "l";
            await Task.Delay(500);
            yield return "o";
        }

Sidenote: In C# 7.0 or < a Helper class does exist for async enumerables: NuGet: AsyncEnumerator

Ranges and Indices

C# 8.0 delivers the possibility to select a subset of an array. For better visuals, check out the following code, written without C# 8.0, to select a subset of the given array data.

        static void Main(string[] args)
        {
            int[] data = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            foreach (int cData in data.ToList().Skip(2).Take(4))
                Console.Write(cData + " ");
            Console.ReadLine();
        }

C# 8.0 on the other hand makes things much more easy.

        static void Main(string[] args)
        {
            int[] data = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            foreach (int cData in data[2..6])
                Console.Write(cData + " ");
            Console.ReadLine();
        }

data[2..6] indicates a Range. Another useful feature with C# 8.0 (regarding indexing) is the newly introduced ‘^’ index operator. It indicates to start counting from the end of the array. E.g.:

        static void Main(string[] args)
        {
            int[] data = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            foreach (int cData in data[2..^3])
                Console.Write(cData + " ");
            Console.ReadLine();
        }

The index operator ‘^’ not only works within ranges, but also as a standalone index.

        static void Main(string[] args)
        {
            int[] data = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

            Console.WriteLine(data[^9]); //Output: 1
            Console.WriteLine(data[^1]); //Output: 9
            Console.WriteLine(data[^0]); //System.IndexOutOfRangeException
            Console.ReadLine();
        }

To use this feature already, install the Sdcb.System.Range NuGet.

Switch Expressions

C# 8.0 extends the “C# 7.0 Pattern matching” with switch expressions. Consider the following models:

        public class Shape { }

        public class Line : Shape
        {
            public int length;
        }

        public class Circle : Shape
        {
            public int radius;
        }

Pattern matching with C# 7.0:

        static void Main(string[] args)
        {
            Shape shape = new Line();

            switch (shape)
            {
                case Line l:
                    Console.WriteLine(l.length);
                    break;
                case Circle c:
                    Console.WriteLine(c.radius);
                    break;
                default:
                    throw new NotImplementedException();
                    break;
            }

            Console.ReadLine();
        }

Switch expressions with C# 8.0, combined with Pattern matching:

        static void Main(string[] args)
        {
            Shape shape = new Line();

            switch (shape)
            {
                Line l => Console.WriteLine(l.length);
                Circle c => Console.WriteLine(c.radius);
                _ => throw new NotImplementedException();
            }

            Console.ReadLine();
        }

It rips of the unnecessary case/break syntax and offers a higher readability.

Target-Typed Expressions

Lets start off again with a none-C# 8.0 example:

        static void Main(string[] args)
        {
            Line[] line = new Line[] { new Line(0), new Line(1), new Line(2) };
        }

        public class Line 
        {
            public Line(int bla) { }
        }

C# 8.0 finally gets rid of all the unnecessary types.

        static void Main(string[] args)
        {
            Line[] line = { new (0), new (1), new(2) };
        }

        public class Line 
        {
            public Line(int bla) { }
        }

Recursive pattern

One last time we will start with a C# 7.0 example:

        static void Main(string[] args)
        {
            var shapes = GetShapes();

            foreach(var cShape in shapes)
            {
                if(cShape is Line)
                {
                    Line cLine = (Line)cShape;

                    if (cLine.length == 0 && cLine.x == 100 && cLine.y == 1000)
                        Console.WriteLine(cLine.ToString());
                }
            }
        }

        public class Shape { }

        public class Line : Shape
        {
            public int length;
            public int x;
            public int y;

            public override string ToString() => $"{length} {x} {y}";
        }

C# 8.0 combines the casting and expression into a single liner.

        static void Main(string[] args)
        {
            var shapes = GetShapes();

            foreach(var cShape in shapes)
            {
                if(cShape is Line { length: 0, x: 100, y: 1000})
                    Console.WriteLine(cShape.ToString());
            }
        }

More interesting are comparison operators like: “>, >=, <, <= and !=”. I still have to figure that one out 😉

Nullable reference types

I personally wouldn’t call that a language feature; but however, here it is. If an object’s variable has been used without initialization, the compiler throws a warning. This feature may prevent some Null-Refs runtime exceptions.

Finally, I would like to point out a helpful and informative broadcast by Mads Torgersen.

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.