* Rust traits are more flexible than C# interfaces, especially when combined with generics (implementing traits for foreign types, associated types, each method can have its own constraints, conditional trait implementation, #derive)
* Rust has much stronger thread safety guarantees (absence of data races, preventing access to a mutex's data without locking)
* Options are cleaner than null (though at least C# supports non-nullable reference types nowadays)
* Cleaner error handling through `Result`s. In C# you either have to use exceptions (discouraged for errors that are expected to happen), or use ad-hoc approaches which don't benefit from syntax sugar like the `?` operator.
* Support for discriminated unions ("enums")
* I like cargo better than msbuild+nuget ("it just works", feature-flags + conditional dependencies, distribution as source instead of binaries)
On the other hand C# has better IDEs and you don't have to deal with the rigors of ownership and the borrow checker. In particular I struggle with LINQ style functional programming in Rust, since the lifetimes of closures and the fields they close over are difficult to reason about.
I haven't used Kotlin, but expect it to be similar to C# in its strengths and weaknesses. Python is not an option for me, since I like static typing.
However, it's surprising to me that you can get a better experience than VScode + rust-analyzer. It feels like magic when I use it.
But Rust already has perfectly nice control flow mechanisms (look at std::ops::ControlFlow for example) and perfectly nice error handling. So you can use either, or both, as necessary, you don't need this particular Frankenstein's Monster.
Using Exceptions for things that aren't actually exceptional is perverse.
In python using exceptions for some control flow is actually accepted practice if you don't go overboard.
I think it's better that way because it turns them into something familiar instead of pushing them back to some extraordinary circumstances.
The thing that C++ does with exception means that programmers mind is encouraged to stay on happy path because when he'll walk off this path he'll have to deal with this exception handling monster.
So it's better to either not have exceptions and expose unhappy paths to programmer clearly like Rust or Go does, or familiarize exceptions and their handling so programmers can use them easily in all circumstances like in Python.
Just don't do what C++ and similar do. Offer them as a heavy and weird side mechanism reserved only for super exceptional cases.
If you follow “rule of zero” they just work. The problems come when you start implementing your own destructors “to handle exceptions”, which unfortunately seem to be a very common practice in the wild.
Have you checked out mypy recently (past few years)? With all the strict flags, it's pretty damn hard to get anything but bulletproof static typed code to pass. The only major downside is it lacks higher kinded types at the moment, and there is a PR in the works to add that. But it has all of the other accoutrements you'd expect from a modern type system: generics, structural subtyping, co/contravariance.
1. No recursive types, so you can't express a lot of basic things like a "Json" type. This ends up coming up a lot more than you might imagine.
2. Not great support for every pep ie: Protocol
3. Generics are extremely confusing in mypy. Generic classes suffer from the lack of recursive types I'd mentioned as well.
4. Implicit 'Any' everywhere unless you use the strictest settings, which I don't believe anyone does on real code because of the above issues. I've never once managed to get a real codebase to typecheck with mypy.
Honestly mypy works as a somewhat fancy linter, not as a type checker.
- GC pauses, Rc<T> does not, it's simply a deallocation by the last reference holder
- Rc<T> still has predictable memory allocation/deallocation, GC is not guaranteed to run until needed
- More efficient memory use, no need to keep track of allocated objects
- The rest of the Rust language is a pleasure to use (imo)
- Python is slow for certain applications, and not concurrent
- Kotlin is kind of more niche than Rust, it's very popular in the Android community, like C# is popular on Windows
Just my 2 cents, I love progamming in all languages. There are some use-cases where a high level language is absolutely the way to go. For others, Rust provides much more control with a handy escape hatch. Absolutely don't go wrapping everything in Rc<T> like a madman, only when the reduced complexity is more beneficial than dealing with references/lifetimes.
While the last bit is true, it may actually end up having to do lots of work. Think large object graphs where the last reference to any of it goes out of scope.
Also, as a matter of terminology: Rc is a form of garbage collection. It's also a pretty bad general GC strategy at that -- which is why one doesn't just slap an Rc on everything.
There are pauseless GCs (e.g., Azul for JVM), and Go kicked off a trend of super-low-latency GC. Also, RC deallocation is O(N) while GC is usually O(1) (ignoring pedantry about how RC is a type of GC). Further, RC can't handle cycles automatically.
> Rc<T> still has predictable memory allocation/deallocation, GC is not guaranteed to run until needed
Deterministic GC exists, but admittedly isn't widespread. For most non-critical real time systems (e.g., video games) a low latency GC is probably sufficient.
> More efficient memory use, no need to keep track of allocated objects
I'm not sure if this is true? Presumably each RC has an int for its reference counter? I'm not sure what the bookkeeping overhead is for tracing GCs, but I'm guessing it's not O(N)?
> The rest of the Rust language is a pleasure to use (imo)
Agreed, but the borrow checker affects everything so this is a pretty small consolation in practice.
What is N here? Number of allocations?
I don't know about these days but, historically, the received wisdom was "Take the memory your algorithm should need and double it, to leave room for floating garbage and bookkeeping overhead. Otherwise, your performance will suffer as the GC is forced to run more frequently than is ideal."
("Floating garbage" being the jargon for stuff that's inaccessible but not yet collected.)
You do 10,000 reference deferments and free()s.
Reference counting might feel more incremental than GC, but really is not. There are tricks you can use, but you're better off with a fast, modern, pauseless real GC that comes with tons of other benefits.
Look: modern GCs simply don't have pause time issues. The problem has been solved. We have concurrent marking and concurrent sweeping. Stop living in the 1990s!
> You do 10,000 reference deferments and free()s.
You do all that work at the precise point where the last reference was dropped.
What people who complain about GC pauses dislike is the GC causing pauses in completely unrelated threads, including these threads that aren't even allocating or deallocating anything at that moment.
While it is true that many (most?) GCs have STW sections, it's not at all clear that it is a bad tradeoff.
True, but rarely meaningful in practice. People don't anticipate spikes at implicit deallocation sites. On the contrary, they typically use RCs to share data without worrying about exact destructure time (i.e. same reason as GC), and thus will be caught off guard in either case. In fact so much so that even experts frequently introduce accidental RC cycles (causing hard-to-debug leaks).
In the cases where you actually want destruction to be deterministic (e.g. large allocations, graceful teardown, kernel resources etc), neither a traditional GC nor RCs are a good solution.
The simplicity and elegance of Rusts RAII scoped ownership model largely goes away with async, due to the unavoidable "Arc-hell".
This should result in no pause for main thread.
Apparently you need Arc for that not Rc which shouldn't have much overhead over Rc in reasonable scenarios.
RC uses a traditional malloc/free-style heap, which requires bookkeeping for allocations. It also allocates an _additional_ word to track the reference count, which leads to poor cache behaviour (even in single-threaded programs, but especially in multi-threaded programs). Bumping/copying nursery incurs no bookkeeping overhead.
If anything, Rust with refcounts for everything is much closer to Swift.
One of its selling points is zero-cost abstractions and pretty powerful abstractions at that.
And of course, with Python your code will run 100x slower than either of the above, your dependency management will suck, you won't be able to statically compile, and to top it all off everyone will give you recommendations for your ailments which take a long time to try out and inevitably will fail miserably for one glaring reason or another. By way of example: "just rewrite the slow bits in C" -> you rewrite the slow bits in C -> your code is now slower because the marshaling costs exceed the gains + your build system is dramatically more complex and you have the sheer joy of debugging segfaults and undefined behavior (yeah, I know Cython exists).
1. Anything running on the JVM will need a 50-500ms of startup time.
2. The JVM implementation will not reach max performance until the runtime has optimized and JIT'd the relevant code paths.
3. There is memory overhead of the VM itself so your runtime will (all else equal) probably require more memory.
If you do need to use Graal to generate native images though it can actually be quite nice. You can run locally (or in benchmarking environments) on the VM and get all the tooling and metrics that come along with that, but build a native binary to actually deploy. I agree that it can be kind of a pain though.
For what kind of code? I've never experienced comparable performance between JVM and C++ implementations, and I've had the misfortune of writing a couple parallel implementations in recent years where it was literally an "apples to apples" comparison. C++ is faster by default and isn't particularly close, even if you ignore Java's startup and warmup time. I've never seen a native implementation run slower than a JVM one in my entire career, which has included a lot of Java.
This is the expected outcome and easily explainable in technical terms. Performance in modern systems is dominated by memory handling efficiency, where C++ is very strong and JVM is not.
The other thing is that true warmup (not just startup) time to reaching optimization is a lot weirder and less predictable than people realize. See e.g. [1] and [2] for a fascinating analysis (caveat: about a decade old) of the actual warmup characteristics of a number of different VMs (Graal, HHVM, HotSpot, LuaJIT, PyPy, TruffleRuby, and V8). Spoilers: there are bizarre deopt cliffs, scenarios where it appears to optimize and then de-optimizes if you run it long enough, and programs which just never optimize… and this is on benchmark tests!
[1]: https://tratt.net/laurie/blog/entries/why_arent_more_users_m...
[2]: https://tratt.net/laurie/blog/entries/why_arent_more_users_m...