All posts

How Learning Rust Made Me a Better C# Developer

Learning Rust changed the way I write C# code. Rust’s strict approach to memory safety taught me to be mindful of every object allocation and to carefully choose between stack-allocated structs and heap-allocated classes. The result? A more efficient, thoughtful approach to .NET development.

Mariano Santoro
How Learning Rust Made Me a Better C# Developer

Introduction

I’ve been a C# developer for years, comfortably relying on .NET’s garbage collector to manage memory and rarely thinking about what’s happening under the hood. That changed when I decided to learn Rust. Rust is a systems language without a garbage collector, which forced me to confront details I used to take for granted. The experience was eye-opening. In this post, I’ll share how learning Rust improved my .NET skills and critical thinking – especially in terms of memory management – and how I’ve become far more cautious about heap allocations and deliberate in choosing between structs, classes, and records in C#.

A Shift in Mindset Through Rust

Diving into Rust felt like learning to drive a manual transmission after years of cruising with an automatic. In C#, memory management is largely automatic – the garbage collector silently cleans up for you – so you “cruise” along without worrying much about allocation or deallocation. Rust, on the other hand, is manual (but with guardrails): you have to manage memory ownership and lifetimes yourself, with the compiler double-checking your work. This contrast immediately shook up my routine. As one article cleverly put it, choosing between C# and Rust for memory management is like choosing between automatic and manual cars – C# offers ease of use, while Rust gives you more control and precision.

Initially, Rust’s strict compiler felt like a harsh teacher, constantly reminding me about ownership, borrowing, and lifetimes. I couldn’t even create or drop objects without understanding who owned the data and when it should be freed. It was frustrating at first, but it instilled a discipline in me. I began to appreciate the reasons behind Rust’s rules: they prevent bugs and optimize performance by design. Over time, I noticed this discipline crossing back into my C# work. There’s a saying that learning a new language sharpens your skills in others, and I found that to be true. Rust’s unique approach to memory management, error handling, and data modeling forced me to think differently, and the ideas I picked up have started to make my C# code better.

Becoming Mindful of Memory Allocation

One of the biggest changes in my mindset is how I think about memory allocation when writing C# code. Before Rust, I’ll admit, memory was mostly an afterthought for me in .NET. I would freely create objects, use new all over the place, and trust that the garbage collector would sort it out eventually. If you asked me whether a particular piece of data lived on the stack or heap, or how many allocations a loop was doing, I might shrug – it didn’t seem to matter much for everyday business applications. In fact, a .NET developer can often remain oblivious to who owns what memory or how the GC works, except in the most performance-critical code. That was me: if the app wasn’t obviously slow or leaking, I didn’t worry about memory.

Learning Rust flipped that perspective on its head. In Rust, there is no garbage collector. You quickly learn that every value is either on the stack or in the heap by explicit design, and you must keep track of it. If you clone a string or vector in Rust, you know you’re allocating memory. If you pass objects around, you need to decide whether to transfer ownership (which might avoid copying) or borrow references. This made me much more conscious of each allocation. Coming back to C#, I suddenly found myself thinking, “What’s the cost of this line of code?” For example, if I write a LINQ query inside a hot loop, I now realize it might be generating lots of short-lived iterator objects on the heap. Previously, I wouldn’t have given it a second thought. Now, I consider whether I could refactor that code to use a Span or an array to reduce allocations, or if I should cache some results to avoid repetitive work.

In day-to-day C# coding, this means I avoid unnecessary heap allocations much more aggressively than before. I use techniques that I might not have bothered with in the past, especially if I’m working on performance-sensitive parts of an application. For instance, I might use Span<T> and stack allocation (stackalloc) for working with short-lived arrays of primitives to keep them off the heap. I’m more likely to utilize object pools (e.g. ArrayPool<T>) or reuse objects rather than constantly creating new ones in tight loops. These are things seasoned .NET performance experts recommend, but I only truly appreciated their importance after Rust made me see how often I was allocating without thinking. The result is code that generates less garbage and puts less pressure on the GC, which can improve performance and reduce sporadic GC-related delays.

Perhaps the biggest change is that I no longer see memory management as “magic.” Rust pulled back the curtain, showing me what happens when you allocate and free memory explicitly. With that insight, the .NET garbage collector now feels less like an omnipotent babysitter and more like a tool whose workload I can help manage by writing smarter code. I still love that C# handles garbage collection for me (it’s one of the reasons C# is so productive), but I now strive to help the GC by being careful with allocations. In short, Rust taught me to treat memory as a finite resource, even in a managed language.

Stack vs. Heap: No Longer Invisible

A key part of my newfound caution is understanding what goes on the stack vs. the heap in .NET, and why it matters. Before, terms like “stack” and “heap” were abstract concepts I learned in school or read in blogs, but they didn’t factor into my daily coding. After working with Rust, these concepts became very concrete. In Rust, simple values and structs (without a Box) are stored on the stack by default, and you explicitly use heap allocation (like using Box, Vec, or String) when needed. This made me start asking of my C# code: “Is this data being allocated on the stack or the heap?”

In C#, the rule of thumb is that value types (structs) live on the stack (when they’re local or contained in another object) while reference types (classes, records, arrays, etc.) live on the heap. Understanding this helped me realize why large numbers of small allocations can hurt performance – allocating on the heap is relatively expensive and scattered, whereas stack allocation is blazingly fast and gets cleaned up automatically when a function returns. Moreover, I learned that stack memory is limited (each thread has a fixed stack size), so you can’t just throw everything on the stack either – there’s a balance to maintain.

Now when I design data structures or algorithms in C#, I actively consider where my data should live. For example, if I have a method that’s called millions of times per second, I think about using local value types or Span<T> to do the work on the stack, to avoid generating tons of garbage on the heap. If I need to create a large object or a collection that outlives the current scope, I know it will go on the heap – so I ask myself, “Can I reuse an existing object here? Could this be a struct instead, or will that cause too much copying?” This is a level of critical thinking that I simply didn’t apply before. Rust essentially trained me to ask these questions regularly, whereas earlier in my C# life I only asked them when chasing a performance bug.

Choosing Struct over Class (and Record) Wisely

One very tangible change in my coding style is how I decide between defining a struct versus a class or record in C#. Like many C# developers, I used to default to classes (or records for immutable types) for most objects without a second thought. Structs in .NET were something you only reach for in special cases – often we’re cautioned that making everything a struct can actually hurt performance due to copying costs or boxing. That caution is valid, but Rust gave me a new appreciation for the power of value types when used appropriately.

In Rust, everything is basically a value type by default. When you create a struct in Rust and use it normally, it’s allocated on the stack (or inside another object) and moved around by value. There’s no concept of a class vs struct – there’s just struct, and you choose to allocate it on the heap only if needed (by boxing it or putting it in a heap-allocating collection). Coming back to C#, I saw my old code with fresh eyes: a lot of the classes I had written could have been structs! Many types I create are relatively small, hold just a few primitive values, and don’t need to live beyond a single function or be shared across many places. By making such a type a class, I was forcing it to be on the heap and incurring GC overhead, when it could’ve been a simple struct living on the stack or inside another object.

Now, when defining a new type, I consciously apply the framework design guidelines for choosing between class and struct. The guidelines suggest using a struct for types that are small (ideally less than 16 bytes), short-lived, and represent a single value or concept. They also recommend that structs be immutable and not frequently boxed to avoid performance pitfalls. I’ve started following these rules closely. For example, if I’m representing something like a 2D point (x, y) or a small descriptor that gets created and used within one method, I make it a struct. Such a type is small and often short-lived, so it benefits from stack allocation and value semantics. By contrast, if I have a larger object (say, a complex data structure or an entity with many fields) or something that needs reference semantics (like it’s shared widely or mutated in place), I stick with a class.

I also consider the new record types in this decision. Records in C# are great for immutability and value-like behavior (like auto-generated equality), but a record by default is still a class (hence a heap allocation) unless you explicitly declare a record struct. After learning Rust, I’m more aware of this distinction. If I want an immutable value-object in C#, I ask: should it be a record class (for convenience, but with GC overhead) or a record struct (value type, no GC overhead for each instance)? For small value objects, I often lean towards record struct now, which gives me the best of both worlds: value-type allocation and record-style immutability and equality. This was something I never thought about before – I would just use record (class) without considering the heap impact. Rust’s influence is that I now weigh these trade-offs deliberately.

It’s worth noting that using structs requires some caution. Rust makes value semantics feel seamless (thanks to the compiler checking moves and copies), but in C# a struct can still be misused. If a struct gets too large, copying it around can hurt performance more than using a reference type would. And if you need polymorphism or certain kinds of behaviors, classes are still the way to go. The point is, I no longer default to class out of habit. Rust expanded my toolbox – I now reach for the struct option when it makes sense, whereas before I might have ignored it. This change alone can lead to lower GC pressure in many scenarios.

Better Critical Thinking and Code Quality

Beyond just memory management and type choices, learning Rust has improved my overall critical thinking as a developer. Rust has a reputation for being strict and picky – it forces you to think through your logic carefully to satisfy the compiler. While that can be challenging, it cultivates a mindset of thoroughness and precision. I’ve carried that mindset into my C# programming. I find myself reasoning about code more rigorously now: considering edge cases, thinking about ownership of resources (not just memory, but things like file handles or database connections), and making my functions more self-contained and pure when possible.

A few examples of this broader influence:

  • Immutability by default: In Rust, variables are immutable unless you explicitly make them mutable. This taught me the value of immutability. In C#, I now often make fields readonly, use immutable data structures, or use records for data transfer, to reduce unintended side-effects. This not only prevents bugs but also makes concurrency safer by design (since immutable objects can be shared freely). I credit Rust for getting me into the habit of questioning, “Does this really need to be mutable?”.
  • Null safety and error handling: Rust famously has no null and instead uses the Option type for optional values, and Result types for error handling instead of exceptions. This hasn’t made me ditch exceptions or null in C#, but I’m far more cognizant of the possibility of null references and the cost of exceptions. I’ve embraced C# 8’s nullable reference types to catch null issues at compile time, a practice I might have ignored before. And for certain scenarios, I’ve started to use the Result pattern (returning a Result<T>-like object or using OneOf/Either types from NuGet) to handle errors explicitly when it leads to clearer code. These ideas come straight from Rust’s influence.
  • Performance mindset: Rust’s focus on performance (both in execution speed and memory usage) has made me more performance-conscious in C# even when writing high-level code. I profile more often, think about algorithmic complexity, and consider memory layout. While C# lets you write high-level code without sweating the small stuff, knowing what’s under the hood means I can spot potential bottlenecks or inefficiencies early. It’s a bit like having a performance coach sitting on my shoulder while I code – a coach that sounds a lot like the Rust compiler’s warnings!

In summary, Rust taught me to ask “why” and “how” more frequently in my code. Why use this approach? How exactly will this code run under the hood? This critical mindset leads to cleaner, more robust C# code. It’s not that I couldn’t write good code before, but now I have an extra layer of insight and caution that elevates my programming practice.

Final Thoughts

I started learning Rust out of curiosity, but I didn’t expect it to have such a profound effect on the way I write C# and .NET code. It’s as if Rust trained me to be a more mindful and meticulous developer. By wrestling with Rust’s ownership model and strict compiler, I gained a deeper understanding of concepts that I had taken for granted in the managed world. Memory management, which I used to leave entirely to the garbage collector, is now something I actively consider and optimize when needed. Choosing the right type (struct vs class vs record) and writing high-performance C# code has become more intuitive, because I can see the memory costs and benefits more clearly.

Importantly, this doesn’t mean I treat C# like Rust or try to micro-optimize everything. Rather, I’ve integrated Rust’s lessons in a balanced way: I still enjoy C#’s productivity and don’t prematurely optimize, but I have the knowledge to dig deeper when the situation calls for it. If most of your code is not performance critical, the garbage collector and high-level abstractions are fine – but when I do need to squeeze out more efficiency or ensure better memory usage, I now know how to do it and think of it proactively.

For anyone sitting comfortably in one language ecosystem, I highly recommend stepping out of your comfort zone and learning something like Rust. Even if you don’t use Rust professionally, it can change how you think about your day-to-day programming. In my case, Rust was the rigorous mentor that made me a better C# developer. I’m more careful, I understand my tools at a deeper level, and I write code that’s both safer and faster. And perhaps the best part is that going back to C# with this new perspective hasn’t made me like it any less – in fact, I appreciate both languages more. Each has its strengths, and knowing Rust’s way of doing things has simply expanded my perspective as a programmer.

Learning Rust improved not just my .NET performance tuning skills, but also my general critical thinking in software design. It’s been a challenging journey, but one that paid off in the quality of code I write today. If you’re a .NET developer, give Rust a try – you might be surprised at how it levels up your programming game!

Mariano Santoro
Published January 15, 2026
All posts