C# Best Practices
Category | C# |
---|
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
- When passing collections as parameters, pass an existing instance instead of allocating a new one.
- Implement object pooling
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.
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.