Czy możemy zdefiniować niejawne konwersje wyliczeń w języku C #?

132

Czy można zdefiniować niejawną konwersję wyliczeń w języku C #?

coś, co mogłoby to osiągnąć?

public enum MyEnum
{
    one = 1, two = 2
}

MyEnum number = MyEnum.one;
long i = number;

Jeśli nie, dlaczego nie?

Adam Naylor
źródło
2
Też chciałbym to zrobić. Mamy wyliczenie, enum YesNo {Yes, No}które można niejawnie przekonwertować na bool.
Colonel Panic,
Zauważ, że ta koncepcja wyłącza sprawdzanie bezpieczeństwa typu kompilatora. W dłuższej perspektywie, wyraźne skrócenie konwersji, takie jak końcowe „~”, może być lepsze.
crokusek
Link nie jest już ważny - czy możemy go usunąć lub ponownie opublikować gdzieś stronę?
ワ イ き ん ぐ

Odpowiedzi:

128

Jest rozwiązanie. Rozważ następujące:

public sealed class AccountStatus
{
    public static readonly AccountStatus Open = new AccountStatus(1);
    public static readonly AccountStatus Closed = new AccountStatus(2);

    public static readonly SortedList<byte, AccountStatus> Values = new SortedList<byte, AccountStatus>();
    private readonly byte Value;

    private AccountStatus(byte value)
    {
        this.Value = value;
        Values.Add(value, this);
    }


    public static implicit operator AccountStatus(byte value)
    {
        return Values[value];
    }

    public static implicit operator byte(AccountStatus value)
    {
        return value.Value;
    }
}

Powyższe oferuje niejawną konwersję:

        AccountStatus openedAccount = 1;            // Works
        byte openedValue = AccountStatus.Open;      // Works

To trochę więcej pracy niż deklarowanie normalnego wyliczenia (chociaż niektóre z powyższych można zmienić na wspólną ogólną klasę bazową). Możesz pójść jeszcze dalej, wprowadzając w klasie bazowej IComparable & IEquatable, a także dodając metody zwracające wartość DescriptionAttributes, zadeklarowane nazwy itp.

Napisałem klasę bazową (RichEnum <>) do obsługi większości prac gruntowych, co upraszcza powyższą deklarację wyliczeń do:

public sealed class AccountStatus : RichEnum<byte, AccountStatus>
{
    public static readonly AccountStatus Open = new AccountStatus(1);
    public static readonly AccountStatus Closed = new AccountStatus(2);

    private AccountStatus(byte value) : base (value)
    {
    }

    public static implicit operator AccountStatus(byte value)
    {
        return Convert(value);
    }
}

Klasa bazowa (RichEnum) jest wymieniona poniżej.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Resources;

namespace Ethica
{
    using Reflection;
    using Text;

    [DebuggerDisplay("{Value} ({Name})")]
    public abstract class RichEnum<TValue, TDerived>
                : IEquatable<TDerived>,
                  IComparable<TDerived>,
                  IComparable, IComparer<TDerived>
        where TValue : struct , IComparable<TValue>, IEquatable<TValue>
        where TDerived : RichEnum<TValue, TDerived>
    {
        #region Backing Fields

        /// <summary>
        /// The value of the enum item
        /// </summary>
        public readonly TValue Value;

        /// <summary>
        /// The public field name, determined from reflection
        /// </summary>
        private string _name;

        /// <summary>
        /// The DescriptionAttribute, if any, linked to the declaring field
        /// </summary>
        private DescriptionAttribute _descriptionAttribute;

        /// <summary>
        /// Reverse lookup to convert values back to local instances
        /// </summary>
        private static SortedList<TValue, TDerived> _values;

        private static bool _isInitialized;


        #endregion

        #region Constructors

        protected RichEnum(TValue value)
        {
            if (_values == null)
                _values = new SortedList<TValue, TDerived>();
            this.Value = value;
            _values.Add(value, (TDerived)this);
        }

        #endregion

        #region Properties

        public string Name
        {
            get
            {
                CheckInitialized();
                return _name;
            }
        }

        public string Description
        {
            get
            {
                CheckInitialized();

                if (_descriptionAttribute != null)
                    return _descriptionAttribute.Description;

                return _name;
            }
        }

        #endregion

        #region Initialization

        private static void CheckInitialized()
        {
            if (!_isInitialized)
            {
                ResourceManager _resources = new ResourceManager(typeof(TDerived).Name, typeof(TDerived).Assembly);

                var fields = typeof(TDerived)
                                .GetFields(BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public)
                                .Where(t => t.FieldType == typeof(TDerived));

                foreach (var field in fields)
                {

                    TDerived instance = (TDerived)field.GetValue(null);
                    instance._name = field.Name;
                    instance._descriptionAttribute = field.GetAttribute<DescriptionAttribute>();

                    var displayName = field.Name.ToPhrase();
                }
                _isInitialized = true;
            }
        }

        #endregion

        #region Conversion and Equality

        public static TDerived Convert(TValue value)
        {
            return _values[value];
        }

        public static bool TryConvert(TValue value, out TDerived result)
        {
            return _values.TryGetValue(value, out result);
        }

        public static implicit operator TValue(RichEnum<TValue, TDerived> value)
        {
            return value.Value;
        }

        public static implicit operator RichEnum<TValue, TDerived>(TValue value)
        {
            return _values[value];
        }

        public static implicit operator TDerived(RichEnum<TValue, TDerived> value)
        {
            return value;
        }

        public override string ToString()
        {
            return _name;
        }

        #endregion

        #region IEquatable<TDerived> Members

        public override bool Equals(object obj)
        {
            if (obj != null)
            {
                if (obj is TValue)
                    return Value.Equals((TValue)obj);

                if (obj is TDerived)
                    return Value.Equals(((TDerived)obj).Value);
            }
            return false;
        }

        bool IEquatable<TDerived>.Equals(TDerived other)
        {
            return Value.Equals(other.Value);
        }


        public override int GetHashCode()
        {
            return Value.GetHashCode();
        }

        #endregion

        #region IComparable Members

        int IComparable<TDerived>.CompareTo(TDerived other)
        {
            return Value.CompareTo(other.Value);
        }

        int IComparable.CompareTo(object obj)
        {
            if (obj != null)
            {
                if (obj is TValue)
                    return Value.CompareTo((TValue)obj);

                if (obj is TDerived)
                    return Value.CompareTo(((TDerived)obj).Value);
            }
            return -1;
        }

        int IComparer<TDerived>.Compare(TDerived x, TDerived y)
        {
            return (x == null) ? -1 :
                   (y == null) ? 1 :
                    x.Value.CompareTo(y.Value);
        }

        #endregion

        public static IEnumerable<TDerived> Values
        {
            get
            {
                return _values.Values;
            }
        }

        public static TDerived Parse(string name)
        {
            foreach (TDerived value in _values.Values)
                if (0 == string.Compare(value.Name, name, true) || 0 == string.Compare(value.DisplayName, name, true))
                    return value;

            return null;
        }
    }
}
znak
źródło
Poprawiono mały błąd offguarda w poście :-) To publiczny statyczny operator niejawny AccountStatus (wartość bajtu) {return Convert (wartość); } NIE zwraca Convert (bajt);
Mehdi LAMRANI
Skompilowałem tę klasę bazową. Czy masz coś przeciwko, jeśli edytuję zmiany?
sehe
68
To rozwiązanie może być „właściwe” jako ćwiczenie lub testowanie czyichś umiejętności programistycznych, ale proszę, nie rób tego w prawdziwym życiu. Nie tylko jest to przesada, ale także nieproduktywne, nie do utrzymania i brzydkie jak diabli. Nie musisz używać wyliczenia tylko ze względu na to. Albo umieszczasz jawne rzutowanie, albo po prostu piszesz klasę statyczną ze stałymi ints.
Pułapka
3
Czy nie jest to po prostu ponowna implementacja wyliczenia Java?
Agent_L
2
Jednym z głównych problemów jest to, że nie można używać statycznych stałych tylko do odczytu w instrukcjach switch.
Ian Goldby,
35

Nie możesz wykonywać niejawnych konwersji (z wyjątkiem zera) i nie możesz pisać własnych metod instancji - jednak prawdopodobnie możesz napisać własne metody rozszerzające:

public enum MyEnum { A, B, C }
public static class MyEnumExt
{
    public static int Value(this MyEnum foo) { return (int)foo; }
    static void Main()
    {
        MyEnum val = MyEnum.A;
        int i = val.Value();
    }
}

To jednak nie daje ci wiele (w porównaniu do robienia wyraźnej obsady).

Jednym z głównych momentów, w których ludzie chcieli tego robić, było dokonywanie [Flags]manipulacji za pomocą leków generycznych - tj bool IsFlagSet<T>(T value, T flag);. Metody. Niestety, C # 3.0 nie obsługuje operatorów w rodzajach generycznych, ale można to obejść, używając takich rzeczy , które sprawiają, że operatory są w pełni dostępne dla typów ogólnych.

Marc Gravell
źródło
Tak, to był jeden z moich najbardziej poszukiwanych w C # 4: stackoverflow.com/questions/138367/ ... i stackoverflow.com/questions/7244
Keith
@Keith - dobra robota, to się udało ;-p Wsparcie dynamiczne / operator nie dotarło do CTP, ale mam gotowe do użycia stanowisko testowe, aby porównać dwa podejścia dla operatorów z dynamicznym ( vs generics / Expression), kiedy się tam dostanie.
Marc Gravell
@Keith - możesz chcieć nadać klasie Operator w MiscUtil wir; Jestem prawie pewien, że zrobi większość tego, co chcesz.
Marc Gravell
23
struct PseudoEnum
{
    public const int 
              INPT = 0,
              CTXT = 1,
              OUTP = 2;
};

// ...

var arr = new String[3];

arr[PseudoEnum.CTXT] = "can";
arr[PseudoEnum.INPT] = "use";
arr[PseudoEnum.CTXT] = "as";
arr[PseudoEnum.CTXT] = "array";
arr[PseudoEnum.OUTP] = "index";
Glenn Slayden
źródło
ale dlaczego struct?
Konrad
2
Tak naprawdę bez powodu. static classPrzypuszczam, że mógłbyś użyć . W końcowym ILkodzie nie ma żadnej korzyści z argumentowania za żadnym z przypadków .
Glenn Slayden
18

Dostosowałem doskonałą ogólną klasę bazową RichEnum Marka.

Ustalenie

  1. szereg problemów z kompilacją z powodu brakujących bitów z jego bibliotek (w szczególności: nazwy wyświetlane zależne od zasobów nie zostały całkowicie usunięte; są teraz)
  2. inicjalizacja nie była idealna: jeśli pierwszą rzeczą, którą zrobiłeś, był dostęp do statycznej właściwości .Values ​​z klasy bazowej, otrzymasz NPE. Naprawiono to, zmuszając klasę bazową do ciekawie-rekurencyjnego ( CRTP ) wymuszania statycznej konstrukcji TDerived just in time podczas CheckInitialized
  3. w końcu przeniosłem logikę CheckInitialized do statycznego konstruktora (aby uniknąć kary za sprawdzanie za każdym razem warunku wyścigu przy inicjalizacji wielowątkowej; być może była to niemożliwość rozwiązana przez mój punktor 1.?)

Uznanie dla Marka za wspaniały pomysł + wdrożenie, oto dla was wszystkich:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Resources;

namespace NMatrix
{

    [DebuggerDisplay("{Value} ({Name})")]
    public abstract class RichEnum<TValue, TDerived>
                : IEquatable<TDerived>,
                  IComparable<TDerived>,
                  IComparable, IComparer<TDerived>
        where TValue : struct, IComparable<TValue>, IEquatable<TValue>
        where TDerived : RichEnum<TValue, TDerived>
    {
        #region Backing Fields

        /// <summary>
        /// The value of the enum item
        /// </summary>
        public readonly TValue Value;

        /// <summary>
        /// The public field name, determined from reflection
        /// </summary>
        private string _name;

        /// <summary>
        /// The DescriptionAttribute, if any, linked to the declaring field
        /// </summary>
        private DescriptionAttribute _descriptionAttribute;

        /// <summary>
        /// Reverse lookup to convert values back to local instances
        /// </summary>
        private static readonly SortedList<TValue, TDerived> _values = new SortedList<TValue, TDerived>();

        #endregion

        #region Constructors

        protected RichEnum(TValue value)
        {
            this.Value = value;
            _values.Add(value, (TDerived)this);
        }

        #endregion

        #region Properties

        public string Name
        {
            get
            {
                return _name;
            }
        }

        public string Description
        {
            get
            {
                if (_descriptionAttribute != null)
                    return _descriptionAttribute.Description;

                return _name;
            }
        }

        #endregion

        #region Initialization

        static RichEnum()
        {
            var fields = typeof(TDerived)
                .GetFields(BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public)
                .Where(t => t.FieldType == typeof(TDerived));

            foreach (var field in fields)
            {
                /*var dummy =*/ field.GetValue(null); // forces static initializer to run for TDerived

                TDerived instance = (TDerived)field.GetValue(null);
                instance._name = field.Name;
                                    instance._descriptionAttribute = field.GetCustomAttributes(true).OfType<DescriptionAttribute>().FirstOrDefault();
            }
        }

        #endregion

        #region Conversion and Equality

        public static TDerived Convert(TValue value)
        {
            return _values[value];
        }

        public static bool TryConvert(TValue value, out TDerived result)
        {
            return _values.TryGetValue(value, out result);
        }

        public static implicit operator TValue(RichEnum<TValue, TDerived> value)
        {
            return value.Value;
        }

        public static implicit operator RichEnum<TValue, TDerived>(TValue value)
        {
            return _values[value];
        }

        public static implicit operator TDerived(RichEnum<TValue, TDerived> value)
        {
            return value;
        }

        public override string ToString()
        {
            return _name;
        }

        #endregion

        #region IEquatable<TDerived> Members

        public override bool Equals(object obj)
        {
            if (obj != null)
            {
                if (obj is TValue)
                    return Value.Equals((TValue)obj);

                if (obj is TDerived)
                    return Value.Equals(((TDerived)obj).Value);
            }
            return false;
        }

        bool IEquatable<TDerived>.Equals(TDerived other)
        {
            return Value.Equals(other.Value);
        }


        public override int GetHashCode()
        {
            return Value.GetHashCode();
        }

        #endregion

        #region IComparable Members

        int IComparable<TDerived>.CompareTo(TDerived other)
        {
            return Value.CompareTo(other.Value);
        }

        int IComparable.CompareTo(object obj)
        {
            if (obj != null)
            {
                if (obj is TValue)
                    return Value.CompareTo((TValue)obj);

                if (obj is TDerived)
                    return Value.CompareTo(((TDerived)obj).Value);
            }
            return -1;
        }

        int IComparer<TDerived>.Compare(TDerived x, TDerived y)
        {
            return (x == null) ? -1 :
                   (y == null) ? 1 :
                    x.Value.CompareTo(y.Value);
        }

        #endregion

        public static IEnumerable<TDerived> Values
        {
            get
            {
                return _values.Values;
            }
        }

        public static TDerived Parse(string name)
        {
            foreach (TDerived value in Values)
                if (0 == string.Compare(value.Name, name, true))
                    return value;

            return null;
        }
    }
}

Przykładowe użycie, które uruchomiłem na mono:

using System.ComponentModel;
using System;

namespace NMatrix
{    
    public sealed class MyEnum : RichEnum<int, MyEnum>
    {
        [Description("aap")]  public static readonly MyEnum my_aap   = new MyEnum(63000);
        [Description("noot")] public static readonly MyEnum my_noot  = new MyEnum(63001);
        [Description("mies")] public static readonly MyEnum my_mies  = new MyEnum(63002);

        private MyEnum(int value) : base (value) { } 
        public static implicit operator MyEnum(int value) { return Convert(value); }
    }

    public static class Program
    {
        public static void Main(string[] args)
        {
            foreach (var enumvalue in MyEnum.Values)
                Console.WriteLine("MyEnum {0}: {1} ({2})", (int) enumvalue, enumvalue, enumvalue.Description);
        }
    }
}

Tworzenie wyniku

[mono] ~/custom/demo @ gmcs test.cs richenum.cs && ./test.exe 
MyEnum 63000: my_aap (aap)
MyEnum 63001: my_noot (noot)
MyEnum 63002: my_mies (mies)

Uwaga: mono 2.6.7 wymaga dodatkowego wyraźnego rzutowania, które nie jest wymagane przy używaniu mono 2.8.2 ...

sehe
źródło
Użycie .Single () do uzyskania atrybutu opisu nie jest dobrym pomysłem. Jeśli nie ma atrybutu, Single () zgłasza wyjątek, SingleOrDefault () nie.
kerem
@kerem dobra uwaga, zaktualizowałem go (używając FirstOrDefault, aby uniknąć założenia, że ​​istnieje tylko jeden atrybut). To, czy założenie takich rzeczy jest
``
1
Uwielbiam to, ale napotkałem problem: w Windows 7 / .NET 4.5 ta linia TDerived instance = (TDerived)field.GetValue(null);powoduje instancepowstanie null. Wydaje się, że środowisko uruchomieniowe Mono musi mieć inną kolejność inicjalizacji typów niż ta .NET, która to umożliwia. Zagadkowe! Zamiast tego musiałem przenieść ten kod do metody statycznej i wywołać go z inicjatora typu w podklasie.
agentnega
@agentnega Dzięki za ten dodatek. To mogłoby komuś pomóc.
sehe
@agentnega. Mam ten sam problem na .net 4.5.1. Wydaje się, że „narusza” specyfikację C # b / c nie inicjuje wartości przed pierwszym użyciem - przynajmniej nie podczas korzystania z odbicia. Zaimplementowałem obejście, które nie wymaga udziału podklasy („TDerived”). @ sehe czy powinienem edytować twoją odpowiedź i dodać obejście do twojej odpowiedzi, czy powinienem opublikować nową odpowiedź?
BatteryBackupUnit
6

Nie można zadeklarować niejawnych konwersji typów wyliczeniowych, ponieważ nie mogą one definiować metod. C # ukryte kompiluje słów kluczowych do metody zaczynające się od „op_”, a to nie będzie działać w tym przypadku.

Igal Tabachnik
źródło
5

Prawdopodobnie tak, ale nie w przypadku wyliczenia (nie możesz dodać do niego metody). Możesz dodać niejawną konwersję do własnej klasy, aby umożliwić konwersję wyliczenia do niej,

public class MyClass {

    public static implicit operator MyClass ( MyEnum input ) {
        //...
    }
}

MyClass m = MyEnum.One;

Pytanie brzmi: dlaczego?

Ogólnie .Net unika (i Ty też powinieneś) wszelkich niejawnych konwersji, podczas których dane mogą zostać utracone.

Keith
źródło
3

Jeśli zdefiniujesz podstawę wyliczenia jako długą, możesz przeprowadzić jawną konwersję. Nie wiem, czy można używać niejawnych konwersji, ponieważ wyliczenia nie mogą mieć zdefiniowanych metod.

public enum MyEnum : long
{
    one = 1,
    two = 2,
}

MyEnum number = MyEnum.one;
long i = (long)number;

Należy również pamiętać, że niezainitalizowane wyliczenie będzie miało domyślnie wartość 0 lub pierwszą pozycję - więc w powyższej sytuacji prawdopodobnie również najlepiej byłoby zdefiniować zero = 0.

Popiół
źródło
5
Nie potrzebujesz : longtutaj; jawna konwersja działałaby bez niej. Jedyną legalną niejawną konwersją jest zero.
Marc Gravell
3
Nie; domyślnym wyliczeniem jest Int32
Marc Gravell
1
Zobacz: wyliczenie Foo {A, B, C} Console.WriteLine (Enum.GetUnderlyingType (typeof (Foo)));
Marc Gravell
14
Dlaczego jest to oznaczone jako odpowiedź i ma tak wiele punktów? To NIE dotyczy pytania OP !!! Mówi o IMPLICIT Conversion ... Wartość dodana jest zerowa.
Mehdi LAMRANI
3
Pytanie już sugeruje, że wyraźne rzucanie jest zrozumiałe, pytanie jest równoważne pytaniu „Jak uniknąć jawnego rzucania?”, Do którego TEN post nie ma zastosowania.
Kit10,
2

wyliczenia są dla mnie w dużej mierze bezużyteczne z tego powodu OP.

W końcu cały czas robię zdjęcia związane z piciem:

proste rozwiązanie

Klasycznym przykładem problemu jest zestaw VirtualKey do wykrywania naciśnięć klawiszy.

enum VKeys : ushort
{
a = 1,
b = 2,
c = 3
}
// the goal is to index the array using predefined constants
int[] array = new int[500];
var x = array[VKeys.VK_LSHIFT]; 

Problem polega na tym, że nie możesz zindeksować tablicy za pomocą wyliczenia, ponieważ nie może ona niejawnie przekonwertować wyliczenia na ushort (nawet jeśli oparliśmy wyliczenie na ushort)

w tym konkretnym kontekście wyliczenia są przestarzałe przez następującą strukturę danych. . . .

public static class VKeys
{
public const ushort
a = 1,
b = 2, 
c = 3;
}
Steven Ventura
źródło
2

Stworzyłem to narzędzie, aby pomóc mi przekonwertować Enum na PrimitiveEnum i PrimitiveEnum nabyte, sbyte, short, ushort, int, uint, long, or ulong .

Więc to technicznie konwertuje dowolne wyliczenie na dowolną jego pierwotną wartość.

public enum MyEnum
{
    one = 1, two = 2
}

PrimitiveEnum number = MyEnum.one;
long i = number;

Zobacz commit na https://github.com/McKabue/McKabue.Extentions.Utility/blob/master/src/McKabue.Extentions.Utility/Enums/PrimitiveEnum.cs

using System;

namespace McKabue.Extentions.Utility.Enums
{
    /// <summary>
    /// <see href="https://stackoverflow.com/q/261663/3563013">
    /// Can we define implicit conversions of enums in c#?
    /// </see>
    /// </summary>
    public struct PrimitiveEnum
    {
        private Enum _enum;

        public PrimitiveEnum(Enum _enum)
        {
            this._enum = _enum;
        }

        public Enum Enum => _enum;


        public static implicit operator PrimitiveEnum(Enum _enum)
        {
            return new PrimitiveEnum(_enum);
        }

        public static implicit operator Enum(PrimitiveEnum primitiveEnum)
        {
            return primitiveEnum.Enum;
        }

        public static implicit operator byte(PrimitiveEnum primitiveEnum)
        {
            return Convert.ToByte(primitiveEnum.Enum);
        }

        public static implicit operator sbyte(PrimitiveEnum primitiveEnum)
        {
            return Convert.ToSByte(primitiveEnum.Enum);
        }

        public static implicit operator short(PrimitiveEnum primitiveEnum)
        {
            return Convert.ToInt16(primitiveEnum.Enum);
        }

        public static implicit operator ushort(PrimitiveEnum primitiveEnum)
        {
            return Convert.ToUInt16(primitiveEnum.Enum);
        }

        public static implicit operator int(PrimitiveEnum primitiveEnum)
        {
            return Convert.ToInt32(primitiveEnum.Enum);
        }

        public static implicit operator uint(PrimitiveEnum primitiveEnum)
        {
            return Convert.ToUInt32(primitiveEnum.Enum);
        }

        public static implicit operator long(PrimitiveEnum primitiveEnum)
        {
            return Convert.ToInt64(primitiveEnum.Enum);
        }

        public static implicit operator ulong(PrimitiveEnum primitiveEnum)
        {
            return Convert.ToUInt64(primitiveEnum.Enum);
        }
    }
}
McKabue
źródło
+1 Mam framework do gry z wieloma rzeczami oznaczonymi jako, uintktóre sama gra zazwyczaj tworzy enum, ale framework nic nie zna. Konieczność (uint)wywoływania frameworka była uciążliwa. Twój pomysł wstecz działa idealnie. Zamiast structprzechowywania Enum, mam a, struct IdNumberktóry przechowuje, uintale niejawnie konwertuje z Enums, których używa gra. Zamiast wpisywać parametry frameworka jako uint, mogę je wpisać IdNumber, a framework może wewnętrznie je wydajnie przekazywać, nawet wykonując na nich integralne operacje.
Kevin
1

Rozwiązałem problem z odpowiedzią sehe podczas uruchamiania kodu na MS .net (nie-Mono). Dla mnie problem wystąpił w .net 4.5.1, ale wydaje się, że dotyczy to również innych wersji.

Problem

Dostęp do public static TDervied MyEnumValuewyniku odbicia (przez FieldInfo.GetValue(null)nie nie initialize wspomnianym polu.

Obejście problemu

Zamiast przypisywania nazw TDerivedinstancjom na statycznym inicjatorze, RichEnum<TValue, TDerived>odbywa się to leniwie przy pierwszym dostępie do TDerived.Name. Kod:

public abstract class RichEnum<TValue, TDerived> : EquatableBase<TDerived>
    where TValue : struct, IComparable<TValue>, IEquatable<TValue>
    where TDerived : RichEnum<TValue, TDerived>
{
    // Enforcing that the field Name (´SomeEnum.SomeEnumValue´) is the same as its 
    // instances ´SomeEnum.Name´ is done by the static initializer of this class.
    // Explanation of initialization sequence:
    // 1. the static initializer of ´RichEnum<TValue, TDerived>´ reflects TDervied and 
    //    creates a list of all ´public static TDervied´ fields:
    //   ´EnumInstanceToNameMapping´
    // 2. the static initializer of ´TDerive´d assigns values to these fields
    // 3. The user is now able to access the values of a field.
    //    Upon first access of ´TDervied.Name´ we search the list 
    //    ´EnumInstanceToNameMapping´ (created at step 1) for the field that holds
    //    ´this´ instance of ´TDerived´.
    //    We then get the Name for ´this´ from the FieldInfo
    private static readonly IReadOnlyCollection<EnumInstanceReflectionInfo> 
                            EnumInstanceToNameMapping = 
        typeof(TDerived)
            .GetFields(BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public)
            .Where(t => t.FieldType == typeof(TDerived))
            .Select(fieldInfo => new EnumInstanceReflectionInfo(fieldInfo))
            .ToList();

    private static readonly SortedList<TValue, TDerived> Values =
        new SortedList<TValue, TDerived>();

    public readonly TValue Value;

    private readonly Lazy<string> _name;

    protected RichEnum(TValue value)
    {
        Value = value;

        // SortedList doesn't allow duplicates so we don't need to do
        // duplicate checking ourselves
        Values.Add(value, (TDerived)this);

        _name = new Lazy<string>(
                    () => EnumInstanceToNameMapping
                         .First(x => ReferenceEquals(this, x.Instance))
                         .Name);
    }

    public string Name
    {
        get { return _name.Value; }
    }

    public static implicit operator TValue(RichEnum<TValue, TDerived> richEnum)
    {
        return richEnum.Value;
    }

    public static TDerived Convert(TValue value)
    {
        return Values[value];
    }

    protected override bool Equals(TDerived other)
    {
        return Value.Equals(other.Value);
    }

    protected override int ComputeHashCode()
    {
        return Value.GetHashCode();
    }

    private class EnumInstanceReflectionInfo
    {
        private readonly FieldInfo _field;
        private readonly Lazy<TDerived> _instance;

        public EnumInstanceReflectionInfo(FieldInfo field)
        {
            _field = field;
            _instance = new Lazy<TDerived>(() => (TDerived)field.GetValue(null));
        }

        public TDerived Instance
        {
            get { return _instance.Value; }
        }

        public string Name { get { return _field.Name; } }
    }
}

która - w moim przypadku - opiera się na EquatableBase<T>:

public abstract class EquatableBase<T>
    where T : class 
{
    public override bool Equals(object obj)
    {
        if (this == obj)
        {
            return true;
        }

        T other = obj as T;
        if (other == null)
        {
            return false;
        }

        return Equals(other);
    }

    protected abstract bool Equals(T other);

    public override int GetHashCode()
    {
        unchecked
        {
            return ComputeHashCode();
        }
    }

    protected abstract int ComputeHashCode();
}

Uwaga

Powyższy kod nie zawiera wszystkich cech oryginalnej odpowiedzi Marka !

Dzięki

Dziękuję Markowi za udostępnienie jego RichEnumimplementacji i dzięki sehe za wprowadzenie ulepszeń!

BatteryBackupUnit
źródło
1

Znalazłem jeszcze łatwiejsze rozwiązanie zaczerpnięte stąd /codereview/7566/enum-vs-int-wrapper-struct Wklejałem poniższy kod z tego linku na wypadek, gdyby nie zadziałał w przyszłości.

struct Day
{
    readonly int day;

    public static readonly Day Monday = 0;
    public static readonly Day Tuesday = 1;
    public static readonly Day Wednesday = 2;
    public static readonly Day Thursday = 3;
    public static readonly Day Friday = 4;
    public static readonly Day Saturday = 5;
    public static readonly Day Sunday = 6;

    private Day(int day)
    {
        this.day = day;
    }

    public static implicit operator int(Day value)
    {
        return value.day;
    }

    public static implicit operator Day(int value)
    {
        return new Day(value);
    }
}
adminSoftDK
źródło
0

@BatteryBackupUnit Hej, to brzmi jak fajne rozwiązanie, ale czy możesz wyjaśnić tę część tutaj?

Ponieważ korzystam z .NET 4.7.2, niestety „InvalidCastException”: /

 _name = new Lazy<string>(
                () => EnumInstanceToNameMapping
                     .First(x => ReferenceEquals(this, x.Instance))
                     .Name);

Nie wiem dlaczego, utworzyłem pochodny typ RichEnum i zainicjowałem tak jak wszystko, co zrobiłeś w przykładzie, ale otrzymałem ten wyjątek.

Byłbym zadowolony z pomocy, ponieważ bardzo lubię to podejście.

Shpendicus
źródło
-2

Wprowadzenie niejawnych konwersji dla typów wyliczeniowych naruszyłoby bezpieczeństwo typów, więc nie polecam tego robić. Dlaczego chcesz to zrobić? Jedynym przypadkiem użycia, jaki widziałem, jest sytuacja, gdy chcesz umieścić wartości wyliczenia w strukturze ze wstępnie zdefiniowanym układem. Ale nawet wtedy możesz użyć typu wyliczenia w strukturze i po prostu powiedzieć Marshallerowi, co ma z tym zrobić.

OregonGhost
źródło
Mam zastosowanie do niejawnej konwersji wyliczeń. Używanie SPMetal do generowania klas LINQ to SharePoint w wielu witrynach tego samego zbioru witryn. Niektóre z moich list znajdują się w jednej podstronie, inne w innej. Ze względu na sposób, w jaki SPMetal generuje kod, kolumny witryny używane na wielu listach kolekcji mogą być definiowane w wielu przestrzeniach nazw. Jednak muszę przekonwertować między wyliczeniem pola wyboru w jednej przestrzeni nazw na to samo wyliczenie w innej przestrzeni nazw. Niejawna konwersja byłaby bardzo pomocna.
Zarepheth