NDepend Blog

Improve your .NET code quality with NDepend

Top 10 C# Recent Improvements

June 3, 2024 7 minutes read

Top 10 C# Recent Improvements

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:

Makes single-line Hello World program possible

Nowadays, single-line C# programs are valid:

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.

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:

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:

Reference: Deconstruction in C#

Pattern matching to simplify complex expressions

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

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.

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:

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.

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.

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

References:

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.

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:

References:

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.

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.

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 """.

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

Console-C#11-Raw-Literal-Strings

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:

can be simplified to:

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.

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.

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.

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.

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.

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:

C# 11 File Scope Type

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.