.NET Tools
Essential productivity kit for .NET and game developers
Fixed pattern – C# 7.3 in Rider and ReSharper
The latest versions of ReSharper 2018.2 and Rider 2018.2 come with support for C# 7.3. In this series, we are looking at these new language features in C# 7.3. Today we will look at the fixed pattern and simplified access to fixed size buffer elements.
This post is part of a series:
- Declaration expressions in initializers and queries
- Tuple equality
- Unmanaged, delegate and enum type constraints
- Ref local re-assignment
- Fixed pattern and simplified access to fixed size buffer elements
- C# updates for stackalloc
Fixed statement
One of the common themes in C# 7.0 and the following point releases is to make it easier to write high-performance code. The feature we are going to cover today keeps this theme and is aimed to tackle the limitations of the fixed
statement.
Let’s refresh our memory first and look at what the fixed
statement does, and how to use it. Sometimes when interoperating with native code or when writing a performance-critical piece of code, we need to take a variable address and perform some operation on the pointer that stores the address. Because variables allocated on the managed heap can be moved during garbage collection, it’s required to fix them in order to use their addresses safely.
The fixed
statement is intended exactly for this purpose, it “fixes” the movable variable so that its address remains constant for the duration of the statement. The example below demonstrates the use of the fixed
statement:
void Example(int[] arr, string str, Span span) { int arrSum = 0; // array is allocated on the managed heap, fixing required fixed (int* startPtr = arr) { // increment the sum while moving the pointer from the end to start of the array int* endPtr = startPtr + arr.Length; while (endPtr != startPtr) arrSum += *(--endPtr); } // string is allocated on managed heap, fixing required fixed (char* ptr = str) { // pass the pointer and the length into native code function var hash = NativeMethods.HashStringFast(ptr, str.Length); } // taking address of the variable on the stack doesn't require fixing int* sumAddress = &arrSum; // error: cannot take the address of, get the size of, or declare a pointer to a managed type string* strPtr = &str; // error before C# 7.3: the given expression cannot be used in a 'fixed' statement fixed (int* ptr = span) { Console.WriteLine(ptr[0]); } }
Fixed pattern
Before C# 7.3, the fixed
statement was able to work only with arrays, System.String
, fixed-size buffers, or unmanaged variables. The knowledge of how to obtain the pointer to underlying data was hardcoded in the compiler.
This limitation makes using certain types (like Span<T>
, ReadOnlySpan<T>
or ImmutableArray<T>
), which would naturally fit the fixed
statement, either impossible, or unpleasant because of having to resort to the System.Runtime.CompilerServices.Unsafe
API. Fortunately C# 7.3 extended the set of types valid for the fixed
statement by introducing the fixed pattern.
Patterns are used in C# all over the place: the foreach
statement, LINQ queries, await
expression – all these language constructs rely on the shape of the type, i.e. their set of available instance or extension methods and properties. For example, if the type has a method GetEnumerator
and its return type declares method MoveNext
and property Current
, then the type can be used in a foreach
statement. Neither IEnumerable<T>
nor IEnumerator<T>
are demanded by the compiler.
In order for the type to implement the fixed pattern, it should declare a parameterless method GetPinnableReference
that returns the value of the unmanaged type by reference (or read-only reference). The following code snippet shows and example of pattern-based fixed
statement.
Note: read more about unmanaged types in our previous post about new C# 7.3 type constraints.
public sealed class MyFixableType { private readonly int _value; public MyFixableType(int value) => _value = value; public ref readonly int GetPinnableReference() => ref _value; } public unsafe class Test { public static void Main() { var fixableObject = new MyFixableType(42); fixed (int* ptr = fixableObject) { Console.WriteLine(*ptr); // prints '42' } // in .NET Core 2.1 Span<T> and ReadOnlySpan<T> implement the 'fixed' pattern var span = "JetBrains".AsSpan(); fixed (char* ptr = span) { Console.WriteLine(ptr[0]); // prints 'J' } } }
ReSharper and Rider come with a code inspection and quick-fix that allows to automatically generate the GetPinnableReference
method from usage. Moreover, if the GetPinnableReference
method already exists but its signature doesn’t match the expected one, another quick-fix is available to correct the method declaration.
Simplified access to fixed size buffer elements
Let’s cover one more change introduced in C# 7.3: indexing fixed size buffer elements. Fixed size buffer fields are struct members available in unsafe contexts that represent C style in-line arrays. Such fields are rare beasts and primarily used for interoperation with native code. Let’s take a look at how they are declared.
public unsafe struct MyBufferWrapper { // note: size is specified after field name, not after type name as in usual array declarations public fixed byte Buffer[4]; // roughly equivalent to declaring four separate 'byte' fields: // // public byte Buffer_0; // public byte Buffer_1; // public byte Buffer_2; // public byte Buffer_3; }
Although the field declaration above uses the same fixed
keyword as in fixed
statement, here it has a completely different meaning and expresses the constant length of the in-line array rather than constant address.
Before C# 7.3 accessing the elements of a fixed size buffer which can possibly reside in memory movable by GC was allowed only after pinning the buffer with the fixed
statement, while fixed size buffers guaranteed to be located in non-movable memory could be indexed directly.
public unsafe struct MyBufferWrapper { public fixed byte Buffer[4]; public int Foo() => Buffer[0] + Buffer[1] + Buffer[2] + Buffer[3]; // error before C# 7.3 public int Bar() { fixed (byte* ptr = Buffer) { return ptr[0] + ptr[1] + ptr[2] + ptr[3]; // ok } } }
The requirement to introduce auxiliary fixed pointer declaration is unjustified for cases when fixed size buffer is only used to access its elements, because unless address of the buffer is stashed somewhere indexing is always safe.
Luckily C# 7.3 removed the unneeded limitation for indexing movable fixed size buffers and made their use more natural:
public unsafe struct MyBufferWrapper { public fixed byte Buffer[4]; public int Foo() => Buffer[0] + Buffer[1] + Buffer[2] + Buffer[3]; // ok since C# 7.3 public int Bar() { byte* ptr = Buffer; // error: taking address of the fixed size buffer still requires pinning return ptr[0] + ptr[1] + ptr[2] + ptr[3]; } }
Sure enough, ReSharper and Rider detect places where the use of the fixed
statement isn’t required and provide a convenient quick-fix to remove redundant fixed pointer declaration. Needless to say, that quick-fix can be applied globally in the current file, project, or our whole solution.
It’s interesting to see C# not only evolve towards powerful abstractions and more expressive language constructs, but also towards supporting interoperability and writing high-performance code.
Download ReSharper 2018.2 now! Or give Rider 2018.2 a try. We’d love to hear your thoughts!