Write cleaner code in C# with changes you may not know about
Published: Monday 10 June 2024
Every year, C# adds new features to its programming language.
With so many new changes being added, it's easy to miss them or forget what is different from the previous version.
C# coding challenges
We'll have a look at some of the new features that have been added in previous versions to help you write cleaner code.
Small changes to help write cleaner code
We'll start off by looking at the smaller additions that have been added:
File-scoped namespace (C# 10, 2021)
Before this change, a namespace would have to include curly braces around it meaning the code inside the namespace would be indented.
namespace RoundTheCode.CSharpChanges
{
public class MyClass
{
}
}
But with file-scoped namespaces, we can add a semi colon to the namespace and the class declaration wouldn't need to be indented.
namespace RoundTheCode.CSharpChanges;
public class MyClass
{
}
The benefit of this is that you have more horizontal space on the screen meaning that you can write more code on one line.
Global using directive (C# 10, 2021)
Find yourself using the same namespace over and over again in different classes?
You can add the global
keyword when importing a namespace and it will be available for all classes, records, structs, interfaces and enums in a project.
We recommend creating a separate file for storing these. We normally add them to a GlobalUsing.cs
file.
// GlobalUsing.cs
global using RoundTheCode.CSharpChanges.Models;
global using RoundTheCode.CSharpChanges.Exceptions;
Target-typed new expressions (C# 9, 2020)
When creating a new instance of a class, you used to specify the type that is being initialised, even if the variable has already been declared with a type.
MyClass myClass = new MyClass();
By using a target-typed new expression, you can just add the new
keyword and it will create a new instance.
MyClass myClass = new();
And it also works with constructors that have parameters.
// MyClass.cs
public class MyClass
{
public MyClass(int a)
{
}
}
MyClass myClass = new(122);
The benefit of this is that if you know the type of the variable, you don't need to declare it again when initialising it.
Note that this doesn't work with the var
keyword, because we don't know what type we are creating an instance of.
Using declarations (C# 8, 2019)
Did you know that using declarations don't need to be wrapped with curly braces?
Previously, the syntax would look something like this:
using (var stream = new MemoryStream())
{
// Your code here
}
The instance in the using
statement would be disposed with the closing curly brace.
But you can add a semi colon to the using
statement and it will be disposed at the end of the method.
public class MyClass
{
public void DoSomethingWithMemoryStream()
{
using var stream = new MemoryStream();
// Do some code
// Stream is disposed
}
}
It's another benefit of having one less indent and having more horizontal space.
Init-only setters (C# 9, 2020)
Previously if you want to set read-only properties when creating an instance, you would have to include them all as parameters in a constructor and set each one to it's corresponding property.
public class MyClass
{
public int A { get; }
public string B { get; }
public bool C { get; }
public int D { get; }
public MyClass(int a, string b, bool c, int d)
{
A = a;
B = b;
C = c;
D = d;
}
}
With the addition of the init
keyword for the property set accessor, it means that these properties can be set when a class is initialised.
public class MyClass
{
public int A { get; init; }
public string B { get; init; }
public bool C { get; init; }
public int D { get; init; }
}
MyClass myClass = new()
{
A = 1,
B = "MyClass",
C = true,
D = 9
};
This avoids unnecessary boiler plate code. If you try to change any of these properties after the class has been initialised, a compile exception is thrown.
MyClass myClass = new()
{
A = 1,
B = "MyClass",
C = true,
D = 9
};
myClass.D = 10; // Throws compile exception
Constant interpolated strings (C# 10, 2021)
Interpolated strings were introduced back in C# 6 which allow you to add an object to a string without coming out of the string.
var language = "C#";
var languageText = $"{language} is the best language ever.";
Prior to C# 10, this only worked with variables before it was added to constants.
const string language = "C#";
const string languageText = $"{language} is the best language ever.";
This makes constants more readable and avoids common errors such as forgetting to add spaces, or putting variables in the wrong order.
Using C# in a older version of .NET
When a new major version of .NET is released, it also ships a new version of C#. This has happened every November since 2020.
But did you know that you can use the newest version of C# on older versions of .NET?
C# 12 was released as part of .NET 8. However, by specifying the <LangVersion>
in your .csproj
file, you can use it in your project, whilst still using the older .NET version.
Here is an example of how to use C# 12 in a .NET 6 project:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <!-- Using .NET 6 -->
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion> <!-- Uses C# 12 -->
</PropertyGroup>
</Project>
However, you'll need the .NET version installed on your machine that corresponds to the C# version that you'll using.
Substantial changes that improves code readability
Let's have a look at some of the other changes that made their way into C# that have made substantial changes in writing cleaner code.
Recursive patterns (C# 8, 2019)
Before switch expressions came along, you would have to write a lengthy switch statement like this:
int rating = 3;
string ratingDescription;
switch (rating)
{
case 1:
case 2:
ratingDescription = "Bad";
break;
case 3:
case 4:
ratingDescription = "OK";
break;
case 5:
ratingDescription = "Great";
break;
default:
ratingDescription = "Unknown";
break;
}
But with the introduction of recursive patterns, this is simplified.
int rating = 3;
string ratingDescription = rating switch
{
1 => "Bad",
2 => "Bad",
3 => "OK",
4 => "OK",
5 => "Great",
_ => "Unknown"
};
Depending on how complex your switch statement determines how many lines of code you reduce.
Logical and relational patterns (C# 9, 2020)
Staying on the theme of switch statements, C# 9 introduced relational patterns into switch statements so conditions like "greater than", or "less than" can be used.
It also introduced logical patterns, meaning we can use keywords like and
or or
.
This meant support for multiple cases in switch expressions that weren't in C# 8.
int rating = 3;
string ratingDescription = rating switch
{
1 or 2 => "Bad",
3 or 4 => "OK",
5 => "Great",
_ => "Unknown"
};
int rating = 3;
string ratingDescription = rating switch
{
1 or 2 => "Bad",
>= 3 and <= 4 => "OK",
5 => "Great",
_ => "Unknown"
};
This further reduces the number of lines of code you need to write.
List patterns (C# 11, 2022)
List patterns allows for pattern matching for elements in an array for the list.
var myNumbers = new int[] { 1, 3, 5 };
var isPattern = myNumbers is [1, 3, 5]; // returns true
As well as specifying values in the order they appear, you can also specify:
_
The array/list value can be any value..
The array/list value can be any values with any length>=
The array/list value must be greater or equal to this value>
The array/list value must be greater to this value<=
The array/list value must be less than or equal to this value<
The array/list value must be less than this value
In this example, a list pattern will return true if the array has a length of 3, starts with 1 and ends with 5.
var myNumbers = new int[] { 1, 98, 5 };
var isPattern = myNumbers is [1, _, 5]; // returns true
With this pattern, it will return true if the array is any length and starts with 1 and ends with 5.
var myNumbers = new int[] { 1, 98, 22, 54, 43, 5 };
var isPattern = myNumbers is [1, .., 5]; // returns true
This means we don't have to write complex LINQ queries to see the pattern order of a collection type.
Collection expressions (C# 12, 2023)
Collection expressions brings the spread operator to C#.
Popular in JavaScript, you can use ..
to join an int
, Span
, or List
together into one collection type.
In this example, the join
list joins all the collection types from a
, b
and c
, as well as 6
and 5
.
So it would be [1, 2, 3, 2, 4, 5, 4, 4, 4, 6, 6, 5, 6, 5]
.
int[] a =[1, 2, 3];
Span<int> b = [2, 4, 5, 4, 4];
List<int> c = [4, 6, 6, 5];
List<int> join = [..a, ..b, ..c, 6, 5];
This means we don't have to use nested concat methods to join collection types together.
Primary constructors (C# 12, 2023)
Primary constructors allows you to override the default behaviour of initalising a class by adding parameters.
Traditionally, a parameterless constructor would be the default behaviour.
public class Employee {
}
var employee = new Employee();
But with primary constructors, parameters can be added to the class. This means the default way to initialise a class is to add those parameters.
public class Employee(TimeSpan startTime, TimeSpan finishTime)
{
public double GetStartTimeInHours()
{
return startTime.TotalHours;
}
}
Employee employee = new(new TimeSpan(9, 0, 0), new TimeSpan(17, 0, 0));
Console.WriteLine(employee.GetStartTimeInHours()); // Returns 9
This avoids the need for an additional constructor in your class.
It also has support for dependency injection, although developers have some concerns about using primary constructors with dependency injection.
Watch the video
Watch the video where we take you through some of the C# changes over the years that help you write cleaner code and make it more readable.