SoFunction
Updated on 2025-03-06

How to simplify code using modern C# syntax

Intro

There have been many changes in the syntax of recent versions of C#. There are many syntaxes that can help us greatly simplify the complexity of the code and make the code more concise. I share a few syntaxes that I think are more practical to make the code more concise.

Default literal expressions

After C# 7.1, we can use default instead of a type's default value, for example:

public void Test(string str = deault){}

string str = default;

In previous versions, we needed to explicitly specify the type, such as default(string), so we did not need to write the type, and the compiler will infer the type

Target-Typed New Expression

In C# 9, the Target-Typed New Expression syntax was introduced. Similar to the default above, when creating an object, we no longer need to write the type where the compiler can infer the type. This is sometimes very useful, especially when writing a field with very complex types, we only need to declare it once. You can refer to the following example:

// target-typed new expression
//private static readonly Dictionary<string, Dictionary<string, string>>
//    Dictionary = new Dictionary<string, Dictionary<string, string>>();
private static readonly Dictionary<string, Dictionary<string, string>>
    Dictionary = new();

// array
ReviewRequest[] requests =
{
    new()
    {
        State = 
    },
    new(),
    new(),
};

Named Tuple

Starting from C# 7, we can use Named Tuple to optimize the use of Tuple. In the previous version, we could only use Item1 and Item2 to use Tuple's Value, but this is difficult to understand, especially without documentation. You may have to go to the return value to see what each element represents. After Named Tuple appears, it is equivalent to a strong type of Tuple, which can make the code better understand. The meaning of the tuple element is clear at a glance. For example, you can give me a chestnut:

(string Alpha, string Beta) namedLetters = ("a", "b");
($"{}, {}");

(int code, string msg) result = (1, "");

private static (int code, string msg) NamedTuple()
{
    return (0, );
}

var result = NamedTuple();
();

Deconstruct

As we appear at the same time as Named Tuple, we can declare a Deconstruct corresponding to the Constructor in the class, but the Construct is the input parameter and the Deconstruct is the output parameter. Let's take a look at an example:

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

    public double X { get; }
    public double Y { get; }

    public void Deconstruct(out double x, out double y) =>
        (x, y) = (X, Y);
}

var p = new Point(3.14, 2.71);
(double X, double Y) = p;

The above example is an example of the official document. Let’s take a look at an example we are actually using:

public class IdNameModel
{
    public int Id { get; set; }
    public string Name { get; set; }

    public void Deconstruct(out int id, out string name)
    {
        id = Id;
        name = Name;
    }
}

When multiple return values ​​are not concerned, some data can be used to indicate the discarded return value. The example is as follows:

using System;
using ;

public class Example
{
    public static void Main()
    {
        var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

        ($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
    }

    private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
    {
        int population1 = 0, population2 = 0;
        double area = 0;

        if (name == "New York City")
        {
            area = 468.48;
            if (year1 == 1960)
            {
                population1 = 7781984;
            }
            if (year2 == 2010)
            {
                population2 = 8175133;
            }
            return (name, area, year1, population1, year2, population2);
        }

        return ("", 0, 0, 0, 0, 0);
    }
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149

Pattern-Matching

Pattern matching first started in C# 7.1, with the earliest form such as: if(a is string str), which is the simplest and most classic pattern matching. It combines the functions that require two sentences before and can be translated into:

var str = a as string;
if(str != null) //...

In addition to if, we can also use pattern matching in switch

void SwitchPattern(object obj0)
{
    switch (obj0)
    {
        case string str1:
            (str1);
            break;

        case int num1:
            (num1);
            break;
    }
}

In C# 9, the logical operator and/or/not is introduced to make pattern matching more powerful. Let’s see a change in a method to determine whether it is a legal Base64 character:

Code before C# 9:

private static bool IsInvalid(char value)
{
    var intValue = (int)value;
    if (intValue >= 48 && intValue <= 57)
        return false;
    if (intValue >= 65 && intValue <= 90)
        return false;
    if (intValue >= 97 && intValue <= 122)
        return false;
    return intValue != 43 && intValue != 47;
}

Code after enhanced pattern matching using C# 9:

private static bool IsInvalid(char value)
{
    var intValue = (int)value;
    return intValue switch
    {
            >= 48 and <= 57 => false,
            >= 65 and <= 90 => false,
            >= 97 and <= 122 => false,
            _ => intValue != 43 && intValue != 47
    };
}

Is it much clearer all of a sudden?

Switch Expression

Switch Expression is a new feature introduced by C# 8. C# 9 has been further enhanced in combination with pattern matching, making its functions more powerful. Let’s take a look at the example:

The code before modification is as follows:

var state = ;
var stateString = ;
switch (state)
{
    case :
        stateString = "0";
        break;

    case :
        stateString = "1";
        break;

    case :
        stateString = "-1";
        break;
}

The code after using switch expression is as follows:

var state = ;
var stateString = state switch
{
         => "0",
         => "1",
         => "-1",
        _ => 
};

Does it look much more concise, and there are further added optimizations, let’s take a look at the next example:

(int code, string msg) result = (0, "");
var res = result switch
{
        (0, _) => "success",
        (-1, _) => "xx",
        (-2, "") => "yy",
        (_, _) => "error"
};
(res);

Guess what the output results are like in different situations, and try to run them yourself to see if the results are in line with expectations.

Index Range

Index/Range is a new feature introduced in C# 8. It mainly optimizes the operation of tuples, which can make indexing and slicing operations more convenient.

I have had a detailed introduction article before, so please refer to: C# Use Index and Range to simplify collection operations

We can use the ^(hat) operator to reverse index objects in the array, and we can create a subset of the collection through .. to see a simple example:

var arr = (1, 10).ToArray();
($"last element:{arr[^1]}");

var subArray = (1, 3).ToArray();
(arr[..3].SequenceEqual(subArray) ? "StartWith" : "No");

Record

Record is a new feature introduced in C# 9. Record is a special class. The compiler will help us do a lot of things. It will automatically implement a set of value-based comparisons, and it can easily implement object copying functions. For detailed introduction, please refer to the previous record introduction article.C# 9 New Features — Record Interpretation, you can see the following simple example:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Person student = new Student("Nancy", "Davolio", 3);
    (teacher == student); // output: False

    Student student2 = new Student("Nancy", "Davolio", 3);
    (student2 == student); // output: True
}

Top-Level Statement

Top-Level statement is a new feature supported by C# 9. We can write the method body directly without writing the Main method. It will be more convenient for some simple gadgets and small test applications.

using static ;
WriteLine("Hello world");

More

In addition to the above new features, what other practical new features do you think are there? Welcome to leave a message to discuss it together~

References

/en-us/dotnet/csharp/whats-new/csharp-9

/en-us/dotnet/csharp/whats-new/csharp-8

/en-us/dotnet/csharp/whats-new/csharp-7

/WeihanLi/SamplesInPractice/blob/master/CSharp9Sample/

Summarize

This is the end of this article about how to use modern C# syntax to simplify code. For more related C# simplified code content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!