Value types have gained more functionality in recent versions.
You can quickly create record structs or value tuples (that auto-implement Equals(obj) & GetHashCode()) and pass them around by reference instead of copying.
record struct RGBA(byte R, byte G, byte B, byte A);
// allocate a small collection on the stack
Span<RGBA> colors = stackalloc RGBA[3]
{
new RGBA(255, 0, 0, 255),
new RGBA(0, 255, 0, 255),
new RGBA(0, 0, 255, 255)
};
// get value by reference
ref var c = ref colors[2];
c = new RGBA(127, 127, 127, 255);
// prints: RGBA { R = 127, G = 127, B = 127, A = 255 }
Console.WriteLine(colors[2]);
// value tuple
(byte R, byte G, byte B, byte A) white = (255,255,255,255);
Heap allocated generic collections of value types have better data locality than collections of reference types.
Ref struct is a type of struct that is always stack allocated and cannot be promoted to the managed heap or boxed. `Span<T>` is ref struct.
Spans allow you to create views over contiguous regions of memory that is located on the stack or on the heap or over native memory. You can pass spans to methods that then read/modify the data in the view.
And many other features that help you manage memory or avoid allocations.
For example. When working with interfaces you can avoid boxing allocation of value types by using a generic type constrained to the interface instead of the interface.