8

A Mostly Complete Guide to C# 11’s Future Features

 2 years ago
source link: https://medium.com/young-coder/a-mostly-complete-guide-to-c-11s-future-features-5658ce32f0fa
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

A Mostly Complete Guide to C# 11’s Future Features

What stays in? What gets cut? Let the speculation begin!

Today, it’s called C# Next. It’s still just a bundle of ideas struggling to make it out into the world. But in nine months, parts of C# Next will become the official C# 11. (Side note: That’s a relatively solid deadline, because the evolution of C# is tied to .NET’s November release cadence. In other words, we get .NET 7 in November 2022, and C# 11 comes along for the ride.)

For the next nine months months, we’re going to track the new C# Next features that might make it in to C# 11. That’s possible because the C# team does its language planning in the open. You can see the current status of all the C# Next ideas that are under consideration on GitHub.

But be warned that the features being considered for C# Next won’t all be included in C# 11. In fact, a couple of these features were demonstrated by C# language designers before C# 10, and they still didn’t make it into the final release. Which is good, because nothing kills a mature language faster than wedging in a few too many hasty changes without thinking through all the consequences. C# is at a stage where it needs to balance stability, cognitive load, and consistency. Stability, because C# is absolutely uncompromising about backward compatibility, even if something seems a little “off”. Cognitive load, because there are only so many reserved keywords you can use before the language sags under its own weight and new coders shy away from it. And consistency, because sometimes different features start to evolve into the same space that’s already occupied by legacy parts of the C# language. In this case, it doesn’t matter if you could reimplement something more cleanly (events? functional programming?), because we’re never going back to a blank slate.

With that out of the way, lets look at the lineup of potential features.

1. Line breaks in embedded expressions

Status: Almost certain to be includedSpec: https://github.com/dotnet/csharplang/blob/main/proposals/new-line-in-interpolation.md

Right now, there’s an unnecessary limitation with $ strings, line breaks and embedded expressions. First, consider this completely reasonable example of a $ string:

var legalString =
$"Total number of people is {people.Count}";

Problems happen if you attempt to split the line anywhere between the quotation marks. So this won’t compile:

var illegalString = 
$"Total number of
people is {people.Count}";

And you also can’t split the line inside an embedded expression:

var illegalString =
$"Total number of people is {students.Count
+ teachers.Count}";

But that limitation is just a side effect of the way the current compiler implementation works. It’s not necessary, and expressions with line breaks will compile error-free in C# 11.

2. List patterns

Status: Very likely to be included

Spec: https://github.com/alrz/csharplang/blob/list-patterns/proposals/list-patterns.md

One of the themes of modern C# is an increasing embrace of functional programming techniques. And one of the ways this is happening is through expressions, which let developers replace imperative code with declarative code. (Functional programmers love declarative code because it doesn’t change data or introduce unexpected side effects. To understand a bit more about why that’s so, and how expressions are implemented in C#, you can start over here.)

In C# 11, we’re likely to get a nifty extension for the switch expression syntax that lets you test arrays. It’s called list expressions, and you can recognize them by the square brackets []. Here’s a trivial example:

int[] numbers = {24, 320, 32, 700};

string arrayDesc = numbers switch
{
[24, 320, 32, 700] => "It's the array we expected!",
_ => "no match"
};

This code creates an array of numbers, and then uses a switch expression to examine it. If it matches the first array (which it will), the string “It’s the array we expected!” is returned from the expression and assigned to arrayDesc. Otherwise, arrayDesc is set to “no match” instead.

List patterns get much more interesting when you use the .. or _ operators to create slices. Here’s an example of that technique:

int[] numbers = {24, 320, 2430, 700};

string arrayDesc = numbers switch
{
[24, 320, 32, 700] => "It's the array we expected!",
[24, 400, _, 700] => "The third element could be anything",
[24, .., 700] => "Starts with 24 and ends with 700",
_ => "no match"
};

The .. represents a series of elements (any number) while the _ represents a single mystery element (any value). So [24, .., 700] matches arrays like [24, 200, 700] or [24, 324, 254, 525, 4, -4, 700].

List patterns don’t just work with explicitly defined arrays. Any type that has an indexer and Length and Count properties works for fixed matches. Slice patterns work if the type has an indexer that takes a Range argument or has a Slice method with two int parameters. String processing here we come!

3. Null-checking parameters

Status: Likely to be included

Spec: https://github.com/dotnet/csharplang/blob/main/proposals/param-nullchecking.md

A common theme in new versions of C# is fixing the easy stuff. Sometimes, a feature seems so minor that you almost wonder why bother? But research tells us that minor mistakes in boilerplate code is the source of a surprisingly large number of bugs. Some are easily fixed, while others are overlooked, and either way you’re losing time and development effort.

Parameter null checking is an example of a small feature for a simple, yet common task. You’ve likely written methods that require certain parameters, and you’ve probably added null checks to enforce this requirement, like this one:

public UpdateAddress(int personId, Address newAddress)
{
if (newAddress == null)
{
throw new ArgumentNullException("newAddress");
} ...
}

Hard-coding a parameter name is a bad look. The nameof() operator can help with, but it’s an even better idea to use C# 10’s new ArgumentNullException.ThrowIfNull() convenience method:

public UpdateAddress(int personId, Address newAddress)
{
ArgumentNullException.ThrowIfNull(newAddress);
}

But C# 11 wants to make life even easier by adding a declarative way to flag parameters that need null parameter checking. The proposed syntax uses a double exclamation mark !! operator:

public UpdateAddress(int personId, Address newAddress!!)
{
ArgumentNullException.ThrowIfNull(newAddress);
}

Now if you pass a null value in the place of an Address object, the ArgumentNullException is thrown automatically.

4. Raw string literals

Status: Very likely to be included

Spec: https://github.com/dotnet/csharplang/blob/main/proposals/raw-string-literal.md

Ordinary C# strings are notoiously messy, because you need to escape quotes (") and backslashes (\) and line breaks (\n). C# offers a solution with verbatim strings that allow special characters. Just prefix your string with @, and you get free reign to add all these details, with no escaping:

// This is awkward
string path = "c:\\myfilepath\\with-backslashes";// This is better
string path = @"c:\myfilepath\with-backslashes";

This approach works well enough for file paths and regular expressions, but it has its own subtle escaping problem. Strings are still delimited with quotation marks, and quotation marks are a common character in code, markup, and regular expressions. That means you can’t stuff any of that type of content into a verbatim string without carefully reviewing it.

The raw string literal feature creates a new path to sidestep the escaping problem. It uses a new delimiter — a series of quotation marks followed by a line break (to start), and a line break followed by the same number of quotation marks (to end). You just paste your block of code or markup in between.

It’s easiest to understand raw strings with an example:

string xml = """
<part number="1976">
<name>Windscreen Wiper</name>
<description>The Windscreen wiper automatically removes
rain from your windscreen. It has a rubber
<ref part="1977">blade</ref>, which can be ordered
separately.
</description>
</part>
""";

If you’re concerned that a triple-quotation mark sequence might occur inside your string, you have the option of extending the delimiter to use as many quotation marks as you want, as long as you’re consistent in both the start and the end:

string xml = """" 
Now """ is safe to use in your raw string.
"""";

As with @ strings, line breaks and whitespace is preserved in a raw string. But there’s one exception. Common whitespace — the amount that’s used to indent the entire example — is trimmed. That means the first example goes from this:

<part number="1976">
<name>Windscreen Wiper</name>
<description>The Windscreen wiper automatically removes
rain from your windscreen. It has a rubber
<ref part="1977">blade</ref>, which can be ordered
separately.
</description>
</part>

To this:

<part number="1976">
<name>Windscreen Wiper</name>
<description>The Windscreen wiper automatically removes
rain from your windscreen. It has a rubber
<ref part="1977">blade</ref>, which can be ordered
separately.
</description>
</part>

Raw strings aren’t meant to replace the current @ strings you’re using right now. They’re designed for the special case where you need to paste a block of arbitrary code or markup, and you need an approach to encoding that’s guaranteed to be safe.

5. Required members

Status: Speculative

Spec: https://github.com/dotnet/csharplang/blob/main/proposals/required-members.md

In the old days, you relied on class constructors almost exclusively to make sure objects were created in the right state. Today, we often work with more lightweight constructs, like the autoimplemented properties in this record:

public record Employee
{
public string Name { get; init; }
public decimal YearlySalary { get; init; }
public DateTime HiredDate{ get; init; }
}

And when we create instances of these lightweight objects, we like to do it quickly with object initializer syntax:

var theNewGuy = new Employee
{
Name = "Dave Bowman",
YearlySalary = 100000m,
HiredDate = DateTime.Now()
};

But what if your object doesn’t make sense unless certain properties are set? You could add a constructor, like usual, but you’ll need to pay for that formalization with some more boilerplate. And copying values from parameters to properties is yet another example of a mistake that’s easy to understand but common to make.

In C# 10, the required keyword makes this problem go away:

public record Employee
{
public required string Name { get; init; }
public decimal YearlySalary { get; init; }
public DateTime HiredDate{ get; init; }
}

Now the compiler won’t allow you write code that creates an Employee but doesn’t set the Name property.

6. Semi autoimplemented properties

Status: Speculative

Spec: https://github.com/dotnet/csharplang/blob/main/proposals/semi-auto-properties.md

The C# team has done a lot to streamline code over the years with autoimplemented properties. The Employee record shown above is a good example — it declares three immutable properties using the get and init keywords. The data is stored in three private fields, but these fields are created for you automatically, and managed without your intervention. You never see them.

Autoimplemented properties are great, but they can only take you so far. When they don’t suit, you’re forced to add the backing field to your class and write the usual property methods like you’re back in C# version 2. But in C# 10, there’s a new backdoor with the field keyword, which exposes the automatically created backing field.

For example, let’s say you want to create a record that does a little bit of processing to an initial property value. Here’s a revision of the Employee class that makes sure the HiredDate field only contains the date information from the DateTime object, and no time information:

public record Employee
{
public required string Name { get; init; }
public decimal YearlySalary { get; init; }
public DateTime HiredDate{ get; init => field = value.Date(); }
}

This cleanup code is nice, simple, and almost declarative.

You can use the field keyword to access the backing field in a get, set, or init procedure. So where you might have needed something like this to validate a property in an ordinary class:

private string _firstName;public string FirstName
{
get
{
return _firstName;
}
set
{
if (value.Trim() == "")
throw new ArgumentException("No blank strings"); _firstName = value;
}
}

Now you can use an autoimplemented property and field:

public string FirstName {get;
set
{
if (value.Trim() == "")
throw new ArgumentException("No blank strings"); field = value;
}
}

Basically, as long as you don’t need to change the data type of your property, there’s no longer a need to declare the backing field yourself.

7. Primary Constructors

Status: Speculative

Spec: https://github.com/dotnet/csharplang/blob/main/proposals/non-record-primary-constructors.md

In recent releases, C# has taken significant strides in reducing boilerplate code, with features like automatic properties. Despite what new-ish developers sometimes think, these efforts are not simple conveniences. Reducing the amount of code you write reduces the amount of code everyone needs to read. It makes codebases easier to navigate, makes it easier for us to focus on the important logic, and reduces the places where accidents can happen and careless bugs can hide.

Primary constructors is a feature that continues this theme of reducing the amount of hand-written code for common patterns — in this case, constructors. Consider this ordinary class that includes two read-only properties and a constructor:

public class DataSlice
{
public string DataLabel { get; }
public float DataValue { get; }

public DataSlice(string dataLabel, float dataValue)
{
DataLabel = dataLabel;
DataValue = dataValue;
}
}

According to one estimate, nearly 75% of their classes have constructors, and 95% of them don’t do anything more than copy parameters into properties, like this example DataSlice class. Constructors like these are so common that it’s no surprise that they eventually collect errors. Assigning a parameter to the wrong property (or to another parameter) is a surprisingly common mistake.

With a primary constructor, the basic constructor is generated automatically, using constructor arguments that are added to the class declaration. It looks like this:

public class DataSlice(string dataLabel, float dataValue)
{
...
}

In this example, the dataLabel and dataValue parameters become local private variables that are accessible anywhere in your class code. As long as you use them somewhere (in a property or method), their values are automatically stored with the object. If your intention is simply to provide access through public properties, as in the DataSlice example, you can write your class like this:

public class DataSlice(string dataLabel, float dataValue)
{
public string DataLabel { get => dataLabel; }
public float DataValue { get => dataValue; }
}

Even though you haven’t written any constructor code, you can still create and use the DataSlice class in the same way, using the automatically generated constructor:

var adultData = new DataSlice("Vaccinated adults", 494);

Using a primary constructor doesn’t preclude property validation. You can still enforce your rules in a property setter, like this:

public class DataSlice(string dataLabel, float dataValue)
{
public string DataLabel
{
get => dataLabel;
set
{
if (value < 0) throw new ArgumentOutOfRangeException();
dataLabel = value;
}
}
public float DataValue { get => dataValue; }
}

Other details, like adding extra constructors, calling the base constructor in a derived class, and so on, are all possible (and work pretty much the way you would expect, although the feature isn’t formalized yet). The most obvious downside is that primary constructors could collide with positional records, another proposed feature. Follow the ongoing feature discussion on GitHub for more.

8. UTF8 string literals

Status: Speculative

Spec: https://github.com/dotnet/csharplang/blob/main/proposals/utf8-string-literals.md

Every string in C# uses UTF16 encoding, but the Internet is still tied to UTF8. That means that there are still quite a few tasks that will force you to manually convert a strings, like writing to the network stack. There are a few different string conversion approaches, but this one is typical:

string utf16data = "HEAD / HTTP/1.1";
byte[] utf8data = Encoding.UTF8.GetBytes(utf16data);

This doesn’t look awful, but it is somewhat inefficient. With UTF8 string support, there’s an implicit type conversion that can map your string into a byte array, like this:

byte[] utf8data = "HEAD / HTTP/1.1";

This looks similar to the previous example, but the compiler actually converts this string to a sequence of fifteen bytes, without allocating the extra memory required for UTF16 string, or doing the manual conversion step. However, this magic can only work with constant values, like the hardcoded string shown here.

There’s also a u8 suffix that you can use to specify the UTF8 data type:

var utf8data = "HEAD / HTTP/1.1"u8;
// Now utf8data is a byte[] array

And there’s an implicit conversion to spans, so this code works too:

ReadOnlySpan<byte> utf8data = "HEAD / HTTP/1.1";

You’ll get a compile-time error if you try to assign a string that has characters that aren’t represented in UTF8:

byte[] badString = "Special chars:\uD801\uD802";

And a bit more…

Here’s a quick roundup of a few proposed changes that I didn’t cover above, mostly because they’re more specialized:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK