Proposal: "Closed" enum types #9010
Replies: 72 comments 148 replies
-
If you add There must be a way to convert an integer to the closed enum (unsafe code). |
Beta Was this translation helpful? Give feedback.
-
I imagine you could add |
Beta Was this translation helpful? Give feedback.
-
Since // if you can do this
enum struct Option<T> { Some(T), None }
// there's nothing to stop you from doing this
enum struct Bool { False, True } I think this is more of a lowering strategy question - whether or not this should be emitted as a proper enum. If we go with proper enums, we can't manage external values, so I think we will need to emit it as a struct anyways. |
Beta Was this translation helpful? Give feedback.
-
Why? I see no reason for closed enums to have any sort of fixed underlying value. If you want to convert, then use a switch expression: Bool b = i switch {
0 => False,
_ => True
}; @alrz, However, isn't there still an issue pattern matching on those as I'd imagined DU pattern matching would be type based, ie I'd write something like: var x = option switch {
Some v => v,
None => default
}; But with I can invent some syntax here: enum struct ShapeName { Point, Rectangle, Circle }
enum struct Shape
{
Point,
Rectangle(double Width, double Length),
Circle(double Radius)
} and I'm happy that this is consistent: a closed enum is a DU where the values have no parameters. And I can imagine how I'd pattern match this stuff: var area = shape switch {
Point => 0,
Rectangle { Width: var w, Length: var l } => w * l,
Circle { Radius: var r } => Math.PI * r * r
}; But, I've no idea how this lot gets lowered by the compiler. For a struct, |
Beta Was this translation helpful? Give feedback.
-
Does it mean that existing |
Beta Was this translation helpful? Give feedback.
-
@dsaf, existing enums are already open. For: enum Shape { Point, Rectangle, Shape }
But it would indeed be prudent to talk of them as being open enums to clarify the difference between existing enums and the closed variety. |
Beta Was this translation helpful? Give feedback.
-
No they wouldn't be types (or members), if it's a struct DU, all parameters get emitted as flat fields into the struct, the rest is compiler magic. See what F# produces for your example as a value DU. Though I can imagine C# could do a better job utilizing runtime support for |
Beta Was this translation helpful? Give feedback.
-
That's inefficient unless it's a trivial enum like the above Bool enum. Deserialization should be quick. It should be as simple as it is today, except the programmer must verify that it's a valid value (or it's undefined behavior) int value = 1;
if (InvalidValue(value)) throw ...;
unsafe { return (MyEnum)value; } |
Beta Was this translation helpful? Give feedback.
-
@0xd4d You can add it yourself, or have a generator to do it. enum struct Bool {
False, True;
public static explicit operator Bool(int v)
=> v switch { 0 => False, 1 => True, _ => throw InvalidCastEx };
} I think unlike enums the corresponding "integer tag" is solely an implementation detail for DUs, so I don't think it's a good idea for the compiler to generate that out of the box. |
Beta Was this translation helpful? Give feedback.
-
@alrz Your code is the same as @DavidArno's code, a big switch statement which generates a lot of code and causes extra CPU usage at runtime. My solution to use a cast in an unsafe block has the same perf and code size as code we can already use today. |
Beta Was this translation helpful? Give feedback.
-
@alrz, "compiler magic" suits me. It's then @gafter's job to solve how to have struct-based DUs (including closed enums) work with @0xd4d, it's very unlikely that the solution to serialization of closed enums and other DUs is to convert them to ints. I guess it all depends on whether closed enums are implemented as a completely different solution to DUs or not. Doing so would be a mistake in my view. So I'd argue that any solution to serializing enum struct Bool { False, True } also has to be able to serialize enum struct Option<T> { Some(T), None } |
Beta Was this translation helpful? Give feedback.
-
The only difference with the original proposal would be that False and True in that example wouldn't be compile-time constants so we can't use That is because an |
Beta Was this translation helpful? Give feedback.
-
Because open is the default today, and we can't change that. |
Beta Was this translation helpful? Give feedback.
-
@gafter would your intent to be that adding a new value to a "closed" enum is a breaking change? would this be binary compat breaking change, or just a "switch statement which must pick a value entered unknown territory by not finding a match" exceptional territory. eg: what if you wanted to add trinary logic "not provided by customer" to the bool enum (bad naming example) |
Beta Was this translation helpful? Give feedback.
-
@AartBluestoke I believe its true, false and filenotfound. |
Beta Was this translation helpful? Give feedback.
-
It's certainly a breaking change in your API. But the question on the consumption side is: is it possible for me to write code that is resilient to breaking changes. I would hope so. Especially something as basic as "in case of something i don't understand, bail out". |
Beta Was this translation helpful? Give feedback.
-
I updated my post above to correct that point. |
Beta Was this translation helpful? Give feedback.
-
@TahirAhmadov I think that we'd want to warn on all cases that don't have a I think that would make your above list something like this:
I feel like this pretty much kills the entire point of this proposal, though. I wonder if we should ping Gafter and ask his thoughts 🙂 |
Beta Was this translation helpful? Give feedback.
-
@Bosch-Eli-Black I don't think we need to go that far; the compiler will output a hidden To the extent that either reflection or new DLL can introduce an unexpected value, an argument can even be made that you still shouldn't define a PS. Another approach to consider would be an even stricter version: the binder refuses to work if a DLL with an unexpected enum option is copied to the executable folder; and verification code is emitted (not sure if it's possible during IL emit or JIT) whenever any value is set to a |
Beta Was this translation helpful? Give feedback.
-
@TahirAhmadov Sorry, I just read through more of the thread, and I think now I understand things a bit better 🙂 Your list looks good to me! (#3179 (comment)) |
Beta Was this translation helpful? Give feedback.
-
Closed enum for me is a tool for error handling. Error handling often requires additional informations (messages, args, etc) I propose "constructor" syntax for those enums that's similar to the concept used in records
Random ass usage example:
|
Beta Was this translation helpful? Give feedback.
-
See #113, as adding data elements definitely turns them into discriminated unions. |
Beta Was this translation helpful? Give feedback.
-
because even 'closed' enums can have added values, is it even physically possible to load a library of "closed" value and have that make sense? library v1 has library v2 has you compile against v1. loading v2 is now a binary breaking change, where you now unexpectedly fall through the previously "Complete" switch. |
Beta Was this translation helpful? Give feedback.
-
Probably jumping in to beat a dead horse, but it seems to me that much confusion has been caused by using the term "enum" here. C#'s "original sin" IMO was to borrow the C concept of a list of values backed by integers and to allow the use of bitfield operations (Flags). What I want almost all of the time when declaring an "enum" is actually a closed set of distinct tags that are strongly typed. It's usually desirable to be able to enumerate over that set. In other words, I want the strongly-typed enum pattern without confusing things by providing access to the backing type (which allows people to inject unrepresentable values). |
Beta Was this translation helpful? Give feedback.
-
The int-backed C style enums (what is now |
Beta Was this translation helpful? Give feedback.
-
Yes, I appreciate that enums are what we have now just pointing out that when people talk about "closed sets" they may be coming at it from a perspective that is inconsistent with the assumption that the set must be based on a numeric backing field so perhaps it's better (or at least useful) to consider solving the general problem rather than 'just' patching up int-backed enums. For example #2849 is effectively a proposal for closed set of string-backed values. IOW at some point maybe it's better not to call these things "enums". |
Beta Was this translation helpful? Give feedback.
-
I don't have any strong opinions on naming here, however my main point is that, the int and string backed "enums" (or however we name them) are needed irrespective of other proposals, specifically because in some cases you want to assign an undefined backing value to it, such as, in many network communication scenarios. PS. Please see this comment of mine: #2849 (comment) |
Beta Was this translation helpful? Give feedback.
-
I don't know if this is the right place to comment as it seems that the design has evolved a lot since what was discussed here but the discussion link of the proposal leads here. The most recent version of this proposal #9011 has the closed enums with an integer backed value (which I think is preferable) but requires that 0 is a valid value. I think this is problematic for a very common real world scenario - storing the enum in a database while avoiding the bug of unassigned value. In theory the current open enum could result in an invalid value in the database in many ways but in practice the only way it happens is if you forget to initialize - i.e. the value 0. This problem has a simple solution - start from 1 and restrict the column in the database so that 0 is not a valid value. This solves the vast majority of enum bugs (i.e. turns them into runtime errors rather than corrupt data errors). If we want to use a closed enum we will have the problem of the default 0 again. We could keep using open enums for storing data in the database but then we lose the much desired switch exhaustiveness check. I understand why this 0 is required for closed enums but I hope you can think of some trick to solve the problem. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Champion issue: #9011
Related to #485, it would be helpful to be able to declare an enumerated type that is known to the language to be closed. The syntax is an open question, but as a first proposal I'll suggest using
closed
as in #485.For example, a closed enum
Could be used like this
This code would be considered OK by flow analysis, as the method returns for every possible value of the parameter
b
. Similarly a switch expression that handles every possible declared enumeration value of its switch expression of aclosed enum
type is considered exhaustive.To ensure safety:
closed enum
types are not defined. For example, there is nooperator Bool +(Bool, int)
. Alternately, we might consider them defined but only usable inunsafe
code.Bool
(or only inunsafe
code)Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#discriminated-unions
Beta Was this translation helpful? Give feedback.
All reactions