The Developer Space

Developer's Cafe

  • Cloud
  • Database
  • Open Source
  • Programming
  • Web Dev
  • Mobile
  • Security
  • QuickRef
  • Home
  • Programming
  • C# 8 New Features

C# 8 New Features

Shameel Ahmed - .NET, C#, Programming
October 8, 2019October 11, 2019 2 Comments
2 0
Read Time13 Minute, 0 Second

Introduction

C# 8 comes with a host of changes and language improvements that make it more interesting than ever to write code in the world’s most popular programming language. This article describes the most important new features and enhancements to the language.

Nullable Reference Types

Nullable value types have existed in C# since long. The same functionality has now been made available to reference types. What this essentially means is that you can now mark a reference variable as Nullable, which tells that compiler that it is okay if the variable is assigned a null value. For variables that are not marked as Nullable, the compiler treats them as Non-Nullable Reference Types.

A Nullable Reference Type is declared using the same syntax as a Nullable Value Type, i.e. suffixing it with a ? operator. If the ? is not appended to the type name, then it is treated as a non-nullable reference type. That includes all reference type variables in existing code when you have enabled this feature.

The compiler uses static analysis to determine if a nullable reference is known to be non-null. The compiler warns you if you dereference a nullable reference when it may be null. You can override this behavior by using the null-forgiving operator ! following a variable name. For example, if you know the myAddress variable isn’t null but the compiler issues a warning, you can write the following code to override the compiler’s analysis:

class Address {
    string StreetName { get; set; }
	string City { get; set; }
	string State { get; set; }
	string ZipCode { get; set; }
}

Address? myAddress; //Nullable Reference Type

var state = myAddress.State //Compiler may issue a warning for null reference
var city = myAddress!.City //Compiler issues no warning for null reference

Asynchronous streams

C# 8.0 allows you to create and consume streams asynchronously. To enable your methods to return an asynchronous stream, your method must:

public static async IAsyncEnumerable<int> GetRandomSequence(int size, int max) {
  for (var i = 0; i < size; i++) {
    yield return _random.Next(max - (size - 1));
  }
  await Task.Delay(50);
}
  1. be decorated with the async modifier.
  2. return an IAsyncEnumerable<T>.
  3. contain yield return statements to return successive elements in the asynchronous stream.

To enumerate the return value from this method, use:

await foreach (var number in GetRandomSequence(1000, 1000000)) {
  Console.WriteLine(number);
}

Index and Range

The new Index and Range types and the two new associated operators provide a neat and readable syntax while working with sequences.

  • System.Index represents an index in a sequence.
  • System.Range represents a sub range within a sequence.
  • The index from end operator ^, which specifies that an index is relative to the end of the sequence.
  • The range operator .., which specifies the start and end of a range as its operands.

Consider an array words. The 0 index is the same as words[0]. The ^0 index is the same as words[words.Length]. Note that words [^0] throws an exception, just as words[words.Length] does. For any number n, the index ^n is the same as words.Length - n.

A Range specifies the start and end of a range. The start of the range is inclusive, but the end of the range is exclusive, meaning the start is included in the range but the end is not included in the range. The range [0..^0] represents the entire range, just as [0..words.Length] represents the entire range.

Here are few examples:

string[] words = new string[]
{
                    // index from start    index from end
        "She",      // 0                   ^8
        "sells",    // 1                   ^7
        "sea",      // 2                   ^6
        "shells",   // 3                   ^5
        "on",       // 4                   ^4
        "the",      // 5                   ^3
        "sea",      // 6                   ^2
        "shore",    // 7                   ^1
};                  // 8 (or words.Length) ^0
Console.WriteLine($"The last word is {words[^1]}"); // writes "shore"

//The following code creates a subrange with the words "sells", "sea", and "shells".It includes words[1] through words[3]. The element words[4] is not in the range.
var sellsSeaShells = words[1..4];

//The following code creates a subrange with "sea" and "shore". It includes words[^2] and words[^1]. The end index words[^0] is not included:
var seaShore = words[^2..^0];

var allWords = words[..]; // contains "She" through "shore".
var firstPhrase = words[..4]; // contains "She" through "shells"
var lastPhrase = words[5..]; // contains "the", "sea" and "shore"

Ranges can also be declared as variables and used in expressions.

Range phrase = 1..4;
var text = words[phrase];
//text contains "sells", "sea", and "shells"

Default Interface Members

Developers who write and distribute components must have gone through the problem with managing versioning of interfaces. In the COM/ActiveX world, it was referred to as “DLL Hell”. That is, once you publish an interface, you cannot add new members without breaking existing classes that implement the interface. Traditional method of handling such cases was to publish a new interface with the new members and ask your component users to start implementing your new interface in future releases of their code.

C# 8.0 allows you to provide a default implementation for your new interface members to ensure that existing classes that implement your interface are not broken. What this essentially means is that your interface methods and properties can now have a body and they can also have static members.

//Version 1
public interface IDevSpaceControl {
  bool WinFormsSupported { get; }
}
//Version 2
public interface IDevSpaceControl {
  bool WinFormsSupported { get; }
  bool WinFormsSupportedOnDotNetCore {
    get {
      return false;
    }
  }
}

In this example, the IDevSpaceControl interface adds a new member WinFormsSupportedOnDotNetCore without breaking existing clients by providing a default implementation.

To me personally, this is a design problem where the developer can get around by using abstract classes instead of interfaces, but Microsoft has purportedly done this to enable C# to interoperate with Android and Swift languages that already support similar features.

Switch Expressions

The switch statement has been enhanced to return an expression, this is called a switch expression. Here’s an example of a switch statement:

enum Importance {
  None,
  Trivial,
  Regular,
  Important,
  Critical
}

//switch statement
internal int GetPriority(Importance importance) {
  switch (importance) {
    case Importance.None:
      return 0;
    case Importance.Trivial:
      return 25;
    case Importance.Regular:
      return 50;
    case Importance.Important:
      return 75;
    case Importance.Critical:
      return 100;
    default:
      return 50;
  };
}

Here’s equivalent switch expression. Note the brevity of the switch expression and how the code is very easy to read and understand.

//switch expression
internal int GetPriority(Importance importance) =>
  importance switch {
      Importance.None => 0,
      Importance.Trivial => 25,
      Importance.Regular => 50,
      Importance.Important => 75,
      Importance.Critical => 100,
      _ => 50
  };

Switch Statement vs Switch Expressions

There are several syntax differences between a switch statement and a switch expression:

  • The variable comes before the switch keyword, which makes it easy to distinguish the switch expression from the switch statement.
  • The case: is replaced with =>
  • The default clause is replaced with a _ discard.
  • The bodies are expressions, not statements.

A switch expression must either return a value or throw an exception. If none of the cases match, the switch expression throws an exception. As with switch statements, the compiler generates a warning if all possible cases are not covered in the switch expression.

Property Patterns

Property pattern allows you to examine the property of an object and take action. Let’s take an example of Income Tax calculation where the tax rates change based on the State. The Address class has a property called State. Look how the State property is elegantly examined and an appropriate expression returned.

public static decimal ComputeIncomeTax(Address location, decimal income) =>
  location switch
  {
      { State: "TX" } => income * 0.25M, //Property pattern
      { State: "MA" } => income * 0.31M,
      { State: "NY" } => income * 0.35M,
      // other cases removed for brevity...
      _ => 0.30M
  };

Tuple Patterns

Tuple patterns enable you to switch by comparing multiple inputs to values expressed as a tuple. The following code shows an updated version of the switch expression code above:

enum CustomerRanking {
  Regular,
  Gold,
  Platinum,
  Titanium
}

internal int GetPriority(Importance importance, CustomerRanking customerRanking) =>
(importance, customerRanking) switch
{
    (Importance.None, CustomerRanking.Regular) => 0,
    (Importance.None, CustomerRanking.Gold) => 5,
    (Importance.None, CustomerRanking.Platinum) => 10,
    (Importance.None, CustomerRanking.Titanium) => 15,
    (Importance.Trivial, CustomerRanking.Regular) => 20,
    (Importance.Trivial, CustomerRanking.Gold) => 25,
    (Importance.Trivial, CustomerRanking.Platinum) => 30,
    (Importance.Trivial, CustomerRanking.Titanium) => 35,
    (Importance.Regular, CustomerRanking.Regular) => 45,
    (Importance.Regular, CustomerRanking.Gold) => 50,
    (Importance.Regular, CustomerRanking.Platinum) => 55,
    (Importance.Regular, CustomerRanking.Titanium) => 60,
    (Importance.Important, CustomerRanking.Regular) => 65,
    (Importance.Important, CustomerRanking.Gold) => 70,
    (Importance.Important, CustomerRanking.Platinum) => 75,
    (Importance.Important, CustomerRanking.Titanium) => 80,
    (Importance.Critical, _) => 100,
    _ => 50
};

Note that you can ignore an argument in the switch expression by passing the _ discard. In the above example, different combinations of Importance and CustomerRanking are compared to arrive at a priority. However, when the Importance is Critical, priority is always 100 irrespective of the value of CustomerRanking argument.

Compare it to the traditional way of writing multiple if statements or nested switch statements. Neat, isn’t it?

Readonly members in Structs

Any member of a struct can be declared as readonly by specifying the readonly modifier. It is a design time intent to let the compiler know that the member does not undergo state change. It enables the compiler to perform optimizations when necessary to improve performance. It provides more granular control than declaring the entire struct as readonly.

public struct Circle
    {
        public double Radius { get; set; }
        public double Diameter => Radius * 2;
        public readonly double Circumference => Math.PI * Diameter;
        public readonly double Area => Math.PI * Math.Pow(Radius, 2);
        public readonly override string ToString() =>
            $"Circle: Radius={Radius}, Diameter={Diameter}, Circumference={Circumference}, Area={Area}";
    }

Note that Diameter is not readonly and Circumference is declared as readonly and references Diameter. When a readonly member references a non-readonly member, the compiler throws a warning:

Warning	CS8656	Call to non-readonly member 'Circle.Diameter.get' from a 'readonly' member results in an implicit copy of 'this'

You can decorate the Diameter property with readonly to fix this issue.

Using Declarations

The using declarations can now be scoped to a variable for better code readability. Tired of declaring multiple nested using declarations to efficiently dispose off objects? Try this new syntax:

static void WriteToFile(List<string> lines) {
    using var file = new System.IO.StreamWriter("sample.txt");
    foreach (string line in lines) {
        if (!line.Contains("Second")) {
            file.WriteLine(line);
        }
    }
//Dispose() called here
}

Note that the scope of the using variable is the scope where the variable is defined. On the contrary, in a using statement, the scope would be the end of the using statement itself.

static void WriteToFile(List<string> lines) {
    using file = new System.IO.StreamWriter("sample.txt") {
      foreach (string line in lines) {
          if (!line.Contains("Second")) {
              file.WriteLine(line);
          }
      }
	//Dispose() called here
    }
}

If not used properly, this could have significant consequences on the working memory of your applications.

Conclusion

C# has been a very exciting language from the beginning and one of the very few languages that keeps getting new features and enhancements very often. C# 8.0 adds more arms to your already-rich arsenal. If you liked this article, feel free to leave a comment and share this article with your friends and colleagues who might be interested.

More Reading

New Features and Enhancements in .NET Core 3.0

Share

Facebook
Twitter
LinkedIn
Email

Post navigation

New Features and Enhancements in .NET Core 3.0
Minimize Non-Critical Database Workload costs in AWS

Related Articles

ChatGPT landing page

Learn Python with ChatGPT

Shameel Ahmed
December 26, 2022December 26, 2022 7 Comments
Migrate your SQL Server Workloads to PostgreSQL: Quick Reference: Second Edition

Book: Migrate your SQL Server Workloads to PostgreSQL: Quick Reference – Second Edition

Shameel Ahmed
October 18, 2022October 22, 2022 1 Comment

Increase Visual Studio Code Terminal Buffer Size

Shameel Ahmed
July 14, 2022July 14, 2022 No Comments

Average Rating

5 Star
0%
4 Star
0%
3 Star
0%
2 Star
0%
1 Star
0%
(Add your review)

2 thoughts on “C# 8 New Features”

  1. Tim says:
    October 8, 2019 at 2:23 pm

    var city = myAddress!.City //Compiler issues no warning for null reference

    should be
    var city = myAddress?.City //Compiler issues no warning for null reference

    Reply
    1. Shameel Ahmed says:
      October 10, 2019 at 12:52 am

      Hi Tim, the ! is a new operator that tells the compiler not to enforce null check on the variable. The ? is used to define a Nullable type, they’re both different.

      Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Categories

.NET Architecture Artificial Intelligence ASP.NET AWS Azure Books C# Career Cloud CodeProject Conversational Bots Database Data Security Facade IDEs Java Mobile MongoDB MySQL Open Source Patterns PostgreSQL Programming Python Redis Security SQL Server Tools Uncategorized Web Development Windows Phone

Recent Posts

  • Developer to Architect Series (Red Hat Enable Architect) January 16, 2023
  • Can ChatGPT replace Google Search? January 11, 2023
  • Learn Python with ChatGPT December 26, 2022
  • Book: Migrate your SQL Server Workloads to PostgreSQL: Quick Reference – Second Edition October 18, 2022
  • Increase Visual Studio Code Terminal Buffer Size July 14, 2022

Archives

  • January 2023 (2)
  • December 2022 (1)
  • October 2022 (1)
  • July 2022 (2)
  • February 2022 (1)
  • November 2021 (1)
  • July 2021 (1)
  • June 2021 (1)
  • September 2020 (1)
  • May 2020 (2)
  • April 2020 (1)
  • October 2019 (1)
  • September 2019 (4)
  • July 2019 (2)
  • May 2018 (1)
  • September 2017 (1)
  • April 2017 (1)
  • April 2014 (1)
  • August 2011 (1)
  • June 2009 (1)
Copyright 2022. The Developer Space | Theme: OMag by LilyTurf Themes
  • DbStudio
  • Re:Link
  • shameel.net
  • Privacy Policy
We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept”, you consent to the use of ALL the cookies.
Do not sell my personal information.
Cookie settingsACCEPT
Privacy & Cookies Policy

Privacy Overview

This website uses cookies to improve your experience while you navigate through the website. Out of these cookies, the cookies that are categorized as necessary are stored on your browser as they are as essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may have an effect on your browsing experience.
Necessary
Always Enabled
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
SAVE & ACCEPT