C# Generics und Casting

xxhagixx

Ensign
Registriert
Dez. 2011
Beiträge
142
Hallo,
kennt sich hier jemand mit Generics aus?
Ich habe eine Generische abstrakte Klasse A mit einem value und action (klassiches Observer Pattern mit Events)
Dann habe ich eine Klasse B die die Klasse A mit dem Typ TestTest (ein Enum) erbt.
In einer Klasse C will ich nun Subscriben, weiß aber den genauen Typ des Enums nicht.
Das Beispiel ist fiktiv und passt daher nicht genau, daher bitte nicht an Kleinigkeiten aufhhängen.
Den kann (im realen durch ein Interface realisiert) Abfragen, aber natürlich nicht als Parameter benutzen (Im Beispiel Option 2).
Auch kann ich nicht die Setter Methode als Parameter Enum verwenden, da es ja nicht mit TestTest übereinstimmt (Option 1)
Auch kann ich nicht eine weitere Subscribe Methode hinzufügen mit Action<Enum> als Parameter und dann zu Action<T> casten (Option 3).

Zurzeit ist das ganze so implementiert, das Action<string> ist und alles vor und zurück gecastet wird, was ich aber gerne vermeiden würde. Weiß jemand wie man dieses Problem "schön" lösen, sprich ohne object oder string als zwischenspeicher zu benutzen?
Vielen Dank schon eimal.

C#:
    public enum TestTest
    {
        Test1 = 0,
        Test2 = 1,
        Test3 = 2
    }

    public interface IA
    {
        void Subscribe<T>(Action<T> obs);
        void Subscribe(Action<Enum> obs);
        Type GetT();
    }

    /*Alternative funktioniert nicht, da auch hier der Type zur Kompilierung angegeben werden muss
    public interface IA<T>
    {
        void Subscribe(Action<T> obs);
        void Subscribe(Action<Enum> obs);
        Type GetT();
    }*/

    public abstract class A<T> :IA
    {
        T Value;
        Action<T> Observer;

        public virtual void Subscribe<T>(Action<T> obs)//Implementierung des Interfaces... 
        {
            Observer = obs;//Cannot implicitly convert type 'System.Action<T>' to 'System.Action<T>' 
        }

        public virtual void Subscribe(Action<Enum> obs)
        {
            Action<T> tmp = (Action<T>)Convert.ChangeType(obs, typeof(Action<T>));
            //Action implementiert IConvertible nicht
            Subscribe(obs);
        }


        public Type GetT()
        {
            return typeof(T);
        }
    }

    public class B : A<TestTest>
    {

    }

    public class Display
    {
        private static IA _instance;

        void Start()
        {
            _instance.Subscribe(SetValue);//Option 1

            Type t = _instance.GetT();
            _instance.Subscribe<t>(SetValue); //Option 2

            _instance.Subscribe<Enum>(SetValue);//Option 3


        }


        public void SetValue(Enum val)
        {

        }

        /* public void SetValue(TestTest val) // Soll vermieden werden
         {

         }*/
    }
}
 
Zuletzt bearbeitet:
Ich habe den Beispielcode oben um ein Interface erweitert, da es das eigentlich schon gibt aber ich im Beispiel weggelassen habe. Das Problem mit dem Interface ist, dass entweder a) das Interface auch generisch ist -> Ich weiß den Type erst zur Laufzeit
oder B nicht generisch ist und man das Interface nicht richtig implementieren kann (siehe Beispiel).
Ich hoffe es kommt rüber wo das Problem liegt.
Akutell haben wir int, float, double und diverse Enums als Type, daher weiß ich nicht was ich da einschränken soll, da Action ja nicht gecastet werden kann, oder steh ich da aufm Schlauch?
 
Das ganze Konstrukt sieht merkwürdig aus und erschließt sich mir nicht ganz.

Vielleicht solltest du zunächst mal erklären, warum du das so aufbauen willst.

EDIT :

Die Klasse B zum Beispiel verwendest du im Beispiel nicht. Würdest du jetzt in der Klasse Display nicht das Interface IA als Typ verwenden sondern B, könntest du nur Actions<TestTest> verwenden und du wärst wieder typsicher.

Darum halte ich dein Vorhaben im Moment für nicht ganz durchdacht, sodass ein komplett anderer Ansatz evtl. besser geeignet wäre.
 
Zuletzt bearbeitet:
SomeDifferent schrieb:
Die Klasse B zum Beispiel verwendest du im Beispiel nicht.
In dem Fall ist _instance = B und wird durch IA addressiert.
Ich hatte das Interface im Beispiel zuerst wegelassen und nach dem ersten Kommentar noch hinzugefügt, dabei ist untergangen _instance = B ist.



SomeDifferent schrieb:
Würdest du jetzt in der Klasse Display nicht das Interface IA als Typ verwenden sondern B, könntest du nur Actions<TestTest> verwenden und du wärst wieder typsicher.

Genau das geht nicht. Ich weiß den Type erst zur Laufzeit. Die Display Klasse soll immer ein Objekt abbonnieren, welches ein spezifisches (hier TestTest) Enum als Generischen Parameter benutzt. Mit dem Status des Enums werden verschiedene Bildchen dargestellt. Egal welches Enum abonniert wird, es wird immer aus einem Array ein Bildchen anhand des Wertes des spezifischen Enums ausgewählt.
Es passiert also immer das gleiche, unabhängig vom Enum. Um jetzt nicht für jedes einzelne Enum die gleiche "Bildchen anzeigen" Klasse zu schreiben mit dem Unterschied des zu abonnierenden Enums zu schreiben würde ich gerne eine generische "Bildchen anzeigen" Klasse schreiben. Während das Programm läuft wird dort dann ein Enum hineingeschoben.

Das ganze läuft schon. Ich muss halt alles zu einem string casten und zurück. Was natürlich nicht so schön ist. Daher die Frage ob es auch anders geht.
Um es noch mal zu verdeutlichen:

Edit: Das ganze System ist deshalb abstract und generisch, da dann so die Werte im Editor, denn wir verwenden, angezeigt werden können und dort dann auch verändert werden können.
Die Display Klasse wird auch vom Editor initialisiert und nicht über new Display() erstellt. Das sollte vielleicht zur Aufklärung noch beitragen.

C#:
    class Z
    {
        TestTest value;//TestTest => Enum
        Action<string> Observer;

        public void Subscribe(Action<string> obs)
        {
            Observer = obs;
        }

        public virtual void SetValue(object value)
        {
            value = (TestTest)Enum.Parse(typeof(TestTest), value.ToString(), true);
            Notify();
        }

        void Notify()
        {
            Observer.Invoke(value.ToString());
        }
    }

    class X
    {
        static Z _instance;
        public X()
        {
            _instance.Subscribe(SetEnum);
        }

        private void SetEnum(string val)
        {
            TestTest tmp = (TestTest)Enum.Parse(typeof(TestTest), val, true);
        }
    }

Code:
   class Z
    {
        TestTest value;//TestTest => Enum
        Action<TestTest> Observer;
        public void Subscribe(Action<Enum> obs)
        {
            Observer = obs;
        }
        public virtual void SetValue(Enum val)
        {
            value = val;
            Notify();
        }

        void Notify()
        {
            Observer.Invoke(value);
        }

    }
    class X
    {
        static Z _instance;

        public X()
        {
            _instance.Subscribe(SetEnum);
        }

        private void SetEnum(Enum val)
        {
           
        }
    }
 
Zuletzt bearbeitet:
Ich hoffe ich hab dich richtig verstanden, aber hier meine Interpretierung. Dadurch, dass du aber zur Compilezeit nicht weißt, welche Enumeration genau erwünscht ist (durch das Interface) kann das zu sehr fiesen Fehlern zur Laufzeit führen. Da musst du entscheiden ob du eine Exception wirfst oder den neuen Wert schlichtweg ignorierst oder sonstwas :)

Ich hab den Callback für die Subscription in den Konstruktor gepackt, kannst du natürlich auch austauschbar über eine Methode machen und somit ggf. mit in das Interface packen.

C#:
internal interface IObservableEnum
{
    Type Type { get; }

    void SetValue(Enum newValue);
}

internal class ObservableEnum<T> : IObservableEnum where T : Enum
{
    private readonly Action<Enum> _notifyCallback;

    public Type Type => typeof(T);

    public T CurrentValue { get; private set; }

    public ObservableEnum(Action<Enum> notifyCallback)
    {
        _notifyCallback = notifyCallback ?? throw new ArgumentNullException(nameof(notifyCallback));
    }

    public virtual void SetValue(Enum newValue)
    {
        if (typeof(T) != newValue.GetType())
        {
            throw new InvalidOperationException("Wrong enumeration type supplied!");
        }

        CurrentValue = (T)newValue;
        _notifyCallback?.Invoke(newValue);
    }
}

internal class Program
{
    private enum TestEnum1
    {
        A,
        B,
        C,
        D
    }

    private enum TestEnum2
    {
        E,
        F,
        G,
        H
    }

    private static void Display(Enum enumeration)
    {
        Console.WriteLine(enumeration.ToString());
    }

    public static void Main(string[] args)
    {
        IObservableEnum _observableTestEnum1 = new ObservableEnum<TestEnum1>(Display);
        IObservableEnum _observableTestEnum2 = new ObservableEnum<TestEnum2>(Display);

        _observableTestEnum1.SetValue(TestEnum1.A);

        try
        {
            _observableTestEnum1.SetValue(TestEnum2.E);
        }
        catch (InvalidOperationException ex)
        {
            // expected exception because we set the wrong type
            // this can lead to errors inside the program as we don't know the correct type during the lifetime
        }

        _observableTestEnum2.SetValue(TestEnum2.E);

        Console.ReadLine();
    }
}
 
Zuletzt bearbeitet:
Der Sinn von Generics ist doch, die Typsicherheit zur Compile-Zeit zu erhöhen. Da aber in deinem Fall die Typen anscheinend erst zur Laufzeit feststehen, sind Generics hier nicht hilfreich.

Mein Vorschlag wäre: Anstatt die Enums (als strings) hin und herzuschicken, schickst du direkt die Informationen, die die Display-Klasse konkret benötigt. Also z.B. das Bild, welches angezeigt werden soll.
 
  • Gefällt mir
Reaktionen: SomeDifferent
Da habe ich mich wohl unklar ausgedrückt. Die Typen (also die Klassen die von der abstrakten Klasse erben) stehen zur Kompilierung fest. Ich packe auch einfach mal meinen aktuellen Code hier rein, der so funktioniert aber eben nicht Typsicher ist. Ich hoffe das funktioniert besser als meine kleinen Beispiele.
Auf der anderen Seite gibt es jeweils eine Klasse, die einen string als Key bekommt und dann in einem Dictionary die jeweilige ValueHolder-Klasse raus sucht und die Ints oder doubles oder sonstige Primitive typen abonniert. dies funktioniert auch wunderbar, da die Setter Methode ja auch jeweils int, double, etc. als Parameter hat. Allerdings ist die zubeobachtende Variable ein vom Typ Enum, dann muss die Setter Methode genau dem Type der Enum (in dem Beispiel TestTest) entsprechen und akzepiert nicht Enum als Parameter. Ich versuche jetzt also eine spiezielle Subscribe Methode zuschreiben, für denn Fall das T vom Typ (implementierer) Enum ist, der Action<Enum> als Parameter hat und dieses dann in Action<T> umwandelt.
Dann kann ich im vorhanden Code die Action<object> und das ganze hin und her gecaste nämlich wegschmeißen.


Code:
 public abstract class ValueHolder<T> :IValueHolder
    {
        T value;
        List<Action<object>> Observer;

        public virtual void Subscribe(object target, string functionName)
        {
            Action<object> action = (Action<object>)Delegate.CreateDelegate(typeof(Action<object>), target, functionName);
            Subscribe(action);
        }

        public virtual void Subscribe(Action<object> action)
        {
            if (Observer == null)
                Observer = new List<Action<object>>();
            Observer.Add(action);
        }

//Todo
        //public List<Action<T>> NotWorkingList;
        public virtual void Subscribe(Action<Enum> action)
        {
            if (typeof(T).IsEnum)
            {
                Delegate[] hallo = action.GetInvocationList();
                foreach (var blub in hallo)
                {
                    Debug.Log("Delegate: method: " + blub.Method + " Type: " + blub.GetType());
                }
                

                // Action<T> newAction = (Action<T>)Convert.ChangeType(action, typeof(Action<T>));
                //Delegate d = Delegate.CreateDelegate(typeof(Action<T>), hallo[0].Method);
                //Action<T> newAction = (Action<T>)d;
                //NotWorkingList.Add(newAction);
            }
        }

        public virtual void UnSubscribe(object target, string functionName)
        {
            Action<object> action = (Action<object>)Delegate.CreateDelegate(typeof(Action<object>), target, functionName);
            UnSubscribe(action);
        }

        public virtual void UnSubscribe(Action<object> action)
        {
            if (Observer != null && Observer.Contains(action))
                Observer.Remove(action);
        }

        public virtual void SetValue(T val)
        {
            value = val;
            Notify();

        }

        public virtual void SetValue(object value)
        {

            T tmp;
            if (typeof(T).IsEnum)
            {
                tmp = (T)Enum.Parse(typeof(T), value.ToString(), true);
            }
            else if (typeof(T).IsPrimitive)
            {
                tmp = (T)Convert.ChangeType(value, typeof(T));
            }
            else
            {
                Debug.Log("Type of T: " + typeof(T) + "Type of object: " + typeof(object));
                throw new InvalidOperationException("Wrong value type supplied");
            }
            SetValue(tmp);
        }

        void Notify()
        {
            ThreadInfo info = new ThreadInfo(value, Observer, gameObject.name);
            ThreadPool.QueueUserWorkItem(new WaitCallback(NotifyThread), info);

        }

        void NotifyThread(object state)
        {
            ThreadInfo info = (ThreadInfo)state;
            Profiler.BeginThreadProfiling("SettingsController", "NotifySubs_" + info.name);
            try
            {
                foreach (var subs in info.observer)
                {
                    subs.Invoke(info.value);
                }
            }
            catch (Exception e)
            {
                Debug.Log("Error: " + e.Message + "\nStacktrace:\n" + e.StackTrace, this);
            }
            Profiler.EndThreadProfiling();
        }

        public Type GetT()
        {
            return typeof(T);
        }

        struct ThreadInfo
        {
            public T value;
            public List<Action<object>> observer;
            public string name;

            public ThreadInfo(T value, List<Action<object>> observer, string name)
            {
                this.value = value;
                this.observer = observer;
                this.name = name;
            }
        }
    }
Code:
    public interface IValueHolder
    {
        Type GetT();
        void Subscribe(object target, string functionName);
        void Subscribe(Action<Enum> action);
        void Subscribe(Action<object> action);
        void UnSubscribe(object target, string functionName);
        void UnSubscribe(Action<object> action);
        void SetValue(object value);

    }
Code:
    public class ValueHoldeInt : ValueHolder<int>
    {

    }
Code:
    public class ValueHolderDouble : ValueHolder<double>
    {

    }


Code:
 public class ValueHolderTestEnum : ValueHolder<TestTest>
    {

    }

    public enum TestTest
    {
        Test1 = 0,
        Test2 = 1,
        Test3 = 2
    }
Code:
public class TestScript
    {
        public string SubscribeTo = "TestValue1";
        SettingsController controller;
        public int modulo =3;
        IValueHolder test;

        private void Start()
        {
            controller = FindObjectOfType<SettingsController>();
            
            test = controller.GetReferenceToValueHolder(SubscribeTo);
            test.Subscribe(SetValue);
        }

        public float timer = 0;
        public float nextSet = 1;
        public int counter = 0;
        private void Update()
        {
            if (timer > Random.Range(1, 3))//Hau in zufälligen abständen Zahlen in den ValueHolder
            {
                nextSet = (counter + nextSet) % ((modulo > 0) ? modulo : float.MaxValue);
                test.SetValue(nextSet);
                timer = 0;

            }
            timer += Time.deltaTime;
            counter++;
        }
        public int Test { get; set; }

//object soll durch int, float, double etc. ersetzt werden im Fall eines Primitiven typens oder durch Enum, falls //die Variable zwar vom typ Enum ist, aber bereits definiert ist.
        public void SetValue(object val)
        {
            Debug.Log("Value: " + val);
        }

Also ich würde gerne das List<Action<object>> durch List<Action<T>> ersetzen wie es sich gehört,
aber da man generische parameter ja nicht verändern kann im Sinne von

Code:
if(typeof(T).isPrimitive)
    Observer = List<Action<T>>
else if(typeof(T.Enum))
    Observer = List<Action<Enum>>

würde ich gerne die die die Action verändern, oder irgendwas anderes machen, damit ich nicht object als Rückgabewert der Action benutzen muss.
Vielen Dank aber schonmal für eure Antworten
 
Vielleicht hilft dir die MakeGenericeMethod()-Methode etwas weiter.

Diese habe ich vor einiger Zeit mal benutzt. Zum Testen, ob diese Funktion für dich in Frage kommt habe ich folgendes Beispiel ausprobiert.

Rich (BBCode):
Imports HIlfe

Module Module1

    Sub Main()

        Dim t = New Test()

    End Sub

End Module

Public Class Test

    Sub New()
        Dim doubleValueHolder = New ValueHolder(Of Double)(2.2)
        Dim integerValueHolder = New ValueHolder(Of Integer)(1)
        Dim testEnumValueHolder = New ValueHolder(Of TestEnum)(TestEnum.Test3)
        Dim autoEnumValueHolder = New ValueHolder(Of AutoEnum)(AutoEnum.Audi)


       'Keine Polymorphie möglich :(
       'Dim list = {doubleValueHolder, integerValueHolder, testEnumValueHolder, autoEnumValueHolder}


        Dim basemethod = GetType(Test).GetMethod(NameOf(SetValue))


        doubleValueHolder.Subscribe(Sub(x) basemethod.MakeGenericMethod({GetType(Double)}).Invoke(Me, {x}))
        integerValueHolder.Subscribe(Sub(x) basemethod.MakeGenericMethod({GetType(Integer)}).Invoke(Me, {x}))
        testEnumValueHolder.Subscribe(Sub(x) basemethod.MakeGenericMethod({GetType(TestEnum)}).Invoke(Me, {x}))
        autoEnumValueHolder.Subscribe(Sub(x) basemethod.MakeGenericMethod({GetType(AutoEnum)}).Invoke(Me, {x}))


        doubleValueHolder.DoSomething()
        integerValueHolder.DoSomething()
        testEnumValueHolder.DoSomething()
        autoEnumValueHolder.DoSomething()

    End Sub
    Public Sub SetValue(Of T)(value As T)
        Console.WriteLine(value.ToString)
    End Sub

End Class


Public Class ValueHolder(Of T)

    Private _Value As T
    Private _Action As Action(Of T)

    Sub New(value As T)
        _Value = value
    End Sub

    Sub DoSomething()
        _Action.Invoke(_Value)
    End Sub

    Sub Subscribe(action As Action(Of T))
        _Action = action
    End Sub

End Class


Public Enum TestEnum
    Test1 = 1
    Test2 = 2
    Test3 = 3
End Enum

Public Enum AutoEnum
    Audi = 10
    BMW = 20
    Mercedes = 30
End Enum
780477


Meiner Meinung nach ist deine Struktur nicht passend. Ganz arge Probleme hatte ich beispielsweise mit deinem Interface, da dieses kein Typattribut besitzt. So kannst du auch nicht <T> als Parametertyp der Action bei der Subscribe()-Methode verwenden, sondern musst auf action<object> zurückgreifen.

Du wirst nie die Polymorphie richtig nutzen können, da das Interface die passende Methode nicht anbietet, oder du dich direkt auf einen Typen beschränken musst.

Vielleicht fehlt mir auch einfach das Hintergrundwissen um deinen Quellcode korrekt zu verstehen. Aber vielleicht hilft dir ja wenigestens der Tipp ein wenig :)

LG

@xxhagixx Hast du eine Lösung gefunden?
 
Zuletzt bearbeitet: (Interesse :D)

Ähnliche Themen

T
Antworten
7
Aufrufe
1.327
Antworten
7
Aufrufe
1.621
Antworten
4
Aufrufe
1.880
Zurück
Oben