Errors
- Unknown style found:
codesnippetdarkblue - Unknown style found:
codesnippetdarkblue - Unknown style found:
codesnippetdarkblue - Unknown style found:
codesnippetdarkblue - Unknown style found:
codesnippetdarkblue - Unknown style found:
codesnippetdarkblue - Unknown style found:
codesnippetdarkblue - Unknown style found:
codesnippetdarkblue
How New C# Language Features Solve Old Problems
The year was 2013. I was Senior Software Engineer at an up-and-coming biotech startup when I did something crazy: I stopped coding. That's right. I hung up my compiler. I resigned my position as a programmer and became the head of our new field service department. Goodbye WCF and XML and the joys of large object heap fragmentation. Hello teeny-tiny hex wrenches and oscilloscopes and the joys of long international flights. It was an abrupt shift in direction, and one which ultimately led to management and leadership roles. I don't regret it one bit.
Fast forward to 2021 and the thrill of management was wearing off. I wanted to be in charge of less but somehow be able to deliver more. I realized that I missed my compiler. I missed the creativity and expressiveness. I missed the feeling of actually making something. So, I started applying for C# programmer jobs.
There was just one problem: a lot had changed. WCF was dead. XML had been eaten by JSON. The large object heap was supposedly fixed. I realized I was going to need to come up to speed fast. I had missed C# 6.0, 7.0, 8.0, 9.0, and 10.0. What a pleasant surprise it was, then, to realize that C# was full of great new language features that solved my old problems in elegant new ways.
In this article, I want to share my perspective on these changes. If you're an experienced programmer who's been doing C# for a while, maybe you have some old habits and styles that could use updating. If you're newer to the language, maybe you wonder why some of that legacy code looks the way it does. I invite you to join me on a journey to look at how C# used to do certain things, and how it can do them now. This is my journey rediscovering C#.
Target-Typed Constructors
That Was Then
In C# 2.0 if you wanted to instantiate a class, you would likely have written code that looked something like this:
// declare your type and call the constructor
Here, our imaginary IceCream class has a constructor that takes two parameters, a flavor and a numberOfScoops. The annoying thing about this is that you have to specify the type, IceCream, twice, once for the variable declaration and once when calling the constructor. This syntax is clear, but wordy. C# 3.0 seemed to provide a way to shorten things by using the var keyword like this:
// use var to avoid the initial type declaration
var iceCream = new IceCream("Vanilla", 2);
This syntax works, but I have never really been a fan of var. To me, in a strongly-typed language like C#, one should declare one's types up front. I understand that var is just shorthand for the actual type and to the compiler it makes no difference (it is nothing like a JavaScript var or a VB variant which are evaluated at run time). But I still think it makes the code less readable. Fortunately, starting in C# 9.0 there is a better way.
This Is Now
C# 9.0 introduced the concept of target-typed constructors. With a target-typed constructor you explicitly declare the type of a variable and then C# will figure out which constructor to call. This allows you to write code like this:
IceCream iceCream = new ("Vanilla", 2);
You don't have to list the type twice and you also don't have to use var. C# simply picks out the correct constructor based on your declared variable type and the parameters supplied after new. Problem solved! If you're not using target-typed constructors, you should start. They are a gift.
Target-typed constructors are a gift. You should start using them if you haven't already.
String Interpolation
That Was Then
In 2001 if you were new to C# and were coming from a language like Visual Basic or Delphi (yep, that was me) and you wanted to create a complex string, you might have been tempted to use string concatenation like this.
string msg = "You ordered " +
"";
" scoops of " + ic.Flavor;
// result: You ordered 2 scoops of Vanilla on OCT 2
Most programmers learn quickly to avoid this. It is hard to read, error prone, and also inefficient because of all the multiple string allocations. Instead, you would likely have written composite formatting code like this:
// Using a format string and composite formatting
const string msgFmt =
"You ordered {0} scoops of {1} on {2:MMM dd}";
// result: You ordered 2 scoops of Vanilla on OCT 2
Using format strings like this makes the code a lot more readable and efficient, but it is still somewhat error prone because it relies on positional referencing. What if you had accidentally swapped the Scoops and Flavor parameters. The message would read, “You ordered Vanilla scoops of 2 on OCT 2.” I would guess you have seen some fairly humorous and confusing cases where things got switched. The more parameters in the format string, the trickier things get. Fortunately, starting in C# 10, there is a better way.
This Is Now
C# 10 introduced string interpolation which makes the processes of formatting strings cleaner. With string interpolation you would write code like this:
// using string interpolation (note the $)
// result: You ordered 2 scoops of Vanilla on OCT 2
Note that in this code snippet I've added a line break using string concatenation in order to keep with this publication's line length limits. In real code, you would not need that. The result: instead of multiple lines of code, you end up with one line which is fully self-describing. Are there cases where composite formatting is still a better choice? Yes. For example, if you have a large library of messages that you want to store in a database and load at runtime into an array of format strings, you will want to use composite formatting. But for new code, I almost always find myself using string interpolation: it is simply easier to both read and write.
Stringinterpolation is often easier to read and write than composite formatting. For new code, I almost always find myself usingstringinterpolation.
Null Conditionals
That Was Then
Imagine that the IceCream class needs a static function that prints out the numberOfScoops. In C# 2.0, you might have written the function like this:
static void PrintScoops(IceCream ic)
{
Console.WriteLine("Scoops: " + ic.Scoops.ToString()); // not safe
}
The problem is that ic might be null, which means the code could throw a null reference exception. Because of this, you would have likely included a null check:
static void PrintScoops(IceCream ic)
{
if (ic != null) // null check
{
Console.WriteLine("Scoops: " + ic.Scoops.ToString()); // safe
}
}
Similarly, imagine you needed a static function to automatically make an ice cream a triple scooper. Maybe you have a loyalty program that will give people a triple from time to time! You might have started with a function like this:
static void MakeItATriple(IceCream ic)
{
ic.Scoops = 3; // not safe
}
Again, this is not null safe, so you would have likely revised it like this:
static void MakeItATriple(IceCream ic)
{
if (ic != null) // null check
{
ic.Scoops = 3; // safe
}
}
This code is clean, easy to read and null safe. There's really nothing wrong with it. But C# 6.0 and C# 14.0 gave us some techniques for making these kinds of expressions a little more compact.
This Is Now
C# 6.0 introduced the null-conditional operator and C# 14.0 introduced null-conditional assignment. Here is what the PrintScoops would look like re-written with a null-conditional operator:
static void PrintScoops(IceCream ic)
{
// using the null-conditional operator
Console.WriteLine("Scoops: " + ic?.Scoops.ToString()); // note the ?
}
Combining with string interpolation and the null-coalescing operator (C# 2.0) you can make it even cleaner and handle the “else” case where ic is null.
static void PrintScoops(IceCream ic)
{
// prints "Scoops: 0" if ic is null
Console.WriteLine($"Scoops: {ic?.Scoops ?? 0}");
}
Similarly, using null-conditional assignment, you can re-write the MakeItATriple function like this:
static void MakeItATriple(IceCream ic)
{
// using null-conditional assignment
ic?.Scoops = 3; // note the ?
}
This is lovely and concise. I will say, however, that I do still sometimes write the older, if – else syntax. It is wordier and takes more lines, but it is also very clear and hard to misinterpret. Sometimes I favor that clarity over the brevity of null conditionals. It really depends on the context.
Value Tuples
That Was Then
Sometimes you want to write a function that returns multiple values rather than just one. For example, imagine that you need a function that takes a DateTime as an input and returns the month, day and year as separate integer values. In C# 2.0, you might have implemented this using out parameters like this:
static void SplitDate(DateTime date, out int month, out int day, out int year)
{
month = date.Month;
day = date.Day;
year = date.Year;
}
This is manageable as long as you only have two or three out parameters, but what if you have more? Or what if you wanted to avoid having to declare multiple individual parameters every time this function was used? In those situations, an elegant solution is to use an encapsulating class to hold the output variables, like this:
class DateParts
{
public int Month;
public int Day;
public int Year;
static DateParts SplitDate(DateTime date)
{
return new DateParts()
{
Month = date.Month,
Day = date.Day,
Year = date.Year
};
}
}
This works well. It not only makes the collection of values more portable, reusable and explicitly defined, but it gives you a namespace for the SplitDate function to live in. Obviously, this particular example is a bit contrived (the DateTime class already gives you this functionality) but it illustrates the point: a good alternative to out parameters is to use an encapsulating class.
The problem with this approach is that sometimes you don't really need or want the overhead or extra code of an encapsulating class. Sometimes you just want to define the variables at the function level. Out parameters do this, but they are awkward. Fortunately, starting in C# 7.0, there is a better way.
This Is Now
C# 7.0 introduced the concept of value tuples. A value tuple is a value type, like a struct, which is implicitly defined by clustering a series of value types in a group. Using a value tuple, you can re-write the SplitDate function like this:
static (int, int, int) SplitDate(DateTime date)
{
return (date.Month, date.Day, date.Year);
}
One nice feature of value tuples is that they allow you to use field names to distinguish the parts. Adding field names, your function would look like this:
static (int Month, int Day, int Year)
SplitDate(DateTime date)
{
return (date.Month, date.Day, date.Year);
}
Where field names really come in handy is when you write the calling code that consumes a tuple and you want to refer to the individual elements by name to avoid confusion:
static void UseATuple(DateTime date)
{
(int Month, int Day, int Year) parts = SplitDate(date);
if (parts.Month == parts.Day)
{
Console.WriteLine("Cool! Month = Day!");
}
}
One last thing to note about value tuples is how equivalence is handled. With value tuples equivalence is calculated by comparing the values of all the elements, somewhat like how equivalence is handled for **structs and records.
static void TestEquivalence(DateTime date)
{
// tuple equivalence is based on the values
(int, int, int) t1 = SplitDate(date);
(int, int, int) t2 = SplitDate(date);
(int, int, int) t3 = SplitDate(date.AddDays(1));
if (t1 == t2)
{
Console.Write("T1 == T2");
}
if (t2 != t3)
{
Console.Write("and T2 != T3");
}
// output: T1 == T2 and T2 != T3
}
Overall, I find value tuples to be more elegant than out parameters and more economical than encapsulating classes. I am always looking for opportunities to use them.
Value tuples are more elegant than out parameters and more economical than encapsulating classes. I am always looking for opportunities to use them.
Pattern Matching Switch Expressions
That Was Then
The if-then statement is perhaps the most fundamental of all programming flow control mechanisms. Regardless of your language, from VB to C++ to Python to Scratch or Blocky, if the language is Turing complete, it probably has an if – then conditional. Even pure functional languages like Haskell support if – then, even if they don't really favor it. The only exceptions I know of are certain esolangs whose names are not appropriate to print here (looking at you, Brain***).
As a result, you probably have written many, many if-then statements. Perhaps some of them were pages long! For example, let's say you needed a function that mapped certain integer inputs to string outputs. Using if-then, you could write the function like this:
static string GetFruitName(int input)
{
if (input == 1)
{
return "Lemon";
}
else if (input == 2)
{
return "Orange";
}
else if (input == 3)
{
return "Grape"; //etc...
}
else
{
return "Unknown";
}
}
Another variation of an if-then is when you use it for standard code branching to call subfunctions like this:
static void PeelTheFruit(int input)
{
if (input == 1)
{
PeelALemon();
}
else if (input == 2)
{
PeelAnOrange();
}
else if (input == 3)
{
PeelAGrape(); //etc...
}
else
{
throw new Exception("invalid input");
}
}
As these kinds of function grow in length, they can become unwieldy and I have always looked for ways to simplify and condense them.
One interesting option is the classic switch statement. Re-writing the GetFruitName with a classic switch statement yields this, which is a big improvement:
static string GetFruitName(int input)
{
switch (input)
{
case 1:
return "Lemon";
case 2:
return "Orange";
case 3:
return "Grape";
default:
return "Unknown";
}
}
Rewriting PeelTheFruit with a classic switch statement yields this, which is kind of clunky owing to all the break statements:
static void PeelTheFruit(int input)
{
switch (input)
{
case 1:
PeelALemon();
break;
case 2:
PeelAnOrange();
break;
case 3:
PeelAGrape();
break;
default:
throw new Exception("invalid input");
}
}
Fortunately, starting in C# 8.0, there is a better way to handle both of these situations.
This Is Now
C# 8.0 introduced the concept of pattern matching switch expressions. This is a powerful concept inspired by the pattern matching functionality of functional programming languages. One important thing to note is the difference between a statement and an expression. In C#, a statement is a pure flow-control construct. Other examples of statements are for – each and while. Expressions, however, return a value. This is an important difference between pattern matching switch expressions and classic switch statements. Your pattern matching switch expression always needs to return a value.
Using a pattern matching switch, you can re-write GetFruitName like this:
static string GetFruitName(int input)
{
return input switch
{
1 => "Lemon",
2 => "Orange",
3 => "Grape",
_ => "Unknown" // the discard case
};
}
Note that there are some subtle syntactical differences from the classic switch statement: the switch keyword comes after the switch variable and the branches in the switch expression are separated by commas, not semicolons, because they are not statements themselves. A statement can have sub-statements. An expression cannot.
Using an arrow function, you can condense this function even further:
static string GetFruitName(int input)
{
return input switch
{
1 => "Lemon",
2 => "Orange",
3 => "Grape",
_ => "Unknown"
};
}
Compare this to the original function that used an if-then statement: quite an improvement, I'm sure you will agree!
So, what about the PeelTheFruit function? If an expression cannot have sub-statements, how can you use one to call the sub-functions? The trick is to define the subfunctions as having a return value other than void. For example, we could define our various peel functions to return a Boolean like this:
static bool PeelALemon()
{
// complex lemon peeling code here
return true;
}
static bool PeelAnOrange()
{
// standard orange peeling code here
return true;
}
static bool PeelAGrape()
{
// astonishing grape peeling code here
return true;
}
Then you can use them in a pattern matching switch like this:
static void PeelTheFruit(int input)
{
bool result = input switch
{
1 => PeelALemon(),
2 => PeelAnOrange(),
3 => PeelAGrape(),
_ => throw new Exception("invalid input")
};
}
Now this is truly a thing of beauty. Compare it to the original if-then as well as the original clunky switch statement. It does involve a little hack of defining the Boolean result (which is never used). But it makes the pattern matching switch expressions into a nice function dispatcher.
Imagine using this to build a factory method:
static IFruit FruitFactory(int input)
{
return input switch
{
1 => new Lemon(),
2 => new Orange(),
3 => new Grape(),
_ => throw new Exception("invalid input")
};
}
Imagine using this to await the correct asynchronous function:
static async Task PeelTheFruitAsync(int input)
{
Task t = input switch
{
1 => PeelALemonAsync(),
2 => PeelAnOrangeAsync(),
3 => PeelAGrapeAsync(),
_ => throw new Exception("invalid input")
};
await t;
}
There are many uses for this technique.
So far, all of the pattern matching switch expressions I've shown you have used just one pattern, the so-called “constant” pattern because the input to the switch expression (the thing you switch on) is a constant value. There are others. For example, there are “relational” and “logical” patterns as shown in this function:
static string GetNumberKind(int input)
{
return input switch
{
< 0 => "Negative",
0 => "Zero",
> 0 and < 10 => "Single Digit",
11 or 13 or 17 or 19 => "Two digit prime",
_ => "Some other number"
};
}
Here we are using relational operators such as < and > as well as logical operators such as and and or. This allows for some sophisticated pattern matching, and there are other kinds of patterns as well. Suffice it to say that pattern matching switch expressions are extremely powerful and flexible and are something you should consider using.
Pattern matching switch expressions are extremely powerful and flexible and are something you should consider using.
Collection Initializers and Spread
That Was Then
Initializing a List<T> and populating it with initial values is a common task. In C# 2.0, coming from a VB or Delphi background you (or I) might have written some code like this using the Add method of a list:
// populating a list with Add
List<string> animals = new List<string>();
animals.Add("Iguana");
animals.Add("Wombat");
aminals.Add("Crow");
There is nothing wrong with this code, but even as far back as C# 2.0, the List<T> class had a constructor that accepts an IEnumerable<T>, so I more likely would have written code like this:
string[] animals = { "Iguana", "Wombat", "Crow" };
// List<T> constructor taking IEnumerable<T>
List<string> animalsList = new List<string>(animals);
Starting in C# 3, you would have been able to use the list initializer syntax which is also a nice alternative:
List<string> animals = new List<string>()
{
"Iguana",
"Wombat",
"Crow"
};
If you had multiple lists and wanted to combine them, List<T> has AddRange resulting in code like this (note that I am using C# 9 syntax for the target-typed constructors).
List<string> mammals = new()
{ "Panda", "Peccary", "Porcupine" };
List<string> reptiles = new()
{ "Gecko", "Iguana", "Python" };
List<string> fish = new()
{ "Trout", "Guppie", "Chub" };
List<string> animals new();
animals.AddRange(mammals);
animals.AddRange(reptiles);
animals.AddRange(fish);
All of this code for initializing and manipulating lists is straightforward and clear, but in C# 12.0 we got some fascinating new alternatives.
This Is Now
C# 12.0 introduced collection expressions and the spread element. Here's how initializing the List<T> looks with a collection expression. Note that they also work with arrays, spans and jagged arrays as well.
List<string> list = new List<string> { "Iguana", "Wombat", "Crow" };
This is definitely the most compact syntax so far.
More powerful and interesting is the spread element ..e, which allows you to unpack or expand an array within a containing collection expression. This is easier to show than to explain:
string[] mammals = ["Panda", "Peccary", "Porcupine"];
string[] reptiles = ["Gecko", "Iguana", "Python"];
string[] fish = ["Trout", "Guppie", "Chub"];
// combine using spread elements (..e)
List<string> animals = [..mammals, ..reptiles, ..fish];
Console.WriteLine(string.Join(",", animals));
// output: Panda, Peccary, Porcupine, Gecko, Iguana, Python, Trout, Guppie, Chub
As you can see, the spread element (..e) unpacks the elements of the sub collection expression into the larger collection expression. This is probably not something you will use every day, but it is very nice to know about.
Properties
That Was Then
Few things in C# have evolved as much as how properties are defined. In C# 2.0, there was only one way to define a property: use a manual getter and setter and backing field like this:
class IceCream
{
private int _scoops; // backing field
public int Scoops
{
get
{
return _scoops; // getter
}
set
{
_scoops = value; // setter
}
}
}
If you learned classic object-oriented programming, this makes perfect sense. The class hides its internal state and provides controlled methods for accessing that state. The getter and setter provide natural places for doing things like masking, formatting, validation and event signaling.
In 3.0, C# introduced the concept of an auto-property. This is a property whose backing field, getter, and setter are all automatically generated. Here is what the Scoops property looks like as an auto-property:
class IceCream
{
public int Scoops { get; set; } // auto-property
}
This is an extremely common way of doing things, especially in data model classes where you really don't need any logic built into getters and setters. And way back in 2013, this was pretty much all there was to know about properties. Boy, was I surprised to come back and see how much new functionality had been added to the humble OO concept of a property.
This Is Now
C# 6.0 started things off with a host of property enhancements including property initializers, read-only properties and arrow functions for property getters and setters.
If you are using a traditional backing field, setting a default value is easy:
class IceCream
{
private int _scoops = 1; // default
public int Scoops
{
get
{
return _scoops; // getter
}
set
{
_scoops = value; // setter
}
}
}
With an auto-property, you could not set a default until C# 6 which provided this syntax:
class IceCream
{
public int Scoops { get; set; } = 1; // default
}
In C# 3.0, auto-properties had to have both a get and a set. The compiler would not allow a read-only auto-property with only a getter. In C# 6.0, this became supported:
class IceCream
{
public int Scoops { get; } // read only
}
It is hard to believe this was not always allowed!
C# 6.0 also introduced the ability to use arrow functions to implement getters and setters (in C# 7.0). These can be used both with backed properties and auto-properties. Here are some examples:
class IceCream
{
private int _scoops;
private string _flavor;
public int Scoops
{
get => _scoops;
}
public string Flavor
{
get => _flavor ?? "None";
set => _flavor = value?.Trim();
}
public bool IsMultiScoop
{
get => (_scoops > 1);
}
}
The only time it is now necessary to use a full getter or setter is when there is some multi-statement business logic that needs to be implemented. In that case, I use a hybrid like this with an arrow function for the getter and a full setter:
class IceCream
{
private int _scoops;
public int Scoops
{
get => _scoops;
set
{
if (value < 1 || value > 3)
{
throw new Exception("Unsupported");
}
if (value != _scoops)
{
_scoops = value;
PropChangedEvent("Scoops");
}
}
}
}
C# wasn't done enhancing properties. C# 9.0 introduced init-only properties. Imagine you have a property that you want to only be settable during object creation. Prior to C# 9.0, the only way to do this was to use a constructor and make the property read only, like this:
class IceCream
{
private int _scoops;
// constructor supports setting scoops
public IceCream(int scoops)
{
_scoops = scoops;
}
// Scoops property is read only
public int Scoops
{
get => _scoops;
}
}
This really forced one to use field-backed properties rather than auto-properties. In C# 9.0, however, you can declare an auto-property as init-only, which allows a caller to set the value during an object initializer like this:
class IceCream
{
// init-only auto-property
public int Scoops { get; init; }
}
static void ShowUsage()
{
IceCream ic = new()
{
Scoops = 2 // this compiles
};
ic.Scoops = 3; // this is not allowed
}
Now you can set the property at object construction without needing a backing field or a custom constructor. Nifty!
And last, C# 11.0 introduced the concept of required properties. With a property marked as required, the compiler will check any instantiation of the class and will block compilation if the property is not initialized when exiting the constructor:
class IceCream
{
// required auto-property
public required int Scoops { get; set; }
}
static void ShowUsage()
{
// this compiles because we set Scoops
IceCream ic = new()
{
Scoops = 2 //
};
// this is not allowed since Scoops not set
IceCream ic = new();
}
As you can see, properties are a lot more sophisticated and powerful. By mixing and matching these various techniques, you can succinctly express a wide variety of property-related patterns.
Few things in C# have evolved as much as
properties. They are more sophisticated and powerful, and by mixing and matching some of the newer C# techniques, you can succinctly express a wide variety ofproperty-related patterns.
Conclusion
Is new always better? In life in general, I think most people would agree that the answer is sometimes. New can be cool, but there is often a lot to be said for old. After all, the phrase “tried and true” actually means something. But when it comes to C# and the new features added in the last 12+ years, I think I can say that most of the new really is better. Target-typed constructors, string interpolation, null conditionals, pattern matching switch expressions, collection expressions and the multiple enhancements to properties have made the language better. The old code wasn't wrong, and in some cases, as noted above, the old-school style should still be used. But overall, I tend to favor these new techniques which I think have made C# more concise, readable and literate. I encourage you to give them a try if you haven't already.



