dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.98k stars 4.66k forks source link

Add EnumMember API #28198

Closed TylerBrinkley closed 4 years ago

TylerBrinkley commented 5 years ago

Split off from dotnet/runtime#20008

Rationale and Usage

Currently, to retrieve both the names and values of an enum's members requires two separate calls and requires you to use a for loop which is quite clumsy. Additionally the pattern to associate extra data with an enum member using Attributes is not directly supported and instead requires users to manually retrieve the Attributes via reflection. This pattern is commonly used on enums using the DescriptionAttribute, EnumMemberAttribute, and DisplayAttribute. There should be added direct support for the retrieval of Attributes applied to enum members.

What used to be this to retrieve both the names and values of an enum's members

var values = (MyEnum[])Enum.GetValues(typeof(MyEnum));
var names = Enum.GetNames(typeof(MyEnum));
for (int i = 0; i < values.Length; ++i)
{
    MyEnum value = values[i];
    string name = names[i];
}

now becomes this

foreach (var member in Enum.GetMembers<MyEnum>())
{
    MyEnum value = member.Value;
    string name = member.Name;
}

And what used to be this to retrieve the DescriptionAttribute.Description of an enum member

MyEnum value = ???;
string description = ((DescriptionAttribute)Attribute.GetCustomAttribute(
    typeof(MyEnum).GetField(value.ToString()),
    typeof(DescriptionAttribute),
    false))?.Description;

now becomes this

MyEnum value = ???;
string description = value.GetMember()?.Attributes.Get<DescriptionAttribute>()?.Description;

Proposed API

 namespace System {
     public abstract class Enum : ValueType, IComparable, IConvertible, IFormattable {
         // New Generic API
+        public static EnumMember<TEnum> GetMember<TEnum>(this TEnum value) where TEnum : struct, Enum;
+        public static EnumMember<TEnum> GetMember<TEnum>(string name) where TEnum : struct, Enum;
+        public static EnumMember<TEnum> GetMember<TEnum>(string name, bool ignoreCase) where TEnum : struct, Enum;
+        public static EnumMember<TEnum> GetMember<TEnum>(ReadOnlySpan<char> name) where TEnum : struct, Enum;
+        public static EnumMember<TEnum> GetMember<TEnum>(ReadOnlySpan<char> name, bool ignoreCase) where TEnum : struct, Enum;
+        public static IReadOnlyList<EnumMember<TEnum>> GetMembers<TEnum>() where TEnum : struct, Enum;

         // New Non-Generic API
+        public static EnumMember GetMember(Type enumType, object value)
+        public static EnumMember GetMember(Type enumType, string name);
+        public static EnumMember GetMember(Type enumType, string name, bool ignoreCase);
+        public static EnumMember GetMember(Type enumType, ReadOnlySpan<char> name);
+        public static EnumMember GetMember(Type enumType, ReadOnlySpan<char> name, bool ignoreCase);
+        public static IReadOnlyList<EnumMember> GetMembers(Type enumType);
     }
+    public abstract class EnumMember : IEquatable<EnumMember>, IComparable<EnumMember>, IComparable, IConvertible, IFormattable {
+        public ComponentModel.AttributeCollection Attributes { get; }
+        public string Name { get; }
+        public object Value { get; }
+        public bool Equals(EnumMember other);
+        public sealed override bool Equals(object other);
+        public sealed override int GetHashCode();
+        public sealed override string ToString();
+        public string ToString(string format);
+    }
+    public abstract class EnumMember<TEnum> : EnumMember, IEquatable<EnumMember<TEnum>>, IComparable<EnumMember<TEnum>> {
+        public new TEnum Value { get; }
+        public bool Equals(EnumMember<TEnum> other);
+    }
 }
 namespace System.ComponentModel {
-    public class AttributeCollection : ICollection, IEnumerable
+    public class AttributeCollection : ICollection, IEnumerable, IList<Attribute>, IReadOnlyList<Attribute> {
+        public TAttribute Get<TAttribute>() where TAttribute : Attribute;
+        public Attribute Get(Type attributeType);
+        public IEnumerable<TAttribute> GetAll<TAttribute>() where TAttribute : Attribute;
+        public IEnumerable<Attribute> GetAll(Type attributeType);
+        public bool Has<TAttribute>() where TAttribute : Attribute;
+        public bool Has(Type attributeType);
     }
 }

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.

This proposal stems from my work on the open source library Enums.NET.

Enum API Details

EnumMember API Details

AttributeCollection API Details

Implementation Details

A type forward would need to be added for AttributeCollection so that it's available from corelib. Utilizes performance improved implementation from dotnet/runtime#20008.

Updates

terrajobst commented 4 years ago

Video

We should add this API:

namespace System
{
    public partial class Enum
    {
        public static T[] GetValues<T>();
    }
}

So this code:

var values = (MyEnum[])Enum.GetValues(typeof(MyEnum));
var names = Enum.GetNames(typeof(MyEnum));
for (int i = 0; i < values.Length; ++i)
{
    MyEnum value = values[i];
    string name = names[i];
}

becomes

var values = Enum.GetValues<MyEnum>();
foreach (var value in values)
{
    var name = value.ToString();
}

With respect to custom attributes, you can already do this:

FieldInfo enumField = ...;
var description = enumField.GetCustomAttributes<DescriptionAttribute>()
                           .SingleOrDefault()?.Description ?? "";
jkotas commented 4 years ago

Generic Enum.GetValues was approved as #2364 and added by #33589 two weeks ago.

The EnumMember family of APIs was rejected as too high-level for System.Enum, so there is nothing left to do. @terrajobst Is this the right conclusion?

TylerBrinkley commented 4 years ago

Thanks for the consideration. Enum.GetValues<TEnum>() was already added in https://github.com/dotnet/runtime/issues/2364 so no work to be done there. While .ToString() doesn't handle the duplicate values case I understand if this API is too high-level here, especially including the reflection case of attributes.

terrajobst commented 4 years ago

Generic Enum.GetValues was approved as #2364 and added by #33589 two weeks ago.

Ha, my spider senses told me we already had the API :-)

The EnumMember family of APIs was rejected as too high-level for System.Enum, so there is nothing left to do. @terrajobst Is this the right conclusion?

Correct