Time for another blog series! This time we will focus on the new C# 7.2 language features for which we’ve added support in ReSharper 2018.1 and Rider 2018.1. We will see how they help to improve our code quality and how they work under the hood.
In this series: (we will update the table of contents as we progress)
Let’s get started with something easy …
Leading digit separators in numeric literals
Beginning with C# 7.0, we can use underscores to separate large numeric literals into chunks of digits – thus called digit separators. Using digit separators can help to improve readability in our code base:
// before C# 7.0
var a = 1000000000;
var b = 0x001111000011;
// with C# 7.0
var c = 1_000_000_000;
var d = 0x0011_1100_0011;
Prior to C# 7.2, digit separators were only permitted after the first significant digit of a hexadecimal or binary literal. This limitation has been removed so that we can now use the first group solely to specify the base (or radix) of our numeric literal. Other limitations from C# 7.0 remain the same, including that underscores cannot occur next to the decimal, next to the exponent character or next to the type specifier.
// became valid with C# 7.2
var a = 0b_1_1_0;
var b = 0x_ff_ff_00;
// still invalid
var c = 10_.0; // error: next to decimal
var d = 1.1e_1; // error: next to exponent character
var e = 10_f; // error: next to type specified
Speaking of numeric literals, we hope you didn’t miss our several new context actions for converting between different bases and adding/removing digit separators:
In order to understand ref structs, we first have to understand the concept of value types. Structs, like
char, are value types. They are a more or less a cut-down version of classes, which doesn’t allow inheritance (though they implicitly derive from
System.ValueType) or finalizers. Also, a variable of value type cannot be assigned as
null (we have to use nullable types for that).
When passing a local variable with value type to another method, in reality a copy of this value is passed; so called by-value. This is the opposite of the by-reference approach, where changes in the called method will also be reflected in the call-site:
var i = 5;
Console.WriteLine(i); // Outputs 10
void N(ref int i)
i = 10;
Another characteristic of value types is that they are accessed directly (instead of via pointers) and that they’ll be usually allocated on the stack. This behavior can greatly improve our performance. However, there are cases in which values types can be allocated on the managed heap: whenever we box a value type (e.g. to object or an interface type) or when we declare them as a static or instance member in a reference type. As a result, we might not gain the performance benefit we’ve expected.
Luckily, ref structs can help in high performance scenarios. A ref struct looks like a normal struct, but with a
ref modifier in front of it. When used, the compiler will produce an error for any operation that would expose the object to the heap (for older compilers, it generates an
ObsoleteAttribute on object base classes, like
ToString). Think of it as a stack-only struct:
Ref structs can also be readonly structs, which we will look at a little later.
We’ve already seen how value types can be passed by-reference, and how this can change the value on call-site. While reducing the amount of allocations, we are also exposing ourselves to possible mutations of our local. There is no guarantee that it will keep the initial value we’ve assigned. ReSharper has always notified about this potential issue (and reassignments in general) by highlighting the local in bold (need to enable Options | Highlight Identifiers in Options | Inspection Settings):
Fortunately, C# 7.2 introduces so-called in parameters. We can think of them as readonly ref parameters, which allow to gain all performance improvements while at the same time ensuring that the method (and sub calls) cannot modify the parameter:
public void M(in int number)
number = 5; // error!
As described, the parameter becomes immutable within the declaring method. However, it can still be changed from the outside, for instance when working with several threads. Effectively, the value of an in parameter can still change between multiple accesses.
It also worth to notice that we can pass parameters with or without the
in modifier. However, adding it will enforce our expectation on call-site and prevent changing semantics. The
in modifier is also part of the method signature and therefore influences overload resolution:
There are also cases where in parameters cannot be used: entry point declarations (
static void Main), async methods and iterator methods.
Using in parameters can be quite helpful. However, there is one remaining issue with them that we will examine in the next post. Stay tuned!