Can `struct` be default open ? like c# partial class, or kotlin open class

default open struct is very useful, and can avoid namespace boom.

if struct is default open, we can write code like this :

struct User(age: i32, name:String); 
struct User(phone: String, netadr: String);\

// or even use in MVC

struct User { some model staff}; 
struct User { some view staff }; 
struct User {some controller staff}; 

// or use in ECS

struct User {some entity staff}; 
struct User {some component staff }; 
struct User {some system staff };

we can think, the mod is a special struct or a struct is a special module .

in legacy declare, we need to do like this:

struct UserModel{ some model staff};
struct UserView { some view staff };
struct UserController{ some controller staff };

but in fact, they are all the declare of a User, not anything else.The name

when we impl a struct, the code block of function can define anywhere, and use a mod, we can extend it very easily, why not struct declaration?

1 Like

As someone who is familiar with neither C# nor Kotlin, can you explain what you mean? I've no idea what you're referring to in your post.

12 Likes

I think the OP means allowing multiple partial definitions of a structure and having the compiler merge them into a final definition. Which seems vaguely useful, but would probably need ironing out some details (what about enums?) and Iā€™m not sure it pulls its weight.

2 Likes

I was familiar with basic C# but I'd never heard of this feature before.

From what the official docs and SO answers are telling me, by far the biggest motivation for C# partial classes is allowing code generation to produce most of a class and let the user fill in the rest of it in a separate file, so the generator doesn't have to worry about detecting and working around the user-written code. But AFAIK, the vast majority of use cases for code generators in other languages are implemented with macros in Rust.

The only other interesting argument I saw was that if a class implements several interfaces, it lets you split those up into multiple files. But because Rust puts method definitions in impl blocks separate from the struct itself, this has been always been possible anyway.

So at least from the C# perspective, it seems clear to me that the same motivations simply don't apply to Rust and this feature would indeed not pull its weight at all.

FWIW, in C# this simply isn't supported for enumerations or for delegates (which are kinda sorta like closures).

4 Likes

Please, definitely do not do this.

"It's useful" is not nearly enough of a justification for a feature to be added. Furthermore, I find this redundant at best (for the reasons mentioned by @lxrec), and confusing at worst.

While the ability to define many impl blocks separately is genuinely necessary for a number of reasons (e.g. implementing certain methods only if the generic type arguments satisfy some trait bounds), and it is necessarily true that a single impl block might not contain all the possible behaviors of a type (simply because an impl Trait for Type block might be defined for it even in downstream crates), the ability to do so does not put any more mental pressure on the reader of the code.

In contrast, it's not possible to add fields to a struct after the fact (e.g. in an external crate), and for a good reason: it's pretty much physically impossible to e.g. know the size of such a struct unless every one of its possible complete implementations are known to the compiler upfront, or using extensive runtime introspection, which Rust doesn't, and shouldn't need to, rely on.

In the light of this situation, allowing such "partial definitions" would be a huge source of confusion without the added value/motivation of this feature in C#.

As a closing thought: please don't keep comparing Rust structs to classes in OO languages. Rust structs are not classes, and one shouldn't expect them to behave like so.

12 Likes

It's not possible. Structs must have a fixed size and layout in memory, and Rust expects this to be known immediately when the struct is defined.

Fields are fixed. Methods are more flexible. Within your crate, you can have multiple impl User blocks wherever you want.

impl User {/* entity stuff */}
impl User {/* component stuff */}

Implementation of methods on structs is also sort of open thanks to traits. You can create new traits and implement them on any struct. However, you can only add methods, but not fields.

pub trait View {ā€¦}
impl View for User {ā€¦}
pub trait Model {ā€¦}
impl Model for User {ā€¦}

If you need to extend existing type and add fields, there aren't many good options. You can use a newtype:

struct UserModel {
  user: User,
  model_extras: Model,
}

but that makes a new struct to use instead of User.

7 Likes

As @kornel said, its not possible because we need to know the size and alignment of any struct from the moment it's defined. Moreover, it runs into thorny compatibility conflicts if two different crates define the same field. (This amounts to similar problems to coherence.)

Our concept of how to solve this use case was to allow traits to define fields (possibly also variants), which their implementers would then ultimately fill in. For example:

trait User {
    struct { age: i32, name: String }
}

impl User for MyUser {
    struct { age: self.age, name: self.name }
}

This especially would benefit trit objects, which could implement field access as an offset in the vtable. Today, attempting a similar pattern with accessors imposes a virtual call on each access.

Another way this is useful is as as a better encoding of certain patterns to which composition is not well suited. For example, I have just written some code in which a base type exists called Ring, but this base type is really not meant to be used except as the core logic of another type (such as a File or TcpStream type). Making Ring its own type has the unfortunate effect of adding padding, functionally increasing the size of composite types by 8 bytes. Not a huge deal, but still not great. But users never need to access Ring separately, so padding it is not strictly necessary.

This would be a great use case for representing Ring as a trait with fields. However, for safety and correctness, some of these would want to be fields which the implementer of the trait cannot set (the state field, in particular). This would require some sort of unsafe fields, another idea we've discussed.

All of these ideas are very old, but they aren't being driven by anyone.

11 Likes

I've been trying to think of use cases for this, and the only use case I can think of only involves enums.

One of the issues with creating heterogeneous collections is that you either need to put everything you want to store in the collection in an enum, or you need to store boxed trait objects and downcast to the right type (which still requires you to know about types you want to downcast to). Having an open enum that allows you to add new variants from anywhere in the code would make life much easier.

That said, what I just outlined is quite niche, and would likely cause headaches somewhere else that I haven't thought of. I can't think of any good reason for an open struct, and I can think of lots of reasons not to do so.

So, on balance, I'm against this, both for structs and for enums. However, it would still be useful to be able to do this within a crate. I know virtually nothing about writing macros, but I know several of the people here are very experienced in doing so; is there a way of writing a macro that will let you distribute the definition of a struct or enum across different files/modules/etc. within a single crate? That would let you simulate an open struct/enum, while still keeping the current behavior of rust.

Note also that Kotlin's open class is not a partial. Kotlin's open class is just a regular non-final Java class, and Kotlin's class is Java's final class.

Given your use of something vaguely similar to ECS, I suspect you don't actually understand the principles and design patterns of ECS. Even with MVC though, which allows pushing all three responsibilities onto a single object, it's considered bad practice, as the whole point is separation of concerns.

5 Likes

FWIW, the places where I use partial class in C# are well handled by the fact that in Rust 1) one can have multiple inherent impl blocks for the same type and 2) that one can add trait implementations separate from the struct definition. So it's not obvious to me that Rust needs a feature here.

That said, note that C#'s partial class only works in the same assembly, so the equivalent in Rust would work in the same crate. Under that restriction the "Rust needs to know layout" objections that a few people have mentioned would not be a problem; the compiler could merge them just fine. (Though it'd probably want to prohibit repr(C) on such a thing as the source order would be very unclear.)

5 Likes

Importantly, C# Partial Classes are not intended to be manually written. Their purpose is for code-generators to generate part of the class definition in one file, and then the developer completes the definition of the class in another file. Basically, it is a way to have a well-defined way of having part of a class generated and part hand-written. This is often used by Visual Studio tooling like: GUI designers, Entity Framework/ORM, etc.

5 Likes