The unsafe keyword can be used in type or method declarations or to declare an inline block.

The purpose of this keyword is to enable the use of the unsafe subset of C# for the block in question. The unsafe subset includes features like pointers, stack allocation, C-like arrays, and so on.

Unsafe code is not verifiable and that’s why its usage is discouraged. Compilation of unsafe code requires passing a switch to the C# compiler. Additionally, the CLR requires that the running assembly has full trust.

Despite these limitations, unsafe code has valid usages in making some operations more performant (e.g. array indexing) or easier (e.g. interop with some unmanaged libraries).

As a very simple example

// compile with /unsafe
class UnsafeTest
{
   unsafe static void SquarePtrParam(int* p)
   {
      *p *= *p; // the '*' dereferences the pointer.
      //Since we passed in "the address of i", this becomes "i *= i"
   }

   unsafe static void Main()
   {
      int i = 5;
      // Unsafe method: uses address-of operator (&):
      SquarePtrParam(&i); // "&i" means "the address of i". The behavior is similar to "ref i"
      Console.WriteLine(i); // Output: 25
   }
}

While working with pointers, we can change the values of memory locations directly, rather than having to address them by name. Note that this often requires the use of the fixed keyword to prevent possible memory corruption as the garbage collector moves things around (otherwise, you may get error CS0212). Since a variable that has been “fixed” cannot be written to, we also often have to have a second pointer that starts out pointing to the same location as the first.

void Main()
{
    int[] intArray = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    UnsafeSquareArray(intArray);
    foreach(int i in intArray)
        Console.WriteLine(i);
}

unsafe static void UnsafeSquareArray(int[] pArr)
{
    int len = pArr.Length;

    //in C or C++, we could say
    // int* a = &(pArr[0])
    // however, C# requires you to "fix" the variable first 
    fixed(int* fixedPointer = &(pArr[0]))
    {
        //Declare a new int pointer because "fixedPointer" cannot be written to.
        // "p" points to the same address space, but we can modify it
        int* p = fixedPointer;

        for (int i = 0; i < len; i++)
        {
            *p *= *p; //square the value, just like we did in SquarePtrParam, above
            p++;      //move the pointer to the next memory space.
                      // NOTE that the pointer will move 4 bytes since "p" is an
                      // int pointer and an int takes 4 bytes

            //the above 2 lines could be written as one, like this:
            // "*p *= *p++;"
        }
    }
}

Output:

1
4
9
16
25
36
49
64
81
100

unsafe also allows the use of stackalloc which will allocate memory on the stack like _alloca in the C run-time library. We can modify the above example to use stackalloc as follows:

unsafe void Main()
{
    const int len=10;
    int* seedArray = stackalloc int[len];
    
    //We can no longer use the initializer "{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}" as before.
    // We have at least 2 options to populate the array. The end result of either
    // option will be the same (doing both will also be the same here).

    //FIRST OPTION:
    int* p = seedArray; // we don't want to lose where the array starts, so we
                        // create a shadow copy of the pointer
    for(int i=1; i<=len; i++)
        *p++ = i;
    //end of first option

    //SECOND OPTION:
    for(int i=0; i<len; i++)
        seedArray[i] = i+1;
    //end of second option

    UnsafeSquareArray(seedArray, len);
    for(int i=0; i< len; i++)
        Console.WriteLine(seedArray[i]);
}

//Now that we are dealing directly in pointers, we don't need to mess around with
// "fixed", which dramatically simplifies the code
unsafe static void UnsafeSquareArray(int* p, int len)
{
    for (int i = 0; i < len; i++)
        *p *= *p++;
}

(Output is the same as above)