I put this one first as it's been on my personal wishlist for
years. It's probably the thing I've always missed the most from Java and it always frustrated me that no other C# engineer I ever spoke to seemed to care!
Anyway, the feature is as follows: With C# 10 we can now declare that every member in a file is scoped under a certain namespace; rather than using a specific scope (with
{ and
}). For example:
// Before
namespace BenBowen.Blog.FileScopedNamespaceDemo {
public class User {
}
}
// After
namespace BenBowen.Blog.FileScopedNamespaceDemo;
public class User {
}
View Code Fullscreen • "File-Scoped Namespace Declarations Before and After"
The file-scoped namespace ends with a semicolon
; rather than starting a scope, and must come before any other member declaration in the file (before or after
using declarations, but preferably after by convention).
This means all your code can have one less unnecessary indentation level! Finally 👨🍳👌!
Property patterns have been improved to chain nested members more naturally using the dot
. operator. Take this example from my previous post:
// Taken from https://benbowen.blog/post/two_decades_of_csharp_iv/property_patterns.html
var salary = user switch {
Manager { ManagerialLevel: ManagerialLevel.CLevel } => 100_000, // C-Level managers get 100,000
Manager { ManagerialLevel: ManagerialLevel.UpperManagement } => 70_000, // Upper managers get 70,000
Manager _ => 50_000, // All other managers get 50,000
{ LastAppraisal: { Rating: 10 } } => 40_000, // Users whose last appraisal gave them a 10/10 rating get 40,000
_ => 30_000 // Everyone else gets 30,000
};
View Code Fullscreen • "Property Patterns"
We can now rewrite line 6 to this:
{ LastAppraisal.Rating: 10 } => 40_000, // Users whose last appraisal gave them a 10/10 rating get 40,000
View Code Fullscreen • "Extended Property Patterns"
Don't forget property patterns can be used in
is expressions:
if (user is { LastAppraisal.Rating: 100 } or Manager { KeyMetrics.FinancialTargetStatus: TargetStatus.AboveTarget }) {
GiveBonus();
}
View Code Fullscreen • "Extended Property Pattern in Is-Expression"
Prepending the word
global in front of a
using directive (that's the directive that's used to import namespaces, typically at the top of the file) will mean that namespace is imported in to
every file in your project.
Something I found quite fascinating is that
using aliases also work, which could be very handy for name clashes between two dependency libraries. Here's an example:
global using System.Linq;
global using StringList = System.Collections.Generic.List<string>;
View Code Fullscreen • "GlobalImports.cs"
// No using statements here
var list = new StringList { "Hello", "I", "Am", "A", "List" }; // StringList instead of List<string>!
Console.WriteLine(list.Sum(str => str.Length)); // And we can use Linq!
View Code Fullscreen • "Main.cs"
Until now it has not been possible to specify a custom parameterless constructor in any struct. However, with C# 10 this functionality has been added:
public readonly struct User {
public readonly string Name;
public readonly int Age;
public User() { // Struct constructor defined with no parameters!
Name = "<no name>";
Age = -1;
}
}
// ...
var u = new User();
Console.WriteLine(u.Name + " " + u.Age); // Prints "<no name> -1"
View Code Fullscreen • "Parameterless Struct Constructor"
As you can see in the example above, it's now possible to specify a parameterless constructor for a struct. If you explicitly want the
default value of a struct, you can still use the
default keyword (i.e.
var user = default(User)), which will
not use the user-defined constructor. Parameterless constructors
must be
public.
This feature was previously unsupported because it creates a potential pitfall for unsuspecting library authors. Although the addition of this functionality is a welcome improvement, I'll now demonstrate that there are numerous ways you can make mistakes using parameterless struct constructors (and associated features):
Before C# 10, it was not permitted to inline-assign values to fields/properties in the struct definition. This made sense, as this syntax is essentially a shorthand for assigning the given values at the beginning of every constructor in the type, and this would include the default constructor. However, C# 10 now permits this:
public readonly struct User {
public readonly string Name = "<no name>";
public readonly int Age = -1;
public User(string name, int age) {
Name = name;
Age = age;
}
}
// ...
var u = new User();
Console.WriteLine(u.Name + " " + u.Age);
View Code Fullscreen • "Inline Struct Field Initialization"
However, what do you think will be printed on the console in this example? If you thought
<no name> -1, I'm sorry to tell you that's not correct (but that was my first guess too)! In fact we'll see nothing and
0 which are the
default values for
Name and
Age. But what happens if we take away the
other constructor?
public readonly struct User {
public readonly string Name = "<no name>";
public readonly int Age = -1;
}
// ...
var u = new User();
Console.WriteLine(u.Name + " " + u.Age);
View Code Fullscreen • "Inline Struct Field Initialization With No Constructors"
Now we
do see
<no name> -1 on the console. I will admit at first this really surprised me, but after a little more thought it makes some sense:
In the example above with inline field initialization and a constructor also provided, it's worth thinking about what this code would look like if User was a class. In actuality, we wouldn't be able to invoke a parameterless constructor in this case, because providing any constructor at all in a class type removes the default one.
However, structs must still always be instantiable with a parameterless constructor, so the parameterless constructor in the struct version of User is not removed by the compiler but instead reverts to acting much like it always has done in the past, providing us a default value (and ignoring our field initializers).
So when
would you want to use field initializers? Well, they're still useful for setting default values when
some constructors will not provide those values:
public readonly struct User {
public readonly string Name;
public readonly int Age = -1;
public User(string name) => Name = name;
public User(string name, int age) {
Name = name;
Age = age;
}
}
// ...
var u = new User("Ben");
Console.WriteLine(u.Name + " " + u.Age);
View Code Fullscreen • "Inline Struct Field Initialization With Multiple Constructors"
In the above example, we will see
Ben -1 printed to the console.
The compiler will still show an error if you neglect to initialize a field in any constructor; either through direct assignment, field initializers, or explicitly chaining your constructor to call another that
does assign it.
All-in-all, I'm not sure allowing us to shoot ourselves in the foot this way is wise. It feels like a bit of a
footgun- adding a non-parameterless constructor to a struct that was previously using only field initialization will change the behaviour of every invocation of its parameterless constructor throughout the codebase from using the field initializers to instead acting like
default(). I
do like the addition of custom parameterless struct constructors, but I could have happily lived without field initializers. I don't think they add much but they have the potential to lead to constly and confusing mistakes, and in my codebases I will probably not use them as much as is possible. I also feel like programmers who aren't aware of these subtleties may get confused when using them or reading any code written that uses them.
So what happens when we have a struct type with a custom parameterless constructor that we want to use as a field in another class or as an array type?
public readonly struct User {
public readonly string Name;
public readonly int Age;
public User() {
Name = "<no name>";
Age = -1;
}
}
public sealed class UserWrapper {
public User WrappedUser { get; }
}
// ...
var uw = new UserWrapper();
Console.WriteLine(uw.WrappedUser.Name + " " + uw.WrappedUser.Age); // What do you think this will print?
View Code Fullscreen • "Struct With Custom Parameterless Constructor as Field"
This prints nothing and a
0 again, i.e. the
default value of the
User type. This means the parameterless constructor is
not being invoked.
This is what I expected, and you should too. As we already saw above, C# now differentiates between the idea of a struct's
default value and its value as initialized by a parameterless constructor. Before C# 10 we could consider these two values one and the same, but now we need to be more careful in thinking about which we're going to get in any given situation. Because we never explicity invoked a constructor for
WrappedUser, we get its default value.
Note that this differentiation also has implications for anywhere that treats new MyStructType() as a constant. For example, whereas public static void MyExampleMethod(MyStructType input = new MyStructType()) { ... } would always compile, it now may not if MyStructType specifies a custom parameterless constructor.
So what about with arrays?
var userArray = new User[3];
Console.WriteLine(userArray[0].Name + " " + userArray[0].Age);
View Code Fullscreen • "Struct With Custom Parameterless Constructor as Array Type"
Hopefully you will be expecting this to print nothing and a
0 again, because that's what we get.
And what about as generic type parameters?
public readonly struct User {
public readonly string Name;
public readonly int Age;
public User() {
Name = "<no name>";
Age = -1;
}
}
// ...
static void PrintStructDetails<T>() where T : struct {
void EnumerateFieldsOnToConsole(T instance, [CallerArgumentExpression("instance")] string? instanceName = null) { // If you're confused about this line, see the "Caller Argument Expressions" section below
foreach (var field in typeof(T).GetFields()) {
Console.WriteLine($"Field '{field.Name}' in {instanceName}: {field.GetValue(instance)}");
}
}
var newedInstance = new T();
var defaultInstance = default(T);
EnumerateFieldsOnToConsole(newedInstance);
EnumerateFieldsOnToConsole(defaultInstance);
}
View Code Fullscreen • "Generic Structs With Custom Parameterless Constructors"
Invoking
PrintStructDetails<User>() results in the following being printed on the console:
Field 'Name' in newedInstance: <no name>
Field 'Age' in newedInstance: -1
Field 'Name' in defaultInstance:
Field 'Age' in defaultInstance: 0
View Code Fullscreen • "PrintStructDetails Output"
I described records in my previous installment of this series:
Record Types. This new feature, enabled by custom parameterless constructors for structs, simply allows you to declare records as structs (value-types) rather than the default (classes/reference-types). You can additionally declare struct records as readonly (just like regular structs):
public readonly record struct User(string Name, int Age);
// ...
var user = new User { Name = "Ben", Age = 32 };
user.Name = "Seb"; // Won't compile, 'Name' is init-only
View Code Fullscreen • "Simple Struct Record Type Definition"
Unlike with 'standard' (i.e. class/reference-type) records, by default a struct record's auto-generated properties are mutable (i.e. they have setters generated for them). You must specify the struct record as readonly to make its auto-generated properties init-only. Therefore, simply changing a standard record to a struct record actually makes your auto-generated properties more mutable than they were before, which is probably not what you want!
You may recall
Caller Info Attributes from a previous version of C#. C# 10 now adds another, the
CallerArgumentExpressionAttribute. This attribute will automatically fill the value
in code that was passed to another parameter. It's probably easiest to understand with an example:
static void Evaluate(int value, [CallerArgumentExpression("value")] string? expression = null) {
Console.WriteLine($"{expression} = {value}");
}
Evaluate(1512 - 19 * 7); // Prints "1512 - 19 * 7 = 1379" on the console
View Code Fullscreen • "CallerArgumentExpressionAttribute Example"
'With' expressions were
introduced in C# 9 as a way to create new record-type instances by slightly modifying a copy of an existing instance. In C# 10 they can now be used automatically with mutable structs:
public struct User {
public string Name;
public int Age;
public User(string name, int age) {
Name = name;
Age = age;
}
}
// ...
var user = new User("Ben", 31);
var birthdayBoy = user with { Age = user.Age + 1 };
Console.WriteLine(birthdayBoy.Name + " is now " + birthdayBoy.Age); // Prints "Ben is now 32"
View Code Fullscreen • "'With' Expression on Struct"
Mutable structs are
generally ill-advised, but luckily this feature also works with init-only properties:
public readonly struct User {
public string Name { get; init; }
public int Age { get; init; }
}
// ...
var user = new User { Name = "Ben", Age = 31 };
var birthdayBoy = user with { Age = user.Age + 1 };
Console.WriteLine(birthdayBoy.Name + " is now " + birthdayBoy.Age); // Prints "Ben is now 32"
View Code Fullscreen • "'With' Expression Support for Immutable Structs"
Thanks to /u/meancoot on Reddit
for pointing that out to me!
You can now use interpolated strings in constant declarations when every component of that string is itself a constant string:
const string AppVersion = "1.2.3";
const string WelcomeMessage = $"Thanks for installing SuperDuperApp. You are running version {AppVersion}.";
View Code Fullscreen • "Const Interpolated String"
This feature allows you to manually handle the interpolation logic for
interpolated strings in your APIs. The primary use-case for this feature is performance-oriented scenarios; for example allowing you to avoid building the resultant string when you know it won't be used anyway.
There are
examples in MS's docs of this scenario already, so I'm going to mix it up a bit. Let's pretend we're dealing with sending data to an embedded hardware device, and we know our string can never be more than 100 UTF-8 bytes due to memory limitations on that device.
And then, let's say that in the case where we have an interpolated string that will end up larger than 100 bytes, we want to prioritize showing the dynamic data (i.e. the data within the
{} braces) rather than the static/constant string literal parts. We have no choice but to truncate the string somehow (because the max is 100 bytes), so it's probably better to keep as much of the dynamic data as we can!
First of all, we must define our API that will send data to our embedded device:
public class EmbeddedClientStream {
public const int MaxMessageLength = 100;
public void SendMessage(string message) {
// TODO truncate to 100 bytes max after encoding and send message
Console.WriteLine($"Sent message: \"{message}\" ({Encoding.UTF8.GetBytes(message).Length} bytes UTF8)");
}
public void SendMessage(EmbeddedClientStreamInterpolatedStringHandler interpolatedStringHandler) {
SendMessage(interpolatedStringHandler.GetFormattedText());
}
}
View Code Fullscreen • "Embedded Client Stream API"
This is just our API, the actual implementation here doesn't matter. We're offering a method
SendMessage that takes a
string parameter to send to our embedded device; and an additional overload that takes an
EmbeddedClientStreamInterpolatedStringHandler. This type, which we will define in a moment, is how we can inject special interpolation logic in to our API. We call
GetFormattedText() on it to get our final resultant string post-interpolation.
It's always a good idea to offer an overload that works with plain string inputs when creating an API that uses a custom interpolated string handler. This means that your API can be used with regular non-interpolated strings as effortlessly as with interpolated ones.
Here's our implementation for
EmbeddedClientStreamInterpolatedStringHandler:
[InterpolatedStringHandler]
public readonly ref struct EmbeddedClientStreamInterpolatedStringHandler {
readonly List<(bool WasDynamicElement, Memory<byte> EncodedData)> _data;
public EmbeddedClientStreamInterpolatedStringHandler(int literalLength, int formattedCount) {
// literalLength is the sum total length of all the literal 'sections' of the interpolated string
// formattedCount is the number of non-literal components to the string (i.e. the number of elements demarcated with {} braces)
// I'm not going to use either of them here
_data = new();
}
public void AppendLiteral(string s) { // This method is called to append a section of the literal (non-interpolated) part of the string
_data.Add((false, Encoding.UTF8.GetBytes(s)));
}
public void AppendFormatted<T>(T obj) { // This method is called to append a dynamic object (i.e. an element enclosed in {} braces)
_data.Add((true, Encoding.UTF8.GetBytes(obj?.ToString() ?? "")));
}
public void AppendFormatted<T>(T obj, string format) where T : IFormattable { // This method is called to append a dynamic object with a format string
_data.Add((true, Encoding.UTF8.GetBytes(obj?.ToString(format, null) ?? "")));
}
public void AppendFormatted(byte[] obj) { // You can even supply specific methods for handling specific types
AppendFormatted(String.Join(null, obj.Select(b => b.ToString("x2"))));
}
public string GetFormattedText() {
var totalLength = _data.Sum(tuple => tuple.EncodedData.Length);
// Note: There's a more efficient way to do this; we could pre-calculate this as the data comes in.
// But it's a blog post about InterpolatedStringHandlers, not efficient algorithms, and I'm tired ;).
// And this will be at the bottom of the post where no one gets to anyway. Prove me wrong by leaving a comment!
while (totalLength > EmbeddedClientStream.MaxMessageLength && _data.Count > 0) {
var lastStaticElementIndex = -1;
totalLength = 0;
for (var i = _data.Count - 1; i >= 0; --i) {
if (lastStaticElementIndex > 0 || _data[i].WasDynamicElement) totalLength += _data[i].EncodedData.Length;
else lastStaticElementIndex = i;
}
_data.RemoveAt(lastStaticElementIndex > -1 ? lastStaticElementIndex : _data.Count - 1);
}
return String.Join(null, _data.Select(tuple => Encoding.UTF8.GetString(tuple.EncodedData.Span)));
}
}
View Code Fullscreen • "Embedded Client Stream Custom Interpolated String Handler"
Now, here's what happens when I send messages with this API:
var clientStream = new EmbeddedClientStream();
var messageAData = new byte[] { 0x3F, 0x7B, 0x14, 0x00 };
var messageBData = new byte[] { 0x47, 0x21, 0xAE, 0x10, 0x3F, 0x7B, 0x14, 0x00 };
var messageCData = new byte[] { 0x4B, 0x6A, 0x77, 0xFF, 0x47, 0x21, 0xAE, 0x10, 0x3F, 0x7B, 0x14, 0x00 };
clientStream.SendMessage($"Discovered missing packet (data {messageAData}). Please ensure shielding is applied (on {LineVoltage.FiveVolt:G} line)!");
clientStream.SendMessage($"Discovered missing packet (data {messageBData}). Please ensure shielding is applied (on {LineVoltage.FiveVolt:G} line)!");
clientStream.SendMessage($"Discovered missing packet (data {messageCData}). Please ensure shielding is applied (on {LineVoltage.FiveVolt:G} line)!");
// Result on console:
// Sent message: "Discovered missing packet (data 3f7b1400). Please ensure shielding is applied (on FiveVolt line)!" (97 bytes UTF8)
// Sent message: "Discovered missing packet (data 4721ae103f7b1400). Please ensure shielding is applied (on FiveVolt" (98 bytes UTF8)
// Sent message: "Discovered missing packet (data 4b6a77ff4721ae103f7b1400FiveVolt" (64 bytes UTF8)
View Code Fullscreen • "Embedded Client Stream API Usage"
Notice how our first message that contained only 4 bytes of data was short enough to include the entire string. However, message 'B' had 8 bytes of data to display, and that was enough to push our encoded data length past 100 bytes, so our interpolated string handler chose to lop off the end of the line ("
line)!"). Finally, message 'C' had so much data that our interpolated string handler had to remove even more; but instead of just truncating off the end of the message it chose to remove the next string literal component and keep the important
LineVoltage parameter in the message content.
In reality, you'd probably want to implement something a bit smarter in a scenario like this that made it clear where parts of the string were being smashed together, but this is just an example!
So, how does this work?
First of all, notice that this is a readonly ref struct. This example can actually work without the ref modifier just fine, but I wanted to demonstrate that these handlers can be ref structs which means you can store spans as fields in them. The readonly modifier is also optional.
Secondly, notice that we've annotated this type with the
InterpolatedStringHandlerAttribute. Omitting this attribute means the compiler isn't aware that our struct is an interpolated string handler, and instead the compiler falls back to binding to the
SendMessage(string message) overload in our API. The constructor must take two
int parameters. If it doesn't, the compiler gives an error when trying to compile calls to
SendMessage().
When we actually invoke SendMessage(), instead of interpolating the string using String.Format() and passing that to SendMessage(string) as usual, the compiler will notice that we have an overload of SendMessage() that takes a type marked as an InterpolatedStringHandler. It will construct an instance of our handler, and then begin using the AppendLiteral() and AppendFormatted() methods to construct the resultant string. I've attached the IL output of one call to clientStream.SendMessage() below:
IL_003d: ldloc.0 // clientStream
IL_003e: ldloca.s V_4
IL_0040: ldc.i4.s 81 // 0x51
IL_0042: ldc.i4.2
IL_0043: call instance void TestingStuff.EmbeddedClientStreamInterpolatedStringHandler::.ctor(int32, int32)
IL_0048: ldloca.s V_4
IL_004a: ldstr "Discovered missing packet (data "
IL_004f: call instance void TestingStuff.EmbeddedClientStreamInterpolatedStringHandler::AppendLiteral(string)
IL_0054: nop
IL_0055: ldloca.s V_4
IL_0057: ldloc.1 // messageAData
IL_0058: call instance void TestingStuff.EmbeddedClientStreamInterpolatedStringHandler::AppendFormatted(unsigned int8[])
IL_005d: nop
IL_005e: ldloca.s V_4
IL_0060: ldstr "). Please ensure shielding is applied (on "
IL_0065: call instance void TestingStuff.EmbeddedClientStreamInterpolatedStringHandler::AppendLiteral(string)
IL_006a: nop
IL_006b: ldloca.s V_4
IL_006d: ldc.i4.1
IL_006e: ldstr "G"
IL_0073: call instance void TestingStuff.EmbeddedClientStreamInterpolatedStringHandler::AppendFormatted<valuetype TestingStuff.LineVoltage>(!!0/*valuetype TestingStuff.LineVoltage*/, string)
IL_0078: nop
IL_0079: ldloca.s V_4
IL_007b: ldstr " line)!"
IL_0080: call instance void TestingStuff.EmbeddedClientStreamInterpolatedStringHandler::AppendLiteral(string)
IL_0085: nop
IL_0086: ldloc.s V_4
IL_0088: callvirt instance void TestingStuff.EmbeddedClientStream::SendMessage(valuetype TestingStuff.EmbeddedClientStreamInterpolatedStringHandler)
IL_008d: nop
View Code Fullscreen • "IL For One Invocation of ClientStream.SendMessage()"
Edit 22nd Jan '22: I had originally said that 'with' statements on structs were not possible with immutable/readonly structs. That is actually wrong, they work with init-only properties. Thank you to /u/meancoot on reddit for pointing that out.