When most developers think of C#, they think safety, productivity, and managed memory. You don’t worry about buffer overruns, dangling pointers, or manual memory cleanup. That’s the beauty of the .NET runtime - it handles all that for you.
But what if you want more control? What if you’re building something performance-critical - like a game engine, a graphics renderer, or a native interop layer - and you need to work directly with memory? That’s where unsafe code comes in.
In this article, we’ll take a deep dive into unsafe code in C#. We’ll explore what it is, why it exists, how to use it, and when to avoid it. You’ll see real examples, learn about pointers and memory addresses, and walk away with a solid understanding of how to use unsafe code responsibly.
What Is Unsafe Code?
By default, C# is a “safe” language. That means the compiler and runtime enforce type safety, bounds checking, and garbage collection. These features prevent entire categories of bugs - like buffer overflows and memory leaks - that plague lower-level languages.
Unsafe code lets you bypass some of those protections. By using the unsafe
keyword, you tell the
compiler: “I know what I’m doing - let me work with pointers directly.”
- Declare and use pointers
- Access memory addresses
- Perform pointer arithmetic
- Call unmanaged code more efficiently
unsafe
{
int value = 10;
int* pointer = &value;
Console.WriteLine(*pointer); // Outputs 10
}
In this snippet, we create a pointer to an integer and dereference it to print the value. This is something you’d
never see in safe C# code - but it works inside an unsafe
block.
Enabling Unsafe Code
By default, unsafe code is disabled. You have to explicitly allow it in your project settings.
In Visual Studio:
- Right-click your project → Properties
- Go to the “Build” tab
- Check “Allow unsafe code”
Or in your .csproj
file:
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
This is a deliberate choice - like flipping a switch labeled “Handle with care.”
Working with Pointers
At the heart of unsafe code are pointers. If you’ve worked with C or C++, this will feel familiar. In C#, a pointer is a variable that holds the memory address of another variable.
unsafe
{
int number = 42;
int* ptr = &number;
Console.WriteLine($"Value: {*ptr}"); // 42
*ptr = 100;
Console.WriteLine($"New Value: {number}"); // 100
}
By dereferencing ptr
, we directly change the value stored at that memory address. This is powerful -
but also dangerous if misused.
Pointer Arithmetic
One of the most unique aspects of unsafe code is pointer arithmetic. You can move a pointer forward or backward in memory - often used when working with arrays.
unsafe
{
int[] numbers = { 1, 2, 3, 4, 5 };
fixed (int* ptr = numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(*(ptr + i));
}
}
}
The fixed
keyword is crucial here. It prevents the garbage collector from moving the array in memory,
which ensures our pointer remains valid.
fixed
when working with arrays or strings in unsafe
code. It prevents pointers from becoming “dangling” due to garbage collection.
Why Use Unsafe Code?
So why would you use unsafe code if it breaks the safety guarantees of C#? Good question. The truth is, unsafe code should be the exception - not the rule. But there are valid scenarios where it shines:
- Performance optimization: Direct memory manipulation can squeeze out extra speed in performance-critical sections.
- Interop with unmanaged code: Unsafe code makes it easier to call native APIs or work with memory buffers passed by C libraries.
- Memory-mapped files: Direct access to memory allows faster I/O in certain applications.
- Specialized algorithms: Some cryptographic or compression algorithms benefit from pointer-level control.
Risks and Pitfalls
With great power comes great responsibility. Unsafe code introduces risks that safe C# code was designed to eliminate:
- Memory corruption: Incorrect pointer arithmetic can write outside the intended memory region.
- Security vulnerabilities: Unsafe code can open the door to buffer overflows and exploits.
- Harder debugging: Bugs in unsafe code are often difficult to track down.
- Portability issues: Unsafe code may behave differently across platforms.
Practical Example: Copying Arrays
Let’s look at a practical example: copying data from one array to another. Normally, you’d use
Array.Copy
or LINQ, but unsafe code gives you another option.
unsafe
{
int[] source = { 1, 2, 3, 4, 5 };
int[] destination = new int[source.Length];
fixed (int* src = source, dest = destination)
{
for (int i = 0; i < source.Length; i++)
{
*(dest + i) = *(src + i);
}
}
Console.WriteLine(string.Join(", ", destination));
}
While this may not outperform Array.Copy
in practice, it shows how unsafe code gives you fine-grained
control over memory.
Best Practices for Unsafe Code
If you decide to use unsafe code, here are some guidelines to keep your project maintainable and secure:
- Minimize usage: Keep unsafe code blocks as small and isolated as possible.
- Encapsulate complexity: Hide unsafe operations behind safe abstractions.
- Profile first: Don’t assume unsafe code will be faster. Measure it.
- Document thoroughly: Make sure future developers understand why unsafe code was necessary.
- Test rigorously: Unsafe code is harder to reason about - invest in good tests.
Summary
Unsafe code in C# is like a double-edged sword. On one hand, it gives you raw power - direct access to memory, pointers, and low-level constructs. On the other hand, it removes many of the safety guarantees that make C# such a productive and reliable language.
Use it sparingly and deliberately. When performance, interop, or specialized algorithms demand it, unsafe code can be the right tool. But always weigh the trade-offs, isolate unsafe sections, and keep the rest of your codebase clean and safe.
In short: unsafe code is powerful, but with power comes responsibility. Treat it with care, and your C# projects will remain both fast and dependable.