There are two kinds of types in C#: reference types and value types.

  • Variables of reference types store references to their data (objects), while variables of value types directly contain their data.

  • With reference types, two variables can reference the same object; therefore, operations on one variable can affect the object referenced by the other variable.

  • With value types, each variable has its own copy of the data, and it’s not possible for operations on one variable to affect the other (except in the case of in, ref, and out parameter variables).

  • Value types are lighter weight than reference types because they are not allocated as objects in the managed heap, not garbage collected, and not referred to by pointers .

  • A value type instances is usually allocated on a thread’s stack (although it can also be embedded as a field in a reference type object) .

  • A value type as a field in a reference type is stored on the heap as part of the reference type instance.

  • If a value type contains a data member of a reference type, only the reference to the instance of the reference type is copied when a value-type instance is copied.

  • A nullable value type T? represents all values of its underlying value type T and an additional null value.

  • A nullable reference of a reference type is not a new type, but rather an annotation on an existing reference type within a nullable aware context for compile-time analysis.

// M is a method that returns a reference to an integer.
// 'in int x': x is passed as a read-only input parameter.
// 'ref int a': a is passed by reference.
// 'out int b': b is passed by reference as an output parameter.
ref int M(in int x, ref int a, out int b)
{
    // ref local or reference variable: an alias that refers to another variable called the referent.
    ref int c = ref a;

    int[] array = [0];
    // ref assign: makes its left-hand operand an alias to the right-hand operand.
    ref int d = ref array[0];
    d = 25; // array is [25]

    b = x + a + array[0]; // assign a value by the callee.

    // ref return: return a value to the caller by reference.
    return ref a;
}
// 'in T': The input parameter 'arg' is contravariant.
// 'out TResult': The return type 'TResult' is covariant.
public delegate TResult Func<in T, out TResult>(T arg);
// a ref struct type are allocated on the stack and can't escape to the managed heap.
public readonly ref struct Span<T>
{
    // a ref field can have the null value.
    // Use the Unsafe.IsNullRef<T>(T) method to determine if a ref field is null.
    internal readonly ref T _reference;
    private readonly int _length;

    // Omitted for brevity...
}
// a stackalloc expression allocates a block of memory on the stack.
int length = 3;
Span<int> numbers = stackalloc int[length];
for (var i = 0; i < length; i++)
{
    numbers[i] = i;
}

unsafe
{
    int length = 3;
    int* numbers = stackalloc int[length];
    for (var i = 0; i < length; i++)
    {
        numbers[i] = i;
    }
}

// limit the amount of memory allocated with stackalloc, otherwise will overflow stack
const int MaxStackLimit = 1024;
Span<byte> buffer = inputLength <= MaxStackLimit ? stackalloc byte[MaxStackLimit] : new byte[inputLength];
// Like Span<T>, Memory<T> represents a contiguous region of memory.
// Unlike Span<T>, however, Memory<T> is not a ref struct.
// Memory<T> can be placed on the managed heap, whereas Span<T> cannot.
public readonly struct Memory<T> : IEquatable<Memory<T>>
// ArraySegment<T> is a wrapper around an array that delimits a range of elements in that array.
// Multiple ArraySegment<T> instances can refer to the same original array and can overlap.
// The original array must be one-dimensional and must have zero-based indexing.
public readonly struct ArraySegment<T> : ICollection<T>, IEnumerable<T>, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>