String-based enums #8804
Replies: 161 comments
-
HttpMethod is another example. It also has to be possible to set values different from identifier names public enum OSPlatform : string
{
FreeBSD = "free bsd",
Linux = "linux",
Windows = "windows"
} |
Beta Was this translation helpful? Give feedback.
-
Seems like something that could be hammered out alongside DUs, which also feel like enums. IMO, the syntax in this case should also allow for the named member to reference a different string in the case that the string either doesn't fit with the naming conventions of C# members or doesn't fit the rules for being an identifier. |
Beta Was this translation helpful? Give feedback.
-
I've always wanted this, and would further want the equality check/deserialization to be case-insensitive, or at least configurable for that. For example, |
Beta Was this translation helpful? Give feedback.
-
HttpMethod as you say and HeaderNames also. HeaderNames having non-C# values in the actual string values such as |
Beta Was this translation helpful? Give feedback.
-
In Azure SDK for .NET, so far we've settled on a structure defined like in https://gist.github.com/heaths/d105148428fe09a2631322b656f04ebb. The main problem comes from a lack of IntelliSense built into VS or VSCode/OmniSharp. If there were a way to enabled this - perhaps through Roslyn - such that |
Beta Was this translation helpful? Give feedback.
-
Another question is if And |
Beta Was this translation helpful? Give feedback.
-
Yes for Also, and without giving it much thought, what if tuples could be used to specify multiple acceptable variations of the string value: [Flags]
public enum OSPlatform : string
{
Linux = ("Linux", "linux", "LINUX")
} |
Beta Was this translation helpful? Give feedback.
-
I'd also like to see something that interops well with Xamarin.iOS/Mac. String enums are a large part of the native API surface of macOS and iOS. Swift's enums were designed with this in mind as well. |
Beta Was this translation helpful? Give feedback.
-
I'm not sold on flags support. This would either require string parsing with some separator or storing them as a collection, which seems unnecessarily heavyweight for the general case. |
Beta Was this translation helpful? Give feedback.
-
Could you describe what this would entail? |
Beta Was this translation helpful? Give feedback.
-
One thing I love doing with enum-ish strings in C# is to emulate Ruby symbols like this: namespace StringSymbols
{
public static class OSPlatform
{
public const string FreeBSD = nameof(FreeBSD);
public const string Linux = nameof(Linux);
public const string Windows = nameof(Windows);
}
} Then use it like this: using System;
using static StringSymbols.OSPlatform;
namespace StringSymbols
{
class Program
{
static void Main(string[] args)
{
var os = Windows;
if( os == Linux ){
Console.WriteLine("Hello Linux, command line ninja");
}
else if( os == Windows ){
Console.WriteLine("Hello Windows, seattle sunshine");
}
else if( os == FreeBSD ){
Console.WriteLine("Hello FreeBSD, go cal bears, go");
}
}
}
} Output:
|
Beta Was this translation helpful? Give feedback.
-
Here is an example of what we do in the AWS .NET SDK to solve this problem. Our main requirement is to be forward compatible with enum values that a service might return in the future. /// <summary>
/// Constants used for properties of type ContainerCondition.
/// </summary>
public class ContainerCondition : ConstantClass
{
/// <summary>
/// Constant COMPLETE for ContainerCondition
/// </summary>
public static readonly ContainerCondition COMPLETE = new ContainerCondition("COMPLETE");
/// <summary>
/// Constant HEALTHY for ContainerCondition
/// </summary>
public static readonly ContainerCondition HEALTHY = new ContainerCondition("HEALTHY");
/// <summary>
/// Constant START for ContainerCondition
/// </summary>
public static readonly ContainerCondition START = new ContainerCondition("START");
/// <summary>
/// Constant SUCCESS for ContainerCondition
/// </summary>
public static readonly ContainerCondition SUCCESS = new ContainerCondition("SUCCESS");
/// <summary>
/// This constant constructor does not need to be called if the constant
/// you are attempting to use is already defined as a static instance of
/// this class.
/// This constructor should be used to construct constants that are not
/// defined as statics, for instance if attempting to use a feature that is
/// newer than the current version of the SDK.
/// </summary>
public ContainerCondition(string value)
: base(value)
{
}
/// <summary>
/// Finds the constant for the unique value.
/// </summary>
/// <param name="value">The unique value for the constant</param>
/// <returns>The constant for the unique value</returns>
public static ContainerCondition FindValue(string value)
{
return FindValue<ContainerCondition>(value);
}
/// <summary>
/// Utility method to convert strings to the constant class.
/// </summary>
/// <param name="value">The string value to convert to the constant class.</param>
/// <returns></returns>
public static implicit operator ContainerCondition(string value)
{
return FindValue(value);
}
} |
Beta Was this translation helpful? Give feedback.
-
I believe DU should be sufficient for this. |
Beta Was this translation helpful? Give feedback.
-
This proposal is a a special case of Discriminated Unions. |
Beta Was this translation helpful? Give feedback.
-
Maybe a more general concept of typed strings, taken from Bosque language?
So in example scenario from above it would be:
|
Beta Was this translation helpful? Give feedback.
-
i still don't know the purpose of a comparison operator here. Had we ever done this as some sort of built in, i can't see why we would support this. We have no usages of case-insensitive strings anywhere else in the language. Heck, constants, patterns and switches already support strings just fine and they have never done anything in an insensitive fashion. If we didn't support case-insensitivity up to now, i can't see why string-enums would change that at all. |
Beta Was this translation helpful? Give feedback.
-
Just copy enums from Dart: https://dart.dev/language/enums and make them have properties, methods, support inheritance, etc. enum NativePlatform {
android("a"),
ios("i"),
windows("w"),
macos("m"),
linux("l"),
web("b"),
unknown("-");
const NativePlatform(this.value);
final String value;
T when<T>({
T Function()? onAndroid,
T Function()? oniOS,
T Function()? onWindows,
T Function()? onMacOS,
T Function()? onLinux,
T Function()? onWeb,
T Function()? orElse,
}) {
if (onAndroid == null &&
oniOS == null &&
onWindows == null &&
onMacOS == null &&
onLinux == null &&
onWeb == null) {
throw UnsupportedError("At least one NativePlatform should be provided");
}
if (onAndroid == null ||
oniOS == null ||
onWindows == null ||
onMacOS == null ||
onLinux == null ||
onWeb == null) {
if (orElse == null) {
throw UnsupportedError(
"If not all NativePlatforms are provided, orElse should be provided");
}
}
switch (this) {
case NativePlatform.android:
return (onAndroid ?? orElse)!();
case NativePlatform.ios:
return (oniOS ?? orElse)!();
case NativePlatform.windows:
return (onWindows ?? orElse)!();
case NativePlatform.macos:
return (onMacOS ?? orElse)!();
case NativePlatform.linux:
return (onLinux ?? orElse)!();
case NativePlatform.web:
return (onWeb ?? orElse)!();
case NativePlatform.unknown:
return orElse!();
}
}
} |
Beta Was this translation helpful? Give feedback.
-
Object enums in general haven't been on the table because Sun/Oracle holds a patent on them, although that is set to expire soon. It's likely that DUs will shape up similar to object enums, except also supporting additional data elements. More like Swift's or Rust's implementation of enums. |
Beta Was this translation helpful? Give feedback.
-
I would love to see this come to life. I've found myself wanting to do this more times than I can count. |
Beta Was this translation helpful? Give feedback.
-
The patent cited by @HaloFour is just expired https://patents.google.com/patent/US7263687B2/en |
Beta Was this translation helpful? Give feedback.
-
The patent was never an impediment of this proposal (specifically string enums), but rather general purpose object enums where each case is a singleton instance. With the team already dedicating a working group on discriminated and type unions I still feel that is the better route, as they cover both all of the capabilities of object enums and then some. It's also my belief that discriminated unions would handle the concept of "string enums" just fine. |
Beta Was this translation helpful? Give feedback.
-
With all due respect, I'm not very satisfied with words like "feel" and "belief". For example, if we take the example below (similar to what OP has): public enum OSPlatform : string
{
FreeBSD = "Free BSD",
Linux,
Windows
}
var p = OSPlatform.Linux;
p = (OSPlatform)"Apple Toaster";
Console.Write(p); // outputs "Apple Toaster"
switch(p)
{
case OSPlatform.Windows: .... break; // works - because these are constants
case (OSPlatform)"Apple Toaster": .... break;
default: ... break;
}
[SomeAttibute(OSPlatform.Linux)] // works - because these are constants
void Foo(OSPlatform p = OSPlatform.Linux) { ... } // works - because these are constants
class JsonWriter
{
// very easy to add method by JSON library authors (ditto for XML serialization, etc.)
public void Value<T>(T stringEnum) where T : StringEnum
{
this.Value(stringEnum.ToString());
}
}
var jw = new JsonWriter(...);
jw.Value(p);
class Dto
{
public OSPlatform OSPlatform { get; set; } // trivial to add recognition of all StringEnum types
}
JsonSerializer.Serialize(jw, new Dto()); // to JSON serializers
class SomeDataEntity
{
public OSPlatform OSPlatform { get; set; } // trivially mapped with one generic converter in your favorite ORM
}
var entity = SomeORM.Query<SomeDataEntity>().First(); // no need to do anything for each StringEnum type How are all of these accomplished with DUs? I think people who want to get string enums (including me) would like to see concrete code examples of all the above scenarios and not just vague assurances. |
Beta Was this translation helpful? Give feedback.
-
DUs can have data elements, and one case can be "other" to cover any unknown cases at the time the DU was defined: public struct enum Color {
Red,
Blue,
Green,
Other(byte r, byte g, byte b);
} Depending on how the members on the DU are defined can blur whether you need to deal with the individual cases or can work with R/B/G values directly, etc. As for serialization, that also depends on how the DUs are defined. I'd expect some degree of support out of the box, but if you want special handling that should also be possible. This isn't uncommon in Java object enums where JSON serialization uses a data element instead of the case name. This could be established via convention (e.g. enum OrderStatus: Decodable {
case pending, approved, shipped, delivered, cancelled
case unknown(value: String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let status = try? container.decode(String.self)
switch status {
case "Pending": self = .pending
case "Approved": self = .approved
case "Shipped": self = .shipped
case "Delivered": self = .delivered
case "Cancelled": self = .cancelled
default:
self = .unknown(value: status ?? "unknown")
}
}
} I also think behavior like "string enums" as proposed here might fall out of extensions: public implicit extension OSPlatform for string {
public static readonly OSPlatform FreeBSD = "Free BSD";
public static readonly OSPlatform Linux = nameof(Linux);
public static readonly OSPlatform Windows = nameof(Windows);
} |
Beta Was this translation helpful? Give feedback.
-
I'm not sure how the switch(p)
{
case OSPlatform.Windows: .... break; // error - constant expected
}
[SomeAttibute(OSPlatform.Linux)] // error - constant expected
void Foo(OSPlatform p = OSPlatform.Linux) { ... } // error - constant expected |
Beta Was this translation helpful? Give feedback.
-
You asked how DUs address these kinds of cases. I'm showing you that languages that have DUs do use them to address these cases.
That's an option. It's possible that the values could be declared as |
Beta Was this translation helpful? Give feedback.
-
You cited DUs which address other scenarios, not string enums. That's why I asked to see what a DU representing specifically the
That's actually an interesting idea. If public explicit extension OSPlatform for string {
public const OSPlatform FreeBSD = (OSPlatform)"Free BSD"; // is casting required?
public const OSPlatform Linux = nameof(Linux);
public const OSPlatform Windows = nameof(Windows);
} It's still more ceremony than desirable, especially if casting is needed, but I can live with it. |
Beta Was this translation helpful? Give feedback.
-
Regarding // how do we represent this:
void Foo<T>(T stringEnum) where T : StringEnum { }
void Foo(StringEnum stringEnum) { }
// using the explicit extension approach?
void Foo<T>(T stringEnum) where T : string extension { } 1, is the above a useful feature, 2, what would the syntax look like? |
Beta Was this translation helpful? Give feedback.
-
As was discussed in #7771, that does not seem to be the case:
|
Beta Was this translation helpful? Give feedback.
-
Yes, if the encoding uses erasure as described there that would mean that you couldn't overload a method between |
Beta Was this translation helpful? Give feedback.
-
It may be necessary in those cases where multiple overloads exist which do different things for a string and a "string enum". The strong typing aspect of enums is valuable despite their "openness". |
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.
-
We've noticed a trend, especially in cloud services, that there is a need for extensible enums. While enums can in principle be extended by casting any int to the enum, it has the risk for conflicts. Using strings has a much lower risk of conflicts.
In the BCL, we've called this concept "strongly typed strings". Examples are:
It would be nice if we could make this a language feature so that instead of this:
one only has to type this:
/cc @pakrym @heaths @JoshLove-msft
Discriminated Unions
As was pointed out by @DavidArno, this won't be solved by discriminated unions because those are about completeness. The primary value of string-based enums is that they are extensible without changing the type definition:
This is vital for things like cloud services where the server and the client can be on different versions.
Beta Was this translation helpful? Give feedback.
All reactions