Laden...

ToString() bei Enumerationsmembern mit demselben Wert

Erstellt von LaTino vor 8 Jahren Letzter Beitrag vor 8 Jahren 2.138 Views
LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren
ToString() bei Enumerationsmembern mit demselben Wert

Ich renne gerade in ein nettes kleines Problem, das die MSDN folgendermaßen zusammenfasst:

If multiple enumeration members have the same underlying value and you attempt to retrieve the string representation of an enumeration member's name based on its underlying value, your code should not make any assumptions about which name the method will return.

Das Problem ist, dass genau das versucht wird. Die Zeichenkette ist im Zusammenspiel mit einem zusätzlichen Parameter tatsächlich eindeutig, so dass meine derzeitige Lösung so aussieht:


enum ExampleEnum
{
    [MultipleEnum(MinParamValue = 0, MaxParamValue = 5)]
    ExampleEnumValue1 = 1,
    [MultipleEnum(MinParamValue = 6, MaxParamValue = 11)]
    ExampleEnumValue2 = 1,
    [MultipleEnum(MinParamValue = 12, MaxParamValue = 25)]
    ExampleEnumValue3 = 1
}
//in einer Extension-Klasse
static string ToString(this ExampleEnum enumValue, int compareValue)
{
    var names = Enum.GetNames(typeof(ExampleEnum));
    foreach (var enumName in names)
    {
        ExampleEnum foundValue;
        try { foundValue = (ExampleEnum)Enum.Parse(typeof(ExampleEnum), enumName); }
        catch (Exception) { continue; } //TryParse ist mir bekannt, Targetframework ist aber 2.0

        if (enumValue != foundValue) continue;
                
       var memberInfo = typeof(TestEnum).GetMember(enumName);
       if (memberInfo == null || memberInfo.Length == 0) continue;
       
        var attributes = memberInfo[0].GetCustomAttributes(typeof(MultipleEnumAttribute), false);
        if (attributes == null || attributes.Length == 0) return enumName;

        var description = (MultipleEnumAttribute)attributes[0];
        return (compareValue >= description.MinParamValue && compareValue <= description.MaxParamValue)  ? enumName : string.Empty;
    }
}

Problem 1: für multiple Werte muss das Attribute richtig und vollständig eingetragen sein
Problem 2: die Iteration geht wirklich über alle Member der Enumeration, was deutlich ineffizienter ist als die BinarySearch, die .NET intern beim Mapping Enum <-> Wert macht.
Problem 3: die komplette Enumeration "ordentlich" von vorn aufzuziehen entfällt wegen zu vieler Abhängigkeiten. Attribute einführen ging grad noch so.

Bin insgesamt relativ unzufrieden mit dem Code oben. Fällt jemandem eine bessere Lösung ein?

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

C
224 Beiträge seit 2009
vor 8 Jahren

Offtopic:

aus meiner Sicht sollte eine Enumeration immer eindeutig sein.
Bei einer Flag-Enumeration als Werte = 1,2,4,8,16 usw..., damit keine Mehrdeutigkeiten entstehen, oder ich verwende ein Enumerations-Array.

"die komplette Enumeration "ordentlich" von vorn aufzuziehen entfällt wegen zu vieler Abhängigkeiten. " --> schade 😃

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

Offtopic:

aus meiner Sicht sollte eine Enumeration immer eindeutig sein.
Bei einer Flag-Enumeration als Werte = 1,2,4,8,16 usw..., damit keine Mehrdeutigkeiten entstehen, oder ich verwende ein Enumerations-Array.

"die komplette Enumeration "ordentlich" von vorn aufzuziehen entfällt wegen zu vieler Abhängigkeiten. " --> schade 😃

Full ack, schon weil eine Enumeration die Eindeutigkeit meiner Meinung nach im Namen hat. Leider besteht die Arbeit als Softwareentwickler nur selten darin, dass man etwas "auf der grünen Wiese" bauen kann, und dann schlägt man sich mit solchen Konstrukten herum.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

D
985 Beiträge seit 2014
vor 8 Jahren

Bist du dir sicher, dass das überhaupt so funktioniert?


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication3
{
    public class MultipleEnumAttribute : Attribute
    {
        public int MinParamValue { get; set; }
        public int MaxParamValue { get; set; }
    }

    public enum ExampleEnum
    {
        [MultipleEnum( MinParamValue = 0, MaxParamValue = 5 )]
        ExampleEnumValue1 = 1,

        [MultipleEnum( MinParamValue = 6, MaxParamValue = 11 )]
        ExampleEnumValue2 = 1,

        [MultipleEnum( MinParamValue = 12, MaxParamValue = 25 )]
        ExampleEnumValue3 = 1
    }

    public static class Extension
    {
        //in einer Extension-Klasse
        public static string ToString( this ExampleEnum enumValue, int compareValue )
        {
            var names = Enum.GetNames( typeof( ExampleEnum ) );
            foreach ( var enumName in names )
            {
                ExampleEnum foundValue;
                try { foundValue = (ExampleEnum) Enum.Parse( typeof( ExampleEnum ), enumName ); }
                catch ( Exception ) { continue; } //TryParse ist mir bekannt, Targetframework ist aber 2.0

                if ( enumValue != foundValue ) continue;

                var memberInfo = typeof( ExampleEnum ).GetMember( enumName );
                if ( memberInfo == null || memberInfo.Length == 0 ) continue;

                var attributes = memberInfo[ 0 ].GetCustomAttributes( typeof( MultipleEnumAttribute ), false );
                if ( attributes == null || attributes.Length == 0 ) return enumName;

                var description = (MultipleEnumAttribute) attributes[ 0 ];
                return ( compareValue >= description.MinParamValue && compareValue <= description.MaxParamValue ) ? enumName : string.Empty;
            }
            return null;
        }
    }

    internal class Program
    {
        private static void Main( string[ ] args )
        {
            Console.WriteLine( "1: " + ExampleEnum.ExampleEnumValue1.ToString( 0 ) );
            Console.WriteLine( "2: " + ExampleEnum.ExampleEnumValue2.ToString( 6 ) );
            Console.WriteLine( "3: " + ExampleEnum.ExampleEnumValue3.ToString( 12 ) );
        }
    }
}

ergibt bei mir


1: ExampleEnum1
2: 
3: 

erwartet hätte ich jetzt


1: ExampleEnum1
2: ExampleEnum2
3: ExampleEnum3

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

Hm, möglich, dass beim Anonymisieren mir was durch die Lappen gerutscht ist. Die Tests schlagen keinen Alarm, und die aufbauende GUI sieht so aus, wie sie soll. Ich schau mir den oben geposteten Code nochmal genau an, ob ich da was vermasselt habe.

LaTino
Gefunden - wenn man im MyCSharp-Editor nochmal dran herumfummelt. Der unäre Operator in der letzten Zeile der Iteration (in der Extension) muss lauten:


if (compareValue >= description.MinParamValue && compareValue <= description.MaxParamValue) return enumName;
//statt:
//return ( compareValue >= description.MinParamValue && compareValue <= description.MaxParamValue ) ? enumName : string.Empty;

Sorry!

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

D
985 Beiträge seit 2014
vor 8 Jahren

Somit soll also bei


ExampleEnum.ExampleEnumValue2.ToString( 0 )

der Wert

ExampleEnumValue1

herauskommen?

Dann kannst du doch in deiner Extension-Klasse dieses Mapping

Dictionary<Tuple<ExampleEnum,int>,string>

aufbauen und nachher nur noch abfragen. Das sollte zumindest erheblich schneller gehen als immer wieder die Attribute durchzukauen.

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

Im Prinzip ja, eine Art Caching, um die schlimmsten Auswüchse zu mildern. (Tuple<,> gibt's im FW 2.0 zwar nicht, wir haben aber, wie vermutlich fast jeder, irgendwo eine äquivalente Klasse seit Ewigkeiten schlummern. Werd' ich auch nutzen, denk ich.)

Mich persönlich stört aber mehr diese orthogonale Injektion mit dem Attribut. Das Ding muss gepflegt werden - lass den nächsten Azubi dort einen zusätzlichen Wert einfügen, dieses etwas undurchsichtige Verhalten ist bestens geeignet, um Mist zu bauen. Daher meine Frage nach einer besseren Lösung.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

D
985 Beiträge seit 2014
vor 8 Jahren

Wenn du das Mapping am Anfang machst, dann kannst du auch gleich ein paar Regeln prüfen.

Attribut fehlt, Bereiche überlappen (wird mit dem Dict eh schon abgefangen), etc.

Damit bekommt man das schon sehr stabil hin ...

5.658 Beiträge seit 2006
vor 8 Jahren

Hi LaTino,

also ich sehe auch das Problem, das solche Konstruktionen Wartungsarbeiten erschweren oder sogar verhindern. Enumerationen und Attribute sind nicht dafür da, die Programmlogik zu definieren. Vor allem, wenn schon die Microsoft-Doku davor warnt, Enumerationen auf diese Weise zu verwenden. Man sollte das mit normaler objektorientierten Programmierung und einem aussagekräftigen Modell besser und übersichtlicher lösen können. Für genauere Hinweise müßtest du allerdings erstmal erklären, welches Problem damit überhaupt gelöst werden soll. Mir ist das bisher noch nicht ganz klar geworden.

Andererseits hatte ich dich so verstanden, daß du im Moment nicht die Möglichkeit hast, da Änderungen vorzunehmen. In diesem Fall würde ich aber zumindest den derzeitigen Stand ausführlich dokumentieren, damit sich der nächste Entwickler nicht wieder genau die gleichen Fragen stellt.

Christian

Weeks of programming can save you hours of planning

LaTino Themenstarter:in
3.003 Beiträge seit 2006
vor 8 Jahren

Man sollte das mit normaler objektorientierten Programmierung und einem aussagekräftigen Modell besser und übersichtlicher lösen können. Für genauere Hinweise müßtest du allerdings erstmal erklären, welches Problem damit überhaupt gelöst werden soll. Mir ist das bisher noch nicht ganz klar geworden.

Es handelt sich um eine Enumeration von Objekttypen, die in einer entsprechenden Spezifikation vorgegeben sind und entsprechend (als Enumeration) implementiert werden müssen. Die Implementierung als Enumeration ist Teil der Zertifizierung, die die Umsetzung der erwähnten Spezifikation prüft und die Grundvorraussetzung dafür ist, dass die Software überhaupt an den entsprechenden Ausschreibungen teilnehmen darf.

Die Spezifikation existiert in mindestens 7 Versionen, die zum allergrößten Teil jeweils eine Weiterentwicklung sind. An einigen wenigen Stellen jedoch wurden Weiterentwicklungen zurückgenommen bzw. durch bessere Konstrukte ersetzt. Und in einigen wenigen von diesen Fällen wurde für die Objekttypen, die notwendig waren, derselbe Wert in der Enumeration genommen wie für ihre ersetzten Vorgänger.

Da jede Version der Spezifikation implementiert sein muss, gibt es den seltenen Fall, dass anhand des Enumerationswertes (der dahinter stehenden Zahl, Enums selbst sind nur Zucker für die Entwickler) nicht eindeutig der Objekttyp bestimmt werden kann, wenn man nicht auch die Spezifikationsversion mit ins Spiel bringt. Unsere Implementierung kann das funktional auch - aber hin und wieder muss der Name des Enumerationswertes dem Benutzer angezeigt werden, und an der Stelle kommt mein Problem ins Spiel.

Andererseits hatte ich dich so verstanden, daß du im Moment nicht die Möglichkeit hast, da Änderungen vorzunehmen. In diesem Fall würde ich aber zumindest den derzeitigen Stand ausführlich dokumentieren, damit sich der nächste Entwickler nicht wieder genau die gleichen Fragen stellt.

Nicht nur momentan, wenn die Software weiterhin verkauft werden soll, kann ich die Spec nicht meinen Wünschen entsprechend ändern 😉. Und selbst wenn solche Inkonsistenzen durch die nächste Version der Spec ausgeräumt werden, müssen die alten Versionen weiterhin unterstützt werden. Dokumentation ist dabei eine Selbstverständlichkeit.

Die Sache ist, dass ich den sinnngebenden Kontext (also die Spec-Version) derzeit durch Attribute injiziere. Eine andere Möglichkeit, die ich sehe, wäre, für jede Version eine extra-Enumeration anzulegen und das ganze durch eine Adapterklasse mit der Außenwelt kommunizieren zu lassen, die den Kontext kennt. Der Adapter müsste sich allerdings identisch der bisher genutzten Enumeration verhalten, damit ich die Funktionalität in etlichen Bereichen nicht breche: ein Haufen Arbeit ggü. den Enumerations-Attributen.

(wir reden hier von irgendwas bei 7 Spec-Versionen mit je ~800 (Tendenz steigend) Objekttypen bei etwa 5, vielleicht 6 Fällen insgesamt, die zum Problem geworden sind.)

Der Post war in der Hoffnung, dass jemandem eine naheliegende dritte Option (Option1: orthogonale Injektion per Attribut, Option2: Kapselung per Adapter) einfällt, die ich bisher übersehen habe.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)