-
Notifications
You must be signed in to change notification settings - Fork 1k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal : struct "inheritance" #524
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 |
Personally, I'd rather see the "Default Interface" solution substitute any kind of inheritance. Especially for |
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. |
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. |
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. |
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. |
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... 🤷♂️ |
@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 |
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. |
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. |
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. |
Uncle! I'll stick with classes until proven performance problems (or null) gets in the way. |
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 :) |
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 |
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? 🤷♂️ |
That's not the claim that was made.
That's not what's being asked for. -- It's hard to engage with you David as you seem to resort to strawmen a lot. |
@DavidArno Currently now I can't compile my code using Sometimes new feature just not become standard immediately because it cannot work with old code so people need to use old patter for such times. It need sometimes such a years to have people acknowledge and adopt any new feature. This was always a problem since generic in C# 2 and async in C# 5. Most people need to have sometimes to get used to new feature Not to mention many people write code to have backward compatibility. And not to mention most code is legacy and they don't want to change old one. And all things rely on that legacy library would be more likely to adopt the same pattern as the library their used (When people use library that still use Even today there are people still using C# in unity still cannot even use |
David, the claim is not that in order to get fast code that you need something like "structs everywhere". The issue is more that eventually you get to the point where you you have squeezed what you can out of the system, and you still face things like scalability bottlenecks preventing you from fully utilizing the system, and you are spending cycles on things that aren't desirable. Asp, for example, has come to us many times with issues they've run into where they've done everything possible on the existing .net fx + C# and they are still being hit with perf issues that there is no existing solution for. This is one of the drivers around the Again, contrary to your strawman, there is no need for them to make "heavy" use of this feature. They simply need to make effective use of the feature for their domain. The same is true for something like "ref returns". In realistic codebases, you might only end up using ref-returns in a smattering of places. For example, some of the core update loops of your game engine. The intent is not that you would ref-return your entire codebase (indeed, that could likely lead to worse performance). Instead, the intent is to give you this capbility so that in the few places you need it, you actually have it available as it can amount of a significant win in performance. -- I also wanted to address this point on a personal level:
Oh how i wish taht were so. Roslyn itself has direct experience on how that isn't the case, and the problems with the heap, and GC have been a constant source of issues with us from the start. This is why, for example, types like SyntaxList and SyntaxToken are structs. It's not because we liked them better as structs, it's because the performance problems we had with these being classes were significant. Similarly, we invested ImmutableArray (another struct) precisely because we had the need for a collection with immutable semantics that was 0-overhead over the underlying array data. Likewise, many core parts of Roslyn cannot use Linq, Lambdas, or Iterators, because the cost of those features is simply too high and there is no non-heap alternative to them available. Sure, 90% of roslyn can still use the heap just fine. But it is absolutely the case that neither the heap, nor GCs is sufficient for us to get the performance we need. This is why we've ensured that Tuples can be used effectively even in perf critical code. It's why we've invested in ValueTuple. It's why we're looking deeply into more and more features that provide rich expressiveness, while not hitting the heap. Our own direct experience makes us painfully aware of exactly the sorts of issues that so many customers (1st and 2nd party) are facing all the time. |
|
You stated: "then obviously those folk will now be making heavy use of structs" I was literally referring to your exact statement. -- Again, to make the point clear: Heap issues are felt consistently by teams working to reach high levels of performance and scalability. We've been hearing this for 10+ years and we've been working closely with many teams, both internal and external about this over the years. Asp.net is one such team, and their direct experiences have been driving several efforts recently to address both that:
None of my claims have been that people with perf needs would be heavily using these features. that's your own claim, which you then proceeded to knock down. |
And again, please read about our own direct experiences with the heap+allocations+GC. These issues are significant and man decades of effort have been spent on the problem so far in roslyn alone. Heck, i've been putting in tons of effort into this space in several roslyn features, to attempt to get things to scale up even more than they can today, precisely because we have people using our products that are having memory issues, and we want to find ways to alleviate things for them. |
Oh, so you were. That's embarrassing. My apologies then as I was wrong to accuse you in that way. |
@DavidArno Just a personal piece of advice. We have a lot of customers that we have to consider when thinking about the language and where it is going. Some of these customers themselves are quite important and represent huge platforms themselves with many millions of their own customers. Please understand that your own experiences and your own views on what is or isn't important may not be at all representative of the rest of the ecosystem. It's also true that your views may be representative of some group (including a large one), but that we may still feel work is important if the net impact is significant. i.e. we may do work that will help out a few dozen direct customers, but which then ends up impacting millions of customers indirectly. I would recommend not just jumping to the conclusion that something is "trendy" and doesn't actually represent a real and important concern out there. As you have seen (and lamented), we actually tend to be quite conservative over here. So when we actually feel like something is worth addressing, it's usually because we now have so much data, and so much continued feedback about the necessity of this work, that we've gone far past 'trendiness' and we're doing things because they're actually important. :) |
Other people recommending something very similar: dotnet/roslyn#104 Pseudo-inheritance https://roslyn.codeplex.com/discussions/562037 Related to method contracts dotnet/roslyn#119; however if you're writing a contract to ensure that a parameter is in a particular range that could be solved more elegantly by defining a new type whose name clearly identifies what is expected. Databases have column-level constraints for length, nullability, and range expressions. Another proposal (with same EmailAddress case), like aliases dotnet/roslyn#58 Single-case union types in F#, As I scan the list of ideas for the next version of C#, it seems like there are many big ideas with a big payoff. I am a hobbyist programmer so I can't argue from real-world experience that this struct inheritance capability is relatively important. As I write code, all those generic Guid, string, int references don't map to the way I think about the problem and I worry about invalid data getting passed around. Enums feel good/specific in a way that strings and ints don't. But fixing it via custom structs (or even classes) for every CustomerId, ProductId, Email, etc. is cumbersome and non-idiomatic. I would love to see some kind of lightweight solution here that lets me use friendly domain-specific type names throughout my code, across assemblies, and makes it a bit harder (like an explicit conversion) to pass around invalid data. |
Struct inheritance would actually have been very useful for a better public class A
{
public (int Count, int Total) Sum(IEnumerable<int> items)
{
var count = 0;
var total = 0;
foreach(var num in items)
{
total += num;
count++;
}
return (Count, Total);
}
}
public class B
{
public int GetSum(IEnumerable<int> items)
{
var a = new A();
return a.Sum(items).Total;
}
} Could have compiled to something like: public struct ValueTuple_A_Sum_int_int : ValueTuple<int, int>
{
public count
{
get { return Item1; }
set { Item1 = value }
}
public total
{
get { return Item2; }
set { Item2 = value; }
}
}
public class A
{
public ValueTuple_A_Sum_int_int Sum(IEnumerable<int> items)
{
var count = 0;
var total = 0;
foreach(var num in items)
{
total += num;
count++;
}
return new ValueTuple_A_Sum_int_int(total, count);
}
}
public class B
{
public int GetSum(IEnumerable<int> items)
{
var a = new A();
return a.Sum(items).Total;
}
} |
This sort of thing makes me wonder if open-sourcing C# was such a good idea after all. As we saw with scope leakage for out vars, no matter how large the community around that open source project grows, it will still remain tiny compared with those important customers. No matter how united its opposition to something, it will be ignored if those big customers want that something. |
@DavidArno He who pays the piper, calls the tune. After all we can make our points and maybe give some unexpected input to the team. They in reverse forward that input to their key customers. IMHO, it is better to have a little say than none at all. 😊 |
I'm also going to assume that the number of active participants in these discussions on github are vastly outnumbered by the people with whom Microsoft communicates regularly about product direction and development. It's likely less that our voice matters less, there just aren't as many of them. It would be nice to drive all of that discussion through one place, like here, but I'm sure that's not possible. |
You are right, of course. I had hoped that them creating this repo would make it that one place, but that probably won't happen. |
I also think that they expected a lot more community interaction. I've seen in mentioned on threads before that there was disappointment in how few active participants were drawn to either the Codeplex or Github repos. There's probably, what, 2-3 dozen of us that regularly comment/complain/debate here? I've tried a number of times to get coworkers and friends involved at some level and most just claim that they don't have the time. |
That proposal has not been implemented. It is discussing a different concept. The implemented idea is that you can statically import a type in a file and get the extensions defined within that static-class brought into scope. THe referenced proposal is stating that if you have an instance of any named type, that you also bring in automatically any extensions to it defined in teh same namespace that the named type was declared in. in effect, it's the opposite of what we support. you would need no usings at all but could still get extensions. |
You'll have to provide explanations in order to get others to understand why. :) |
I'd say that putting my @CyrusNajmabadi hat on -- the new C# feature of global using's provides the near equivalent benefit... you just state it once, and now you have what you want automatically - everywhere inside your project. Isn't that what you say about nearly everything I propose? Yet here is a proposal where it's very very true -- yet you don't come to this same conclusion? (this is why I tend to think your comments on my ideas may be biased against me specifically) |
I think most know why. It's more direct/clear, not accomplished via a more mysterious "global using classExtension" statement found somewhere within your project. Plus, you get fine grained control everywhere, easily, to determine if you want to see those new extensions or not. The struct extension notation is superior without doubt, and I've already explained it (although it should be obvious). However, I can accept the Extensions/Roles solution, as it still gets the job done easy enough. |
It is not the same benefit. One requires you to even know tehre are extensions and then add teh namespace so you can use it. The other no knowledge or explicit step to make them available :) They're solving different problems. |
I said nothing about conclusions. I was simply linking you to the corresponding issue. Why do you think my conclusions are different? |
Correct - there is nominal benefit here. You omit the requirement of the library user to "know about the extensions". I realize this as this is part of why I Extensions/Roles isn't the optimum notation for solving my desire for "struct extension derivations". I don't want users to have to "know about these mysterious/helpful extensions" (which are easily pulled in, once you know about them). |
Right. That's why i pointed you to this proposal as it solves the issue you seem to be concerned with, and it doesn't require creating a duplicate system for defining extensions.
I literally was linking you to this to support the objective you were claiming you wanted. Why would i do that if i were biased against you? I was showing that there's a proposal, and community interest in solvign such a topic. And that proposal shows a way to solve things taht doesn't require a duplicative approach to adding members that is outside of the extensions-space of the language.. |
Because you recommended I look at that proposal without noting that "It's mostly been satisfied now by C#10 with global usings", and therefore, may no longer be viable. |
I literally just explained that that isn't the case. In a post you've already responded to. I do not think that proposal has been satisfied. |
OK - I'll accept that. I think that proposal is nice too. If an extension exists inside the same namespace (even if that namespace is not from the base library) -- I'd like to see it "auto-included" without a "using". I see your point. I do like this proposal. I retract my statement about you being biased against me. :) |
Again... that's the point i'm making. You are assuming extensions/roles work a certain way and thus are not sufficient for your needs, and thus you've proposed an entirely disparate system for solving the same problems. I'm pointing out that extensions/roles can not only solve the problems they aim to solve, but yours as well, through proposals liek the one linked. We dont' have a need to build some entirely new struct-inheritance system to solve that particular problem when there are much simpler and effective solutions that would solve thigns for roles/extensions, as well as all the extensions shipped so far. |
When I kept griping that extensions are "namespace-wide", this was based on a misunderstanding that you and I BOTH had. Neither of us seemed aware that you could easily just pull in extensions from a single class. Had you known this, you could have corrected me on it near the start of my griping. This would have removed a good portion of my ammunition against Extensions in general. So looks like we both learned something today, thanks to @HaloFour . |
Here's a meta point that i think is worth getting across. Say an existing feature solves 98% of a problem space. We're not really going to then work on creating an entirely new solution that overlaps tons of it, but hits that 2% place. We'd much rather figure out how to improve these areas to grow to meet that gap, rather than come up with an thing that has a lot of superfluousness and which itself will certainly add its own gaps :) |
I literally worked on that feature :) The purpose of my posts was to show you that even if that feature did not exist, that that still is not a justification for the approach you were proposing. Heck, let's do the thought experiment here. Say we didn't have Again, even if we didn't have this feature, the counterpoints being made were valid. They did not depend on the existence of this happenign prior to your request. Indeed, that's a vital aspect of lang design. We have to consider htings that are potentially coming later. And we definitely will nto do something in the short term if it's going to be subsumed by something else we have strong belief we want to do. So, for the past, present and future, as long as we feel that extensions are an appropriate space, adn that we can continue on providing improvements on them to address pain points, then insisting that we build up a duplicative system against them just to address a tiny pain point you had, is not going to fly. Finally, it's not my job to be aware of what you do or don't know. It took a long time just to even figure out waht you were asking for, given that the requirements kept changing (for example, if it should be a class or struct. if it's just adding members or not. if protected is available or not. if there's a single type at runtime or not). All these parts were both unclear and in flux from post to post. It takes time and effort to even try to condense down what issues it is you're having only to realize you don't know about certain lang features. This came up with object-initializers with things like collections/dictionary-init, and with extensible-enums where it turned otu a lot of your misinformation was based on errant views on performance that were not valid. Trying to detangle all the stuff which you do/don't know is non-trivial. :) |
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. |
@najak3d let's talk offline about that. |
@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". |
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). |
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. |
You say jump, I ask how high. |
@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. |
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. |
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 |
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. |
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. |
@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) |
Please see my proposal here: #5897 |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
The text was updated successfully, but these errors were encountered: