Ben Bowen's Blog • Home / Blog • • About • • Subscribe •

Two Decades of C#: A Reference - C# 7 

Since C# was introduced in 2000 the language has grown immensely in size and I'm not sure it's possible for any one person to have intimate knowledge of every language feature in their head at all times. Therefore I wanted to write a series of quick reference posts summarizing all the major new language features ever since C# 2.0. I won't go in to detail with any of them, but instead I want this series to act as a reference for myself (and hopefully you too!) that I can go back to from time-to-time to remember what tools I have in the toolbox. :)

One small note before we start: I'm going to skip some of the more fundamental stuff (for example C# 2.0 introduced generics, but they're so widespread in usage that they're not really worth including); and I may also 'glue' a few features together for the sake of making this more terse. This series isn't meant to be a definitive or historical record of the language; instead it's meant as more of a "cheat-sheet" of important language features that might come in handy. You may find it useful to skim through the table-of-contents on the left to search for any features you don't recognise or need a quick reminder on.

C# 7.0 

Tuples 

Tuples are structures that contain a set of two or more related objects. They're useful for handling or returning multiple related values that would not usually be related together in to a class or struct.

static void Test() {
	// This line creates a 3-tuple (a tuple with 3 items) named 'userDetails'.
	// The tuple's three properties are an int named Age, a string named Name, and a float named TestScore
	var userDetails = (Age: 30, Name: "Ben", TestScore: 50f);
	
	// ... Later on we can use the tuple like any old struct/class:
	var age = userDetails.Age;
	var name = userDetails.Name;
	var testScore = userDetails.TestScore;
}
View Code Fullscreen • "Tuple Declaration"
What if we want to pass a tuple to another function or store it in a container? We can declare a tuple type using a similar syntax:

static readonly List<(int Age, string Name, float TestScore)> _userList;

static void AddUserToList((int Age, string Name, float TestScore) user) {
	_userList.Add(user);
}

// ...

static void Test() {
	AddUserToList((30, "Ben", 50f));
}
View Code Fullscreen • "Tuple Type Declaration"
A great use of tuples is as a more suitable replacement for 'out' parameters on methods:

static (int Min, int Max) GetLimits(IReadOnlyCollection<int> values) {
	return (values.Min(), values.Max());
}
View Code Fullscreen • "Tuple Return Type"

static void Test() {
	var values = new[] { 1, 2, 3, 4, 5, 6, 7 };
	
	// Using the GetLimits() function from previous example
	var (min, max) = GetLimits(values);
	
	Console.WriteLine($"Min value is {min}");
	Console.WriteLine($"Min value is {max}");
}

// Alternative syntax for pre-existing variables:

static int _min;
static int _max;

static void Test() {
	var values = new[] { 1, 2, 3, 4, 5, 6, 7 };
	
	(_min, _max) = GetLimits(values); // No 'var' because we're not declaring new variables. 
	
	Console.WriteLine($"Min value is {_min}");
	Console.WriteLine($"Min value is {_max}");
}
View Code Fullscreen • "Tuple Deconstruction"
Note however that tuples shouldn't be overused. It's dangerously easy to start using tuples everywhere when what you actually need is a proper class or struct. Encapsulate related data together in to a 'real' type! For example, in reality, the fields in these examples should be encapsulated as a User object. I advise against using tuples in public APIs (I only use them in implementations of methods/classes and for private helper functions).

Tuples can be declared with or without programmer-defined property names (from C# 7.1 onwards):

static void Test() {
	var age = 30;
	var name = "Ben";
	var testScore = 50f;

	var implicitlyNamedTuple = (age, name, testScore);
	var explicitlyNamedTuple = (Age: age, Name: name, TestScore: testScore);

	//var userAge = implicitlyNamedTuple.age; // Looks ugly! 'age' property is lower-case
	var userAge = explicitlyNamedTuple.Age; // Much better :)
}
View Code Fullscreen • "Tuple Property Naming"
Notice that when the names are created automatically, the compiler simply copies the name of the parameter, field, local or property passed in to create the tuple; hence the properties for our implicitlyNamedTuple are lower-case. It is for this reason that I always like to use explicit naming (because of course, camelCase naming in C# is non-conventional for public members).

The underlying type of any tuple is ValueTuple<T1, T2, ..., Tn> where the number of type parameters is equal to the number of items in the tuple. Be careful not to use Tuple<> instead of ValueTuple<>; this is an older, mostly-deprecated type that is less efficient and offers almost no advantages over ValueTuple<>.

The naming of tuple members is actually a compiler trick. The 'real' names of the properties in a ValueTuple<> are always Item1, Item2, Item3, etc. When declaring a tuple type, the compiler adds a specialized attribute behind the scenes to make it work, which intellisense is also aware of.

Although tuples are value types, interestingly they are mutable. This has some performance implications, as well as the usual 'gotchas' when working with mutable value-types.

Custom Deconstruction 

You can make any type deconstructible like a tuple by declaring a public Deconstruct() method. The method must be void-returning and all parameters must be out params- these will become the deconstructed variables that will be assigned. Here's an example:

class User {
	public string Name { get; set; }
	public int Age { get; set; }
	
	public void Deconstruct(out string name, out int age) {
		name = Name;
		age = Age;
	}
}

// ...

static void Test() {
	var user = new User { Name = "Ben", Age = 30 };

	var (name, age) = user;
}
View Code Fullscreen • "User Class Deconstructor"

Simple Pattern Matching 

This is an assortment of various new features designed to make writing certain procedures easier.

The is expression is most useful for checking whether an object is an instance of a given type and simultaneously creating a local alias of that type for said object:

	var user = GetUser();
	
	if (user is Manager manager) {
		Console.WriteLine($"User is a manager and has {manager.Reports.Count} direct reports!");
	}
View Code Fullscreen • ""Is" Expression for Type Patterns"
Null values will never match an is expression and therefore is expressions can be used to filter out null values:

	var userList = GetUserList();
	
	if (userList.FirstOrDefault() is User user) {
		Console.WriteLine($"User database contains at least one user!");
	}
View Code Fullscreen • ""Is" Expression for null-checking"
As well as type patterns, constant patterns are supported. These can both be used in switch expressions. Constant patterns are most useful when combined with a when expression, which allows filtering matches.

When more than one switch case would be matched, only the first matching case encountered is entered (you can not even deliberately jump between cases with goto case). The one exception is the default case, which is always evaluated last.

	var user = GetUser();
	
	// Type pattern matching
	switch (user) {
		case null:
			Console.WriteLine("No user found.");
			break;
		case Manager m when m.Department == Department.Engineering:
			Console.WriteLine($"User is a manager in the engineering department and has {m.Reports.Count} direct reports.");
			break;
		case Manager m when m.Department == Department.Marketing:
			Console.WriteLine($"User is a manager in the marketing department and manages {m.CustomerAccounts.Count} customer accounts.");
			break;
		case Trainee t when t.AgeInYears >= 18:
			Console.WriteLine($"User is a trainee and has completed {t.Courses.Count(c => c.IsCompleted)} of their training courses.");
			break;
		case Trainee t: // This case will only be entered if the one above was not
			Console.WriteLine($"User is a junior trainee.");
			break;
		default:
			Console.WriteLine($"User is just a user.");
			break;
	}
View Code Fullscreen • "Switch Pattern Matching with When Expressions"
Similar to is expressions, a null user can not match any case that first checks its type. Even if we declared user as a local variable of type Manager, if GetUser() returns a null value then the case case Manager m: would never be entered (even if we removed the case null:).

Local Functions 

This feature allows declaration of functions inside functions. These inner (i.e. local) functions are only accessible within the outer function's scope.

static void PrintUserReport(List<User> users) {
	string CreateBioString(User u) {
		var bioStart = $"{u.Name}: {u.AgeInYears} years old, {(DateTime.Now - u.JoinDate).TotalYears:N1} years at company";
		if (u.AgeInYears <= 18) return bioStart;
		else return $"{bioStart}; marital status: {u.MaritalStatus}";
	}
	
	foreach (var user in users) {
		Console.WriteLine(CreateBioString(user));
	}
}

// ... On User.cs ...

bool DueForPayRaise {
	get {
		bool IsEligible() {
			return AgeInYears >= 18 && (DateTime.Now - u.JoinDate).TotalYears >= 1d;
		}
		
		return IsEligible() && (DateTime.Now - u.LastPayRaise).TotalYears >= 1d;
	}
}
View Code Fullscreen • "Local Functions"

Inline "Out" Variable Declaration 

This simple feature allows for more terseness when using out variables:

// BEFORE
static void Test() {
	int parseResult;
	if (Int32.TryParse(someString, out parseResult)) {
		// ... Use parseResult here
	}
}


// AFTER
static void Test() {
	if (Int32.TryParse(someString, out var parseResult)) {
		// ... Use parseResult here
	}
}
View Code Fullscreen • "Inline Out Variables"

Throw Expressions 

This very handy feature also helps terseness. It allows you to throw an exception in a place that would normally expect a value. Examples:

User _currentUser;

public User CurrentUser {
	get {
		return _currentUser;
	}
	set {
		_currentUser = value ?? throw new ArgumentNullException(nameof(value));
	}
}
View Code Fullscreen • "Throw Expressions Example A"
public MaritalStatus MaritalStatus {
	get {
		return _currentUser.AgeInYears >= 18 
			? _currentUser.MaritalStatus 
			: throw new InvalidOperationException($"Can not disclose marital status of non-adult.");
	}
}
View Code Fullscreen • "Throw Expressions Example B"

Ref Locals and Returns 

This performance-related feature allows using, storing, and returning references to variables/data locations.

Since early C#, ref parameters have allowed us to pass a reference to a variable in to a method. Now we can also return a reference to a property, field, or other heap-allocated variable (such as an array value):

int _viewMatricesStartIndex;
int _projectionMatricesStartIndex;
int _worldMatricesStartIndex;
Matrix4x4[] _matrices;

public ref Matrix4x4 GetMatrix(MatrixType type, int offset) {
	switch (type) {
		case MatrixType.ViewMatrix:
			return ref _matrices[_viewMatricesStartIndex + offset];
		case MatrixType.ProjectionMatrix:
			return ref _matrices[_projectionMatricesStartIndex + offset];
		case MatrixType.WorldMatrix:
			return ref _matrices[_worldMatricesStartIndex + offset];
		default: 
			throw new ArgumentOutOfRangeException(nameof(type));
	}
}
View Code Fullscreen • "Ref Returns"
This method returns a reference to a Matrix4x4 in the _matrices array, rather than a copy of its value. This can convey a performance advantage for situations where copying large value-type instances around would otherwise be unavoidable.

Using the returned reference in a method requires declaring a ref local:

static void Test() {
	// Sets _matrices[_viewMatricesStartIndex + 3] to Matrix4x4.Identity
	ref var viewMatrix = ref GetMatrix(MatrixType.ViewMatrix, 3);
	viewMatrix = Matrix4x4.Identity;
	
	// We can dereference the reference and copy its value to a local here by using the standard local variable declaration syntax
	var projMatrix = GetMatrix(MatrixType.ProjectionMatrix, 2);
	projMatrix.M11 = 3f; // Changes only the local 'projMatrix', does not affect anything in _matrices
}
View Code Fullscreen • "Ref Locals"
The two separate syntaxes allow 'opting-in' to using ref locals on methods that return-by-reference; all the while ignoring it when we don't want or need it.

We can also set a value through a reference returned from a ref-returning method directly:

static void Test() {
	// Sets _matrices[_viewMatricesStartIndex + 3] to Matrix4x4.Identity
	GetMatrix(MatrixType.ViewMatrix, 3) = Matrix4x4.Identity;
}
View Code Fullscreen • "Setting Value via Returned Reference"

Discards 

This feature allows declaring your intent to ignore a required parameter. Use an underscore (_) to denote that you don't want to use an out parameter, result of an expression, or lambda parameter:

static void Test() {
	// Just want to test if this is a valid value; we don't need the parsed value
	if (!Int32.TryParse(_userInput, out _)) {
		// ..
	}
	
	// Don't want the result of this method, just need to invoke it
	_ = _someInterface.SomeMethod();
	
	// Don't want to use these parameters in a lambda (C# 9+ only)
	_someInterface.DoThing((_, _, param3, _) => param3 == "hello"); 
}
View Code Fullscreen • "Discards"

Digit Separators 

This feature lets you separate the digits of an integer literal with underscores:

const int DecimalConstant = 123_456_789;
const int HexadecimalConstant = 0xAB_CD_EF;
View Code Fullscreen • "Digit Separators"

Binary Literals 

This feature allows declaring an integer constant in binary format:

const int BinaryConstant = 0b1110_0011_1101_0001;
View Code Fullscreen • "Binary Literal"

C# 7.1 

ValueTask/ValueTask<T> and IValueTaskSource<T> 

The predominant way to encapsulate a future in C# is with the Task and Task<T> classes. In most cases this paradigm works well, but in tightly controlled performance scenarios the continuous creation of Task/Task<T> objects applies unwanted pressure on the garbage collector.

ValueTask and ValueTask<T> are two types that allow using task-like semantics (including async/await) but without always creating an instance of a reference type to track the asynchronous operation.

For async functions where you expect the function to be part of a hot-path, invoked frequently, and where the function will usually be able to complete synchronously, ValueTasks make a lot of sense:

// If we assume most users are not logged in, we can avoid allocating a Task object every time we invoke this method for most cases
// In the case where the user IS logged in, we wrap an actual Task<int> which will then be deferred to
public ValueTask<int> GetUserIDAsync(User u) {
    if (!u.IsLoggedIn) return ValueTask.FromResult(0);
    else return new ValueTask<int>(FetchUserIDFromDatabaseAsync(u)); // Assume FetchUserIDFromDatabaseAsync() returns a Task<int>
}
View Code Fullscreen • "ValueTask Example"
The returned ValueTask or ValueTask<T> object can be awaited just like a regular Task or Task<T>- but only once.

Note: C# provides await support for any type that declares a public GetAwaiter() method (or has one defined by way of extension method) that returns an object with a small set of prerequisite public members. ValueTask and ValueTask<T> implement this interface.

Note: In actuality, the framework caches some common Task<int> results.

This approach allows for eliminating unnecessary garbage when the method can complete synchronously.

Both ValueTask and ValueTask<T> have constructors that can take an object of type IValueTaskSource/IValueTaskSource<T>. These types allow you to reuse/pool objects to handle the asynchronous state machine and invocation of continuations. These types just need to implement IValueTaskSource/IValueTaskSource<T>.

There are three methods to implement:
GetStatus will be invoked by the async state machine to get the current status of the async operation.
GetResult will be invoked by the async state machine to get the result of the asynchronous operation when it is completed.
OnCompleted will be invoked by the async state machine to pass a continuation to your implementation that must be invoked when the asynchronous operation is completed; or invoked immediately if it is already completed.

As mentioned above, it is an error to await or get the result from any ValueTask more than once; this allows us to assume that GetResult will only be invoked once per operation (any more than that is an error by the user and can be considered unsupported). Similarly, it also allows us to assume that once GetResult has been invoked the IValueTaskSource instance can be re-used for the next asynchronous operation.

The short token passed to all methods can be used to ensure this condition is being honored.

Default Literal 

This small feature allows for omitting the type name when specifying the default value for a type:

// Before
const int Zero = default(int);

// After
const int Zero = default;
View Code Fullscreen • "Default Literal Constants"
public string GetUserName(User u) {
	// ...
}



// Before
GetUserName(default(User)); // Passes null

// After
GetUserName(default); // Passes null
View Code Fullscreen • "Default Literal Method Invocations"

Async Main 

This feature allows using async/await "all the way up". It allows making the Main() function (entry-point of the application) async.

public static async Task<int> Main(string[] args) {
	try {
		await Engine.Initialise();
		return 0;
	}
	catch (Exception e) {
		Log.Error(e);
		return 1;
	}
}
View Code Fullscreen • "Async Main"

C# 7.2 

In Parameters, Readonly Structs, Readonly Ref Returns 

Following on from ref locals and returns, this feature adds some more capabilities for passing around references to structs. These features are mostly provided for performance-sensitive scenarios.

A readonly struct is one whose fields can never be modified (i.e. it is immutable):

readonly struct BigStruct {
	public readonly int Alpha;
	public readonly float Bravo;
	public readonly int Charlie;
	public readonly float Delta;
}
View Code Fullscreen • "Readonly Struct"
As well as helping you maintain immutability, declaring structs as readonly helps the compiler avoid defensive copies when using in parameters. An in parameter is, like a ref parameter, a paremter that is passed by reference. Additionally however, in parameters are read-only:

void Test(in Matrix4x4 viewMatrix) {
	viewMatrix.M11 = 123f; // Won't compile even though Matrix4x4 is a mutable struct, 'in' parameters are readonly
}
View Code Fullscreen • "In Parameters"
Although the compiler makes every effort to prevent direct modification of a struct passed in via in reference; it is not always possible to guarantee no modifications whatsoever. Therefore, to ensure correctness, the compiler must make a defensive copy of the parameter anyway in certain cases unless the struct type itself is marked as readonly.

Because in parameters are a performance feature, using them with non-readonly structs is almost always a bad idea. For more information, see Avoid Mutable Structs as an In Argument on MSDN.

When invoking a method with an in parameter, the in specifier at the call-site is optional. However, specifying it has two uses:

// First case: Explicitly invoking an overloaded method that takes an [c]in[/c] parameter:

static void PrintFirstElement(Matrix4x4 m) => Console.WriteLine(m.M11);
static void PrintFirstElement(in Matrix4x4 m) => Console.WriteLine(m.M11);

static void Test() {
	var m = GetMatrix();

	PrintFirstElement(m); // Invokes first method, passes 'm' by value (i.e. copied)
    PrintFirstElement(in m); // Invokes second method, passes 'm' by readonly reference 
}
View Code Fullscreen • "In Parameter Specification at Call Site for Method Overload"
// Second case: Forcing the passing of an 'in' parameter to be a reference to live variable 

static void PrintFirstElement(in Matrix4x4 m) => Console.WriteLine(m.M11);

static void Test() {
	// Matrix4x4.Identity is a static property that returns a new Matrix4x4

	PrintFirstElement(Matrix4x4.Identity); // Compiles, because the compiler creates a temporary variable on the stack that is what is referred to
    PrintFirstElement(in Matrix4x4.Identity); // Fails, because we're creating a reference to something that only exists as a temporary variable
}
View Code Fullscreen • "In Parameter Specification at Call Site for Explicit Pass by Reference"
Finally, readonly ref returns allow returning a reference to a variable that does not permit modification of the variable it refers to. To use such a reference (rather than taking a copy of the returned reference), the local variable must be declared as a ref readonly as well:

static Matrix4x4 _viewMat;

static ref readonly Matrix4x4 GetMatrix() => ref _viewMat;

static void Test() {
    ref readonly var mat = ref GetMatrix();
    var matCopy = mat;

    mat.M11 = 3f; // This line won't compile, we can not modify a readonly ref
    matCopy.M11 = 3f; // This line is fine, 'matCopy' is a local stack copy of the variable pointed to by 'mat'
}
View Code Fullscreen • "Readonly Ref Returns and Locals"

Ref Structs, Span<T>, Memory<T> 

Ref structs are a new type of struct (i.e. value type) that contain "interior pointers"; i.e. references to data or offsets in to objects (as opposed to references to objects themselves). Instances of ref structs can only live on the stack; and therefore there are some restrictions on how they can be used (see second example below).

The most prominent usage of ref structs is the Span<T> type. Spans are references to contiguous memory chunks containing 0 or more elements of the same type. The way this memory is declared and stored is irrelevant- a Span<T> can always point to the data regardless.

static char[] _charArray = { 'A', 'l', 'p', 'h', 'a' };
static List<char> _charList = new List<char> { 'T', 'a', 'u' };

static void PrintCharSpanData(ReadOnlySpan<char> charSpan) {
    Console.Write($"Given span is {charSpan.Length} characters long: ");
    Console.WriteLine($"\"{new String(charSpan)}\"");
}

unsafe static void Test() {
    var heapArraySpan = _charArray.AsSpan();

    var listSpan = CollectionsMarshal.AsSpan(_charList);

    Span<char> stackArraySpan = stackalloc char[] { 'O', 'm', 'e', 'g', 'a' };

    const string UnmanagedDataString = "Epsilon";
    var numBytesToAlloc = sizeof(char) * UnmanagedDataString.Length;
    var pointerSpan = new Span<char>((void*) Marshal.AllocHGlobal(numBytesToAlloc), UnmanagedDataString.Length);
    UnmanagedDataString.AsSpan().CopyTo(pointerSpan);

    var singleCharOnStack = 'O';
    var stackSpan = new Span<char>(&singleCharOnStack, 1);

    var stringSpan = "Delta".AsSpan();

    // =======

    PrintCharSpanData(heapArraySpan); // Given span is 5 characters long: "Alpha"
    PrintCharSpanData(listSpan); // Given span is 3 characters long: "Tau"
    PrintCharSpanData(stackArraySpan); // Given span is 5 characters long: "Omega"
    PrintCharSpanData(pointerSpan); // Given span is 7 characters long: "Epsilon"
    PrintCharSpanData(stackSpan); // Given span is 1 characters long: "O"
    PrintCharSpanData(stringSpan); // Given span is 5 characters long: "Delta"
}
View Code Fullscreen • "Span<T> Declaration and Usage"
The example above demonstrates six different ways to create a span of chars. But regardless of how a Span<char> is created, it can then be used the same way: As a contiguous span of characters.

ReadOnlySpan<T> is an alternative type that, as the name implies, is a Span<T> but doesn't allow modification of the data it points to. Span<T> is implicitly castable to ReadOnlySpan<T> (assuming the type parameter T is the same); this allows us to pass a Span<char> to PrintCharSpanData(), even though the method takes a ReadOnlySpan<char>.

The code above is only meant as an example of creation and usage of Span<T>/ReadOnlySpan<T>. Some of the operations are 'unsafe' or require care when using. Of particular note, the memory allocated manually (with AllocHGlobal) should be freed again, and when accessing the array backing a list (via CollectionsMarshal) it is important that the list is not modified before the usage of the related Span<T> is finished.

Because Span<T>, ReadOnlySpan<T>, and any other ref struct must not escape the stack (or the interior reference may be invalidated), there are usage restrictions on variables of their type:

// Invalid: Ref struct types can not be the element type of an array 
// 	Because arrays are stored on the heap
static readonly Span<int>[] _intSpanArray;

// Invalid: Ref struct types can not be fields or properties of any class or struct except ref structs
// 	Because class instances are stored on the heap, and struct instances may be boxed (i.e. a copy stored on the heap)
public Span<int> SomeSpan { get; set; }

// Invalid: Ref struct types can not implement interfaces
// 	Because using them as their interface type would always require boxing
readonly ref struct MyRefStruct : IEquatable<MyRefStruct> { }

// Invalid: Ref struct types can not be cast to object (or boxed in any way)
//  Because boxed copies of structs are stored on the heap
var boxedSpan = (object) mySpan;

// Invalid: Ref struct types can not be type arguments
//	Because usage of elements can not currently be verified as valid (and some usages will never be valid, i.e. List<T>)
var list = new List<Span<int>>();

// Invalid: Ref struct types can not be closed-over (captured) by a lambda/anonymous function
//	Because captured variables must be stored in a heap object so that they're still available when the lambda is executed
var filtered = someEnumerable.Where(x => x[0] == mySpan[0]);

// Invalid: Ref struct types can not be used in an async method (locals or parameters)
//	Because locals in async methods may be stored in heap objects to become part of the internal state machine built by the compiler
async Task SomeMethodAsync(Span<int> mySpan) { /* ... */ }
View Code Fullscreen • "Ref Struct Usage Restrictions"
Because of these restrictions, another non-ref struct type called Memory<T> is provided. A Memory<T> must encapsulate managed-heap-allocated, GC-tracked memory only.

static char[] _charArray = { 'A', 'l', 'p', 'h', 'a' };
static List<char> _charList = new List<char> { 'T', 'a', 'u' };

unsafe static void Test() {
	// Create a Memory<T> that wraps a new array copy of the data, 
	// rather than pointing to the actual list data directly like we did with the Span<T> example:
    var charListAsMemory = _charList.ToArray().AsMemory();
	
	// Alternatively, create a Memory<T> that encapsulates just part of an existing array
	// (this can also be done with Span<T>)
    var charArraySubstringAsMemory = new Memory<char>(_charArray, 1, 3);
	
	PrintCharMemoryData(charListAsMemory); // Given memory is 3 characters long: "Tau"
	PrintCharMemoryData(charArraySubstringAsMemory); // Given memory is 3 characters long: "lph"
}

static void PrintCharMemoryData(ReadOnlyMemory<char> charMemory) {
    Console.Write($"Given memory is {charMemory.Length} characters long: ");
    Console.WriteLine($"\"{new String(charMemory.Span)}\""); // Can use the .Span property to create a Span<T> when required
}
View Code Fullscreen • "Memory<T> Instantiation and Usage Example"
Usage guidelines on Span<T> and Memory<T> are extensive but should be read if writing APIs that use them. Microsoft has a page dedicated to this subject here: Memory<T> and Span<T> Usage Guidelines.

Private-Protected Access Modifier 

The private protected access modifier restricts visibility of a member to only derived classes in the same assembly, as opposed to the pre-existing protected internal access modifier that restricts visibility to only derived classes OR classes in the same assembly.

C# 7.3 

Enum, Delegate, and Unmanaged Generic Constraints 

Enum constraints allow specifying a type parameter type must be an enum:

// 'Enum' constraint lets us ensure that T is an enum type
// 'struct' constraint is optional but lets us make 'valueToHighlight' nullable
static void PrintAllEnumNames<T>(T? valueToHighlight) where T : struct, Enum {
    foreach (var value in (T[]) Enum.GetValues(typeof(T))) {
        if (value.Equals(valueToHighlight)) Console.WriteLine($"{value} <----");
        else Console.WriteLine(value.ToString());
    }
}
View Code Fullscreen • "Enum Constraint"
Similarly, Delegate constraints allow specifying a type parameter type must be a delegate:

// Example from MSDN: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters#delegate-constraints

public static TDelegate TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
    where TDelegate : System.Delegate
    => Delegate.Combine(source, target) as TDelegate;
View Code Fullscreen • "Delegate Constraint"
The unmanaged generic constraint allows specifying that the type parameter type must be amenable to direct/pointer-based manipulation and/or 'blittable'. Using this constraint allows you to use pointers and other 'unsafe' constructs with your generically-typed variables:

// This method copies a T reference to a T value via pointer
static unsafe void Copy<T>(ref T src, T* dest) where T : unmanaged => *dest = src;

static unsafe void Test() {
    int dest = 0;
    int src = 3;
    Copy(ref src, &dest);

    Console.WriteLine(dest); // Prints '3'
}
View Code Fullscreen • "Unmanaged Constraint"

Stackalloc Initializers 

These allow initializing stack-allocated memory via inline initializers:

var intArray = stackalloc[] { 1, 2, 3 };
View Code Fullscreen • "Initialization of stack-allocated int array"