Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
in Rust, methods should be object safe (nora.codes)
61 points by fanf2 on June 1, 2024 | hide | past | favorite | 75 comments


I'll start off by disagreeing with the first sentence:

> I think we should use “method”, in Rust

Just don't. Object-oriented programming is a 1980s buzzword. Let's help industry catch up.

I don't care if people call them functions or methods in daily speech. I even do that sometimes, depending on whom I'm talking to.

But let's not make up a highly technical definition of what a Rust method is.

Methods are loosely the same as functions that take a '&self', and only because your brain had that word for it, not because it's a good word.


Rust-specific nomenclature aside, I think it's useful in technical discussions to distinguish between (object) methods, class methods, and functions if your working language supports any combination of the three.

The word "method" is baked into the official Rust docs[0], and while I personally find the definition in the docs to be satisfying, I don't think the author's argument for narrowing that definition down is unreasonable or (more importantly) unworthy of discussion.

    [0] https://doc.rust-lang.org/rust-by-example/fn/methods.html


> Methods are loosely the same as functions that take a '&self', and only because your brain had that word for it, not because it's a good word.

Isn't this just true of any word with increased specificity? Why call it a square when we already have rectangle?

I'm not really seeing how understanding method vs. function is "highly technical". At least, not more technical than, I don't know, writing Rust code in the first place?


It’s just not a particularly useful distinction IMO. I have worked in C++, C#, Ruby, and Rust over the past 25 years of programming and outside of academic contexts I have never had a discussion with coworkers that hinged upon whether or not a particular function is, was, or ought to be a method.

In academic circles, there’s some meaningful distinctions to be made. In software engineering I’ve yet to find a place where having a very specific name mattered or reduced communication burden.


I write Java daily. I find a lot of value in marking functions as static to make clear that they are not methods. There is a lot less cognitive overhead wondering if they only operate on their arguments or if they drag in their closure and the rest of the universe.


This might be your experiences, but I've never worked with people who called member functions methods in C++. I've seen C# programmers who had never worked with C++ call them that, but in the C++ teams I can't recall an instance of that, even for multi lingual engineers

Though I do tend to work with teams where the average age is higher than the industry average, that might be warping my anecdotal experiences

I have had situations where the distinction matters, and they aren't academic either, but in API design, especially for libraries


I’ve had a similar experience, but if someone called a method a function or vice versa, it’s never been unclear what they meant. And it’s never been something that people would have felt the need to correct someone on.

I am aware of the distinction and I (probably?) instinctively use the two terms “correctly” the majority of the time. But if I just picked one or the other and used that word 100% of the time I would wager there would be zero confusion, except amongst pedants.

For what it’s worth, I say this as an avowed pedant.


> I have never had a discussion with coworkers that hinged upon whether or not a particular function is, was, or ought to be a method

Because _you_ haven’t means the entire industry is wrong?


I’m not saying the industry is wrong. The two words have clear and distinct meaning.

My point is only that fretting over the distinction between the words serves nearly zero utility in day to day software engineering. It’s like “less than” vs. “fewer”. Each has a correct use, but nobody is confused when you use the wrong one. Many confuse the two, to the point that it’s becoming less and less wrong to use one when the other should be used. “Method” and “function” are similar.

In twenty years, nobody is going to care that they once had a distinctive purpose because that distinction serves little utility.


“People will know what I mean, so why bother being correct or specific” is a flimsy argument, to me, as to why a person shouldn’t care about using correct and precise language. Nor does it really challenge the need for correct and precise language in the first place.

Small children say things incorrectly and we still understand them. Should we all talk like that?


Language is fluid and changes over time. Pedantically clinging to distinctions that no longer serve useful purposes doesn’t stop this from happening.

If a clear separation in meaning between the two words was useful, their distinct use would become more closely adhered to over time. If not, people will start to blend and confuse the two. Of those options, the latter has happened and will likely continue happening with or without your objections.

That this has happened is pretty clear evidence that the distinction hasn’t been worth maintaining.

> Small children say things incorrectly and we still understand them. Should we all talk like that?

Fully grown, articulate adults say things “incorrectly” all the time and nobody bats an eye. The sentence “Hey, where you going?” is missing an entire verb and every one of us has said and heard that sentence (or one like it) hundreds of times without anyone noticing or caring.


Yes, exactly. Language is fluid and the value of preciseness depends entirely on context. As in, when discussing language design (like we’re doing in this blog post and these comments), method vs function is important. Because of the context.

So, when you’re talking to your coworkers about…whatever… the context probably makes method vs function insignificant. Unless you work in language design, in which case your take is just pretty blatantly awful.


It is a nice read!

I am not super familiar with the definition of "object-safe" (I read the docs but the definition is not completely trivial for me).

Does it work to say that an "object-safe" method mutates the object? That's what happens when you take `&mut self`, right?

But if I understood this correctly, I guess it means that a function without side-effect in Java should not really count as a method? Because in Java I call it a method whether it has a side-effect or not. Which corresponds exactly to the definition of `method` in the Rust docs, I think [1]. In C++, I would think that this is the difference between a const (no side-effect) and a non-const (side-effect) method.

[1]: https://doc.rust-lang.org/reference/items/associated-items.h...


It has to do with type erasure and knowing how big a type is in memory, not mutation. With traits, you can erase the implementing type and get back a vtable of a trait's method - called a trait object.

A trait is object-safe as long as no trait method returns the implementing type (like T::clone() -> T). Once it's a trait object, T is no longer known and the compiler doesn't know how much memory to allocate on the stack or heap, which makes it impossible to use. That's why trait objects have to be boxed or behind references - all pointers have a known size.


> A trait is object-safe as long as no trait method returns the implementing type (like T::clone() -> T)

That, and much more: https://doc.rust-lang.org/reference/items/traits.html#object... .


This is the shortest, clearest explanation I've yet seen of object safety. Thanks.


Not every trait can be made into a trait object. Traits that are "object safe" can be made into a trait object. Nothing to do with mutation, or side effects.


hmm... so there are object-safe methods and non-object-safe methods, and you can make a `Vec<YourType>` as long as `YourType` does not implement any non-object-safe methods?


> there are object-safe methods and non-object-safe methods

Yes. In the reference, below the definition, there is a list of examples of:

  - Traits that are object-safe with object-safe methods
  - Traits that are object-safe with non-dispatchable methods
  - Traits that are not object-safe
https://doc.rust-lang.org/reference/items/traits.html#object...

> you can make a `Vec<YourType>` as long as `YourType` does not implement any non-object-safe methods?

It is traits that are object safe, not concrete types.

You cannot make a `Vec<YourTrait>`, you have to make a `Vec<T> where T: YourTrait` or a `Vec<Box<dyn YourTrait>>` or similar.

You only need object safety if you're passing around values dynamically cast to a trait. That's not a required programming pattern.


> You only need object safety if you're passing around values dynamically cast to a trait. That's not a required programming pattern.

Yep. In 3+ years of writing rust full time, I’ve still never used `dyn Trait` in any of its forms. A vec where every item is independently heap allocated? Why? And yikes! That would perform terribly.


> A vec where every item is independently heap allocated?

Just because that's the example given (by someone who's trying to understand the concept, no less) doesn't mean that that's the only thing that the language feature is usable for.

Trait objects (read: runtime polymorphism) have their pros and cons just as generics (read: parametric polymorphism) have theirs. Not everything can be known at compile time, and that's perfectly okay.


Its a pointer with a vtable, it wont make a difference in like 99.99% of cases. You very very likely use them because a lot of libraries use them, Box<dyn Error + Send + Sync> is very common.

I used to care so much about this stuff then I learned to not give a sh*t and Arc<dyn Trait> to get stuff done. If you use a service oriented architecture with interfaces to reduce coupling this is the way.


You have likely used dyn result, possibly without realising it, if you have used anyhow


I mean if you know all the concrete types ahead of time you can put them in an enum.

It kind of makes me we wish there was a keyword that would grab all implementations of that trait being compiled and do the same thing though. At least for a statically linked codebase it could figure out all the sizes of the implementations


That would be lovely, but it would give you “spooky action at a distance” where some large struct adding an impl for some trait would change the size of a data structure in another unrelated part of the program.

But it would be nice if there was some unification of the idea of an enum where every variant implements a trait and &dyn Trait. Basically, an enum made up of a set of named structs where each struct implements a specific trait could be a lovely concept.


Yes, except that you need a layer of indirection to make a trait object, so like Vec<Box<YourTrait>> or similar.


You might be confusing types and traits (typeclasses) here; it's not about `YourType` but rather when you want to make a Vec of heterogenous types that share a trait. That's when the question of object safety (and needing a vtable) arises.


Author is correct that Rust is not OOP. It simply has optional dispatch (which does not an OOP language make). I'm sure many of my fellow rustaceans will agree that the lack of OOP is one of the most competitive features of Rust.


Let's be precise about what definition of OOP we're each using.

In college, I (and I'm sure many others here) learned that OOP was about "abstraction, encapsulation, inheritance, and polymorphism".

But in practice, looking at the history of programming paradigms in industry, it seems like the real success of OOP was in dragging us away from "it's fine for mutable state to be global and/or scattered willy-nilly throughout your code" and towards "mutable state should be localized, tightly scoped, and/or coupled to the code that will be mutating it". Being generous to the academic definition, we could say that the only pillar of OOP that really mattered was encapsulation.

And by that definition, since Rust abhors global mutable state (but not mutation in general, as a functional language might do), it's not unreasonable to say that Rust, even if it doesn't have "objects" as Java or Smalltalk define them, still hews to the most useful parts of OOP.


> "mutable state should be localized, tightly scoped, and/or coupled to the code that will be mutating it

Typical enterprise OOP did the opposite of this, coupling state to unrelated code via inheritance. To quote the creator of Erlang, when you want the banana it gives you the whole forest.


> towards "mutable state should be localized, tightly scoped,

Ironically, the way to do actual encapsulation / localized-mutable-state is to just use stack variables - the C way.

Fields (what OOP added) are leakier and less localized than stack variables.


C still has fields in structures; you just have to pass a structure as the argument to a function. As long as you're okay with syntax like "self->field" or "self.field" (given a parameter called "self"), you can still kind of do OOP, just that no fields are private.


> no fields are private.

Who needs the private keyword when C has better encapsulation than Java?

  -- foo.h
  typedef struct Bleh Bleh_t;
  Bleh_t *construct(int i);
  int getFoo(Bleh_t *bleh);

  -- main.c
  #include "foo.h"
  void main() {
    Bleh_t *bleh = construct(3);
    // int i = bleh->foo;
    int i = getFoo(bleh);
  }
The commented-out line above will not compile, because the internals of Bleh_t are not exposed.


That's true, and many libraries do this. However, it comes at the cost of an unnecessary heap allocation that wouldn't otherwise be necessary if not for this encapsulation. If you want to do this, but allocate on the stack, you have to do something like this:

  typedef struct Bleh Bleh_t;
  size_t get_bleh_size();
  void init_bleh(Bleh_t *, int);

  int main(void) {
    const size_t bleh_size = get_bleh_size();
    Bleh_t *const bleh = alloca(bleh_size);
    init_bleh(bleh, 3);
    const int i = getFoo(bleh);
  }
If you rewrite "init_bleh" to return the pointer it receives as the first parameter, you could hide it behind a macro:

  #define make_stack_bleh(i) init_bleh(alloca(get_bleh_size()), (i))
Used like this:

  Bleh *const bleh = make_stack_bleh(x);
One advantage of this approach is that you could change the layout of a Bleh without recompiling the other object files, as your compiled code makes no assumptions about the size or the layout of the struct (i.e. the field offsets into a struct aren't hardcoded into the machine code emitted).


> just use stack variables - the C way

The problem is that C doesn't offer any resistance to using globals rather than stack variables, which led to much of the mess that the OOP revolution of the 80s was supposed to address. For every C program that eschews globals, you have a Toyota Camry, whose control systems famously contained "10,000 global variables" https://news.ycombinator.com/item?id=9643204


> The problem is that C doesn't offer any resistance to using globals rather than stack variables

Nor is there any resistance to writing 'public static' on a field anywhere in Java land. If that's a problem, you have to throw out most languages.


> In college, I (and I'm sure many others here) learned that OOP was about "abstraction, encapsulation, inheritance, and polymorphism".

This is what I was referring to. A problem that follows is that many of the Go4 patterns solve problems that OOP causes in the first place (obviously many others are universal).


I always say it differently: OOP is a paradigm. I've done "object oriented programming" in C and in assembly. Might be more correct to say I've done things like polymorphism in C and assembly. By my definition, Rust can do OOP, even without dyn. I tend to take a very low-level view of things, since I was an assembly programmer before I was a C programmer. I think as Rust broadens its footprint, people are going to have to start using dyn more, and as they do, there'll be new patterns becoming idiomatic. The polymorphism-using-enums pattern is reasonable for tight code like embedded systems, but painful for applications generally.


FYI: https://docs.rs/enum_dispatch

This doesn't work for every case of dynamic dispatch (it only works for what I call closed and semi-open universes of choices [1]), but it works for many of them. And there can always be a catch-all variant that uses a trait object.

[1] https://sunshowers.io/posts/open-closed-universes/


Thanks. I was hoping/dreading that someone had already written such a thing. Hoping, because I have this problem. Dreading, because so much “practical” Rust is macros to make up for missing expressiveness of the language itself.


> The polymorphism-using-enums pattern is reasonable for tight code like embedded systems, but painful for applications generally.

Rust rapidly becomes painful if you try to OOP in it. You would be better off using an OOP language for OOP. It wasn't voted the most loved language because people enjoyed learning lifetimes alongside all the accidental complexity OOP creates.

Dynamic dispatch is mostly seen in two circumstances: the type can't be known at compile time, and reducing code bloat as an optimization step.


Complex vs complicated. Complexity is a property of the problem you are trying to solve. If the problem is complex, the solution is complex. Complicated is a property of the implementation. It happens particularly if the abstractions available in the language of choice can’t represent the complexity of the problem. My impression so far is that Rust is powerful enough to do OOP without issue, but, strangely, doing so is frowned upon, and instead developers add needless complexity to avoid using a feature of the language (dyn).


Accidental complexity is an academic term, it means what you refer to as "complicated." Essential complexity would be what you refer to as "complexity." My comment was being precise about it. dyn is avoided because it is typically slower due to the indirection, and because of how its opacity breaks many zero cost abstractions.


I ported the famous Raytracing in a Weeked from C++ OOP into Rust OOP, without any issues, code available on my Github.

Likewise, Microsoft teams not only didn't had any major issues adding COM tooling support for Rust, it is much easier to deal with COM in Rust, than all the C++ frameworks Microsoft has created throughout the years.

Likewise, code available on the official Windows bindings for Rust.


Rust is OOP. You can pass &self, self, etc. You can dynamic dispatch. It has a lot of great OOP features.

What Rust is not: class-based or inheritance-based (discounting default trait impls).

And this is a nice, modern form of OOP that discards orthogonal and useless design baggage.


By that definition nearly every language is OOP, which makes the term useless.


Yep. All modern general-purpose languages allow to write in object-oriented style.


Just because all languages adopted OOP features.


No, it makes the paradigm ubiquitous.


Only those that never picked a Computer Science book on OOP type systems.


Adhod. I have 18 years of professional experience with OOP, all of it a bloody waste of time. Exhibit: https://youtu.be/ns7jP1-SRvw?si=4NyOYebqtobIkFfC


Apparently not enough to actually understand OOP type theory.


No one that claims to know what OOP is, knows what it means. Because the term is loose and hard to pin down.

If OOP encompasses both Smalltalk AND Rust, it's like saying your definition of X includes a neutron star and giraffe.

By OOP type theory, you mean category theory?

----

Most what people recognize as OOP today is just Class/Inheritance oriented programming, and Rust doesn't belong in that camp, trying to use structural patterns from that kind of OOP in Rust is often a recipe for disaster.


Not at all, OOP type theory is OOP type theory.

Not what most people on the street think OOP is all, with their anti-Java bias, rather the Computer Science studies of type systems and their application across all forms of OOP in all programming languages ever invented.

Rust has enough features from OOP type systems, regardless of the anti-OOP feeling trying to pretend it doesn't.

Static and dynamic polymorphism, methods, data encapsulation, type dispatch, all there.


> Not at all, OOP type theory is OOP type theory.

Tell that to Google. OOP type theory returns mostly Category type theory as results.

What sources do you have on OOP type theory? Our OOP classes were heavy on practical part and less so on theory.

> Rust has enough features from OOP type systems

It's missing inheritance. Without inheritance applying OOP design patterns just fails miserably.

So on one side you have stuff like Java, C++, JS, Ruby, Python and on the other Rust.

> Static and dynamic polymorphism, methods, data encapsulation, type dispatch, all there.

By that definition, Python isn't object oriented (no encapsulation), and C is.

I think it's much more natural to reason about types on what they can do versus some nebulous definition.

I mean any language worth its salt will have encapsulation. It's how you preserve your invariants.

Method is mostly just syntax sugar.

And static/dynamic polymorphism is just a fancy way to say we have function overloading and/or templates and/or vtables (even if using pointer casts).


Search better, look into SIGPLAN and IEEE papers.

Class inheritance is not a requirement for OOP languages.

I listed the OOP traits supported by Rust as a language.

Naturally you have to nitpick it to try to make a point out of nothing.

All high level languages are fancy ways to write machine code.


> I listed the OOP traits supported by Rust as a language.

So is the list missing items?

> Naturally you have to nitpick it to try to make a point out of nothing.

What did I nitpick? Does Python have encapsulation beyond gentlemen's agreement?

Can't C have any of the listed behaviors?


Python OOP != Rust OOP, but you naturally already knew that.

However, in case you need some help, in regards to Python

Classe inheritance, function and data members, dynamic dispatch, polymorphism, meta-classes.

And even if C doesn't offer direct language support for a OOP type system, that isn't stopping anyone, like the Linux kernel and GNOME/Gtk+.

Or publishing books on the matter,

http://www.freetechbooks.com/object-oriented-programming-wit...


> Python OOP != Rust OOP, but you naturally already knew that.

No. Bold of you to assume that. I wasn't aware that one OOP isn't another OOP. I see, so OOP is a fuzzyish definition that you check more checkboxes to satisfy.

If Rust is OOP, and Python is OOP and C is OOP, then the that circles back to my initial claims. A category that is so wide it's effectively useless.

Compare this to sync and async. A precise definition, much greater informational content. You can prove something is pure or not[1]. Compare this to OOP, imprecise, less informational context, impossible to (dis)prove something is OOP or not.

[1]https://www.youtube.com/watch?v=43XaZEn2aLc


Come on, it's an automatically generated dependency graphs of `#include`s which is totally useless, but hardly indicative of "a 5 year old being in charge". Absolutely nothing to do with OOP anyway. If you want that look at Java class hierarchies. I found this great art work to illustrate:

https://www.researchgate.net/figure/Class-Association_fig3_2...


People coming to rust attempting to apply typical OOP terms and patterns are likely to hit walls quickly. There’s are some things that of course look familiar. But it’s not where Rust came from, and it’s not what Rust provides.


The word “method” already has a definition in Rust, which includes all intrinsic and trait methods. I feel like ‘dyn’ is relatively rarely useful and defining the term “method” based on what’s compatible with it is a bad idea.


    // counter_associated.rs
    [...]
            let c = Counter::increment(f);
What is `f` here?


Just a typo. It's meant to be `c` which is defined on the previous line.


This is a strong reason to, either: Write your examples as Compiler Explorer links (or to some other playground tech, Rust has one to itself but it's easier to remember rust.godbolt.org) so you can see when your example doesn't work

Or, write in Markdown and use tooling to perform tests the same way Rustdoc does, if you wrote what claims to be code, just scoop it up and execute it, if it doesn't compile then we find out before we publish.

I'd say 80% of the time when I write more than one or two lines I write a bug, often it's a minor typo that the compiler would catch but sometimes it's a larger mistake and means I should re-evaluate my entire premise.


"Object-safe" really ought to be called "dyn-safe".


[dead]


It's not "I have no clue". It's about what do you mean by "clue", i.e. semantics discussion. I'm trying to figure out what does pjmlp really means by OOP. From what I've seen, it's basically a checklist of features, and the more features you check out, the more OOP you are. What I've learned in uni was basically languages derived from Simula/Smalltalk lineage.

If you presume for a moment, pjmlp is 100% in right. Then it's difficult to find a language that isn't OOP. What is the informational contents in saying well C is OOP, Java is OOP, and Rust is OOP? I mean, you could with a straight face claim that Assembly is OOP, since it has some ad-hoc polymorphism.

Why is this important? If the informational content of a statement is low, "language is OOP" approaches the value of "language is language". An utterly useless statement. You can say the sky is blue - that's a useful statement, based on that you can predict sunny weather. Or "sky is red" again useful, you can predict it's probably sunset. But if you say "sky is in one of visible colors", that's useless.

Is this really different from other language paradigms? I can quickly identify and recognize a structural vs non-structural language (presence of if/else/while, lack of dedicated GOTOs). But because the OOP property of language is so nebulous, it's difficult to identify it. And knowing something is OOP doesn't help you.

----

It's in my opinion, much better to look at what languages can do or can't do. With that in hand, you start to see how Rust OOP is nothing like Java/C#. And how C OOP is nothing like Rust or Java OOP.

Want to implement DOM in Rust? You're going to have a terrible time because it depends on the presence of some kind of inheritance mechanism. And that's very useful for a programmer because it's information you can use for reasoning.


C# is way more similar to Rust than Java, please do not bundle it together: proper generics, async, iterator expressions, trait composition (through interfaces), extension methods, higher order functions, low-level control with `unsafe` syntax when necessary, slice types.


C# the project that's basically follow up to Microsoft's failed EEE (MS Java Virtual Machine) of Java, is closer to Rust than Java? Bold statement.

As Java programer that used both, adaption to C# was hours, while Rust took months if not weeks.

> Proper generics

Rust and C# generics are not the same. Rust uses monomorphization, C# uses reification, Java uses type erasure.

> Async

The syntax is the same, because Rust copied. The mechanisms aren't, but fine. Point for you.

> Iterator expressions

Other than syntax, not really. I do know Rust iterator are highly optimized as in bound checks are elided, but don't know enough about iterator expressions to make of this point.

> Trait composition

Traits and interfaces have little in common. The sooner you empty your head from such nonsense the better.

As extra points Traits + Generics enable a style of programming that's not achievable in C#. Try implementing interface for type that implements another interface and doesn't implement another.

Oh. Java feature is similar, albeit it uses type erasure for now.

> Extension methods

Rust doesn't have this.

It has impl blocks and implementing traits. For example you can't implement a method for type without owning Trait or Type.

> High order function

Yeah it has those. Same as Java.

> Unsafe block

It's named similarly. But the meaning is completely different. pointer access vs undefined behavior.

> Slice types.

Sure.. Fair point.

So you raise two and a half points.

And you completely omit what makes them grossly different: ADT, monomorphic generic, lack of GC by default, trait solver, mutability xor sharing, lack of nulls...


Maybe, but Java programmer would have a lot of issues the moment they go beyond regular LOB application code. Or even when they stay there, the expectation of "it's another Java" would lead to them encountering a lot of surprises. They would also use C# completely ineffectively if they would need to write performance-oriented code. Compared to that, switching from C# to Rust feels like a systems programming focused upgrade, with more than half of the moving parts being familiar the moment you get used to the syntax and understand how they map to C# concepts.

And no, C# has generics, like in Rust, while Java has "generics". Generic arguments of class type in .NET result in method body sharing, yes, and represented in the generic signature as __Canon, this is quite close to dispatching by Box<dyn Trait> in Rust. On the other hand, struct generics in C# and struct generics in Rust have identical behavior codegen-wise - they are monomorphized. CoreLib and many other high-performance libraries heavily leverage this fact to make their internal and external abstractions zero-cost. For example System.Numerics.Tensors which is pure C# BLAS package: https://github.com/dotnet/runtime/blob/main/src/libraries/Sy... combines two operators that are then statically dispatched as it's no different than using traits in Rust save for less convenient syntax.

The biggest drawback to .NET generics is that C# type system is not full Hindley-Miller + ability to specify associated types, so the generic signatures can end up being clunky. But this is not an area where work is done and future C# version will see improvements to generic type inference to reduce effort to write zero-cost abstractions and improve simplicity of generic composition.

I find the mention of .NET origins as an argument unfortunate, because events that happened more than 20 years ago have no relevance whatsoever to what .NET (the non-Framework one) is today.

Async mechanism is closer than you might think as Rust futures are, in a way, state machine step groups, while C# tasks are a more granular and lightweight version of Rust tasks. ValueTasks blur the line slightly due to conditional state machine box allocation. In either case this will lose relevance once runtime handled tasks project is done in one of the future versions which will change the implementation details once more.

In general, if you think that low-level and performance-oriented features are comparable with Java, I'd like to ask you to look into this again. Unsafe syntax in Rust allows relaxed memory safety techniques and enables additional syntax, C# is very similar, there are differences, sure, but the latter too exposes full-blown C syntax for pointers, structs with explicit layout and fixed buffers, safe and unsafe variants of stackalloc (Rust lacks it out of box, heh) and more. It's not just pointer syntax. I could also mention FFI and how the amount of boilerplate even with the Panama project is still laughable.

Writing interop for either of the two is quite comparable - declare exports/imports and map language-native types to FFI-safe, or pass them as is as if they are already, with the calls being more or less direct, possibly statically linked. Not the case with Java.

C# extension methods are Rust associated functions.

Also, C# has Kotlin-style nullability, but nullable structs (e.g. int?) are a sugar for Nullable<T> which is similar to Rust's Option<T>, and can be pattern matched (nullable reference types too, but because this was not built into the language at its inception, there is this divide between nullable reference and value types when it comes to type system representation, particularly felt around generics, which is unfortunate).


> Maybe, but Java programmer would have a lot of issues the moment they go beyond regular LOB application code.

Nope, I wrote a zero copy parser for a game localization. C# is essentially Java with value types, reified generics and some other nifty features (extension methods, stuff like `stackalloc` etc.).

> And no, C# has generics, like in Rust, while Java has "generics".

While I agree to there is some difference in behaviour, most of the time it's not important. The difference starts with method/argument overloading, and generic type information at runtime.

> I find the mention of .NET origins as an argument unfortunate, because events that happened more than 20 years ago have no relevance whatsoever to what .NET (the non-Framework one) is today.

If that's the case, the same can be said of this argument. Java is working towards reified generics and value types, the biggest distinctions between C# and Java. Knowing how we got here and why is important.

> C# extension methods are Rust associated functions.

They aren't. In Rust: Define an implementation of struct outside its owning crate.

> Also, C# has Kotlin-style nullability, but nullable structs (e.g. int?) are a sugar for Nullable<T> which is similar to Rust's Option<T>

Similar yes, but hey a dolphin and a pig are similar - genetically. In practice, Rust is built around the Option<T>, while Nullable<T> feels like absolute kludge especially with ref vs value types.

> In general, if you think that low-level and performance-oriented features are comparable with Java, I'd like to ask you to look into this again.

I didn't ever say that. Sure, it has some low-level perf oriented features. Java is working towards adding them (in a way that doesn't break compatibility). You can write performance oriented code in Java, however it's much harder compared to C#.

Coding high-perf C# code is similarly like a bit of a different dialect, and not most help from other libs.


You must be trolling and not evaluating either on their current state, or have little understanding of the subject matter as others have pointed out.

Please write an efficient text element scanner that uses all AVX512 vector width in Java that matches LLVM codegen and can take any* source of memory? You can easily do that in C#, it's impossible to do it in Java alone.

https://github.com/dotnet/runtime/blob/7cd8459e7bd3883f0aa86...

(it uses portable SIMD and monomorphized generics to express negation to deduplicate vectorized paths, just like you would do so in Rust, except it doesn't have portable SIMD in its standard library yet)

*C# 'ref' variables also known as byrefs also work as special GC-aware pointers which can target any memory - stack, GC heap or anything else including unmanaged source and GC is able to disambiguate that and handle that in a performant manner during mark and sweep. This way you can pass a slice from Rust, construct a Span<T> from its ptr and length and then transparently interoperate with the rest of CoreLib, you could do so with the pointers already to an extent, but those required object pinning which was undesirable for something that would be used so ubiquitously in the code that 99% of users end up utilizing without realizing so.


> You must be trolling and not evaluating either on their current state

I'm not trolling, are you projecting?

You don't seem to understand the difference between Rust trait and C# interface, and confuse extension methods for `impl` blocks.

I'm evaluating on the current state of the LANGUAGE, not the LIBRARIES. Likewise, I do this because language syntax changes tend to be much slower than libraries.

Think of it this way: In one year or twenty, Java and C# will have GC, while Rust will not (at least not by default), versus C# having Portable SIMD and Java and Rust not having it atm. I'm searching for truths that hold for as long as possible.

> Please write an efficient text element scanner that uses all AVX512 you can easily do that in C#, it's impossible to do it in Java alone.

You can use the Vector API (see https://openjdk.org/jeps/469). See what I mean, when I say talking about something that's unlikely to change?

If you were using older versions of Java you would abuse primitive types/ Unsafe package to get extra performance, and yes, dip into JNI if you really need some SIMD tricks.

Yes, please tell me more about Span/ReadOnlySpan C# it's not like I wrote a parser (re)using them ad infinitum... /sarcasm

The problem with C# and low-level coding, is that in GC-free C# land, you're on your own. Libraries are missing (i.e. they allocate a lot) or proprietary, Nullable is a horrible compromise, and C# enums are a joke compared to ADTs.


Please try using Vector API in Java, tell me how it goes (you demonstrably have no idea what you are talking about if you compare these with C# SIMD or think ADTs are not expressible in C#, I guess my last few years of experience with it were a mirage).

But I will argue with you no more, you are welcome to stay in the swamp.


> You demonstrably have no idea what you are talking about.

Do you? You said you can't use AVX512 in Java I pointed out, yes you can. Especially now, but support for auto vectorization in Java has existed for quite some time.

Also you have demonstrably no idea what ADTs are if you think you can use both sum and product types in C# see https://github.com/dotnet/csharplang/issues/113

You argue that Rust a no-GC language is going to be similar to a GC language, especially one that was designed as direct competitor to Java? You're going to have to have post more proof than that. Sure Rust copied C# syntax, and C# copied some stuff from Rust, but C# copied way more of Java's semantics/syntax.

You never addressed issues like way generics, trait and impl blocks work in Rust. You never addressed existence of inheritance in C#/Java. Or Rust trait solver or it's tagged (enum)/untagged unions. Nor its borrow checker, nor its AutoTraits, nor its Affine types.

I've tried to argue with you in good faith, but you seem to have some issue with projecting your own issues onto me.

> Please write an efficient text element scanner that uses AVX512

Here you go: https://github.com/simdjson/simdjson-java




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: