8 Sweet Treats in Modern C#

8 Sweet Treats in Modern C#

Originally posted on dev.

8 Sweet Treats in Modern C#

C# through the Ages

Many, many eons ago ✨, C# ’twas but an archaic, OOP focused language. Alas, ’tis no more! Many updates and feature requests later C# has evolved, shedding the object focused shackles of its past, becoming a modern, functional, programming language.

Here are 8 🔥 sizzling 🔥 features to use from C# 7, 8, 9, and 10 to write modern C#.

Throw Expressions

A simple lil’ nugget 💎 that is most useful in constructors, when combined with the null-coalescing operator is the throw expression

public string Color { get; set; }
public int Size { get; set; }

// ctor
public Car(string? color, int? size)
{
    Color = color ?? throw ArgumentNullException(nameof(color));
    Size = size ?? throw ArgumentNullException(nameof(size));
}

If either color or size is null, the application will throw a ArugmentNullException.

Another useful place to use this is in conjunction with the ternary operator.

public void SetCharacterLimitedName(string newName, int characterLimit)
{
    Name = newName.Length > characterLimit
        ? throw new ArgumentOutOfRangeException(nameof(newName))
        : newName;
} 

The method will throw an ArgumentOutOfRangeException if the length of the new name exceeds the character limit.

Out Variables

Functions with out parameters, returning boolean values, make the best use of this feature. It allows us to do a conditional check, and if it passes we know something about the out variable, and can then use the variable within the scope of the check.

Old way

bool isLucky(int someNumber, out string result)
{
    // the number 7 is lucky
    result = someNumber == 7 ? "you win!" : "you lose...";
    return someNumber == 7;
}

string luckyMessage;
var isLucky = isLucky(11, out luckyMessage);

if (isLucky)
    Console.WriteLine(luckyMessage);

New way

bool isLucky(int someNumber, out string result)
{
    // the number 7 is lucky
    result = someNumber == 7 ? "you win!" : "you lose...";
    return someNumber == 7;
}

if (isLucky(7, out var message))
    Console.WriteLine(message);

This is much more concise, and easy to read!

Pattern Matching with andor, and not

The andor, and not keywords have been added to pattern matching in C# to further enrich the pattern matching experience. It makes the code more readable in English, and allows for many combinations of boolean checks in a single line, ensuring there is much less code. With the addition of andor and not pattern matching goes into overdrive.

// Setup
public enum Color
{
    Red,
    Orange,
    Green,
    Yellow
}

public class Fruit
{
    public string? Name { get; set; }
    public Color Color { get; set; }
    public bool IsTasty { get; set; }
    public int Quantity { get; set; }
}

// Implementation
var firstFruit = new Fruit
{
    Name = "Tomato",
    Color = Color.Red,
    IsTasty = true,
    Quantity = 10
};

var secondFruit = new Fruit
{
    Name = "Banana",
    Color = Color.Green,
    IsTasty = false,
    Quantity = 12
};

var thirdFruit = new Fruit
{
    Name = "Orange",
    Color = Color.Orange,
    IsTasty = false,
    Quantity = 50
};

if (firstFruit is {Name:"Tomato", IsTasty:true, Quantity:>1}
or {Name:"Tomato", Color:Color.Red, Quantity:>10})
    Console.WriteLine("Tomato, at least one tasty or at least 10 red");

if (secondFruit is {Name:"Banana"} and {IsTasty:true}
and {Color:Color.Yellow} and {Quantity:>12})
    Console.WriteLine("Banana, tasty and yellow and at least 12");

if (thirdFruit is {Name:"Orange"} and not {IsTasty:true}
or not {Color:Color.Orange, Quantity:<5})
    Console.WriteLine("Orange, not tasty or orange and less than 5");

Pattern Matching 🍨 and out

Combining the aforementioned out variables with pattern matching, gives a wonderfully short, readable result

object numberObject = "12345";

if (numberObject is int number
|| (numberObject is string numberString 
    && int.TryParse(numberString, out number))
{
    Console.WriteLine($"It's a number alright! {number}");
}

Here we check if the object is an int, or a string. If it is a string, we then try and parse it. If it parses then we have the result number available to use within the if block.

Pattern matching also extends far beyond simple type checks. Properties can be matched too!

public class Fighter 
{
    public string? Name { get; set; }
    public string? Style { get; set; }
    public int Strength { get; set; }
    public bool IsChampion { get; set; }
}

var fighter = new Fighter { ... };

if (fighter is {IsChampion:true})
    Console.WriteLine($"{fighter.Name} is a champ!");

if (fighter is {Style:"Karate", Strength:>9000) 
    Console.WriteLine($"{fighter.Name} is powerful!");

Doing property patterns in this way also has the added benefit of an implicit null check. The {} pattern matches with everything except null.

Switch Expressions + Property Patterns 💅

The greatest 👏 the best 👏 the most amazing 👏 of the new features – Switch Expressions! 😎

Combined with property patterns they allow a whole host of conditional operations, in a few short lines of 🧊 crisp C#.

We got em basic 🌼

public string Alakazam(int magicNumber)
{
    return magicNumber switch
    {
        1 => "pow",
        2 => "magic!",
        3 => "behold...",
        // default case
        _ => "ummm, I got nothing..."
    }
}

And extra spicy 🌶️ 🌶️ 🌶️

// The setup
public class Monster
{
    public string? Name { get; set; }
    public int? NumberOfTentacles { get; set; }
    public bool? IsScary { get; set; }
    public Power? Power { get; set; }
}

public class Power
{
    public int Level { get; set; }
    public string? Element { get; set; }
    public bool? DoesPierceArmour { get; set; }
}

// The execution!!!
var monster = new Monster
{
    Name = "Mildred",
    NumberOfTentacles = 23,
    IsScary = true,
    Power = new Power
    {
        Level = 3000,
        Element = "Fire",
        DoesPierceArmour = false
    }
};

var res = monster switch
{
    var m when monster.NumberOfTentacles > 100 =>
        $"Too many damn tentacles! {m.NumberOfTentacles}",
    { IsScary: true } => "Ahhhhhhh!",
    { Power.Element: "Fire"} => "Ooo hot! hot! hot!",
    { Power.DoesPierceArmour: false } => "I ain't afraid of you!",
    var m when monster.Power.Level > 3000 =>
        $"So much power! {m.Power}",
    _ => "ummm, I got nothing?"
};

Each of the case statements are so declarative that their meaning is implicit. Simply by reading the code you already know what’s being checked at each case. So much meaning is suddenly contained in so little code.

This is the true power of 🌟 Switch Expressions 🌟.

(Named) Tuples

Ah, the humble tuple 🌝. So short. So sweet. And so very useful. Tuples really shine in places where you need to return multiple values but don’t want to create a new object/type to do so.


public (string name, bool isReadyToParty) SomeThing() =>
("Gerry", true);

// Destructuring the tuple
var (name, isReadyToParty) = SomeThing();

Console.WriteLine($"Name: {name}, Ready to Party? {isReadyToParty}")

When using named tuples, where each property of the tuple has a name, it can then be deconstructed on invocation, as var (name, isReadyToParty). This gives a clean way to return multiple variables AND extract them in the calling code.

Records

record types have the potential to be amazing. They’re supposed to represent immutable objects – but are only immutaable through the use of init only setters. The init only setters ensure that their properties cannot be set after initialization – and hence guarantees immutability. *BUT record types are not currently immutable by default, and that makes me sad 🐼.

Immutability is on of the cornerstones of the functional programming paradigm.

public record Person
{
    public string? Firstname { get; init; }
    public string? Lastname { get; init; }
};

var person = new Person { Firstname = "Sam" };

// this throws an error
person.Lastname = "Person";

Immutability prevents the following pattern


public record Person
{
    public string? Fullname { get; set; }
    public int? Counter { get; set; }
    public bool? IsTrue { get; set; }
};

var person = new Person { Fullname = "Anna" };

person.Counter++;
person.Fullname += " Luisa";
person.IsTrue != person.IsTrue;

someOtherMethod(person);
andAnotherMethod(person);
yetAnother(person);

/*
 *
 * 20 lines of code
 * passing person around
 *
*/

person.Counter++;
person.Fullname += " Magdalena";
person.IsTrue != person.IsTrue;

/*
 *
 * 20 lines of code
 * passing person around
 *
*/

person.Counter++;
person.Fullname += " Susanna";
person.IsTrue != person.IsTrue;

What is the Person object’s name? What is the value of Counter? Do we know if it was mutated inside one of the methods – or maybe not. What is the value of IsTrue? By making Person immutable we can only set this once, and prevent this sort of mess from forming, or compounding on a single object. It simplifies the logical flow through the code.

Adios Amigos

The breadth and scope of the Modern C# programming language, with its wealth of new features 🪐, introduced over the last 4 years, may seem overwhelming at first 😅. However, learning to use these features a few at a time is easy 🏖️ – and will drastically increase the enjoyment derived from working with C#.

Source: dev