Dotnet logo

.NET Tools

Essential productivity kit for .NET and game developers

.NET Tools How-To's ReSharper Platform Rider

Equality Analysis, Ref Structs, Culture Previews, Using Directives – C# Language Support in 2024.2

Our release for ReSharper and Rider 2024.2 is just around the corner, and we have lots of exciting features shipping for the new C# 13 and current C# and VB.NET! Since there are so many, we will split them into multiple blog posts. So make sure to check those posts out as well!

In this series, we will cover features around:

Make sure to download the latest version of ReSharper or Rider follow along:

Enhanced Equality Analysis

Struct types have been available since the very first .NET Framework. If you’re taking advantage of them, you may have heard that it’s good practice to override Equals and GetHashCode for performance reasons, because the default runtime implementation suffers from boxing and reflection overhead. This is particularly relevant in hot paths that call equality members. Furthermore, the default implementation of GetHashCode works in a way that it takes the hash of the first non-null reference type field, which may lead to a bad distribution.

In 2024.2, we are introducing a set of new inspections to identify such performance issues where equality is checked under the hood. You can fix these issues by generating equality members, or by turning it into a record struct and letting the compiler generate implementations for you:

Fixing inefficient runtime equality check
Fixing inefficient runtime equality check

We have also added a new attribute [DefaultEqualityUsage] to our JetBrains Annotations NuGet package, which allows you to mark types or parameters as being used for equality checks:

public class Equality
{
    object N() => M(new MyPoint()); // warning

    object M<T>([DefaultEqualityUsage] T obj) => null;

    // no Equals/GetHashCode overrides
    public struct MyPoint;
}
Copy to clipboard

As part of this feature, we also annotated the BCL with the new [DefaultEqualityUsage] attribute and discovered surprising details about some APIs. For example, you might assume that in a ConcurrentDictionary the equality is only checked for the TKey type. That is true until you use AddOrUpdate, which can also check TValue for being equal on update:

var idToData = new ConcurrentDictionary<int, Data>();

// ConcurrentDictionary<TKey, TValue>.AddOrUpdate implementation
// actually checks TValue for being equal on update:
idToData.AddOrUpdate(
    key: id,
    // compute what to add
    addValueFactory: static id => default,
    // do the update
    updateValueFactory: static (id, data) => data);

// no Equals/GetHashCode overrides
public struct Data
{
    // members
}
Copy to clipboard

Last but not least, our analysis also catches suspicious non-structurally comparable types in record types. In the following example, two Payload objects would only be considered equal if they share the same byte[] array reference – this is very likely not the intended behavior. You would rather call SequenceEqual in your custom equality members:

Type with suspicious equality
Type with suspicious equality

Note that this inspection will only be checked if the record type is being used in equality-relevant APIs in your solution.

Ref Struct Interfaces

The introduction of ref struct types in C# 7.2 (Span<T> and ReadOnlySpan<T>) has enabled many new high-performance scenarios. Those benefits came with limitations in a few aspects though. They could not be boxed, could not appear as fields in reference types, could not be stored as array elements, and did not allow interface inheritance. Some limitations are by design, but some could be relaxed during language evolution. One of those relaxations is interface inheritance.

In the following example, we’ve got a generic algorithm PrintArrayLike to print all items of an object of the type IArrayLike. With C# 13, we can make our ref struct ArrayLikeSpan implement that interface. However, invoking the method on that object is still not possible yet! Can you guess why?

var obj = new ArrayLikeSpan([1, 2, 3]);
PrintArrayLike(obj); // error

void PrintArrayLike<TArrayLike>(TArrayLike arrayLike)
    where TArrayLike : IArrayLike
{
    for (var index = 0; index < arrayLike.Length; index++)
        Console.WriteLine(arrayLike[index]);
}

public readonly ref struct ArrayLikeSpan(Span<int> span)
    : IArrayLike // now allowed in C# 13!
{
    private readonly Span<int> span = span; // can store `ref struct` inside `ref struct`

    public int Length => span.Length;
    public int this[int index] => span[index];
}

public interface IArrayLike
{
    int Length { get; }
    int this[int index] { get; }
}
Copy to clipboard

In the method PrintArrayLike, you could theoretically add the following line, which would essentially break the “no boxing” rule:

// cannot box ref struct!
var reference = (object)arrayLike;
Copy to clipboard

Therefore, the compiler requires you to make an explicit decision that the generic method can handle ref struct types and disallows boxing. This is achieved with the allows ref struct anti-constraint, which you can easily add through a quick-fix from the error:

Adding allows ref struct through a quick-fix
Adding allows ref struct through a quick-fix

The allows ref struct constraint will also show up in code completion with the shorthand aref:

Code completion for allows ref struct
Code completion for allows ref struct

Culture Previews

Since ReSharper 8, we’ve been helping you choose the correct formatting specifiers in code completion. However, for types like DateTime or DateOnly, we could only guess which culture will eventually be set when your code executes and how ToString renders, so we chose CultureInfo.Invariant as the default.

With 2024.2, we are adding a new tooltip that shows how a value would render in different popular cultures, including the current OS culture:

Culture preview tooltip
Culture preview tooltip

Sorting Using Directives

Are you in the team of developers that just feels good about their using directives at the top of the file to be minimal and in order? In ReSharper and Rider, we are making sure that all file modifications make use of the same internal API which ensures exactly that. You may also know, that our code cleanup profiles have an option to optimize using directives on a shortcut or save operation. Despite all this effort, files can still end up with unordered and redundant lists of imports, mainly explained by manual edits like merge operations.

In 2024.2, we are making it complete by adding a Sort ‘using’ directives context action that can be triggered independently from all other cleanup steps:

Sorting using directives
Sorting using directives

Conclusion

Give it a go with the latest ReSharper 2024.2 and Rider 2024.2, and let us know if you have any questions or suggestions in the comments section below.

image description