Explore C# 8

In previous articles, we covered C# 8 asynchronous streams, C# 8 pattern matching, C# 8 default interface methods, and C# 8 nullable reference types. In this final article, we'll look at static local functions, indices and ranges, and using declarations.

static local functions

C# 7 introduced local functions, which are defined and used inside the caller function. Such functions can change their caller's local variables. To disallow this possibility and require explicit passing of all arguments, local functions can now be marked as static:

void Bar()
{
  int i = 0;
  Foo();
 
  static void Foo()
  {
    i = 3; // CS8421: A static local function cannot contain a reference to 'i'
  }
}

Indices and ranges

C# 8 adds a syntax for ranges. This syntax consists of the range operator (..), which is surrounded by a start and end expression that specifies the index of the first element (which is included) and the index of the last element (which is excluded):

string[] fruits = new string[]
{
    "apple",
    "banana",
    "cherry",
};

string[] allFruits = fruits[0..3];
string[] allFruits2 = fruits[0..fruits.Length];
string[] allFruits3 = fruits[0..(2 + 1)];

In the examples above, we take the range of all fruits by specifying 0 as the start index and 3 as the end index. The end index is one past the last element because C# indexing is zero-based. Because the end index is not included in the range, we've added one.

It’s also possible to index from the end using the ^ operator. ^0 is the index past the last element, ^1 is the last element, ^2 the element preceding it, and so on.

Here is an example:

string[] lastTwoFruits = fruits[^2..^0];

By default, the start index is 0, and the end index is ^0, so one or both might be omitted:

string[] allFruits4 = fruits[..];
string[] skipFirstTwoFruits = fruits[2..];

Reverse indexing can also be used directly:

string lastFruit = fruits[^1];

The C# compiler uses the System.Index and System.Range types to represent the range and index. You can support your own types by adding indexers that take these types. Types that already have an indexer that takes an int and an int Count/Length property implicitly get support for reverse indexing. Types that additionally include a Slice method that takes two ints (assumed offset and count) implicitly get support for taking a range.

The .NET array, string, and Span support both indexes and ranges. The List type supports indexes, but not ranges.

using declarations

C# has support for disposing of variables with the using keyword. Previous versions of C# required a block statement that explicitly scoped the lifetime:

using (FileStream fs = File.OpenRead("myfile.txt"))
{
    // explicit block
    ReadFromStream(fs);
}

With C# 8, this block is no longer needed. The compiler will use the declaring scope:

using FileStream fs = File.Open("myfile.txt");
ReadFromStream(fs);

As you can see, this reduces the indentation.

Conclusion

In this article, we’ve looked at three different C# 8 features:

  • static local functions, which require explicit passing of all arguments to local functions.
  • Indices and ranges, which introduce first-class support to denote ranges and perform reverse indexing.
  • using declarations, which reduce levels of indentation when working with disposable objects.

C# 8 can be used with the .NET Core 3.1 SDK, which is available on RHEL, Fedora, Windows, macOS, and other Linux distributions.

Last updated: February 24, 2024