Part 0 - Nullabe Reference TypesPart 1 - Default Interface MembersPart 2 - Pattern Matching EnhancementsPart 3 - All the Small Things
In part 3 I am going to cover the small additions to the language. Of course small does not mean they are not useful. Some of these improve day to day usage a lot more than things like default interface members.
Readonly Struct Members
C# 7.2 introduced the ability to mark a struct as readonly and in this way require that it is fully immutable. Now let me get this straight – mutable structs are pure evil and you should not write one. Well maybe if you are a gamedev… The benefit here is that a readonly struct passed around as an argument with
in modifier would allow the compiler to access its members because it knows they won’t mutate anything. In C# 8.0 we can mark only certain members as such, the most common example being the method ToString. This way we can read from the Necronomicon and unleash the cosmic evil of mutable structs but still make it compatible with in parameters in situations where the struct is not actually mutated. In some situations the compiler makes defensive copies of the struct and these situations can be avoided if the members involved are marked as readonly.
Using Declarations
This is the C# 8.0 feature that I use the most outside of nullable reference types of course. It allows us to use the using statement without defining a block and the block of the using statement becomes the enclosing block. So
void SomeMethod()
{
using(var something = new Something())
{
something.Use();
}
}
Becomes simply
void SomeMethod()
{
using var something = new Something();
something.Use();
}
The block does not need to be a method body. It might be the body of an if or for or whatever block you like. The variable will be disposed at the end of the block. I have found that this feature greatly reduces the indentation in my methods and results in shorter and cleaner code. Great addition!
Disposable Ref Structs
Apparently someone out there is making ref structs which are also disposable but they can’t implement interfaces. This feature lets the compiler recognize a void Dispose() method on this type of struct.
Asynchronous Streams
Have you ever wanted to write code like this?
await foreach (var number in GenerateSequence())
{
Console.WriteLine(number);
}
Yeah, me neither. What this code does is foreach some sequence where each step (each MoveNext) is an asynchronous operation. This is sometimes needed when you are processing a queue or in some other producer/consumer scenarios that pop up more often these days because of cloud-based architectures. This code is powered by the IAsyncEnumerable interface which can be used in asynchronous iterator methods. For example the method above can be defined like this
public static async IAsyncEnumerable<int> GenerateSequence()
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(100);
yield return i;
}
}
That’s it. It is not tremendously helpful because you don’t need it that often and you can manually do it without foreach or iterators if needed but I guess it is not harmful and people probably expect there will be a way to use foreach and iterators in asynchronous contexts and might end up confused while trying to fit the non-async versions to async context. Now this is possible and it does result in some code cleanup.
Asynchronous disposable
Yes, there is IAsyncDisposable with async dispose methods and you can now do
await using with it. Now this feature I find strange and don’t think I’ve ever seen a use case for this in practice but there are probably clever ways of abusing IDisposable where you don’t actually have stuff to dispose but use it for things like transactions where this might be helpful.
Null-coalescing assignment
You know what
?? does? You know what
= does? Well now there is
??=. You assign a new value to a variable if the variable is null. I thought about it for like 20 seconds and decided to adopt it over
x = x ?? y so I guess I’ve decided it is good.
Unmanaged Constructed Types
You know what unmanaged types are right? Yeah, I forget that too.
Here is a refresher. Basically some simple types are classified as unmanaged and can be used as generic constraints. This helps in some performance and interoperability scenarios but most importantly it can be used with stackalloc to allocate an array of those on the stack. The new thing in C# 8 is that you can do this with a generic struct if all the fields for the specified type parameter end up being unmanaged types.
Stackalloc in Nested Expressions
Ever wanted to stackalloc in a nested expression? Never used stackalloc? We’re in the same camp. Apparently now you can do this
Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
var ind = numbers.IndexOfAny(stackalloc[] { 2, 4, 6, 8 });
Console.WriteLine(ind);
I guess this is an improvement and people were surprised that this doesn’t work so now it does.
Enhancement of Interpolated Verbatim Strings
Very important feature! Interpolated strings start with
$, verbatim strings start with
@. If you wanted a string to be both interpolated and verbatim you needed to write
$@, but you could never remember the order and try
@$ first. Now both work!
Indices and Ranges
So apparently that Python guy that a lot of people like despite the fact that he fucked up Python 3 thought it was very important to be able to index an array backwards because
arr[arr.Length – i] is too much of a hassle. Someone else thought that
Range(1, 5) is too long and his keyboard would amortize too fast if he typed it all the time. This is why we have indexes and ranges.
There is the Index type which converts to and from an int implicitly but there is also the
^5 operator which means index from the back. Python uses
-5 for this but in C# that would not be backward compatible with existing indexers which might be defined to do something else with negative indices so we got the new syntax. One problem here is that forward indexing is 0 based but backward indexing is 1 based so the last element is
arr[^1].
The Range type can be used to specify a range using… indices.
var arr = new[] { 1, 2, 3, 4 ,5 };
int[] subset = arr[1..^1]; //contains 2, 3, 4
So following the programming convention the start is inclusive the end is exclusives. You are not forced to index from the start for the start and from the end for the end. The above range is equivalent to
^4..4. Obviously you wouldn’t write it like this. Right? RIGHT?
It is worth noting that the Index and Range types just store some numbers. They do not represent the actual sequence/collection or whatever. The actual work is done by indexers defined with the syntax we’ve been using since C# v1. You just write an indexer that accepts a Range or Index argument and provide the meaningful return type and logic. Some indexers will return arrays, others will return Spans to save allocations, others will return strings it all depends on how the type with the indexer is designed and what makes sense in that particular case.
I personally don’t like this feature because its syntax is symbol heavy and I find its use-cases limited. However people who write parsers claim that this addition is very important and will be a great improvement for their work. Also fuck Python.
Well I think that’s all for C# 8.0. C# 9.0 comes in a couple of months so expect more of my correct opinion.