Toptal acquires VironIT.com, enhancing custom software leadership

What’s New in C# 8.0

13.06.2019 Alexander N.
Leave a Comment
What’s New in C# 8.0

A version of the language C# 8.0 provides developers with some new functionality to simplify libraries’ development, improve the security of language, its readability and reduce the amount of code. This article reviews the main features of C# version 8.0 preview 5.

Readonly members

This innovation allows you to mark members of the structure with the readonly modifier, and if the implementation of this member does not use readonly members, a warning will be generated. Below is an example of using a readonly modifier on the overridden method ToString.

public struct Employee
    {
        private readonly string _firstName;
        private readonly string _lastName;
        
        private string _department;

        public Employee(string firstName, string lastName, string department)
        {
            _firstName = firstName;
            _lastName = lastName;
            _department = department;
        }

        public string Department
        {
            get => _department;
        }
        
// Warning
        public readonly override string ToString() => $"Name: {_firstName} {_lastName}, Department: {Department}";
    }

Above example will generate a warning:


image4

Theoretically, the new ability to use the readonly modifier should help the developer avoid errors in logic.

Default interface members

C# version 7.0 does not allow interfaces to contain implementations of methods, only their declarations. In preview version 8.0, the ability to write method implementations by default is added. What actually allows applying in C# analogue of multiple inheritances. The example below illustrates this innovation.

  class Program
 {
 static void Main(string[] args)
 {
 Sample sample = new Sample();

 ((ISample1) sample).M();
 ((ISample2) sample).M();

 Console.ReadKey();
 }

 }

 public interface ISample1
 {
 void M() => Console.WriteLine("ISample1");
 }

 public interface ISample2
 {
 void M() => Console.WriteLine("ISample2");
 }

 public class Sample : ISample1, ISample2
 {
 public void ExtendedM()
 {
 ((ISample1) this).M();
 }
 }

As can be seen from the example above, the calling method implemented in the interface is possible only when accessing an instance of the class through this interface.

Microsoft itself presents this opportunity as a tool for library developers. Now you can enter new members into the interface without fear that classes implementing older versions of this interface will stop compiling when the library is updated.

Pattern matching new features

In C# 8.0, many possibilities are added for matching value with patterns and a more compact form of switch expressions (switch expressions).

Switch expression

Switch construct is pretty often used to return values in each case block. The new switch expression is intuitive and will reduce the number of lines of code by reducing the use of the keywords case and break. The code below demonstrates the use of the new syntax.

 public enum ConnectionType
 {
 Type1,
 Type2,
 Type3,
 }

 public static IProvider GetProvider(ConnectionType connectionType)
 {
 var connectionString = ConfigurationManager.AppSettings[$"ConnectionStrings:{connectionType.ToString()}"];

 return connectionType switch
 {
 ConnectionType.Type1 => new Provider1(connectionString),
 ConnectionType.Type2 => new Provider2(connectionString),
 ConnectionType.Type3 => new Provider3(connectionString),
 _ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(connectionType))
 };
 }

Property patterns

Switch expression allows you to match a specific property of an object and, depending on its property value, return a particular value. The example below clearly demonstrates this possibility.

 public class Address
 {
 public string State { get; set; }

 /* 
 * ...
 */
 }

 public static decimal ComputeSalesTax(Address address, decimal price) 
 => address switch
 {
 { State: "WA" } => price * 0.06M,
 { State: "MN" } => price * 0.75M,
 { State: "MI" } => price * 0.05M,
 _ => throw new ArgumentException(message: "invalid argument", paramName: nameof(address))
 };

Tuple patterns

Sometimes it is convenient to compare not one but several values at once. C# allows you to represent a set of values in the form of tuple, and using a switch expression we can match their values. The code below demonstrates the use of a switch expression to switch between several values expressed as tuples.

 public enum Sign
 {
 Rock,
 Paper,
 Scissors
 }

 public static string RockPaperScissors(Sign first, Sign second)
 => (first, second) switch
 {
 (Sign.Rock, Sign.Paper) => "Paper wins",
 (Sign.Rock, Sign.Scissors) => "Rock wins",
 (Sign.Paper, Sign.Rock) => "Paper wins",
 (Sign.Paper, Sign.Scissors) => "Scissors wins",
 (Sign.Scissors, Sign.Rock) => "Rock wins",
 (Sign.Scissors, Sign.Paper) => "Scissors wins",
 (_, _) => "Tie"
 };

In the code above, we define the input as a tuple and process it with a switch expression.

Positional patterns

The Deconstruct method in C# is provided for converting an object into a tuple of values. If some type implements Deconstruct method, then positional patterns can be used to match object properties expressed as a tuple.

Code below demonstrates the Point class that implements Deconstruct method and its use inside a switch expression.

 public class Point
 {
 public int X { get; set; }
 public int Y { get; set; }

 public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
 }

 public enum Quadrant
 {
 Origin,
 One,
 Two,
 Three,
 Four,
 OnBorder
 }

 static Quadrant GetQuadrant(Point point) => point switch
 {
 (0, 0) => Quadrant.Origin,
 var (x, y) when x > 0 && y > 0 => Quadrant.One,
 var (x, y) when x < 0 && y > 0 => Quadrant.Two,
 var (x, y) when x < 0 && y < 0 => Quadrant.Three,
 var (x, y) when x > 0 && y < 0 => Quadrant.Four,
 var (_, _) => Quadrant.OnBorder,
 _ => throw new ArgumentException(message: "invalid argument", paramName: nameof(point))
 };

Using declarations

Starting in C# 8.0, you can use using keyword to create an IDisposable object as a regular object and its Dispose method will be called at the end of the scope, which reduces nesting. The example demonstrates this new syntax.

 class Program
 {
 static void Main(string[] args)
 {
 Sample sample = new Sample();

 ((ISample1) sample).M();
 ((ISample2) sample).M();

 Console.ReadKey();
 }

 }

 public interface ISample1
 {
 void M() => Console.WriteLine("ISample1");
 }

 public interface ISample2
 {
 void M() => Console.WriteLine("ISample2");
 }

 public class Sample : ISample1, ISample2
 {
 public void ExtendedM()
 {
 ((ISample1) this).M();
 }
 }

The written below is equivalent to what is written above.

Static local functions

Now local functions can be made static to make sure that they do not capture variables from the scope. If the function uses a variable from scope, the compiler will not allow it to be static. An example of declaring a static local function is shown below.

image1

Nullable reference types

This feature is designed to help prevent NullReferenceException. Now inside the nullable annotation context, you can declare a nullable type reference variable and if we access it without checking for null or assign a non-nullable variable to the nullable reference of the visual studio, it will generate warnings. An example is given below.


image2

Indices and ranges

Now with C# 8.0 we can get subsets of arrays using a more intuitive syntax, similar to a simple element of a collection by index using square brackets []. Code using the new syntax is presented below.

 var numbers = new []
 {
 // index from start index from end
 "one", // 0 ^5
 "two", // 1 ^4
 "three", // 2 ^3
 "four", // 3 ^2
 "five" // 4 ^1
 }; // 5 ^0

 var twoThree = numbers[1..3];
 var threeFourFive = numbers[^3..^0];
 var allNumbers = numbers[..];
 var oneTwoThree = numbers[..3];
 var fourFive = numbers[3..];

 // Describing of range in variable
 Range range = 0..3;
 var subArray = numbers[range];

As you can see from the example, the new syntax allows us to use literals to describe ranges. In C# 8.0 there are literals denoting indices from the beginning and the end of the array. We can make ranges of them, define ranges as variables.

Conclusions

The C# language from version to version provides more tools for writing concise and understandable code. However, not all the innovations seem intuitive to me. For example, the syntax of the nullable reference type declaration is obviously similar to the nullable value type syntax, while logically they are far from equivalent.

Nevertheless, this is only a preview of version 5 of C# 8.0 and much of the above release can be changed and improved. Some innovations may be changed syntactically or completely abandoned.

Please, rate my article. I did my best!

1 Star2 Stars3 Stars4 Stars5 Stars (1 votes, average: 5.00 out of 5)
Loading…

Leave a Reply