It's dangerous to code alone! Take this.

Modern C#

For the first time, I just caught myself saying the phrase, “In modern C#…” I’ve said that for languages, but not for C#.

For much of C#’s history, new features have simply provided more convenient ways to do some of the same old things. A good example of that is LINQ and lambda expressions. Once upon a time, we might have written this code:

public class SomeClass
{
    public int CountEvenNumbers(int[] numbers)
    {
        int count = 0;
        for (int index = 0; index < numbers.Length; index++)
            if (numbers % 2 == 0)
                count++;
        return count;
    }
}

With LINQ (but without lambdas) you might do this:

public class SomeClass
{
    public int CountEvenNumbers(int[] numbers)
    {
        return numbers.Count(IsEven);
    }

    public bool IsEven(int number)
    {
        return number % 2 == 0;
    }
}

And with lambdas:

public class SomeClass
{
    public int CountEvenNumbers(int[] numbers)
    {
        return numbers.Count(n => n % 2 == 0);
    }
}

A bit later came expression-bodied members–the simple syntax of lambdas finally available for virtually any method that could be written as an expression:

public class SomeClass
{
    public int CountEvenNumbers(int[] numbers) => numbers.Count(n => n % 2 == 0);
}

What started as 12 lines (including blank lines and curly brace lines) is now three.

Obviously, the language has evolved. But something about the evolution of C# between versions 8 and 10 feels different to me, somehow.

I think a handful of recent features have changed how C# ought to look in mind-boggling ways:

1. File-scoped namespace declarations.

In the past, you’d put anything you want in a namespace into a set of curly braces: namespace SomeNamespace { ... }. With C# 10, you just write namespace SomeNamespace; at the top of your file and everything is automatically contained in it, cutting out an extra level of indentation! With Old Code, this is what it looked like:

using System;

namespace HelloWorld
{
    class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

With file-scoped namespaces, that can become this:

using System;

namespace HelloWorld;

class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
    }
}

2. Top-level statements.

Instead of writing out class Program and public static void Main(string[] args) to write a program, you just drop in some statements in a file, and the compiler makes the class and main method for you.

Our prior example becomes:

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

While this was possible in C# 9 and in Visual Studio 2019, the templates have been updated in Visual Studio 2022 to start with top-level statements.

3. Automatic inclusion of certain namespaces without writing using directives.

I remember when the template for a new file had about six or eight using This; and using That; directives at the top. It was maybe six years ago that they streamlined it to only be three or four. And with C# 10 and Visual Studio 2022, the new project template is configured to automatically include several extremely common namespaces, including System (for things like Console and Math), System.IO (for files), System.Threading (for threading), System.Threading.Tasks (for async and await), System.Linq (for all of IEnumerable’s extension methods and the LINQ keywords), and System.Collections.Generic (for List<T>, Dictionary<K, V>, etc.).

Our prior example turns into:

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

And I’ve got to say that I love this feature. The pile of using directives was boilerplate and clutter. With this change, that pile of using directives at the top of each file will only contain non-obvious dependencies used in the file. I can quit mentally ignoring that section, because it is valuable information now.

4. Nullability compiler warnings.

This is, perhaps, the biggest change that makes me think that “modern” C# is completely different from the Old Code. In the past, when you used a reference type, you made no particular distinction between situations where null was considered a valid option and ones where it wasn’t. Some people resorted to making attributes like [NotNull] and [MayBeNull], but that’s a fair bit of overhead, and I didn’t see it too often in practice.

In “modern” C#, you’re able and expected to indicate if something allows null as an option, and the compiler helps you check it at the right times. Consider this Old Code, which is wrong enough to get a compiler warning now:

string input = Console.ReadLine();

I don’t know how many years I lived, not realizing that Console.ReadLine() might actually return null! With C# 10, the line above gave me a compiler warning, helping me understand that I should either:

  1. use string? for input’s type,
  2. escape the Null Realm with something like string input = Console.ReadLine() ?? "";, or
  3. intentionally make a choice to take the risk that somebody might be just crazy enough to hit Ctrl+Z at that prompt, and my program will crash.

(And that last option is, I think, perhaps quite reasonable for low-risk code, like a script I’m not sharing with anybody else.)

Before, I was just assuming ReadLine would return only legitimate values, even if that was an empty string ("").

I’m being a bit philosophical about how the language has evolved, so let me get to the reason I’m writing this at all.

Most of the C# code samples you find on the Internet are now using “old” and “legacy” code. The language has changed in such big, important ways, that 20 years of code is wrong! Okay, that was hyperbole. The Old Code still compiles. It’s just no longer something you want to paste, willy-nilly into your programs without change.

I suppose that has long been my feeling on Internet code, but for C#, the evolution in the last couple of updates have made that vastly more important. When you look things up on Stack Overflow, know that most of it is using Old Code. You will have the extra burden of taking what you find and modernizing it for your application.

These significant changes are, honestly, why I decided I can’t hold off on the 5th Edition. While the majority of the language has remained the same, enough fundamental features have changed in significant ways, that they can’t be ignored. And something needs to exist to help people learn the New Way, instead of just cobbling together fragments of the Old Code, unearthed from Stack Overflow and YouTube tutorials.