C# Take C# 8.0 for a spin

  • Thread starter Mads Torgersen - MSFT
  • Start date
M

Mads Torgersen - MSFT

Guest
#1
Take C# 8.0 for a spin


Yesterday we announced the first preview of both Visual Studio 2019 (Making every developer more productive with Visual Studio 2019) and .NET Core 3.0 (Announcing .NET Core 3 Preview 1 and Open Sourcing Windows Desktop Frameworks).

One of the exciting aspects of that is that you get to play with some of the features coming in C# 8.0! Here I am going to take you on a little guided tour through three new C# features you can try out in the preview. If you want an overview of all the major features, go read the recent post Building C# 8.0, or check the short (13 mins) video "What’s new in C# 8.0" on Channel 9 or YouTube.

Getting ready


First of all, download and install Preview 1 of .NET Core 3.0 and Preview 1 of Visual Studio 2019. In Visual Studio, make sure you select the workload ".NET Core cross-platform development" (if you forgot, you can just add it later by opening the Visual Studio Installer and clicking "Modify" on the Visual Studio 2019 Preview channel).

Launch Visual Studio 2019 Preview, Create a new project, and select "Console App (.NET Core)" as the project type.

Once the project is up and running, change its target framework to .NET Core 3.0 (right click the project in Solution Explorer, select Properties and use the drop down menu on the Application tab). Then select C# 8.0 as the language version (on the Build tab of the project page click "Advanced…" and select "C# 8.0 (beta)").

Now you have all the language features and the supporting framework types ready at your fingertips!

Nullable reference types


The nullable reference types feature intends to warn you about null-unsafe behavior in the code. Since we didn’t do that before, it would be a breaking change to just start now! To avoid that, you need to opt in to the feature.

Before we do turn it on, though, let’s write some really bad code:

using static System.Console;

class Program
{
static void Main(string[] args)
{
string s = null;
WriteLine($"The first letter of {s} is {s[0]}");
}
}


If you run it you get, of course, a null reference exception. You’ve fallen into the black hole! How were you supposed to know not to dereference s in that particular place? Well duh, because null was assigned to it on the previous line. But in real life, it’s not on the previous line, but in somebody else’s assembly running on the other side of the planet three years after you wrote your line of code. How could you have known not to write that? That’s the question that nullable reference types set out to answer! So let’s turn them on!

For a new project you should just turn them on right away. In fact I think they should probably be on by default in new projects, but we didn’t do that in the preview. The way to turn them on is to add the following line to your .csproj file, e.g. right after the LanguageVersion that was just inserted when you switched to C# 8.0 above:

<NullableReferenceTypes>true</NullableReferenceTypes>


Save the .csproj file and return to your program: What happened? You got two warnings! Each represents one "half" of the feature. Let’s look at them in turn. The first one is on the null in this line:

string s = null;


It complains that you are assigning null to a "non-nullable type": Whaaat?!? When the feature is turned on nulls are no longer welcome in ordinary reference types such as string! Because, you know what, null is not a string! We’ve been pretending for the last fifty years of object-oriented programming, but actually null is in fact not an object: That’s why everything explodes whenever you try to treat it like one!

So no more of that: null is verboten, unless you ask for it. How do you ask for it? By using a nullable reference type, such as string?. The trailing question mark signals that null is allowed:

string? s = null;


The warning goes away: we have explicitly expressed the intent for this variable to hold null, so now it’s fine.

Until the next line of code! On the line

WriteLine($"The first letter of {s} is {s[0]}");


It complains about the s in s[0] that you may be dereferencing a null reference. And sure enough: you are! Well done, compiler! How do you fix it, though? Well that’s pretty much up to you – whichever way you would always have fixed it! Let’s try for starters to only execute the line when s is not null:

if (s != null) WriteLine($"The first letter of {s} is {s[0]}");


The warning goes away! Why? Because the compiler can see that you only go to the offending code when s is not null. It actually does a full flow analysis, tracking every variable across every line of code to keep tabs on where it might be null and where it probably won’t be. It watches your tests and assignments, and does the bookkeeping.

Let’s try another version:

WriteLine($"The first letter of {s} is {s?[0] ?? '?'}");


This uses the null conditional indexing operator s?[0] which avoids the dereference and produces a null if s is null. Now we have a nullable char?, but the null-coalescing operator ?? '?' replaces a null value with the char '?'. So all null dereferences are avoided. The compiler is happy, and no warnings are given.

As you can see, the feature keeps you honest while you code: it forces you to express your intent whenever you want null in the system, by using a nullable reference type. And once null is there, it forces you to deal with it responsibly, making you check whenever there’s a risk that a null value may be dereferenced to trigger a null reference exception.

Are you completely null-safe now? No. There are a couple of ways in which a null may slip through and cause a null reference exception:

  • If you call code that didn’t have the nullable reference types feature on (maybe it was compiled before the feature even existed), then we cannot know what the intent of that code was: it doesn’t distinguish between nullable and nonnullable – we say that it is "null-oblivious". So we give it a pass; we simply don’t warn on such calls.
  • The analysis itself has certain holes. Most of them are a trade-off between safety and convenience; if we complained, it would be really hard to fix. For instance, when you write new string[10], we create an array full of nulls, typed as non-null strings. We don’t warn on that, because how would the compiler keep track of you initializing all the array elements?

But on the whole, if you use the feature extensively (i.e. turn it on everywhere) it should take care of the vast majority of null dereferences.

It is definitely our intention that you should start using the feature on existing code! Once you turn it on, you may get a lot of warnings. Some of these actually represent a problem: Yay, you found a bug! Some of them are maybe a bit annoying; your code is clearly null safe, you just didn’t have the tools to express your intent when you wrote it: you didn’t have nullable reference types! For instance, the line we started out with:

string s = null;


That’s going to be super common in existing code! And as you saw, we did get a warning on the next line, too, where we tried to dereference it. So the assignment warning here is strictly speaking superfluous from a safety standpoint: It keeps you honest in new code, but fixing all occurences in existing code would not make it any safer. For that kind of situation we are working on a mode where certain warnings are turned off, when it doesn’t impact the null safety, so that it is less daunting to upgrade existing code.

Another feature to help upgrade is that you can turn the feature on or off "locally" in your code, using compiler directives #nullable enable and #nullable disable. That way you can go through your project and deal with annotations and warnings gradually, piece by piece.

To learn more about nullable reference types check out the Overview of Nullable types and the Introduction to nullable tutorial on docs.microsoft.com.

For a deeper design rationale, last year I wrote a post Introducing Nullable Reference Types in C#.

If you want to immerse yourself in the day-to-day of the design work, look at the Language Design Notes on GitHub, or follow along as I try to put together a Nullable Reference Types Specification.

Ranges and indices


C# is getting more expressiveness around working with indexed data structures. Ever wanted simple syntax for slicing out a part of an array, string or span? Now you can!

Go ahead and change your program to the following:

using System.Collections.Generic;
using static System.Console;

class Program
{
static void Main(string[] args)
{
foreach (var name in GetNames())
{
WriteLine(name);
}
}

static IEnumerable<string> GetNames()
{
string[] names =
{
"Archimedes", "Pythagoras", "Euclid", "Socrates", "Plato"
};
foreach (var name in names)
{
yield return name;
}
}
}


Let’s go to that bit of code that iterates over the array of names. Modify the foreach as follows:

foreach (var name in names[1..4])


It looks like we’re iterating over names 1 to 4. And indeed when you run it that’s what happens! The endpoint is exclusive, i.e. element 4 is not included. 1..4 is actually a range expression, and it doesn’t have to occur like here, as part of an indexing operation. It has a type of its own, called Range. If we wanted, we could pull it out into its own variable, and it would work the same:

Range range = 1..4;
foreach (var name in names[range])


The endpoints of a range expression don’t have to be ints. In fact they’re of a type, Index, that non-negative ints convert to. But you can also create an Index with a new ^ operator, meaning "from end". So ^1 is one from the end:

foreach (var name in names[1..^1])


This lobs off an element at each end of the array, producing an array with the middle three elements.

Range expressions can be open at either or both ends. ..^1 means the same as 0..^1. 1.. means the same as 1..^0. And .. means the same as 0..^0: beginning to end. Try them all out and see! Try mixing and matching "from beginning" and "from end" Indexes at either end of a Range and see what happens.

Ranges aren’t just meant for use in indexers. For instance, we plan to have overloads of string.SubString, SPan<T>.Slice and the AsSpan extension methods that take a Range. Those aren’t in this Preview of .NET Core 3.0 though.

Asynchronous streams


IEnumerable<T> plays a special role in C#. "IEnumerables" represent all kinds of different sequences of data, and the language has special constructs for consuming and producing them.

As we see in our current program, they are consumed through the foreach statement, which deals with the drudgery of obtaining an enumerator, advancing it repeatedly, extracting the elements along the way, and finally disposing the enumerator. And they can be produced with iterators: Methods that yield return their elements as they are being asked for by a consumer. Both are synchronous, though: the results better be ready when they are asked for, or the thread blocks!

async and await were added to C# to deal with results that are not necessarily ready when you ask for them. They can be asynchronously awaited, and the thread can go do other stuff until they become available. But that works only for single values, not sequences that are gradually and asynchronously produced over time, such as for instance measurements from an IoT sensor or streaming data from a service.

Asynchronous streams bring async and enumerables together in C#! Let’s see how, by gradually "async’ifying" our current program.

First, let’s add another using directive at the top of the file:

using System.Threading.Tasks;


Now let’s simulate that GetNames does some asynchronous work by adding an asynchronous delay before the name is yield returned:

await Task.Delay(1000);
yield return name;


Of course we get an error that you can only await in an async method. So let’s make it async:

static async IEnumerable<string> GetNames()


Now we’re told that we’re not returning the right type for an async method, which is fair. But there’s a new candidate on the list of types it can return besides the usual Task stuff: IAsyncEnumerable<T>. This is our async version of IEnumerable<T>! Let’s return that:

static async IAsyncEnumerable<string> GetNames()


Just like that we’ve produced an asynchronous stream of strings! In accordance with naming guidelines, let’s rename GetNames to GetNamesAsync.

static async IAsyncEnumerable<string> GetNamesAsync()


Now we get an error on this line in the Main method:

foreach (var name in GetNamesAsync())


Which doesn’t know how to foreach over an IAsyncEnumerable<T>. That’s because foreach’ing over asynchronous streams requires explicit use of the await keyword:

await foreach (var name in GetNamesAsync())


It’s the version of foreach that takes an async stream and awaits every element! Of course it can only do that in an async method, so we have to make our Main method async. Fortunately C# 7.2 added support for that:

static async Task Main(string[] args)


Now all the squiggles are gone, and the program is correct. But if you try compiling and running it, you get an embarassing number of errors. That’s because we messed up a bit, and didn’t get the previews of .NET Core 3.0 and Visual Studio 2019 perfectly aligned. Specifically, there’s an implementation type that async iterators leverage that’s different from what the compiler expects.

You can fix this by adding a separate source file to your project, containing this bridging code. Compile again, and everything should work just fine.

Next steps


Please let us know what you think! If you try these features and have ideas for how to improve them, please use the feedback button in the Visual Studio 2019 Preview. The whole purpose of a preview is to have a last chance to course correct, based on how the features play out in the hands of real life users, so please let us know!

Happy hacking,

Mads Torgersen, Design Lead for C#

Continue reading...
 
Top