C# 9 Early Review

Nikita Starichenko
Dev Genius
Published in
7 min readJun 17, 2020

--

Already Implemented

1. Pattern matching improvements

1.1 Type Patterns

void M(object o1, object o2)
{
var t = (o1, o2);
if (t is (int, string)) {} // test if o1 is an int and o2 is a string
switch (o1) {
case int: break; // test if o1 is an int
case System.String: break; // test if o1 is a string
}
}

1.2 Parenthesized Patterns

Parenthesized patterns permit the programmer to put parentheses around any pattern. This is not so useful with the existing patterns in C# 8.0, however the new pattern combinators introduce a precedence that the programmer may want to override.

primary_pattern
: parenthesized_pattern
;
parenthesized_pattern
: '(' pattern ')'
;

1.3 Relational Patterns

Relational patterns permit the programmer to express that an input value must satisfy a relational constraint when compared to a constant value:

public static LifeStage LifeStageAtAge(int age) => age switch
{
< 0 => LiftStage.Prenatal,
< 2 => LifeStage.Infant,
< 4 => LifeStage.Toddler,
< 6 => LifeStage.EarlyChild,
< 12 => LifeStage.MiddleChild,
< 20 => LifeStage.Adolescent,
< 40 => LifeStage.EarlyAdult,
< 65 => LifeStage.MiddleAdult,
_ => LifeStage.LateAdult,
};

1.4 Pattern Combinators

Three new pattern forms

  1. pattern and pattern
  2. pattern or pattern
  3. not pattern
//example 1
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
//example 2
switch (o)
{
case 1 or 2:
case Point(0, 0) or null:
case Point(var x, var y) and var p:
}

2. Target-typed new

Do not require type specification for constructors when the type is known. Allow field initialization without duplicating the type.

Dictionary<string, List<int>> field = new() {
{ "item1", new() { 1, 2, 3 } }
};

Allow omitting the type when it can be inferred from usage.

XmlReader.Create(reader, new() { IgnoreWhitespace = true });

Instantiate an object without spelling out the type.

private readonly static object s_syncObj = new();

3. Lambda discard parameters

Unused parameters do not need to be named. The intent of discards is clear, i.e. they are unused/discarded.

Allow discards (_) to be used as parameters of lambdas and anonymous methods. For example:

  • lambdas: (_, _) => 0, (int _, int _) => 0
  • anonymous methods: delegate(int _, int _) { return 0; }

4. Attributes on local functions

Now attributes can be part of the declaration of a local function.

class C
{
void M()
{
int x;
local1();
Console.WriteLine(x);

[Conditional("DEBUG")]
void local1()
{
x = 42;
}
}
}

5. Native ints

The identifiers nint and nuint are new contextual keywords that represent native signed and unsigned integer types. The identifiers are only treated as keywords when name lookup does not find a viable result at that program location.

nint x = 3;
string y = nameof(nuint);
_ = nint.Equals(x, 3);

The types nint and nuint are represented by the underlying types System.IntPtr and System.UIntPtr with compiler surfacing additional conversions and operations for those types as native ints.

6. Extending Partial

The language will change to allow partial methods to be annotated with an explicit accessibility modifier. This means they can be labeled as private, public, etc ...

When a partial method has an explicit accessibility modifier though the language will require that the declaration has a matching definition even when the accessibility is private:

partial class C
{
// Okay because no definition is required here
partial void M1();
// Okay because M2 has a definition
private partial void M2();
// Error: partial method M3 must have a definition
private partial void M3();
}
partial class C
{
private partial void M2() { }
}

Further the language will remove all restrictions on what can appear on a partial method which has an explicit accessibility. Such declarations can contain non-void return types, ref or out parameters, extern modifier, etc ... These signatures will have the full expressivity of the C# language.

partial class D
{
// Okay
internal partial bool TryParse(string s, out int i);
}
partial class D
{
internal partial bool TryParse(string s, out int i) { }
}

This explicitly allows for partial methods to participate in overrides and interface implementations:

interface IStudent
{
string GetName();
}
partial class C : IStudent
{
public virtual partial string GetName();
}
partial class C
{
public virtual partial string GetName() => "Jarde";
}

7. Function pointers

This feature provides language constructs that expose low level IL opcodes that cannot currently be accessed efficiently, or at all: ldftn, ldvirtftn, ldtoken and calli. These low level opcodes can be important in high performance code and developers need an efficient way to access them.

8. Skip locals init

Allow suppressing emit of localsinit flag via SkipLocalsInitAttribute attribute.

In progress

1. Records

Init-only properties are great if you want to make individual properties immutable. If you want the whole object to be immutable and behave like a value, then you should consider declaring it as a record:

public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}

The data keyword on the class declaration marks it as a record. This imbues it with several additional value-like behaviors, which we’ll dig into in the following. Generally speaking, records are meant to be seen more as “values” – data! – and less as objects. They aren’t meant to have mutable encapsulated state. Instead you represent change over time by creating new records representing the new state. They are defined not by their identity, but by their contents.

To help with this style of programming, records allow for a new kind of expression; the with-expression:

var otherPerson = person with { LastName = "Hanselman" };

Short declaration:

public data class Person { string FirstName; string LastName; }//Means exactly the same as the one we had before:public data class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}

Inheritance:

public data class Person { string FirstName; string LastName; }
public data class Student : Person { int ID; }

2. Parameter null-checking

This allows for standard null validation on parameters to be simplified using a small annotation on parameters:

// Before
void Insert(string s) {
if (s is null)
throw new ArgumentNullException(nameof(s));
...
}
// After
void Insert(string s!) {
...
}

3. Covariant Returns

Support covariant return types. Specifically, permit the override of a method to return a more derived return type than the method it overrides, and similarly to permit the override of a read-only property to return a more derived return type. Callers of the method or property would statically receive the more refined return type from an invocation, and overrides appearing in more derived types would be required to provide a return type at least as specific as that appearing in overrides in its base types.
Example:

class Compilation ...
{
virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
override CSharpCompilation WithOptions(Options options)...
}

4. Static lambdas

To avoid accidentally capturing any local state when supplying lambda functions for method arguments, prefix the lambda declaration with the static keyword. This makes the lambda function just like a static method, no capturing locals and no access to this or base.

int y = 10;
someMethod(x => x + y); // captures 'y', causing unintended allocation.

with this proposal you could the static keyword to make this an error.

int y = 10;
someMethod(static x => x + y); // error!
const int y = 10;
someMethod(static x => x + y); // okay :-)

5. Top-level statements

Allow a sequence of statements to occur right before the namespace_member_declarations of a compilation_unit (i.e. source file).

Examples:

// Example #1
await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");
// would be converted tostatic class $Program
{
static async Task $Main(string[] args)
{
await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");
}
}
--------------------------------------------------------------------// Example #2
await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");
return 0;
// would be converted tostatic class $Program
{
static async Task<int> $Main(string[] args)
{
await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");
return 0;
}
}
--------------------------------------------------------------------// Example #3
System.Console.WriteLine("Hi!");
return 2;
// would be converted tostatic class $Program
{
static int $Main(string[] args)
{
System.Console.WriteLine("Hi!");
return 2;
}
}

6. Target-typed conditional

For a conditional expression c ? e1 : e2, when

  1. there is no common type for e1 and e2, or
  2. for which a common type exists but one of the expressions e1 or e2 has no implicit conversion to that type

a new implicit conditional expression conversion is defined that permits an implicit conversion from the conditional expression to any type T for which there is a conversion-from-expression from e1 to T and also from e2 to T. It is an error if a conditional expression neither has a common type between e1 and e2 nor is subject to a conditional expression conversion.

7. Extension GetEnumerator

Allow foreach loops to recognize an extension method GetEnumerator method that otherwise satisfies the foreach pattern, and loop over the expression when it would otherwise be an error.

8. Relax ordering of ref and partial modifiers

Currently, partial must appear directly before struct, class, or another type declaration keyword. If the type is a ref struct, ref must appear immediately before partial or struct. It seems likely that various other keywords could be used to disambiguate these contextual modifiers and allow us to relax the constraints on where partial and ref can appear in the modifier list.

9. Module initializers

  • Enable libraries to do eager, one-time initialization when loaded, with minimal overhead and without the user needing to explicitly call anything
  • One particular pain point of current static constructor approaches is that the runtime must do additional checks on usage of a type with a static constructor, in order to decide whether the static constructor needs to be run or not. This adds measurable overhead.
  • Enable source generators to run some global initialization logic without the user needing to explicitly call anything

P.S. Here you can read more about all new features https://github.com/dotnet/roslyn/blob/master/docs/Language%20Feature%20Status.md

--

--