Proposal : struct "inheritance" #6270
Replies: 130 comments
-
Sounds like most of the use cases here would be covered by traits/"default interface methods". You couldn't store the state of the field in this To actually implement what you've described above would seem to involve a lot of CLR changes. Is The |
Beta Was this translation helpful? Give feedback.
-
Personally, I'd rather see the "Default Interface" solution substitute any kind of inheritance. Especially for |
Beta Was this translation helpful? Give feedback.
-
Tagging @gafter @MadsTorgersen . This relates to recent discussions we've had about structs and how Default-Interface-Members play along with them. I think the desire to be able to have traits for structs will be very real. I'm hoping that if we take an approach that doesn't support struct-traits initially, we won't end up boxing (har har) ourselves into a corner such that we can't add it in the future. |
Beta Was this translation helpful? Give feedback.
-
It seems like a the default-interface-implementation feature resembles an abstract class, with some functionality built-in but NO instance fields. So maybe you'd have something like this interface. The boilerplate around GetHashCode, ToString, and some other stuff would be handled and would not need to be re-implemented each time.
But many of the features in my EmailAddress struct require creating a new instance of that specific struct, or comparing one EmailAddress to another. I don't want to compare any IValidatedString to another IValidatedString since they might be in different domains (Email vs. CountryCode). Would a "recursive" constraint like this work?
Regarding the "zero" case for SurveyRating, I'd work around that like I did in the EmailAddress case. As for whether Validated is actually a "type" I don't know. Assuming...
Maybe I'm describing something more like a "template". If you couldn't subclass Food from another assembly then the feature would lose a lot of value. Like I said before, this "inheritance" approach is really just shorthand for sharing code between one struct and another. There are many times I attempt to move in the "functional" coding direction by using immutability and value semantics for non-null and equality. I also like the DDD approach of having objects in my code that model the real-world problem domain. For smaller objects this leads me toward structs. Unfortunately structs lack the flexibility of classes - no default constructor and no inheritance - so they end up being a pain to use. |
Beta Was this translation helpful? Give feedback.
-
Structs were never designed to be used with inheritance, even to the extent that it's generally a good idea to avoid having structs implement interfaces as any interaction with that interfaces necessitates boxing the value. As it mentions in the C# Language Design Notes for Apr 19, 2017, it would be incredibly hard to use trait interfaces with structs in any meaningful way without breaking changes or performance issues. If you want a type that supports inheritance, use a class. If you want multiple inheritance, use trait-interfaces with those classes when that feature becomes available. If you want a struct, use composition, which is already available. Nothing else is needed. |
Beta Was this translation helpful? Give feedback.
-
For those of us who prefer to avoid the HEAP when possible, Default Interfaces are the first step towards that kind of thing. We'll also need a borrow operator, struct destructors, and ideally empty struct constructors. |
Beta Was this translation helpful? Give feedback.
-
Really? Surely using structs and ref local/returns should be that first step? Or maybe the first (and last) step should be to just stick with C and manage all your own memory... 🤷♂️ |
Beta Was this translation helpful? Give feedback.
-
@DavidArno , do you agree with the goal of being able to easily create small value-based objects like EmailAddress, PersonId, ProductName and numeric structs with clearly-defined units? If yes, what solution do you recommend? Classes aren't value-types and tax the heap. The best struct composition method I know about is to use generics but then you end up with unwieldy type names like Measurement<float,Length,FloatMath,LengthConversions>. Generic improvements like #395 will help get rid of some of the type arguments but you'll never end up with simple names like |
Beta Was this translation helpful? Give feedback.
-
It seems to have become trendy to bitch about the heap recently. Yet managed memory using a heap and garbage collection has served millions of developers well for years. Sure, if you are writing a game for a mobile device, you may need to sacrifice every last gram of code simplicity to maximise performance. But for the vast majority of folk, the worst thing about classes has always been, and will always be
Type abbreviations get my vote. |
Beta Was this translation helpful? Give feedback.
-
Because it's a widespread problem that we hear about across the board. This includes internal customers and large amounts of external customers (including non-game developers). This feedback has been nonstop over the years and has only grown larger as time has gone on. There are very real reasons that things like Nullable and tuples are value types, and it's precisely because we want these features to be wildly usable, and being expensive types works directly against those goals for so many customers. |
Beta Was this translation helpful? Give feedback.
-
And just how much of that is genuine problems of using the heap and how much misinformation and urban myth, such as a common one that Endlessly giving these folk more and more esoteric struct features just treats the symptoms and doesn't address underlying bad designs. |
Beta Was this translation helpful? Give feedback.
-
Uncle! I'll stick with classes until proven performance problems (or null) gets in the way. |
Beta Was this translation helpful? Give feedback.
-
They're very genuine problems. We work closely with partners and we've seen what's going on with them. We also see this issue directly in all our own first party efforts which have business critical scalability needs. Consider the core code that goes into ASP.net. This is not game code. But it absolutely needs to be efficient as every piece of ASP business logic out there runs on top of it. Remember that C# is the primary language that is used to build the platforms and libraries that so many domains sit on top of. You're right that many devs are served by the heap. But what that fails to recognize is that so much critical work is not served by the heap, and those devs you mention depend on those critical pieces :) |
Beta Was this translation helpful? Give feedback.
-
default interface or anything related could not cover field inheritance. Block of memory with direct access is somehow convenient Instead of the need to write some more code just to do the inheritance imitation. It far more easy to just inherit the struct And also it make us could manage construction,readonly,namecollision and so on |
Beta Was this translation helpful? Give feedback.
-
So if the heap doesn't work well for frameworks like ASP.Net, then obviously those folk will now be making heavy use of structs and ref returns, right? Yet a search of that code base shows zero ref returns. That suggests that writing highly efficient code using the heap is working just great for them. But there again why not just add full inheritance to structs, eh? It's what the folk want, so it must be good, no? 🤷♂️ |
Beta Was this translation helpful? Give feedback.
-
Do you know if that "extensions always" feature proposal is being incorporated into "Roles/Extensions" feature, or is gaining traction? It seems like a really good idea. |
Beta Was this translation helpful? Give feedback.
-
@najak3d let's talk offline about that. |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi wrote: "Finally, it's not my job to be aware of what you do or don't know." Are you seriously implying that you were aware of the ability to pull in extensions via class name? At the same point as you were telling me "that this is not necessary; we're not going to augment how extensions are included just for you". It's blatant that you also were unaware of this, at least at the time you were arguing against me. I'm not saying "it wasn't in your memory bank at all; just that you had at minimum temporarily forgotten about it". |
Beta Was this translation helpful? Give feedback.
-
To summarize, struct inheritance using different types cannot work - ever; struct "inheritance" the way @najak3d wants is basically roles - that's #5497. The only remaining topic is my "augmentation" idea - can somebody champion that one? Please thanks :) |
Beta Was this translation helpful? Give feedback.
-
Yes. You initially indicated you wanted an approach whereby only the new type was defined, and that consumers need not do anything to pick that up. Any and all associations with extensions were rejected by you as being entirely inferior. It was only later on that it appeared that you pivoted to potentially being ok with using if it only brought in the extensions from a single file. When it was pointed out that such a concept doesn't even make sense in C#, you pivoted one final time to stating that you woudl then be ok bringing in extensions from a particular class. I cannot guess as to what you do or don't know. Nor can i wrap my head around the set of things you do or do not find acceptable. My experience so far has been that you have an internal list of things (for lack of a better word) that you just don't like and find distasteful (regardless of how the rest of the ecosystem may perceive them). THis list has not been coherent to me, and it constantly gets strange things added and shifts around as i learn about more things you don't like using. Until now, i had no idea that you ahd an issue with usings. And my only internalization was that it was uniformly bad, and that you would reject any solution down that path for the same reason you rejected the Again, i'm not a mind reader here. I'm having ot adjust to the changing arugment and requirements you're coming up with as you're telling us about them.
I said we were not going to take an entirely alternative way to effectively extend types just because you didn't like that bringing in extensions through a using-statement can pull in tons of things. That remains true. Roles/Shapes will most likely follow the way we bring in extensions today. And if we feel like we need better ways to bring in extensions, we'll just do that, instead of investing some entirely different parallel stack for accomplishing the same. Community members have raised good ideas on how to do these things, and i think an approach of To your direct point, i was the person that literally linked you to the proposal that i felt exactly matched what you were asking for. Namely a no-edit way to pull in extensions for a type. The existing capabilities and whatnot did not seem to eb what you were asking for since you still didn't want any usings at all (and i presumed that that counted global-usings which would be even more problematic since they would apply to everything in your compilation). |
Beta Was this translation helpful? Give feedback.
-
I'd recommend actually trying to start a discussion with that narrow scope, explaining what it is you want and what you don't. I'll tell you that if it's in line with the fork of hte convo you and i were having above, then i'm highly likely to champion it as i think it would be quite nice to have. |
Beta Was this translation helpful? Give feedback.
-
You say jump, I ask how high. |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi - I specifically said that extensions wouldn't be so bad if you could pull them in by class name instead of namespace, and you said "we're not going to change this for you; it's fine as is". This blatantly implies that what I was hoping for was "not a thing yet" -- but it was. If you had instead said "we're in luck, it already does work that way" -- it would have changed the conversation sooner. Luckily @HaloFour was reading, and informed us that it does already work (as I believe you should have recognized yourself, being that you are an expert). We all make mistakes, but it appears to me that you simply aren't willing to admit that you made this mistake. Not sure why... So when "Roles/Extensions" adds support for extending "static members" on a struct -- that should suffice, especially now that I know this can be done in a fine-grained fashion -- class by class, instead of an entire namespace of extensions. And you are right - I don't like a lot of "usings" -- apparently I'm not soo alone, given that for C# 10, they've made this easier to do via the "global using" notation. I also don't like abundant usings -- which clutter the intellisense with lots of stuff. I often tend towards this notations: rather than: If I'm only using 1-2 items from a namespace, I prefer the alias approach, because this way when code is cut/paste to another file, it doesn't mix-up "which Point" you wanted (there are several, all called "Point"). So "SD.Point" can never be misconstrued, and so that's what I tend to do, more times than not. |
Beta Was this translation helpful? Give feedback.
-
I meander on what I want, because part of "knowing what I want" requires "knowing what is already available" as well as "what is difficult to achieve" and "what things open a can or worms of risks", etc.. I'm learning quickly as I go here. Thus I meander, as I should. I'm not firm on what I want, because of new information that comes along. And my focus isn't "what I want", but rather when I see things that I think "many would want" because these are related to issues I've seen for years. |
Beta Was this translation helpful? Give feedback.
-
In C, we can use two different struct :
And it's gonna work the same
Maybe we can have inheritance as long we are clear that struct that inheirts struct essentially put that struct on top of the struct in explicit layout |
Beta Was this translation helpful? Give feedback.
-
How do you handle issues of decapitation, especially when the value gets boxed onto the heap? IIRC, C handles it by ignoring the issue, opening fertile ground for errors, memory leaks, out of bounds memory access, and other goodies of similar ilk. |
Beta Was this translation helpful? Give feedback.
-
This is precisely why C# forbids it, and it's incredibly unlikely that the C# team would consider enabling an entire class of bug for a tiny bit of convenience here. |
Beta Was this translation helpful? Give feedback.
-
@HaloFour But it's high performance computing, we put the entire thing in the box, it's evidential via sizeof () which struct you are dealing with and what you can do with you, for one you won't access places you aren't supposed to, while the default behavior of upcasting in c# with classes is legal it should also be safe when upcasting struct because you are visiting region of the struct with guarantees that it's contained (within the struct), meanwhile downcasting will be illegal as it always has been That being said I am very comfortable with not having inheritance at all because I can already cast a struct to another struct by defining an operator that uses parts of the struct which is built via explicit layout (e.g. Layout.Point2D) |
Beta Was this translation helpful? Give feedback.
-
Please see my proposal here: #5897 |
Beta Was this translation helpful? Give feedback.
-
It's possible, people worried about pointers accessing memory regions they are not supposed to because we changed the type of pointer in a way when we allowed struct inheritance while dealing with the so called 'diamond structure' problem by allowing overlaps of region. But what if it's not over lapped it's additive, what if the efforts is put to renaming the variables by the compiler so that you get distinguishable intellisense results in your editor or at least for example let's all agree that |
Beta Was this translation helpful? Give feedback.
-
Summary
Make it easier to create and use small domain-specific value objects through limited struct "inheritance"; this enables code-reuse and enables simplified struct names.
Motivation
We use classes for "big" objects like Person, Address, and Product but usually use generic string, int, Guid for the "small" values like EmailAddress, ProductTitle, and PersonId. But small intuitively-named domain objects have many benefits:
Validation in one place - Rather than sprinkling validation logic and throw expressions throughout the code to ensure that a string representing an email address is really a valid email address, you can put it in one place - the constructor of the class/struct representing the object. Then use that logic in the UI layer, business layer, and data access layer.
Type-safety - Even though a Product and Person might both be identified by a Guid it never makes sense to join on the two. Even though a Length and Weight are both represented by a float it doesn't make sense to compare them. It is an error to do LengthMeters + LengthInches without conversion. Small specialized domain objects make it more likely to fall into the pit of success because the compiler prevents meaningless comparisons/assignments between values in different domains.
Easier-to-understand code - We depend on parameter/tuple names to know what is going on rather than looking at the types of objects involved. This is a big problem with generic and anonymous delegates because parameter names are not allowed. If domain-specific types are used then parameter names become less necessary. For example, here is a view model that uses string, Guid, etc.
This is the same view model but defined using custom primitives.
A place to put the utility methods - Small domain objects need helper methods like Uri.CheckHostName, String.IsNullOrWhitespace, and Guid.Parse. If you have custom types then you've got a place to put methods like these.
You can use classes for these small domain objects, BUT…
Structs are the right technology for small values. BUT…
ValidatedString<string,Email>
versusEmail
.Measurement<float, Length, MathFloat, ConvertLengthFloat>
instead of justLength
. Type-name aliases can help when defined within a single file but don’t work across a project or assemblies.Example - EmailAddress (validated strings in general)
Here is some code that introduces an EmailAddress struct by wrapping a string. The EmailAddress struct has 43 lines of code but only 1 line is email-specific. By changing this single line you could repurpose the struct to validate all different types of strings - zip code, city, product name, etc. Without some kind of code sharing / inheritance it isn't easy to do that.
Example : Units-of-measure
I quickly threw together some code to do units-of-measure-aware math. This is an example of how quickly the number of type parameters can get out of control. If you had an API that expected a float-based
Length
it would be inappropriate and cumbersome to define a method parameter type asMeasurement<float, Length, MathFloat, ConvertLengthFloat>
.Detailed design
I don't know much about IL, Roslyn, CLR, and the technical differences between structs and classes so bear with me. The overall goal is to make it easier to build and use small domain-specific value objects. This can be achieved by (1) sharing code between structs to limit boilerplate, and (2) enabling user-friendly struct names. One way to do this is to allow structs to "inherit" from other structs, like this.
The syntax is nearly the same as for class inheritance so there isn't too much to say. I suspect that prohibiting virtual methods would make it easier to implement. Abstract methods would enable code-reuse and specialization, especially since it isn't possible to customize behavior with constructor parameters - no default constructor. Because there are no virtual methods it should be possible to know exactly what member is being called at compile-time. Under-the-covers, using reflection, the derived struct may not necessarily look like it inherits from anything. SurveyRating would compile to something nearly identical to what you'd get if you copy-pasted the base-struct code into it and prefixed every member with its fully-qualified path name. Maybe there would need to be other limitations; just like today a generic constraint like
where T : struct, SurveyRating
orwhere T : struct, Validated<U>
would not compile; constraints must be classes or interfaces.Drawbacks
When using code that expects the built-in string, int, DateTime primitives, like Entity Framework, various LINQ providers, and serialization libraries, you'd have to do conversions to/from your specialized domain to the general-purpose types they expect.
Lots of code is needed to wrap primitive types to avoid losing useful functionality of those types. For example, when wrapping numeric types you need to write a lot of code to regain the math operators.
Alternatives and partial solutions
Aliases on structs and string
Type name aliases mentioned in #410 like
public alias Length = Measurement<float, Length, MathFloat, ConvertLengthFloat>
solves the problem of unwieldy names, and lets you use those friendly names across file/project/assembly boundaries. But it has some drawbacks: (1) Without abstract methods, you can only reuse struct code with generics - relying on a default(T) to access its static methods. This was a new technique to me and at first it seemed a bit like a hack, but I'm getting more comfortable with it. It causes type names to keep expanding (more and more type parameters) It seems less OOP, less intuitive, and more heavy-weight than overriding an abstract method likebool IsValid(string s)
. (2) Type-name aliases don't allow you to introduce type-specific instance or static methods, or use type-specific naming.If ALL you had was a type name alias like
public alias EmailAddress = string
that crossed file/assembly boundaries and provided NO restrictions on how to create one other than an explicit cast then that would still be pretty useful. Users of the code might go hunting for the official "CreateAndValidateEmailAddress" method rather than just doing an explicit cast on a string. Not totally type-safe but pretty good.If we had a type aliasing feature I'd want IMPLICIT conversion from a complex named thing to its alias. A method that accepts a LengthFloat should implicitly accept a Measurement<float, Length, MathFloat, ConvertLengthFloat>. But a method that accepts an EmailAddress should only allow a string that has been EXPLICITLY converted. This kind of syntax could work:
The alias feature could be beefed up to include some built-in validation:
This alias feature could be beefed up even more to provide a natural place for type-specific instance and static methods.
Code generation
T4 code templates. I've tried this. It works eventually but is cumbersome due to lack of VS IDE support. You've got to learn a new language (the template language). Maybe some other form of code-generation is in the plan that could work.
Classes instead of structs
Use classes instead of structs. Must be careful dealing with null. Maybe for a lot of apps the performance hit of heap vs. stack doesn't matter. Our computers are insanely fast so I'd be surprised if it would negatively affect typical desktop apps. This is probably more important for things like web services.
Related issues
Internal aliases #259
Type aliases abbreviations #410
Constrained types #413
Better support for delegates, including naming parameters #470
Beta Was this translation helpful? Give feedback.
All reactions