Skip to content

Enum Improvements #20008

@TylerBrinkley

Description

@TylerBrinkley

Enums are essential commonly used types but have several areas in need of improvement. For each non-generic Enum method there should be an equivalent generic version.

Rationale and Usage

  1. Nearly all of Enum's static methods are non-generic leading to the following issues.
    • Requires boxing for methods with enum input parameters losing type-safety, eg. IsDefined and GetName.
    • Requires casting/unboxing for methods with an enum return value, eg. ToObject and GetValues.
    • Requires the enum type to be explicitly specified as an argument.
    • Requires invocation using static method syntax.

With this proposal implemented what used to be this to validate a standard enum value

MyEnum value = ???;
bool isValid = Enum.IsDefined(typeof(MyEnum), value);

now becomes this

MyEnum value = ???;
bool isValid = value.IsDefined();

With this implemented it will address #18063.

Proposed API

namespace System {
    public abstract class Enum : ValueType, IComparable, IConvertible, IFormattable {
        public static TEnum Parse<TEnum>(ReadOnlySpan<char> value) where TEnum : struct, Enum;
        public static TEnum Parse<TEnum>(ReadOnlySpan<char> value, bool ignoreCase) where TEnum : struct, Enum;
        public static object Parse(Type enumType, ReadOnlySpan<char> value);
        public static object Parse(Type enumType, ReadOnlySpan<char> value, bool ignoreCase);
        public static bool TryParse<TEnum>(ReadOnlySpan<char> value, out TEnum result) where TEnum : struct, Enum;
        public static bool TryParse<TEnum>(ReadOnlySpan<char> value, bool ignoreCase, out TEnum result) where TEnum : struct, Enum;
        public static bool TryParse(Type enumType, ReadOnlySpan<char> value, out object result);
        public static bool TryParse(Type enumType, ReadOnlySpan<char> value, bool ignoreCase, out object result);

        // Generic versions of existing methods
        public static string GetName<TEnum>(this TEnum value) where TEnum : struct, Enum;
        public static IReadOnlyList<string> GetNames<TEnum>() where TEnum : struct, Enum;
        public static IReadOnlyList<TEnum> GetValues<TEnum>() where TEnum : struct, Enum;
        public static bool IsDefined<TEnum>(this TEnum value) where TEnum : struct, Enum;
        public static TEnum ToObject<TEnum>(object value) where TEnum : struct, Enum;
        public static TEnum ToObject<TEnum>(sbyte value) where TEnum : struct, Enum;
        public static TEnum ToObject<TEnum>(byte value) where TEnum : struct, Enum;
        public static TEnum ToObject<TEnum>(short value) where TEnum : struct, Enum;
        public static TEnum ToObject<TEnum>(ushort value) where TEnum : struct, Enum;
        public static TEnum ToObject<TEnum>(int value) where TEnum : struct, Enum;
        public static TEnum ToObject<TEnum>(uint value) where TEnum : struct, Enum;
        public static TEnum ToObject<TEnum>(long value) where TEnum : struct, Enum;
        public static TEnum ToObject<TEnum>(ulong value) where TEnum : struct, Enum;
        public static int CompareTo<TEnum>(TEnum value, TEnum other) where TEnum : struct, Enum;
        public static bool Equals<TEnum>(TEnum value, TEnum other) where TEnum : struct, Enum;
        public static byte ToByte<TEnum>(TEnum value) where TEnum : struct, Enum;
        public static short ToInt16<TEnum>(TEnum value) where TEnum : struct, Enum;
        public static int ToInt32<TEnum>(TEnum value) where TEnum : struct, Enum;
        public static long ToInt64<TEnum>(TEnum value) where TEnum : struct, Enum;
        public static sbyte ToSByte<TEnum>(TEnum value) where TEnum : struct, Enum;
        public static string ToString<TEnum>(TEnum value) where TEnum : struct, Enum;
        public static string ToString<TEnum>(TEnum value, string format) where TEnum : struct, Enum;
        public static ushort ToUInt16<TEnum>(TEnum value) where TEnum : struct, Enum;
        public static uint ToUInt32<TEnum>(TEnum value) where TEnum : struct, Enum;
        public static ulong ToUInt64<TEnum>(TEnum value) where TEnum : struct, Enum;
    }
}

API Details

This proposal makes use of a C# language feature that needs to be added in order for this proposal to make the most impact.

This proposal specifies extension methods within System.Enum and as such requires C# to allow extension methods within non-static classes as is proposed in csharplang#301. Promoting these to extension methods later would be a breaking change due to csharplang#665 but I feel this is acceptable.

Alternatively, the extension methods could be defined in a separate static EnumExtensions class. This is uglier but would avoid this issue and the extension methods would be available immediately instead of needing to wait for a later C# version to support this.

Open Questions

  • Is promoting the System.Enum extension methods later an acceptable breaking change due to csharplang#665? If not should we wait for a later C# version that supports extension methods in System.Enum or should we introduce a separate EnumExtensions class?

Implementation Details

This proposal stems from my work on the open source library Enums.NET which addresses each of these issues and will be the basis for an implementation. For efficiency this is how Enums.NET is implemented.

First, using Enum as a generic type argument causes generic code-explosion in today's runtimes so it's essential that it's included sparingly. For this reason most logic is contained in an EnumCache<TInt, TIntProvider> object where TInt is the underlying type and TIntProvider is the underlying type's operations provider which implements the interface INumericProvider<TInt> which provides the bitwise operations and other needed operations. Including the TIntProvider as a type parameter allows the runtime to inline calls to its methods so there's no interface method dispatch, a technique described in this generic calculations article.

Now how can one access this EnumCache object when the caller only has the enum type as a generic type argument? This is solved by the bridge-like object EnumInfo<TEnum, TInt, TIntProvider> which implements the interfaces IEnumInfo<TEnum> for generic methods and IEnumInfo for non-generic methods. It simply acts as a bridge to delegate calls from the interfaces to EnumCache<TInt, TIntProvider>. In order to do that it needs to be able to efficiently convert values between TEnum and TInt. This is performed by the conversion methods ToInt and ToEnum which are declared as extern methods for efficiency so that they're simply defined as casts from one type to the other. These method's functionality can be achieved with the Unsafe.As method. This EnumInfo<TEnum, TInt, TIntProvider> is created via reflection upon it's first call and is stored in the static field Enums<TEnum>.Info as an IEnumInfo<TEnum> which is then called by Enums's generic methods.

For non-generic methods the RuntimeType.GenericCache property will be set to the IEnumInfo to use.

Updates

  • Added more implementation details.
  • Added more API details.
  • Added enumerating members usage example.
  • Added generic versions of instance methods for performance reasons.
  • Removed the System.Enums namespace and moved its members into the System namespace.
  • Moved PrimaryAttribute into the System.ComponentModel namespace and switched to using the existing System.ComponentModel.AttributeCollection.
  • Removed IsValid method as it's meaning is rather subjective and promoted IsValidFlagCombination as an extension method.
  • Renamed GetAllFlags to AllFlags as it more closely resembles a property but can't be because it is generic. Similar naming to Array.Empty.
  • Removed EnumComparer as one can just use Comparer<T>.Default and EqualityComparer<T>.Default instead.
  • Split the proposed API to a base API addition and an EnumMember support API addition.
  • Added ReadOnlySpan<char> parse overloads.
  • Moved FlagEnum.IsFlagEnum into System.Enum.
  • No longer proposes adding the Enum constraint to existing methods.
  • Split EnumMember API addition into the separate issue dotnet/corefx#34077.
  • Split generic API additions that don't necessitate the performance improvements of this implementation to separate issue API Proposal: Add a Generic version of GetValues to Enum (probably GetName/GetNames) #2364.
  • Split Flag API additions to separate issue dotnet/corefx#34079.
  • Split PrimaryAttribute addition to separate issue dotnet/corefx#34080.
  • Moved Format to API Proposal: Add a Generic version of GetValues to Enum (probably GetName/GetNames) #2364 as well.
  • Moved proposed methods from API Proposal: Add a Generic version of GetValues to Enum (probably GetName/GetNames) #2364 back to this issue.
  • Removed Format as it's no different from ToString(string format) except it throws an ArgumentNullException for a null format parameter.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-needs-workAPI needs work before it is approved, it is NOT ready for implementationarea-System.Runtimedesign-discussionOngoing discussion about design without consensus

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions