C#12Expansion
C#12Expansion
[email protected]
THE C# PLAYER’S GUIDE
C# 12 EXPANSION
.NET 8 PROJECTS
Read after Level 2: Getting an IDE.
To use the features in this expansion, you must have .NET 8 installed.
For Visual Studio, if you already have the correct workflow installed (described in the book), you will need to
update Visual Studio to 17.8 (or newer). If you have an older version, rerun the Visual Studio Installer and
update it.
You will know that you have the correct version if you see .NET 8 (or newer) as an option when making a new
project:
Sold to
[email protected]
You can also upgrade an existing project to .NET 8 by right-clicking on the project in the Solution Explorer,
choosing Properties, and then changing the Target framework setting:
COLLECTION EXPRESSIONS
Read after Level 12: Arrays.
C# gives you quite a few ways to define an array. The most formal is this:
int[] numbers1 = new int[5] { 2, 4, 6, 8, 10 };
This version requires stating the type, the amount, and the new keyword. In the early days of C#, this was the
only way to create a new array, but over time, we picked up other options that allowed us toSold to of
skip some
those elements if the compiler was able to infer them: [email protected]
int[] numbers2 = new int[] { 2, 4, 6, 8, 10 };
int[] numbers3 = new [] { 2, 4, 6, 8, 10 };
C# 12 gives us yet another option that simplifies things even further, called a collection expression:
int[] numbers4 = [2, 4, 6, 8, 10];
I suspect this compact representation will quickly become commonplace because of its simplicity.
While we haven’t yet discussed any collection type besides arrays, this syntax is not limited to arrays. We will
eventually encounter other collection types that support it, including List<T> (Level 32) and anything that
supports collection-initializer syntax.
This syntax also supports the spread operator, which uses the .. symbol, just like the range operator. (The
compiler can tell the difference depending on the context.) This operator lets you unpack an existing
collection directly into another:
int[] group1 = [1, 2, 3];
int[] group2 = [2, 4, 6];
int[] group3 = [3, 6, 9];
int[] allNumbers = [..group1, ..group2, ..group3, 4, 8, 12];
The spread operator makes it trivial to make a new array as a copy of another, appending a few more values:
int[] numbers = [1, 2, 3];
numbers = [..numbers, 4];
These additional tools for making new arrays and other collection types make array-related code easier to
read and write and are a fantastic addition to the language.
PRIMARY CONSTRUCTORS
Read after Level 29: Records.
How objects come into existence is a significant part of object-oriented programming. It should not come as
a surprise that C# has many ways to define how objects are created, nor that more tools are added as the
language evolves.
Sold to
[email protected]
Traditionally, C# classes and structs were required to define their constructors as we’ve seen in the past, with
a method-like member in the class definition like this:
public class Point
{
public float X { get; }
public float Y { get; }
However, records got special treatment in the form of a constructor defined as a part of the class definition
itself:
public record Point(float X, float Y);
This Point type, defined as a record, will have a two-parameter constructor, even though we don’t see it
explicitly defined in the body of the type definition.
You may occasionally find such syntax useful, even if you are defining a normal, non-record class or struct,
and with C# 12, this is now supported:
public class Point(float x, float y)
{
public float X { get; } = x;
public float Y { get; } = y;
}
With a primary constructor, field and property initializers can reference the parameters defined in the
parentheses. However, unlike a record, such fields or properties will not be automatically created. You must
declare them yourself. (If you want that, use a record.)
This example shows the key benefit of a primary constructor: the code just got much shorter. We don’t need
to write the constructor out; our initialization can be done inline.
On the other hand, if your constructor does anything beyond field and property initialization, you must resort
back to a standard constructor to include that. Many constructors only do initialization and can benefit from
a primary constructor, but not all of them.
There is also an element of stylistic preference. Some programmers prefer the traditional constructor for its
versatility, uniformity, and familiarity. Others prefer primary constructors for their conciseness.
When using a primary constructor, note that all other constructors must chain back to the primary
constructor using the this keyword.
public class Point(float x, float y)
{
public float X { get; } = x;
public float Y { get; } = y;
public Point() : this(0, 0) { }
}
• Replace the constructor with a primary constructor without affecting how the rest of the program functions. Ensure
all properties and fields are initialized through the primary constructor’s parameters.
• Answer this Question: Generally, when writing software, the goal isn’t to write it in as few lines as possible. Instead,
the goal is to write code you can easily change, which requires writing code you can understand quickly. Sometimes,
shorter code is easier to understand because there’s less to read. Other times, shorter code is harder to understand
because it is too compact or sheds helpful information. Compare your finished code against your starting point. The
primary constructor made it shorter, but do you feel it made the code more or less readable?
• After the Challenge: After haphazardly remaking the code in your arrows, you send a few more down at the climbing
coyotes. The plan worked. The arrows tear into the beasts, which tumble down the hoodoo's side to the sand below.
After taking out a few more, the coyotes recognize they’re in danger and flee, leaving you alone in the dark, still night.
ALIAS ENHANCEMENTS
Read after Level 33: Managing Larger Programs.
In C# 12, the aliasing feature of using directives became more flexible. Before C# 12, you could do using
Point = PhysicsEngine.Point;, and then in that file, Point refers to the one found in the
PhysicsEngine namespace. However, the limitation was that the type on the right side of the equals sign
had to be a formal, named type. Now, you can use it for virtually anything, including arrays and tuples:
using Point = (float X, float Y);
using Polygon = Point[];
Aliases can be a way to adjust and simplify names, but be cautious about overusing them. Such aliases aren’t
formalized and are simply masking the true name. Features like records make defining types for these things
almost trivial while making them much more “real.” When possible, I’ll usually choose the following
definitions over these aliases:
public record Point(float X, float Y);
public record Polygon(Point[] Points);
For most practical purposes, this is identical to in. However, it doesn’t break existing code because you call
it like this:
Point somePoint = new Point(2, 3);
Display(ref somePoint);
Old, widely-used code can now be changed to ref readonly without breaking anything while guaranteeing
it won’t modify the memory.
You should now think of in as a variation on ref readonly with relaxed expectations. In particular,
compare these two ref readonly calls, which both produce compiler warnings:
int x = 3;
Display(x); // No modifiers! Compiler warning (but not an error).
Display(5); // Passing in a value, not a variable! Compiler warning (but not an error).
Said another way, in is like a ref readonly parameter that is more tolerant of skipping the modifiers and
is happy to accept values in place of a variable. Those are not expected for a ref readonly parameter, but
the compiler can make it work (thus producing a warning rather than an error).
Once again, reference parameters are complicated. You don’t need to memorize all these details now, but
being familiar with the options will make it easier to return to them when you eventually need them.
The compiler is smart enough to spot that the lambda referenced by function has an optional parameter
with a default value of 0, which supports calling it with or without that parameter. In the first case, with a
specific argument of 10 passed in, that is the value used for x inside the method. In the second case, where
no parameter is supplied, the compiler will call it with the default value of 0, as you’d expect with an optional
parameter.
Sold to
[email protected]