csharplang
csharplang copied to clipboard
Enum extensions
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 .
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.
I dont know if I like the "extends" as : already serves this purpose.
As @FaustVX said, inheretence would just work in reverse, and I think there would be no issue with this.
As a perk, this feature allows you to
// TODO remove all usage of ObsoleteLibraryEnum, then remove inheritence
enum NewAPIEnum : ObsoleteLibraryEnum { ... }
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).
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:
- MapModeName
- MapLayerName
- MapLayoutName
- ShaderID
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.
@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.
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?
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.
IMO, if a base assembly decided to use an enum for that task then they do not intend for you to extend them.
How would the Roles/Shapes proposal address the need
You're using a mechanism to create strongly typed strings/ints/etc. this is a core case for roles/shapes.
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.
IMO, if a base assembly decided to use an enum for that task then they do not intend for you to extend them.
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".
How would the Roles/Shapes proposal address the need
You're using a mechanism to create strongly typed strings/ints/etc. this is a core case for roles/shapes.
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.
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".
Keep in mind that Extendible Enums is about having a "Registered List of Valid Names" all of which appear in Intellisense
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.
This method is inferior to Enums
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.
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?)
This method is inferior to Enums
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.
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.
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
I mean, that is true of enums as well. It's just a wrapper around an integral. Anyone can pass in any value from the integral domain of that enum.
To be honest I don't understand the need of using raw strings. enums in fact are structs that extends numbers, if you want to extend them you just need to create a new enum, duplicate the values and do an explicit casting
Even if you rename a value is not a problem, but you can have problems if you change the numeric value, or if you add a new value to the base enum. For example if I define PrimaryColor.INDIGO=8000ff and I forget to define in Color enum. When I try to do myMethod((Color)PrimaryColor.INDIGO) for the compiler everything is fine, but casting will fails at runtime.
My proposal is to create a syntax that automatically copies the value of base enum into extended enum, and converts automatically this code myMethod(PrimaryColor.INDIGO) to this myMethod((Color)PrimaryColor.INDIGO) when my method is defined as :
public void Method(Color primary)
{
// Method know all Color and PrimaryColor
}
This is not an object inheritance and has nothing to do with that, that's why I proposed a new keyword instead of :
Keep in mind that Extendible Enums is about having a "Registered List of Valid Names" all of which appear in Intellisense,
There is no need to augment the language just for tooling. You can do tooling with that. Note that we already support this concept somewhat in that a type can state another type that you should use to get values of it from. e.g. if something takes a Color type, it can have a doc comment marker on it saying that the user should be shown items from a Colors class that contains instance of hte Color type. We could always expand on that to let others add in this scenario.
I want type-safety, which you have with Enums. I can't send in an "int" or "string" when it asks for an Enum type.. thus I'm forced to write good key values. If the accepting function takes raw strings, we lose out on safety.
A strong-typed string would be nice, but I'm guessing that's not on-the-table for implementation.
If I could make a strong-typed String (derive directly from string) - it would make it very easy to solve the problem I'm trying to solve. So either that, or Extendible Enums.
Other thing I can do with Enum is "Find all References", while for string value -- not so easy. Enums are very awesome; exactly what I want to use -- if only they could be extended.
I can't send in an "int" or "string" when it asks for an Enum type..
Yes you can :)
Enums don't give typesafety at all. They're similar to NRT in that it's a general suggestion which the callee doesn't have to respect at all. Indeed, it's normal for APIs to use enums with a public surfce area that is documented, and an internal area that is undoc'ed, but which still works as the caller can pass whatever they want.
Other thing I can do with Enum is "Find all References", while for string value -- not so easy
We support Find-All-References in string-values as well :)
But, again, that's a tooling concern, not the language concern.
There can be multiple instances of a string, that are NOT the Key. So in having strings, you risk the chance of accidently mistaking a matching string value for one that is being used as this strong-typed key name. And if you happen to forget one, while refactoring, then it'll remain unfound/orphaned, yet will compile fine -- but then will fail at some point run-time.
This is 100% a language concern. There is no amount of tooling that could fix the raw-string conundrum. We either need strongly-typed strings, or Extendible Enums, to properly solve this issue best. (Or another base struct, that behaves exactly like an enum, except that it's extendible outside of the assembly that declares the base enum.)
Don't use strings as keys if you don't want people to use strings as keys. If you want a strongly-typed value, declare a strong type and control how instances are created. The language doesn't need to provide a specific solution to a problem, only a solution, and there exist many ways to solve this problem.
There can be multiple instances of a string, that are NOT the Key.
This is hte same with an enum as well. If you want to prevent this you can use an analyzer. But you'll also need to back it up with a runtime check.
We either need strongly-typed strings, or Extendible Enums
Once you have 'extendible enums' your'e entirely saying: the values here are open ended (which is already true of enums and strings anyways). So you're literally saying: any values can be passed in, so nothing would protect you here anyways.
Don't use strings as keys if you don't want people to use strings as keys. If you want a strongly-typed value, declare a strong type and control how instances are created. The language doesn't need to provide a specific solution to a problem, only a solution, and there exist many ways to solve this problem.
The "many ways to solve this problem" are costly. For most C# language changes, there were "many ways to solve" those problems too, but the cost of solving it otherwise was burdensome or problematic. So C# was adapted to make it easy.
The exact type I WANT to use for many key named contexts (where the key name is hard coded inside C#) is an ENUM. It's the most fitting for the situation by far.... except for it's inability to be extended, which then later can bite you, because another user/assembly cannot add values when they need to.
And so what do programmers do instead? Use Strings, because it's the lesser of evils, given that it's a POTENTIAL REQUIREMENT to enable extensibility later. They stop using ENUMS and start using Strings for nearly all Keys, where there MIGHT be a chance that they want to extend the Enum Values later. They do this because they "got bit" by Enum's deficiency of non-extensibility... and therefore avoid them.
Enums are uber-powerful as a Strong-Typed-String equivalent, limiting names to only the registered names. It's an 'int' underneath so super powerful, and when used as a Dictionary-key, works faster than a string.
I'd LOVE to use them, but I can't, because of the POTENTIAL requirement that the Enum Names needs to be extended by a customer. And so we are forced into doing a kludge/burdensome (and inferior) implementation of an Enum type.
I have to include about 200 compact lines of added C# to create this struct that is near equivalent to an Enum Type, which I'd like to write just once as a template -- but this isn't possible because you can't derive from structs, and the private static Names List would then be shared by ALL derivations of this struct -- so that would be problematic too.
MOST C# programmers that I've seen/met don't do what I've done; but instead just use Strings, because it's the "Path of least resistance" without really comprehending the QA risks they've just introduced.
If C# had Extendable Enums -- programmers would naturally again use Enums for these situations, and all would be well again in this Universe.
Yeah I don't really understand what problem we are trying to solve here. This sort of reminds me of subtypes in ada
subtype Rainbow is Color range Red .. Blue;
subtype Red_Blue is Rainbow;
subtype Int is Integer;
subtype Small_Int is Integer range -10 .. 10;
subtype Up_To_K is Column range 1 .. K;
subtype Square is Matrix(1 .. 10, 1 .. 10);
subtype Male is Person(Sex => M);
subtype Binop_Ref is not null Binop_Ptr;
where you could say something like (I dunno making up some C# syntax here)
public enum PrimaryColor subtype Color { RED, GREEN, BLUE, BLACK }
or perhaps for general types (something similar to this use case) you could do:
public class OSPlatform subtype string { "Free BSD", "Linux", "Windows" }
Essentially I think the more common case is wanting to restrict an existing types which roles seems to be current design we are working toward.
Is the widening of an enum just because enums are classes in languages like Java and folks want to do inheritance things to them? I might be able to be convinced that there is some utility in enum inheritance, but it certainly doesn't make apis less error prone. Today enums outside of the defined values are rare and awkward because you need to do an explicit cast. If enums are allowed to have inheritance chains that add new values, you now need to worry about someone passing you a derived enum with value ranges you do not expect.
Here's some sample code that I wrote to get around the Enum deficiency, and create something that approximates an Enum:
(there's a bit of added code, since we were using a struct, I added a little more smarts into it, so that each enum was also aware of it's 'group')
static public MapModeName EFIS;
static public MapModeName Vector;
static public MapModeName VFR;
static public MapModeName LowIFR;
static public MapModeName HighIFR;
static public MapModeName Custom;
static private void ___LoadDefaults()
{
InstrumentsPanel = __Create("InstrumentsPanel", "Instruments Panel", _GROUPS.InstrumentsPanel);
ProcedurePreview = __Create("ProcedurePreview", "Procedure Preview", _GROUPS.ProcedurePreview);
FullScreenPlate = __Create("FullScreenPlate", "Full Screen Plate", _GROUPS.FullScreenPlate);
EFIS = __Create("EFIS", "EFIS", _GROUPS.EFIS);
Vector = __Create("Vector", "Vector", _GROUPS.VectorMap, true);
VFR = __Create("VFR", "VFR", _GROUPS.RasterMap, true);
LowIFR = __Create("LowIFR", "LowIFR", _GROUPS.RasterMap, true);
HighIFR = __Create("HighIFR", "HighIFR", _GROUPS.RasterMap, true);
Custom = __Create("Custom", "Custom", _GROUPS.CustomMap, true);
_DEFAULT = Vector;
}
static public void __LoadDefaults()
{
if (!__UseDefaults)
return;
lock (s_AllNameLookup)
{
if (s_AreDefaultsLoaded)
return; // already loaded!
s_AreDefaultsLoaded = true;
_GROUPS.__LoadDefaults();
___LoadDefaults();
}
}
static public bool __UseDefaults
{
get { return s_UseDefaults; }
set
{
if (s_UseDefaults == value) return; // no change
if (s_AreDefaultsLoaded)
{
Log.CodingError("MapLayerName.UseDefaults.Set() - called after Defaults already loaded.");
}
s_UseDefaults = value;
}
}
static private bool s_UseDefaults = true;
static private bool s_AreDefaultsLoaded;
private class JsonConverter : J.JsonConverter<MapModeName>
{
public override MapModeName ReadJson(J.JsonReader reader, Type objectType, MapModeName existingValue, bool hasExistingValue, J.JsonSerializer serializer)
{
string name = (string)reader.Value;
return MapModeName.__GetByName(name);
}
public override void WriteJson(J.JsonWriter writer, MapModeName value, J.JsonSerializer serializer)
{
writer.WriteValue(value.Name);
}
}
static public readonly MapModeName _NULL;
static public MapModeName _DEFAULT;
static MapModeName()
{
_NULL = __Create("_NULL_", "NullMapMode", "_NULL_");
}
public readonly byte Index;
public readonly byte GroupIndex;
public readonly bool IsEFISMode;
public readonly bool IsNormalMap;
public string Name { get { return __AllInstanceNames[Index]; } }
public string DisplayName { get { return __AllInstanceDisplayNames[Index]; } }
public bool IsNull { get { return Index == 0; } }
public bool IsValid { get { return Index > 0; } }
public bool IsMapMode { get { return !IsEFISMode && IsValid; } }
public string GroupName { get { return _GROUPS.__GetGroupName(GroupIndex); } }
public string GroupDisplayName { get { return _GROUPS.__GetGroupDisplayName(GroupIndex); } }
public override string ToString()
{
return Name; // "MapMode<" + Name + "> = " + DisplayName;
}
static public MapModeName __Create(string nameOfMode, string displayName, string groupName, bool isNormalMap = false)
{
byte groupIndex = _GROUPS.__GetOrCreateGroupName(groupName);
return __Create(nameOfMode, displayName, groupIndex, isNormalMap);
}
static public MapModeName __Create(string nameOfMode, string displayName, byte groupIndex = 0, bool isNormalMap = false)
{
MapModeName modeName;
lock (s_AllNameLookup)
{
isNormalMap = isNormalMap;
if (!s_AllNameLookup.TryGetValue(nameOfMode, out modeName))
{ // Not found - so create -- this is EXPECTED
if (__InstanceCount >= __MAX_NAMES)
{
Log.CodingError("MapModeName.Create() - too many created!: {0}, {1}, {2}", __InstanceCount, modeName, displayName);
__InstanceCount--;
}
bool isEFIS = nameOfMode.Contains("EFIS");
modeName = new MapModeName(__InstanceCount, groupIndex, isEFIS, isNormalMap);
__InstanceCount++;
}
else { } // already exists -- so we're going to overwrite it
int index = modeName.Index;
__AllInstances[index] = modeName;
__AllInstanceNames[index] = nameOfMode;
__AllInstanceDisplayNames[index] = displayName;
s_AllNameLookup[nameOfMode] = modeName;
}
return modeName;
}
private MapModeName(byte index, byte groupIndex, bool isEFISMode, bool isNormalMap)
{
Index = index;
GroupIndex = groupIndex;
IsEFISMode = isEFISMode;
IsNormalMap = isNormalMap;
}
static public MapModeName __GetByIndex(byte index)
{
return __AllInstances[index];
}
static public MapModeName __GetByName(string nameOfMode)
{
if (string.IsNullOrEmpty(nameOfMode))
return MapModeName._NULL;
MapModeName modeName;
if (!s_AllNameLookup.TryGetValue(nameOfMode, out modeName))
{
modeName = MapModeName._NULL;
}
return modeName;
}
public const byte __MAX_NAMES = 255;
static private Dictionary<string, MapModeName> s_AllNameLookup = new Dictionary<string, MapModeName>(__MAX_NAMES);
static private string[] __AllInstanceNames = new string[__MAX_NAMES];
static private string[] __AllInstanceDisplayNames = new string[__MAX_NAMES];
static private MapModeName[] __AllInstances = new MapModeName[__MAX_NAMES];
static public byte __InstanceCount { get; private set; }
static public IEnumerable<MapModeName> __GetAllInstances()
{
for (byte i = 0; i < __InstanceCount; i++)
yield return __AllInstances[i];
}
public override int GetHashCode()
{
return Index;
}
public override bool Equals(object obj)
{
if (obj == null || !(obj is MapModeName))
{
return false;
}
MapModeName other = (MapModeName)obj;
return Index == other.Index;
}
int IComparable<MapModeName>.CompareTo(MapModeName other)
{
return Index.CompareTo(other.Index);
}
int IComparable.CompareTo(object obj)
{
if (obj is MapModeName)
{
MapModeName other = (MapModeName)obj;
return Index.CompareTo(other.Index);
}
throw new ArgumentException("MapModeName.CompareTo() with wrong object type.");
}
static public bool operator ==(MapModeName left, MapModeName right)
{
return left.Index == right.Index;
}
static public bool operator !=(MapModeName left, MapModeName right)
{
return left.Index != right.Index;
}
static public bool operator <(MapModeName left, MapModeName right)
{
return left.Index < right.Index;
}
static public bool operator >(MapModeName left, MapModeName right)
{
return left.Index > right.Index;
}
static public bool operator <=(MapModeName left, MapModeName right)
{
return left.Index <= right.Index;
}
static public bool operator >=(MapModeName left, MapModeName right)
{
return left.Index >= right.Index;
}
}
</Details>
The equivalent of my code above (almost) if it were just an enum is this:
public enum MapModeName
{
_NULL, InstrumentPanel, ProcedurePreview, FullScreenPlate, EFIS, Vector, VFR, LowIFR, HighIFR, Custom
}