👑

C# Best Practices

CategoryC#

General

const vs readonly

Const variables are replaced by their values during compilation.

Readonly variables are not replcaed during compilation.

Union

In C++, union is a special data type that allows storing different data types in the same memory location.

It's possible to have a similar data structure using the StructLayout attribute set to LayoutKind.Explicit and explicitly provide the offset of each field with the FieldOffset attribute.

[StructLayout(LayoutKind.Explicit)]
struct ByteArray
{
	[FieldOffset(0)]
	public byte Byte1;
	[FieldOffset(1)]
	public byte Byte2;
	[FieldOffset(2)]
	public byte Byte3;
	[FieldOffset(3)]
	public byte Byte4;

	[FieldOffset(0)]
	public int Int;
}

Factory methods

When creating a new instance of a type through a generic method, the creation of the instance is performed using reflection with Activator.CreateInstance.

⚠The performance of Activator.CreateInstance are very poor compared to a regular creation because it uses reflection to find the constructor of the type.

If an exception is throw in the constructor of the type, the excption will be wrapped inside a TargetInvocationException exception.

⚠This is problematic for the caller of the factory method that doesn't not expect this exception to be thrown.

It's possible to catch this exception in the factory method, and throw the inner exception in a way that does not alter the exception's stack track using ExceptionDispatchInfo.Capture and ExceptionDispatchInfo.Throw.

public static T Create<T>() where T : new()
{
    try
    {
        return new T();
    }
    catch (TargetInvocationException ex)
    {
        var edi = ExceptionDispatchInfo.Capture(ex.InnerException);
        edi.Throw();
        throw;
    }
}

Array covariance

Array covariance is a reference conversion.

It allows the conversion of reference array types when assigning an array of type T to an array of type object or any other base type of T.

It doesn't allow the conversion of value types, even if the value types have an implicit or explicit conversion.

string[] strings = new[] { "1", "2", "3" };
object[] objects = strings;

If the objects variable is used for reading the data from the array, the conversion is type safe.

If the array is modified, the CLR performs expensives validations and a run-time error can occur if the argument is of an incompatible type.

objects[0] = 42; //run-time error

The type safety for arrays of reference types during writes can be avoided by using a wrapper array of value types.

public struct ObjectWrapper
{
    public readonly object Instance;
    public ObjectWrapper(object instance)
    {
        Instance = instance;
    }
}

private const int ArraySize = 100;
private object[] _objects = new object[ArraySize];
private ObjectWrapper[] _wrappers = new ObjectWrapper[ArraySize];
private ObjectWrapper _wrapperInstanace = new ObjectWrapper(new object());
 
public void DoArrayCovariance()
{
    for (int i = 0; i < _objects.Length; i++)
    {
        _wrappers[i] = _wrapperInstanace;
    }
}

Allocations

Object reuse

String operations

String operations allocate a new string on the heap. When performing multiple operations, such as concatenations, several intermediate strings are allocated.

Prefer using StringBuilder to concatenate strings without allocating for each string operation. StringBuilder works with a buffer to perform multiple string operations and allocates the string after the sequence of operations is completed.

Parameters

When using params arguments, the compile allocates an array. When there are no parameters, the compiler references Array.Empty, but on older versions, an empty array is allocated.

Boxing

When passing a value type to a method expecting a reference type, boxing occurs. Boxing allocates a new object on the heap that contains a copy of the value type.

Lambdas

The compiler rewrites lambdas by capturing local variables into a class.

LINQ

LINQ static methods allocate the Enumerator class.

Iterators

Using yield return generates a state machine and allocates an enumerator.

async await

Using async await also generates a state machine and allocates an enumerator. It also allocates for handling the Task.

Prefer using ValueTask for the common scenario of a synchronous return value.

Reference Semantics

Since C# 7, reference semantics with value types were introduced.

They allow value types to be used like reference types.

in parameters

A value type passed as a in parameter is passed by reference.

ref return

Using ref return returns a value type by reference.

The caller should store the value in a ref local variable.

💡
Using var will not add the ref modifier.

readonly struct

A readonly struct is immutable. The compiler passes its value by reference.

ref stuct

A ref struct can only be allocated on the stack.

It cannot be contained in a reference type.

Span

Use the Span<T> and ReadOnlySpan<T> when working with contiguous blocks of memory.

Span<T> is a ref struct that can only be allocated on the stack.