Top 10 C# Recent Improvements - NDepend Blog (2024)

June 3, 2024 7 minutes read

Over the years, in collaboration with the community, the C# team has introduced numerous impressive new syntax features. Some of these simplify coding and significantly reduce keystrokes. Some others pave the way for unique performance enhancements. Let’s explore these changes:

Index

Makes single-line Hello World program possible

Nowadays, single-line C# programs are valid:

C

1

Console.WriteLine("Hello, World!");

This is possible thanks to

  • C# 9 Top-level statements: This allows the main program to be written without a wrapping Main() method or class definition, streamlining simple applications and scripts.
  • C# 10 Global Using Directives: This feature enables the specification of a set of using directives that are applied globally to all files in a project, eliminating the need to explicitly include them in each file. global using System;
  • C# 10 Implicit using directives: This relieves the omission of repetitive using statements for namespaces in every file, instead automatically including them based on project configurations. A file named YourProjectName.GlobalUsings.g.cs is generated by the compiler to contains global using directives.

Reference: Modern C# Hello World

Value tuple to handle multiple values in a single variable

C# 7.0 introduced value tuples. Tuples provide a simple and efficient means to return multiple values from a method without the need to define a custom class.

C

1

2

3

4

5

6

(string firstName, string lastName, int birthYear) person = GetPerson(Guid.Empty);

if(person.birthYear > 1970) { ... }

static (string firstName, string lastName, int birthYear) GetPerson(Guid id) {

return new("Bill", "Gates", 1955);

}

The C# compiler compiles tuples to generic structures System.ValueTuple<T1, ..., TN>.

Reference: C# Value Tuples

Deconstruction to assign multiple variables at once from an object

C# 7.0 also proposed deconstructors to assign multiple variables at once from an object:

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

var person = new Person() { FirstName = "Bill", LastName = "Gates", BirthYear = 1975 };

// Call the deconstructor to create and initialize 3 variables

// in a single statement thanks to deconstruction

var (firstName, lastName, birthYear) = person;

Console.WriteLine($"FirstName:{firstName} LastName:{lastName} BirthYear:{birthYear}");

class Person {

internal string FirstName { get; init; }

internal string LastName { get; init; }

internal int BirthYear { get; init; }

// Here is the deconstructor

internal void Deconstruct(out string firstName, out string lastName, out int birthYear) {

firstName = FirstName;

lastName = LastName;

birthYear = BirthYear;

}

}

Notice how a tuple can be deconstructed into several variables. In the program below we use the discard character _ to avoid defining a variable for tuple’s lastName:

C

1

2

3

(string firstName, string lastName, int birthYear) person = GetPerson(Guid.Empty);

var (firtName, _, birthYear) = person; // Deconstruct the tuple into 2 variables

if(birthYear > 1970) { ... }

Reference: Deconstruction in C#

Pattern matching to simplify complex expressions

Pattern matching can help simplify complex if-else statements like in this code:

C

1

2

3

4

5

6

7

8

9

public static string WhichShapeWithRelationalPattern(this Shape shape)

=> shape switch {

Circle { Radius: > 1 and <= 10 } => "shape is a well sized circle",

Circle { Radius: > 10 } => "shape is a too large circle",

Rectangle => "shape is a rectangle",

_ => "shape doesn't match ay pattern"

};

The pattern matching various syntaxes are applicable in numerous situations with:

  • type pattern: if(shape is Circle circle) { ... }
  • negated pattern: if(shape is not null) { ... }
  • combinator, parenthesized and relational patterns

First pattern-matching syntaxes were initially introduced in C# 7.0. Since then, nearly every subsequent language version has introduced new pattern-matching expressions.

Reference: C# pattern matching

Index and range to simplify access to elements of a collection

Index ^and range.. operators were introduced in C# 8.0 to support more sophisticated and efficient data manipulation, particularly with arrays and other collections. The index operator allows for indexing from the end of a sequence, while the Range operator facilitates slicing operations.

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

var arr = new[] { 0, 1, 2, 3, 4, 5 };

Assert.IsTrue(arr.Length == 6);

// [^1] means last element

// equivalent to [arr.Length - 1]

Assert.IsTrue(arr[^1] == 5);

Assert.IsTrue(arr[arr.Length - 1] == 5);

// [^2] means second last element

// equivalent to [arr.Length - 2]and so on

Assert.IsTrue(arr[^2] == 4);

// [..] means range of all elements

Assert.IsTrue(arr[..].SequenceEqual(arr));

// range [1..4] returns {1, 2, 3 }

// start of the range (1) is inclusive

// end of the range (4) is exclusive

Assert.IsTrue(arr[1..4].SequenceEqual(new[] { 1, 2, 3 }));

// [..3] returns { 0, 1, 2 }from the beginning till 3 exclusive

Assert.IsTrue(arr[..3].SequenceEqual(new[] { 0, 1, 2 }));

// [3..] returns { 3, 4, 5 }from 3 inclusive till the end

Assert.IsTrue(arr[3..].SequenceEqual(new[] { 3, 4, 5 }));

// [0..^0] means from the beginning till the end

// It is equivalent to [..]

// Remember that the upper bound ^0 is exclusive

// so there is no risk of IndexOutOfRangeException here

Assert.IsTrue(arr[0..^0].SequenceEqual(arr));

// [2..^2] means[2..(6-2)]means[2..4]

Assert.IsTrue(arr[2..^2].SequenceEqual(new[] { 2, 3 }));

// [^4..^1] means[(6-4)..(6-1)]means[2..5]

Assert.IsTrue(arr[^4..^1].SequenceEqual(new[] { 2, 3, 4 }));

Reference: C# Index and Range Operators Explained

Use managed pointers ‘ref’ keyword everywhere

Since C# 1.0 there was the ref keyword that allows to passing of managed pointers to a function:

C

1

2

3

4

5

int i = 0;

Fct(ref i);

Assert.IsTrue(i == 1);

static void Fct(ref int i) { i++; }

Managed pointers are a unique feature of the .NET runtime, offering substantial advantages:

  • It can point toward any kind of memory: a method local variable, a method parameter in or out, a location on the stack, an object on the heap, a field of an object, an element of an array, a string, or a location within a string, unmanaged memory buffer…
  • The Garbage Collector is aware of managed pointers and updates them accordingly when the objects they point to are moved.
  • They operate quickly.

The primary limitation is that a managed pointer must reside on the thread stack.

C# 7.0 introduced ref local and ref return to use managed pointers as local variables and return values.

C

1

2

3

4

5

6

7

8

9

10

11

12

// ref local

int i = 6;

ref int j = ref i;

j = 7;

Assert.IsTrue(i == 7);

// ref return

ref int k = ref GetRef();

static ref int GetRef() {

int[] arr = new int[6];

return ref arr[2];

}

Then C# 7.2 introduced the concept of ref struct, a structure type that must reside solely on the stack. The principal use of ref struct is exemplified by Span<T> introduced in the same release, which essentially acts as a managed pointer paired with a length, representing a segment of memory. At this point the managed pointer of Span<T> is a field, but it was a private runtime trick.

Finally, C# 11 allowed for ref fields. Since then the implementation of Span<T>can use a ref field instead of relying on a private runtime trick. More importantly, you can use ref field within your own ref struct.

C

1

2

3

4

5

6

7

public readonly ref struct Span<T>{

/// <summary>A byref or a native ptr.</summary>

internal readonly ref T _reference;

/// <summary>The number of elements this Span contains.</summary>

private readonly int _length;

...

}

In modern C#, you can utilize managed pointers across all safe code areas to enhance performance in critical sections.

References:

  • Managed pointers, Span, ref struct, C#11 ref fields and the scoped keyword
  • Improve C# code performance with Span<T>

Generic Math and static abstract members

C# 11 introduced static abstract members, enabling the implementation of the .NET Generic Math library in .NET 7.0. This library allows to perform math operations without specifying the exact type involved. For example, the method Middle<T>() generalizes the process of calculating the midpoint between two numbers. The sample program below uses it to compute midpoint of both int and float.

1

2

3

4

5

6

7

8

using System.Numerics;

Assert.IsTrue(Middle(1, 3) == 2);

Assert.IsTrue(Middle(1, 1.5) == 1.25);

static T Middle<T>(T a, T b) where T: INumber<T> {

return (a + b) / (T.One + T.One);

}

The method Middle<T>() constraints the generic type parameter T to be a INumber<T>. The expression T.One uses a static property of the interface INumber<T>. Several other interfaces that rely on C# static abstract members were introduced in the .NET Base Class Library to generalize fundamental mathematical concepts.

Static abstract members can be used in many other situations like for generic parsing with the IParsable<TSelf> interface:

C

1

2

3

4

5

6

7

8

namespace System {

// Summary: Defines a mechanism for parsing a string to a value.

// TSelf: The type that implements this interface.

public interface IParsable<TSelf> where TSelf : IParsable<TSelf>? {

static abstract TSelf Parse(string s, IFormatProvider? provider);

static abstract bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result);

}

}

References:

  • The .NET Generic Math Library
  • C# static abstract members
  • The .NET 7.0 IParsable<TSelf> interface

C# record to avoid boilerplate code

C# records, introduced in C# 9.0, are a type declaration that simplifies the creation of immutable objects with value-based identity. Therefore, a record is well suited for data-carrying objects designed to store data without altering it after creation and without any associated behavior.

C

1

2

3

4

5

6

7

8

9

10

11

12

var personA = new Person("Bill", "Gates");

Assert.IsTrue(personA.FirstName == "Bill");

Assert.IsTrue(personA.LastName == "Gates");

var personB = new Person("Bill", "Gates");

// Demo of value-based equality

Assert.IsTrue(personA == personB);

Assert.IsFalse(object.ReferenceEquals(personA, personB));

// Primary constructor syntax on record

record Person(string FirstName, string LastName);

Moreover, records support with-expressions, allowing you to create a new record instance by copying existing records while modifying some of the properties in a non-destructive manner. This makes records a powerful tool for functional programming patterns where immutability is a key concern.

C

1

2

3

4

5

var personA = new Person("Bill", "Gates");

var personB = personA with { FirstName = "Melinda" };

Assert.IsTrue(personB.FirstName == "Melinda");

record Person(string FirstName, string LastName);

Both record class and record struct are available. Using the keyword record by itself refers to a record class. The C# compiler automatically generates boilerplate code to support value-based equality along with other features such as read-only properties and a deconstruction method.

Record classes support inheritance. Generic records are also possible. Overall, records in C# enhance the readability and maintainability of code, making them particularly effective for data modeling, especially in applications like data transport across service boundaries, where data integrity and consistency are critical.

Reference: C# Record Explained

Raw literal strings to improve string declaration in certain situations

C# 11 introduced raw literal strings to address challenges faced with traditional string literals. Let’s explore an example of a raw string literal with interpolation. Notably, a raw string literal must start and end with at least three double-quote characters """.

C

1

2

3

4

5

6

7

8

9

10

11

12

13

string sister = "Léna";

string brother = "Paul";

Console.ForegroundColor = ConsoleColor.White;

Console.BackgroundColor = ConsoleColor.DarkBlue;

Console.WriteLine(

$$"""

{

"sister": "{{sister}}",

"brother": "{{brother}}",

}

""");

Console.BackgroundColor = ConsoleColor.Black;

Console.ReadKey();/pre>

We use console coloring to highlight the string obtained at runtime:

The introduction of raw string literals in C# 11 brings several benefits:

  • Formatting and Alignment: For instance, the compiler ignores six leading spaces before the { character in the literal.
  • Double Quote Handling: Previously, to include a double quote in a string, you would need to escape it or use verbatim strings. Now, it is seamlessly included.
  • Interpolation: This raw string literal uses string interpolation, where the delimiters are doubled ({{ and }}) to distinguish from the literal braces, streamlining the process significantly and allowing single braces to be included directly in the string.

Reference: C# 11 Raw String Literals Explained

Plenty of convenient syntaxes

Over the years, the C# team has introduced numerous convenient syntax features in addition to the major improvements we’ve discussed.

null coalescing operator

To quickly test for nullity. For example this code:

C

1

2

if(app != null && app.Context != null && app.Context.Name != null) { return app.Context.Name; }

return "";

can be simplified to:

C

1

return app?.Context?.Name ?? "";

Expression-bodied members

Introduced in C# 6.0, expression-bodied members enable concise one-liner definitions for methods, properties, indexers, and event accessors using a lambda-like syntax, streamlining the code significantly.

C

1

public int Add(int x, int y) => x + y;

Auto-property initializers

Introduced in C# 6.0, this feature allows the initialization of auto-implemented properties directly within their declarations, simplifying the syntax for assigning default values to properties.

C

1

public int Url { get; set; } = "www.ndepend.com";

readonly struct

The readonly modifier in C# applied to a struct ensures that the structure is immutable. It means that its members cannot be modified after the instance is created. This can enhance performance and maintainability by preventing unintentional state changes.

C

1

2

3

4

5

public readonly struct Point {

public Point(double x, double y) { X = x; Y = y; }

public double X { get; }

public double Y { get; }

}

Local function

Local functions in C# are methods defined within the scope of another method. It allows for organized and encapsulated code that is easier to read and maintain.

C

1

2

3

4

5

6

7

8

static int Fibonacci(int n) {

return Fib(n); // Call the local function

int Fib(int num) { // Local function

if (num <= 1) return num;

return Fib(num - 1) + Fib(num - 2);

}

}

Default interface methods

Introduced in C# 8.0, this feature allows you to provide default implementations for methods within an interface. This eases backward compatibility by enabling the addition of new methods without requiring all implementing classes to define an implementation.

C

1

2

3

4

5

6

7

public interface IGreeting {

void Greet() => Console.WriteLine("Hello, World!"); // Default method implementation

}

public class Friendly : IGreeting {

// No need to implement Greet unless custom behavior is needed

}

File Scoped Types

C# 11 introduced the file-scoped types feature, which allows a type definition to be restricted to the current file using a new file modifier. This enables multiple classes with the same name (namespace.name) to coexist within a single project. The following project demonstrates this with two classes both named ConsoleApp1.Answer:

Conclusion

Over two decades since C#’s initial release, Microsoft is more committed than ever to enhancing the language, runtime, and platform. This dedication makes .NET a favored choice among developers for programming. The community can take comfort in knowing that their current expertise will remain relevant for many years to come.

Top 10 C# Recent Improvements - NDepend Blog (2024)
Top Articles
Latest Posts
Article information

Author: Greg O'Connell

Last Updated:

Views: 6680

Rating: 4.1 / 5 (62 voted)

Reviews: 93% of readers found this page helpful

Author information

Name: Greg O'Connell

Birthday: 1992-01-10

Address: Suite 517 2436 Jefferey Pass, Shanitaside, UT 27519

Phone: +2614651609714

Job: Education Developer

Hobby: Cooking, Gambling, Pottery, Shooting, Baseball, Singing, Snowboarding

Introduction: My name is Greg O'Connell, I am a delightful, colorful, talented, kind, lively, modern, tender person who loves writing and wants to share my knowledge and understanding with you.