Enum extensions #8819
Replies: 99 comments
-
Calling a method should be in the other way. public void Method(PrimaryColor primary)
{
// This method expect only RED, GREEN, BLUE and BLACK
}
Method(PrimaryColor.Black); // Works fine
Method(Color.WHITE); // WHITE is an unknown value in Method But public void Method(Color primary)
{
// Method know all Color and PrimaryColor
}
Method(PrimaryColor.Black); // Works fine
Method(Color.WHITE); // Works fine too So the inheritance works in reverse order. |
Beta Was this translation helpful? Give feedback.
-
I dont know if I like the "extends" as As a perk, this feature allows you to // TODO remove all usage of ObsoleteLibraryEnum, then remove inheritence
enum NewAPIEnum : ObsoleteLibraryEnum { ... } |
Beta Was this translation helpful? Give feedback.
-
I think the absence of this C# feature is costly and fairly prevalent. The most common solution I keep seeing is the rampant use of naked string values. People just type "{keyName}" and move on, yet they aren't realizing the fragility/weakness of this string-based approach. But in their defense, what other easy options are available? C# doesn't support what we really need, which is extendible enums. The process for using Extendible Enums is to simply go to the Custom Enums list, add your name, and then start using it. While you are adding it, Intellisense can be used to find a "Similar/equivalent name" to prevent the addition of names needlessly. And from then on, that name will be encoded, so that you can't commit a typo. Later if the name is deemed inappropriate and needs to be refactored -- it can now be done easily. It's a shame that we can't already use Enums for contexts where it's serving more as a "Name-key" that is permitted to grow, or be customized. I do think the base enum should be required to have an "[ExtendibleEnum]" Attribute, so that users of the enum can be aware that it's designed to have "new members added" (that won't be known by-name, to the assembly that defined the base-enum). |
Beta Was this translation helpful? Give feedback.
-
I have created kludge structs that behave much like Enums, but require hundreds of lines of added code to make them behave acceptably. I term this kludge as "Strongly Typed Strings". Currently I have the following such types:
and so on.... Each of these is just an "int" underneath, but has infrastructure in place to make it work mostly like an Enum value (or a strongly-typed string), and knows it's "string value" and even "Description value". If this were an Enum... the Description would come from an attribute or comment. I feel pretty clever with this design. But in truth, it's an arduous/inconvenient/inferior kludge -- compared to concept of just having Extendible Enums. |
Beta Was this translation helpful? Give feedback.
-
@najak3d i don't see waht that has to do with this proposal. It soundsl ike you're just discussing Roles/Shapes, which is something we are continuing to look into. |
Beta Was this translation helpful? Give feedback.
-
Please explain what you mean? How would the Roles/Shapes proposal address the need for an "easy to append to" list of strongly-coded Names? This is what an Enum is -- only the C# enum is non-extendible, so you can't customize it to add new names. For example, a base assembly may have a dictionary of Items, keyed off of an Enum key. Well, you are sorta stuck with their existing Enum values -- and can't extend them. Unless Enums were extendible. I see Enums as a better alternative to "strings" - where the string is used as a "Key"... i.e. a typo of the string name will cause the app to "fail". So we want Enums, so that you can't have "typos", and refactoring the name later because easy. While for those using Strings -- you gotta hope you find all instances via string-searches.. And if you mess up -- the app compiles just fine, and you don't see the failure until run-time. So my concern is for giving C# devs a better built-in replacement for using raw String values as Key values, when those String Values are really restricted to a hard-coded list of values. So what other proposal provides this, other than the one here? |
Beta Was this translation helpful? Give feedback.
-
IMO, if a base assembly decided to use an enum for that task then they do not intend for you to extend them. |
Beta Was this translation helpful? Give feedback.
-
You're using a mechanism to create strongly typed strings/ints/etc. this is a core case for roles/shapes. |
Beta Was this translation helpful? Give feedback.
-
UNLESS, they mark up their extension with "ExtendibleEnum" in which case they are marking it as an open-ended list of key names. That assembly may only define a few of them. In short they are creating a base type for a list of Strongly-Typed string values... so if they define: public Dictionary<BaseEnum, CustomDataObject> DataObjects; Now you can fill this dictionary with data, keyed off of "BaseEnum" values --- and the base class does not care what all values exist, but only cares that you KEY for this dictionary is defined as one of them. For example, we maintain a Dictionary of MapLayers.. we have to key that off SOMETHING? We can key it off of a raw-string, or we can key it off of a strongly-typed BaseEnum. Now you can't access this Dictionary with anything but a BaseEnum type (or extension of that BaseEnum). It's perfect. Efficient, makes it easy to work with in Intellisense, because it works like an Enum. I'm only proposing this for Enums marked with the "[ExtendibleEnum]" attribute to make it clear that "these names are not the full list of values that may be used". |
Beta Was this translation helpful? Give feedback.
-
Please explain this more. Which specific version of this proposal do you have in mind? And please give me a hint as to how that proposal will enable the creation of a strongly-typed string value, that can be accessed like an enum? (where you are LIMITED to using ONLY the names in the Enum... unless you register a new name, you cannot use it) Keep in mind that Extendible Enums is about having a "Registered List of Valid Names" all of which appear in Intellisense, as it does for Enums. So you type in the Strongly typed String Name, type "." and boom, there are all of your valid Names to choose from. Just like an Enum... except it can be extended to register new names. |
Beta Was this translation helpful? Give feedback.
-
Right now, I type "MapLayerName." -- and all of my valid MapLayerNames show up in intellisense. In another assembly, I have another "MapLayerName" that maps to the first one... and so I can add in even more Names, which show up via intellisense. In this context, the system will not allow you to use a MapLayerName that has not been registered... so you cannot make mistakes. If we don't like the Name, and want to change it -- we just refactor the static member, and it auto-changes all usages of that Name. This method is inferior to Enums -- but superior to using raw-string values. We have not seen a better way to implement in C#. So far, all we've seen is either people using "enums" or "raw string values". |
Beta Was this translation helpful? Give feedback.
-
You can do this, as i mentioned, with constants or static-readonly's for the well known registered list of names. But you can also decide for your API if you allow people to pass in any name. At which point, users can create their own constants/readonly's for any new values they care about. |
Beta Was this translation helpful? Give feedback.
-
I don't see why it's inferior to enums. That seems to be a subjective assessment on your part. Both are simply using another domain (integers, or strings) to give strong names to. |
Beta Was this translation helpful? Give feedback.
-
Statically defined public strings is better than raw-strings.... but in my book, is still a loosely-typed-string. It does not prevent anyone from just typing a String value -- or for having multiple-conflicting sources of static string names. Why? Because it's all loosely-typed strings. If we can create a "Strongly-Typed String" -- then at least those "Static members" will ALSO be Strongly-Typed Strings -- and so at least then you can search your code for All references of this strongly typed string, and manage it better. As is -- all are just raw-strings -- without any control of which string you use for your Key. Is there a proposal to make Strong-Typed-Strings a thing? Where I can create a class that Derives from String? If so -- then I suppose that could be used to reasonably solve this problem. So if we could declare "static public readonly StrongTypedString" values, then Yes, this will resolve the main problem here. (Because the readonly trait on these static strings should put them into a place that doesn't burden the GC, right?) |
Beta Was this translation helpful? Give feedback.
-
We wrapped ours with int's, because our MapLayerName is a "struct" ... and so doesn't bother the heap or GC. Very cheap, like a flyweight. It's index references back to a static array of Names. We could have wrapped a string, but now we are classes, not structs, which has drawbacks. |
Beta Was this translation helpful? Give feedback.
-
For the Struct-Enums to work, it simply binds these to #'s at startup. Once the #'s are set, they're set, and you can just send around the int - and it works. Within the same Domain space - this is a simple scheme. It's OK that we're not guaranteeing numeric consistent from one run to the next. For those doing remote access API (not in same domain), there would need to be some coordination ahead saying "this struct-enums is dirty" and then a handshake would need to occur to send those values out to the caller, so that they have a consistent look-up table. Admittedly, at this point, the implementation become heavier weight - because the amount of data to be shared could be very large (imagine 10,000 string keys). For our use-case, we're always running together in one domain -- and so everyone can access, directly, the ONE Struct-Enum that contains this list. My implementation currently does not work well with remote access, nor would it be trivial to make it work for remote access. So as I think about it more, the more I see stoppers to this becoming mainstream (part of C# language or .NET framework). I'll continue to use my Struct-Enums, given that anything else to resolve this problem in a better fashion is not likely to get implemented ever. I wish the Struct-Enums method that we use, were more widespread... My experience has been that "once people start using them, they like them very much" and "didn't know what they were missing". They didn't think there was an extendible way to have an Enum-based solution -- which is what my Struct-Enums mechanism emulates. |
Beta Was this translation helpful? Give feedback.
-
If it's not extendible -- then this does not work for us. If it's extendible, then it would work fine, and even better if what it's sending around are "int's" not the strings themselves. For sake of speed, we often rely upon our enums to be tightly packed starting at 0, and going up by 1. This way, everything can be looked up via Array index, rather than dictionary. That's how my Struct-Enums work -- it does NOT allow you to define your own int-values -- it forces the sequence, and caps the max # of entries. For most, I cap it at 255 keys, as most of my Struct-Enums only have 5-40 values, and have no risk of going over 255. I also reserve the 0 index for the "NULL" value... so a non-initialized raw Struct-Enum value will result in the "NULL" value. We love what we have, but am frustrated that it's not "more of a thing" -- and instead see rampant use of "string keys" because they don't know of a better way to achieve "extendible name keys", as is provided by our Struct-Enums. |
Beta Was this translation helpful? Give feedback.
-
That's simply not how enums work, meaning you're suggesting something very out of line with what enums actually are (and waht the OP here is asking for here). Enums are names bound to constant values that are embedded into the use site. THe OP is about being able to add otehr enums that might add more constnat values, which means you absolutely might get collisions across different extensions in different libs.
Right, but this is stuff that would major add to complexity all for 'gain' that hasn't been demonstrated. For example, you continually insist that this must have the same performance characteristics of passing an int. However, in my experience, it would be entirely fine to support this fully passing around some richer handle that gets all the above functionality for free, but which still satisfies the rest of your criteria, albeit at teh size of a pointer, not a 32bit int. I recommend not overfixating on some particular solution that then adds high amounts of complexity to solve one criteria piece that you think is ultra importnat (but which may not be in practice). Instead, consider the set of things you care about, and what things you are willing to relax a bit on if the end solution is still overall highly beneficial. |
Beta Was this translation helpful? Give feedback.
-
Note: you keep mentioning java-enums. But those are not extendible. I'm not sure why you keep bringing that up as an approach we should take as we could have their entire feature set here, and you would be in the same boat you started in.
Every time you shed more light on your particular solution:
It is extremely unlikely that anything we would do in this space would be sufficient for you. We are not going to ape your particular solution you created just for your particular lib. We're going to be making something broadly applicable without the limitations you have in your system that you're ok with.
It's not our job to provide solutions for you instead of you convincing others that your solution is actually beneficial for their needs. If those peers of yours are happy with strings, and they don't see the value in hwat you have created, then that's not a motivation for us to make it first class so that it superficially has more weight being a first party solution. |
Beta Was this translation helpful? Give feedback.
-
Here's the thing. That only works for your domain as you apparently control all extension points.
THis would not work in .net. While there is a point that an app 'starts up', a .net app certainly does not load all its assemblies at startup. Delay-loading of assemblies is just the norm for how an app will run, where the assembly is only even loaded, jit'ed, init'ed, only when needed. So there's no "startup" point for the app/runtime/compiler to hook into to ensure all these things are laid out in a sensible fashion. Because of the dynamic nature of .net (again, the normal case we'd have to make this work for), you will not have any full knowledge of the shapes of things here (and that's ignoring the fact that people can just synthesize code on the fly, and that's also totally normal). As such, your approach of dense packing, and hoping you have enough space in your array for all potential downstream values that are faulted in is just not going to work. This is another reason the solution you've outlined above is extremely hyper-focused on your particular domain where you can make certain decisions and implementation strategies based entirely on how you know your particular apps work. The language/runtime cannot do that. We have to make this work for all the app/lib scenarios .net has already enabled and which already have thousands to millions of apps/libs making use of. |
Beta Was this translation helpful? Give feedback.
-
I wasn't proposing .NET to implement my narrow/optimized solution. I am fine with a more generic, less optimized solution - but for it to be standard. I employ this technique in every project I get involved with, and it spreads and is accepted/loved, because I simply present the "Pros" of the technique and the deficiencies of raw string-based keys -- then it becomes "what we do", because there are no better options. Most don't like the tedious code-behind logic involved, but in the end, appreciate these Struct Enums. I firmly believe that .NET or C# should have this as a built-in type. And since it is new, they can make it more functional, like Java Enums (you can add more properties/smarts to each value). It is widely beneficial to many projects, not just niche. Essentially, what is does is creates a flyweight, that holds the desired amount of meaning - starting with "string Name" but can also include any other fields you want to associate with it. But as you pass this key/flyweight around, it's not just a "random defined flyweight" but rather is a definition that has been hard-coded into your specific class type, such that it works like Enums... which is vital (otherwise, your "string-key" is too loosely typed and error prone). Of course, you would be able to define a new Key/enum from anywhere, so long as the Key Name didn't conflict - so it's encouraged, but not mandatory for each def to be encoded like a C# enum. You'll always be able to look for an enum By-Name (like TryParse(string name)). So we use this a lot. And my teammates like it. If C# implemented it, I'm confident it would we viewed as a excellent upgrade. SG's would be the way to make this feature concept to "go global" in the meantime, but that would take immense effort on advertising/etc... and I'm not in the business of doing this. Without SG's to wrap it, that still leaves coders with about 50 lines of tedious code per-specific-type to write, and that is a deterrent to acceptance, when you don't have someone live to demonstrate/sell the technique. If instead it was universal (not requiring yet another (unheard of) 3rd party lib) - that would vastly increase the acceptance rate. I have other objectives; making a lib of this nature is not on my personal roadmap. |
Beta Was this translation helpful? Give feedback.
-
@najak3d i think you have to really come with a full proposal on what you're asking for. You keep talking about certain things (like java-enums) but then just ask for it to be extendible. I think we'd really need a strong actual description of what the feature actually is, and how it works. Comparing to other features in other languages that don't actually accomplish the core set of features that you are looking for isn't helpful. We could build that other feature and then not at all meet your requirements. A short writeup here of what semantics you want, and generally how it would actually work (including limitations/benefits) would be highly beneficial (in a new discussion) since the feature doesn't seem to be what you want. |
Beta Was this translation helpful? Give feedback.
-
I'm out of energy for this. It seems like too far of a longshot for me to write up such a proposal. I shared here, in hopes that it might inspire thought among those who are actually responsible for the future of C# and it's ecosystem. I have implemented Java-like enums, but that doesn't mean that's what I need. I could easily with a solution that simply allows you to extend enums via creating derived enums. That would work for us. The rest of our Java-like enums would then be built upon that (but with less tedious code). The hardest part of what we need to accomplish is to make the user-code think in terms of "enums" -- hard-coded named members. That's the MAIN thing we want, that C# doesn't supply. Once they do supply it, I'll incorporate it into a flyweight pattern with less code, as will others (because it won't require "weird looking structs" to make it work). But, it would be BETTER, if C# also took it another step and made a whole new type, that works like an enum (in usage) but is decorated with a user-defined amount of added data/meta-data - similar to Java-enums. That would be the ideal situation, and would be useful in many generic contexts. OK -- on second thought -- I'll write this up in a new thread, now that it's been matured. |
Beta Was this translation helpful? Give feedback.
-
Added it here: |
Beta Was this translation helpful? Give feedback.
-
I saw this referenced form the String Based Enums thread (#2849). I'm +1 for using the existing inheritance syntax. i.e., today we can do It's true that existing code does gate on certain values being present -- and I think that just makes this even more so the right way to go... -- because that code is already guarded against unknown values. Additionally, my gut feeling is to avoid adopting Java-like syntax for alternative purposes -- as common keywords that do different things is a huge point of confusion when developers are switching between frameworks. I'm against adopting the java syntax when we already have syntax that can be used for this, that would be intuitive for existing developers to use. -- If there is some "best practice" you would like to enforce, (i.e. "C# 9.x: enums should be sealed") then that's more of a Code Analysis warning / information message that should be addressed separately. |
Beta Was this translation helpful? Give feedback.
This comment was marked as disruptive content.
This comment was marked as disruptive content.
-
I would love to see this implemented, even if just as syntactic sugar for "define all fields from the 'base' enum". Note that I am strongly against any implicit casts between types involved in this relationship, for the following reasons:
I also don't think calling it inheritance (and associated vocabulary) is the correct terminology here, at least for what I have in mind. I'll still use it though, because I didn't manage to come up with a better alternative yet. Words hard. That said, the naive idea I cooked up in my brain over the last half hour before finding this thread looks something like this: Allow
Also, I propose that the
As a nice side effect, this approach would keep enums stable as long as their assembly of origin doesn't get recompiled, regardless of dependencies, just like the enums we have today. It would also keep the const-ness of enum fields intact without having to touch the const rules. And my final point: Given how differently enum inheritance works from regular class inheritance, I think a different syntax would make sense here. Maybe one of these? But very open to suggestions. enum Derived using Base { ... } // I like this one for the default underlying type case
// not a big fan, but it might work. might not even need additional compiler support if we ever get partial enums
// at least for the version without explicit `base` placement (can just be a code generator then)
[ExtendsEnum<Base>] enum Derived { ... }
// I don't really like any of these
enum Derived using Base, ulong { ... }
enum Derived using Base : ulong { ... }
enum Derived : ulong using Base { ... }
// this one, with the base definition taking the place of the `base` "member" has the very big disadvantage of
// requiring a maintainer to read the entire enum definition to find out whether this might be extending another enum.
enum Derived : ulong { using Base, ... } |
Beta Was this translation helpful? Give feedback.
-
I am much interested in this feature as well. I would like to propose some scenario. Let's imagine you want to write your own IAM (because why not, it's an example) and you have... // IAM
class Permission
{
static Permission CreateUsers = new Permission();
static Permission DeleteUsers = new Permission ...
static ...
// now comes all the infra code
private static List<Permission> _allPermissions = new ();
public static IEnumerable<Permission> AllPermissions => _allPermissions;
// probably some backing field - numeric or textual
private int value;
protected Permission()
{
value = nextvalue();
_allPermissions.Add(this);
}
// comparison and parsing logic
...
}
// elsewhere
class DeviceConfigurationPermissions : Permission
{
// custom permissions here
} so it's doable, it's just quite some amount of code (especially that // IAM
extenum Permission
{
CreateUsers,
DeleteUsers,
...
}
// elsewhere
extenum DeviceConfigurationPermissions : Permission
{
// custom permissions here
} |
Beta Was this translation helpful? Give feedback.
-
@marchewek in your example, if you have multiple modules with their own permission sets, it's inevitable that 2 modules will conflict: // IAM
extenum Permission
{
CreateUsers,
DeleteUsers,
...
}
// elsewhere
extenum DeviceConfigurationPermissions : Permission
{
CreateDevices,
DeleteDevices,
}
// elsewhere
extenum GroupConfigurationPermissions : Permission
{
CreateGroups,
DeleteGroups,
} In this situation, |
Beta Was this translation helpful? Give feedback.
-
@TahirAhmadov that's the whole point - the original idea is closer... because of what language currently supports (or doesn't support in this case). See, there is a reason why I think we should distinguish
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I want something that makes simpler to extend an existing enum without replicate values.
I am not proposing inheritance, but just a syntax that allows to avoid code duplication and makes casting simpler and safer.
As sample we can have a base enum:
and an enum that extend that:
The Color enum is converted by the compiler into a new enum that is a merge with PrimaryColor:
This also creates an implicit casting from PrimaryColor to Color.
Converted by the compiler into:
If there is an overload, no conversion is made:
DoSomething(PrimaryColor.RED); //Overload is preferred
Enum values if not assigned, continue in a natural order from the last value defined in base enum.
Declare explicit values is recommended, since it can cause a breaking change in the extended enum if you add a property in the base, but everything can work without problems.
I would disallow enum extensions on enum with [Flags] attribute, because even if it could work, it can cause unexpected behaviors and breaking changes if you change values on the base enum.
If you use an underlying type on the base enum, the extended enum will be of the same underlying type .
Beta Was this translation helpful? Give feedback.
All reactions