dotnet / csharplang

The official repo for the design of the C# programming language
11.62k stars 1.03k forks source link

Enum extensions #3491

Open OrbintSoft opened 4 years ago

OrbintSoft commented 4 years ago

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:

    public enum PrimaryColor: long
    {
        RED = 0xFF0000,
        GREEN = 0x00FF00,
        BLUE = 0x0000FF,
        BLACK = 0x000000,
    }

and an enum that extend that:

    public enum Color extend PrimaryColor
    {
        WHITE = 0xFFFFFF,
        // BLACK = 0x000000, Not Allowed, This name has already been defined.
        CYAN = 0x00FFFF,
        VIOLET = 0xFF00FF,
        YELLOW = 0xFFFF00,
        LGRAY = 0xC0C0C0,
        GRAY = 0x808080,
        TEAL = 0x008080,
        PURPLE = 0x800080,
        OLIVE = 0x808000,
        MAROON = 0x800000,
        DGREEN = 0x008000,
        NAVY = 0x000080,
        AGAIN_BLUE = 0x0000FF //Not a problem, different name, same value
    }

The Color enum is converted by the compiler into a new enum that is a merge with PrimaryColor:

    public enum Color: long
    {
        RED = 0xFF0000,
        GREEN = 0x00FF00,
        BLUE = 0x0000FF,
        AGAIN_BLUE = 0x0000FF,
        BLACK = 0x000000,
        WHITE = 0xFFFFFF,        
        CYAN = 0x00FFFF,
        VIOLET = 0xFF00FF,
        YELLOW = 0xFFFF00,
        LGRAY = 0xC0C0C0,
        GRAY = 0x808080,
        TEAL = 0x008080,
        PURPLE = 0x800080,
        OLIVE = 0x808000,
        MAROON = 0x800000,
        DGREEN = 0x008000,
        NAVY = 0x000080,        
    }

This also creates an implicit casting from PrimaryColor to Color.

        public static void DoSomething(Color color)
        {
            Console.WriteLine(color);
        }
DoSomething(Color.VIOLET); //It's fine

DoSomething(PrimaryColor.RED); //This simpler syntax involves casting

Converted by the compiler into:

 DoSomething((Color)PrimaryColor.RED); 

If there is an overload, no conversion is made:

  public static void DoSomething(PrimaryColor color)
  {
      Console.WriteLine(color);
  }

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 .

CyrusNajmabadi commented 2 years ago

Strings are non-specifically typed and can be mistyped easily, while Enum values cannot be mistakenly mistyped without a compiler error...

Again, this is not true. I think you are operating under a misapprehension about how enums work. They simply give names to a subset of some integral range. But the entire range is still available.

CyrusNajmabadi commented 2 years ago

There is no Intellisense

So write a completion provider for your use case. You keep operating under the idea that the tooling will work a certain way and then designing backwards from that.

CyrusNajmabadi commented 2 years ago

I suppose then you are one of those who uses string-based keys?

I use whatever features are sensible per problem. Sometimes that is enums. Sometimes it's strings. Sometimes it's something else entirely.

CyrusNajmabadi commented 2 years ago

If so, then I may barking to deaf ears.

The problem here is that you continue to simply state that the existing state is a problem, but then are not presenting a solution that does but also suffer from the same problems.

Indeed, as you yourself stated: "we're not trying to say Enums are hack-proof."

You recognize that your enum solution still doesn't actually solve the problems you have stated exist. But even with that, you still insist that we should take it. Ask that you've been able to point to as a reason as well is intellisense. However intellisense is already extensible. If you want a solution for that, you can get one without changing the language.

We already do this in roslyn btw. There is code that has intellisense offer results from other types when in appropriate contexts. It's sounds like that's exactly what you want here, and it doesn't involve changing the language at all.

CyrusNajmabadi commented 2 years ago

Most people who use them have been doing it for so long that they don't see the associated QA issues,

I don't get your argument here. So people use something that works for a long time, and get see no associated quality issues going that route... So you think that's a problem?

If they are not seeing associated qa issues going that route, then why would we have a problem with going that route. You're effectively saying: we need a solution for something that has proven quite effective for a very long time.

CyrusNajmabadi commented 2 years ago

They'll continue using string keys...

If it's working well for them (as you yourself indicated), then what is the issue?

najak3d commented 2 years ago

As with all things that new C# versions have solved -- people used "what was available". And for now, C# pushes devs towards using String-based Key names for situations where extendibility might be needed in the future -- since Enums will not satisfy that requirement, and they are unaware of alternatives that are better -- so they use Strings, despite their QA issues.

All other C# improvements can be described in the same way -- people are doing it the only way available... so why fix anything? Because the fixes make things better.

Extendable Enums would, in many places, replace the use of the more fallible string-based key names.

The fact that I have to explain to you that Enums DO solve this problem sufficiently, and that them not being "hack-proof" has no bearing on this conversation. We're not trying to keep people from going out of their way to write really stupid code (like casting a random int to an enum... why would anyone even do this???) Yet to you, the fact that people can cast a random meaningless int to an Enum -- means that Enums are no good.

This isn't just about Intellisense, but mostly about Compiler-Errors on a Typo. If you type an Enum Name wrong -- you get compiler error, and you correct your mistake immediately. No such defects then ever make it into a product. While for strings, there is no compiler helping you pick "correct names". And refactoring a string-name is more error proned. AND you don't have the same Intellisense. AND enums are more efficient, because they are just a small value type, and when used for Dictionary Key, run faster than strings, as Enums ARE their HashCode. No conversion needed.

Enums are superior for this context, by far. I shouldn't have to make these arguments to you. You should already know this. Yet even after explaining it, you use silly/inappropriate logic to make your rebuttals.

I hope the others on this site who are guardians of the C# language possess better logic skills, than you seem to have. It's discouraging that someone of your caliber is in this position of power. That's my honest opinion of you, from what I've seen and heard from you over the past 2 months.

You have good experience and knowledge. But when it comes to your core logic skills, I think you may be 1-screw-lacking.

najak3d commented 2 years ago

They'll continue using string keys...

If it's working well for them (as you yourself indicated), then what is the issue?

They don't have another viable alternative, so they make it work. That doesn't mean it's the best way for this to be done; because it's just not. But they need "extendibility" so they CAN'T USE ENUMS. So, what else should they do?

CyrusNajmabadi commented 2 years ago

@najak3d why can you not just use a source generator here?

CyrusNajmabadi commented 2 years ago

But they need "extendibility" so they CAN'T USE ENUMS.

You keep saying this. But it's not true. Enums are just names over a subset of an integer range. But the entire range is available (which is effectively infinite for most domains).

CyrusNajmabadi commented 2 years ago

why would anyone even do this

This is a core part of your enums work. And it's entirely expected to work this way in many domains. Lots of Roslyn is built entirely this way.

Again, I think you're operating under a misapprehension of what enums are and how they are intended to be used.

CyrusNajmabadi commented 2 years ago

This isn't just about Intellisense, but mostly about Compiler-Errors on a Typo. If you type an Enum Name wrong -- you get compiler error, and you correct your mistake immediately

You can get this as well with strings. This is a core use case for analyzers.

You can require all strings passed into an API be legal. Or you can require the caller to only pass from named symbols, not random strings.

If all you want are compile errors on statically known information, that's exactly what analyzers are for.

CyrusNajmabadi commented 2 years ago

You should already know this.

Perhaps that should make you consider if what you're saying is accurate then :-)

CyrusNajmabadi commented 2 years ago

AND enums are more efficient,

I have never said you should not use an enum if sensible for you. I've simply said that you can already solve the extendible enum issue today with tools already at your disposal. I've also said that those same tools would work equally well for strings.

CyrusNajmabadi commented 2 years ago

I think you may be 1-screw-lacking.

@najak3d these types of personal attacks violate the .net code of conduct. Please refrain from them when conversing with anyone here (or any other .net forum).

CyrusNajmabadi commented 2 years ago

They don't have another viable alternative,

Says who? You claim this by Fiat, when other options have already been enumerated (no pun intended).

CyrusNajmabadi commented 2 years ago

@najak3d I've reached out over discord about this topic.

TahirAhmadov commented 2 years ago

I like this idea. There is one question: what happens in cross-library situations?

// proj A, v.1
enum Foo { A, B, C, }

// proj B, v.1, ref A v1
enum Bar : A.Foo { D, E, F }

// proj A, v.2
enum Foo { A, B, C, Q, D, }

// proj C, ref A v.2 and B v.1
var k = Bar.D; // what happens here?
var l = Bar.Q;

The only 100% solution I can think of is new logic in the binder to detect these collisions.

najak3d commented 2 years ago

The only 100% solution I can think of is new logic in the binder to detect these collisions.

First off, I think it is important for there to be an added keyword like "extendible". Otherwise, this proposal opens a can-of-worms. The creator of an enum must deliberately mark it as "extendible", in which the host library knows that more values are expected/likely.

Second -- is your example even possible? I thought Proj C would already have trouble compiling already given the version conflict between itself and B, both trying to pull in different versions of A. I'm unfamiliar with the details of this; all I know is that I've been bitten many times with the new form of DLL-Hell where you have to make sure that all of your reference assembly versions are compatible. I just figured that the scenario you proposed above is already forbidden. If not, then I'm learning something new here.

TahirAhmadov commented 2 years ago

The only 100% solution I can think of is new logic in the binder to detect these collisions.

First off, I think it is important for there to be an added keyword like "extendible". Otherwise, this proposal opens a can-of-worms. The creator of an enum must deliberately mark it as "extendible", in which the host library knows that more values are expected/likely.

The OP makes no mention of this keyword. I read through the thread quickly and I am unsure why you would want to require enums to be marked as extendible. With classes, you have to sealed them to prevent sub-classing, why introduce an inconsistency here?

Second -- is your example even possible? I thought Proj C would already have trouble compiling already given the version conflict between itself and B, both trying to pull in different versions of A. I'm unfamiliar with the details of this; all I know is that I've been bitten many times with the new form of DLL-Hell where you have to make sure that all of your reference assembly versions are compatible. I just figured that the scenario you proposed above is already forbidden. If not, then I'm learning something new here.

Yes it is. I'm pretty sure you can make it compile, but even without compilation, in late binding scenarios, you can drop in a DLL and it would still have to work somehow.

najak3d commented 2 years ago

The reason for the keyword "extendible" is that without it - this proposal opens a giant can of worms, changing the way all existing enums work now (where they assume that membership is limited to the list they defined). So changing that is too much risk, thus I've suggested "extendible" to define the special case.

I see your point about late binding. The way I'd view this is to be implemented very much like a dictionary, where names are registered at startup, run-time. The names lists would be merged, and so in this case, you wouldn't get any errors... "D" would equal "D" - but the integer value simply wouldn't be consistent everywhere.

Since the values are registered/numbered during binding, this would still result in all values having a unique integer value. And it's OK if that value varies from context to context, or run-time to next run-time. When it comes to remote transactions or serialization, these enum values should always be translated to/from string values.

When it comes to domains that have lots of remote assemblies communicating with each other -- therein lies the most complexity. If a public API has an extendible enum -- you will be sending it the integer value, from a remote assembly -- where the meaning of that integer value is non-determined.

It's easy to work this out for assemblies directly binded, but not for cases that involve remote transactions.

In short, if enums are extended to allow this functionality -- they will need a special keyword to open them up to it.

CyrusNajmabadi commented 2 years ago

this proposal opens a giant can of worms, changing the way all existing enums work now (where they assume that membership is limited to the list they defined).

Enums should not assume this. It's definitely not part of how they are supposed to work.

najak3d commented 2 years ago

... and yet, if you sift through lots of code from many different authors, the assumption is often that this is the case. For example, in case statements looking at enum value, their "default" case statement throws an exception for all values not-in-the-list. You would never want to throw exceptions for an "extendible enum", only for the normal enums.

TahirAhmadov commented 2 years ago

@najak3d 1, I think you got it backwards. With classes, receiver of BaseClass can get ChildClass. With extended enums, receiver of ChildEnum can get BaseEnum. Your concern WRT "where they assume that membership is limited to the list they defined" is unfounded; that would continue being the case. 2, regarding cross-library collisions, that's not how enums work today, and that cannot change - to a dictionary, or any other approach, other than the approach .NET already uses. And regarding the backing value, it's crucial that value is consistent, because behind the scenes, that's what carries the state; there is no magical "enum" value. If you have enum option D in one project which equals 4, and another project has D with value 5, right there is an evil bug, because equality will fail. And that cannot be allowed.

najak3d commented 2 years ago

My ideas is that the base class itself will see the extension values. Still backed by a # value, but the value of that # is not determined until run-time.

Base class will see these values too in that the Base assembly API will ask for these enums on the API -- and so will have to deal with them. If the base class calls "ToString()" - I want it to spit out the real string value, defined by a dependent assembly.

For my usage here, I want enum to serve as a dictionary of valid Name Keys -- determined at run-time, for the extension values.

So for my needs, I'm going to have to continue using my "struct-based" solution, which does what I want, but suffers some inferiority to raw enums. But is, by far, IMO, better than using pure string-based key names.

CyrusNajmabadi commented 2 years ago

but the value of that # is not determined until run-time.

This is a fairly extreme deviation from what enums actually are and what I think people would expect. They are constants today. So that means the values are known at compile time and are baked into the call site. You're now talking about something very different here.

It feels like this wouldn't really be usable either for all existing codebases as taking any existing enums and using this would now charge what these are (constants embedded at compile time, vs statics resolved at runtime). It's possible this would be binary compatible though. But I'm not totally sure.

CyrusNajmabadi commented 2 years ago

which does what I want, but suffers some inferiority to raw enums.

What is inferior here?

TahirAhmadov commented 2 years ago

@najak3d Like @CyrusNajmabadi explained, that's not what .NET enums do. In .NET, enums are basically strongly-typed constants with extra metadata. If the backing type is int, then passing around an enum is just like passing around an int. That's what everything compiles down to when CPU executes it. And I don't think that can change. For other uses, you will probably have to roll your own struct-based solution. (I think Java "enums" are basically special types of struct, but I may be wrong.)

najak3d commented 2 years ago

I would like a new form of enum that works more like Java Enums, which still passes around a simple 'int' run-time, but doesn't bind to that 'int' at compile time, but instead compiles to the 'name' which binds to the 'int' value run-time.

So what I'm asking is quite a stretch. So I'll just stick to my struct-based Java-like enums, which work pretty well for this -- it just seemed like something that should be incorporated into the C# language .NET System namespace, so that it's available for free everywhere. For me, this is a part of nearly every application I create, because I avidly avoid "raw string key values" -- I call those "non-specific / loosely-typed" while my struct solution is specifically-typed, eliminating confusion/errors, and making maintenance/QA easier.

For example, our functions look like this:

public class MapView
{
    public void EnableLayer(MapLayerName name) {  } // enables the map layer associated to this name.
}

Here you are limited to entering only valid MapLayerNames, and it's clear from where to get this list. It works like an Enum!

Now compare that to what MOST devs seem to want to do, which is this:

public class MapView
{
    public void EnableLayer(string name) {  } // enables the map layer associated to this name.
}

For the 2nd case, you can enter ANYTHING there.

If you hard code a call with "{layerName}" in your code (which they do this already) -- now it becomes harder to refactor that Map layer name -- because you have to locate all instances of it -- and what if the code that uses it is NOT inside of your Solution? (then it gets orphaned without anyone knowing it)

But with the Struct-Enums solution, if we refactor the Layer Name -- other projects that use our library will throw a compiler error - and so they'll discover the refactored name immediately before they can compile. QA tragedy averted.

Java Enums, or with C#, these Struct-Enums are a great idea, IMO. But I don't see them implemented ANYWHERE in C#. I'm the only one that I know of who does this. But the superiority of the method, IMO, is undeniable. It should be a common practice, and would be if Java-style enums (but extendible) were a thing built into C#.

CyrusNajmabadi commented 2 years ago

So what I'm asking is quite a stretch. So I'll just stick to my struct-based Java-like enums, which work pretty well for this -- it just seemed like something that should be incorporated into the C# language .NET System namespace,

If htere is an existing library-solution that works well for this, it's not clear to me why this needs to be at the language level.

Here you are limited to entering only valid MapLayerNames, and it's clear from where to get this list. It works like an Enum!

Great! Looks like a very suitable solution that you and anyone else can apply here. INdeed, i wouldn't even call it a special 'solution' per se, and rather just the normal way people use .net which works well. When you want strong typing, you make a type :)

Now compare that to what MOST devs seem to want to do

I feel like this, like other points, are pushed in as statements of fact without evidence provided. Perhaps this is what some of the devs around you do. But if they're happy with that, and aren't really interested in putting a strong type here, it's not clear to me that actually introducing yet another strong type into the language will be helpful. Not if they're not even willing to use yoru types.

I'm the only one that I know of who does this

This feels like a very normal pattern for any lib that wants to take a strongly typed enumeration of values. Note that this also just feels like it will be even easier as our DU/variants work progresses.

CyrusNajmabadi commented 2 years ago

@najak3d i feel like you've hijacked this issue somewhat. THe core issue seems to be about adding new integral values to an enum in a way that still matches what an enum is, and without making an enum something it is not. For example, you still have no actual safety with enums in the OP proposal.

You seem to be asking for something altogether different. Which is much more like java-enums, or much more akin to the discriminated-union/tagged-union work we're already exploring. I feel like your energy and efforts would be better spent adding your thoughts to those areas, and not this very targetted discussion on the low level raw integral enum concept.

najak3d commented 2 years ago

I agree, what I'm asking for is similar, but not the same, as what the OP asked for. Although if he gets what he wants, then I'll also have what I want too, so long as the extensions can be added from outside of the base assembly. That's all we need for enums to become perfect for our case too.

I only came to this thread, because my own thread was called a "Duplicate" of this thread, and it was very close in nature. It is true that if this OP's proposal were implemented -- we'd also have what we needed.

TahirAhmadov commented 2 years ago

@najak3d WRT to string-backed enums, there is either an issue or a discussion requesting that improvement. Please look for it yourself - I'm too tired at this point. I, for one, support adding string as a valid backing type for enums. (The other question being, how expensive that would be to actually implement.)

CyrusNajmabadi commented 2 years ago

I would like a new form of enum that works more like Java Enums, which still passes around a simple 'int' run-time,

Fundamentally this isn't really something that could happen. If you are just passing around an int, then you have the problem that you have no way to ensure that all the different 'extensions' possibly agree on the int values and can ensure they're not stomping on each other.

Note that even your comparison above is somewhat contradictory. Java-enum's are closed hierarchies. They completely define their entire set of values, which cannot be extended outside of that single declaration. That's how they can have hte efficiency of using an 'int' as an internal impl detail. Once you make this into an open hierarchy, you have to have some coordination system (be that inheritance, or something else) which ensures that all the different parts are ensured to be unique and there is no chance of extenders running into issues (or allowing htem to manipulate things in ways that cause issues for others).

najak3d commented 2 years ago

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.

najak3d commented 2 years ago

@najak3d WRT to string-backed enums, there is either an issue or a discussion requesting that improvement. Please look for it yourself - I'm too tired at this point. I, for one, support adding string as a valid backing type for enums. (The other question being, how expensive that would be to actually implement.)

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.

CyrusNajmabadi commented 2 years ago

For the Struct-Enums to work, it simply binds these to #'s at startup.

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.

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).

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.

CyrusNajmabadi commented 2 years ago

If it's not extendible -- then this does not work for us.

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.

We love what we have, but am frustrated that it's not "more of a thing"

Every time you shed more light on your particular solution:

  1. it seems incredibly limited.
  2. it feels very domain specific.
  3. it feels hyper optimized in some areas at the cost of others.
  4. it seems trivially solvable with source generators.

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.

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.

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.

CyrusNajmabadi commented 2 years ago

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.

Here's the thing. That only works for your domain as you apparently control all extension points.

For the Struct-Enums to work, it simply binds these to #'s at startup.

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.

najak3d commented 2 years ago

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.

CyrusNajmabadi commented 2 years ago

@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.

najak3d commented 2 years ago

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.

najak3d commented 2 years ago

Added it here:

https://github.com/dotnet/csharplang/discussions/5889

BrainSlugs83 commented 2 years ago

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 public enum PrimaryColor: long then it's intuitive to also be able to do public enum Color: PrimaryColor

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.

Aniobodo commented 3 months ago

Sad that this proposal is not getting further attention. We know that Microsoft is best triggered by competition. So if python adopts such extensibility today, you could see it in C# next year.

CMDRNuffin commented 2 months ago

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 enum Derived : Base { ... } and enum Derived : Base, Underlying { ... } (with : being used as a stand-in for the final syntax) as syntactic sugar for enum Derived { /*all fields from Base*/, ... } and enum Derived : Underlying { /* all fields from Base */, ... } respectively, with the following properties:

  1. Explicit values can use base.FieldName to access the FieldName value from the base enum, as a bit of QoL
    • no casting required
  2. If either Base or Derived is decorated with FlagsAttribute, the other must should be as well.
    • Requiring it was my original idea, but I guess just producing a warning would be preferrable, given how that's what all other flags enum stuff does.
  3. Fields defined in Derived must not have the same name as fields defined in Base
    • Yes, I think this should be an error. Since there is no base.ConflictingField escape hatch, you should not be allowed to do that. Unless...
    • (maybe) allow redefining fields if they are declared new, as is already the case in normal inheritance? i.e. enum SomeFlagsEx : SomeFlags { NewFlag = 0x8000, new All = base.All | NewFlag }
      • This would make the old version of the field inaccessible unless a new field is defined with the old field's value.
      • Note that even if this is considered, IMO redefinition without new should still be an error, not a warning
  4. The underlying type of Derived must be not be smaller than the underlying type of Base, and must not change signedness
    • It would make sense to allow larger types, to allow for e.g. a 9th flag/257th value in an extension of an otherwise byte-sized enum.
    • This is to ensure conversion from the base type to the derived type doesn't change the value.

Also, I propose that the base keyword be allowed once in place of an enum member definition (with it being implicitly inserted before the first enum member if not used explicitly) and causing the following behavior:

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, ... }
marchewek commented 1 month ago

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... Permissions :) you want your application to be modular so the permissions related to each of the modules are defined there (you don't want the IAM module to define all permissions, that would be logic leaking); however you still want the IAM to somehow access all the permissions, because you want them to be stored in a single place in the DB (a perfectly valid case when you're building a modular monolith) and a DI container would load the permissions from modules. I know you could now do:

// 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 Permission doesn't have any logic and conceptually is just an enumeration) compared to:

// IAM
extenum Permission
{
    CreateUsers,
    DeleteUsers,
    ...
}

// elsewhere 
extenum DeviceConfigurationPermissions : Permission
{
    // custom permissions here
}
TahirAhmadov commented 1 month ago

@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, (int)DeviceConfigurationPermissions.CreateDevices == (int)GroupConfigurationPermissions.CreateGroups, which would be a huge problem - you allow a user to create devices, and suddenly he can also create groups. Besides, you cannot call a method which accepts Permission with a DeviceConfigurationPermissions or GroupConfigurationPermissions value, because that method does not know how to handle those values; by definition, enum "extension" (or derivation) works the opposite of class derivation. For your use case, your original idea - a class with a value - is a lot closer to the solution than deriving/extending enums. I have a situation just like that in one of my projects and I have a DB table for all permissions, and I find the correct one by module and permission name - not enum's backing integer value.

marchewek commented 1 month ago

@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 enum and extenum by using different keywords: they serve two completely different (I dare say mutually exclusive) designs: