I’ve programmed primarily in C# for over 20 years. Before that, I did a mix of C++ and VB, and a bit of ruby. While I’m a long-time fan of C#, the more I learn about rust, the more it appeals to me. Despite its being a “systems programming” language, I find it has many or all of the important high-level constructs that C# has, and solves some fundamental limitations of C# or its underlying .NET runtime.
Below is a table that contrasts many facets of the two languages. While both languages default to ‘safe’ syntax and offer ‘unsafe’ blocks, I focus here on what can be done without using ‘unsafe’.
|Code sharing||A moderate selection of reusable crates (usually source code), but gaps exist in some areas where rust isn’t well used yet.||A vast ecosystem of experience and reusable libraries via NuGet, usually precompiled.|
|Crate dependencies can be easily patched with a fork from anywhere.||Patching can only be done to the original NuGet package, or by creating an alternate NuGet feed and publishing a proprietary build with a unique version.|
|Memory ownership||Strict memory ownership rules enforced by the language.||API docs and sometimes ‘take ownership’ bool parameters.|
|Freeing memory||Memory released deterministically when it leaves scope. Dereferencing null or a freed pointer is impossible.||Async garbage collection and finalization. Dereferencing null throws an exception at runtime. Memory corruption may occur from reusing buffers after returning them to a shared pool to reduce GC time.|
|Performance||High performance. Standard C toolsets are utilized to leverage decades’ worth of binary optimizations.||Moderately high performance. Tuning may involve recycling memory buffers, which risks ownership and thread-safety problems.|
|Targets||Build for dozens of target architectures and OSs.||Target 3 major OSs and a few major CPUs. Typically build once and run on any of these.|
|Build||Incremental build is superb.|
Initial build speed is slow.
|MSBuild incremental build is often slow, rebuilding far more than is actually necessary.|
Initial build is fast because dependencies are pre-built.
|Compiled to native code protects IP||Compiles to IL which is easily decompiled and inspected, even JIT-decompiled for debugging. Native AOT compilation is sometimes possible but slow to compile, rarely used, and limits the libraries and APIs that are available.|
|One output binary for the application (or dll), with few or no runtime dependencies.||The default builds any number of binary dependencies, plus the .NET runtime. |
Single file executables may be compiled, including a trimmed copy of the runtime itself.
|Testing||Test any code at any level, right next to the code to be tested.||Tests can only be written in a separate project to avoid bloating the shipping assembly. All code to be tested must be visible to the external assembly, even if it’s an implementation detail that shouldn’t be externally exposed.|
|IDE||Great development experience in VS Code, allowing for remote development from a low-powered device to a high-powered workstation.||Great development in Visual Studio (with VS Code support growing). Most development happens on the local machine.|
|Error handling||No exceptions. All failure cases are expressed by returning errors (which can contain arbitrary data) or to crash the process. The language makes it difficult to ignorantly ignore handling these errors.||Unchecked exceptions with arbitrary data that callers may not be prepared to handle. Exceptions come at a substantial runtime cost and are to be avoided in perf-critical paths. They can make it easier to find root causes of bugs with a debugger attached by breaking on first chance exceptions.|
|Multi-threading||Multi-threading is fundamentally safe because of clear memory ownership rules built into the language.||Thread safety is hard due to unclear ownership of data.|
|Async||Await syntax allows minimal code changes to make code async. Complex and less mature than C#, with several ‘runtimes’ that can schedule work.||Mature, friendly async/await syntax. Standard libraries mostly offers both async and sync APIs. Async and await language keywords can be extended to do proprietary things, but this is rarely used.|
|Interop||FFI support allows invoking platform or other native libraries. Win32 FFI support can be auto-generated through the ||P/invoke through static extern methods, but often requires leaving “safe” C#. Support for COM built-in. Win32 p/invokes can be automatically generated with CsWin32.|
|API docs||Unstructured API docs. Publishing the docs is natively supported.||Structured, xml-based API docs that have strong links across APIs. Publishing the docs requires 3rd party tools|
|Polymorphism||Structs + traits replace the need for interfaces and base classes.||Object oriented programming with interfaces and type hierarchies.|
I’m still learning rust. If I got anything wrong (or you like what I wrote), please leave a comment. Did I leave out something important? Share that below too.