C
C#2y ago
musademir

❔ Generic class to fire on data change according to data type

Hi everyone. I am tring to implement generic class that holds generic type of data class. And I will store them in a dictionary and fire their function according to type of data class. Please check code for more detail. Here is my interfaces
public interface IMyDataInterface
{
string SomeData { get; set; }
}

public interface IMyDataListener<TData> where TData : IMyDataInterface
{
void OnDataChange(TData data);
}
public interface IMyDataInterface
{
string SomeData { get; set; }
}

public interface IMyDataListener<TData> where TData : IMyDataInterface
{
void OnDataChange(TData data);
}
Classes that inherits them
public class MyData : IMyDataInterface
{
public string SomeData { get; set; }
public string SomeNewData1 { get; set; }
}

public class MyDataListener : IMyDataListener<MyData>
{
public void OnDataChange(MyData data)
{
}
}

public class MyData2 : IMyDataInterface
{
public string SomeData { get; set; }
public string SomeNewData2 { get; set; }
}

public class MyData2Listener : IMyDataListener<MyData2>
{
public void OnDataChange(MyData2 data)
{
}
}
public class MyData : IMyDataInterface
{
public string SomeData { get; set; }
public string SomeNewData1 { get; set; }
}

public class MyDataListener : IMyDataListener<MyData>
{
public void OnDataChange(MyData data)
{
}
}

public class MyData2 : IMyDataInterface
{
public string SomeData { get; set; }
public string SomeNewData2 { get; set; }
}

public class MyData2Listener : IMyDataListener<MyData2>
{
public void OnDataChange(MyData2 data)
{
}
}
Then I want to add them to a dictionary and call according to their data type.
public class MyDataListenerManager
{
private Dictionary<Type, IMyDataListener<IMyDataInterface>> listeners = new()
{
{typeof(MyData), new MyDataListener()},//Error: Argument type 'MyDataListener' is not assignable to parameter type 'IMyDataListener<IMyDataInterface>'
{typeof(MyData2), new MyData2Listener()} // Error: Argument type 'MyDataListener2' is not assignable to parameter type 'IMyDataListener<IMyDataInterface>'
};

//call from somewhere else
public void OnDataChange(IMyDataInterface data)
{
if (listeners.TryGetValue(data.GetType(), out var listener))
{
listener.OnDataChange(data);
}
}
}
public class MyDataListenerManager
{
private Dictionary<Type, IMyDataListener<IMyDataInterface>> listeners = new()
{
{typeof(MyData), new MyDataListener()},//Error: Argument type 'MyDataListener' is not assignable to parameter type 'IMyDataListener<IMyDataInterface>'
{typeof(MyData2), new MyData2Listener()} // Error: Argument type 'MyDataListener2' is not assignable to parameter type 'IMyDataListener<IMyDataInterface>'
};

//call from somewhere else
public void OnDataChange(IMyDataInterface data)
{
if (listeners.TryGetValue(data.GetType(), out var listener))
{
listener.OnDataChange(data);
}
}
}
Eventhough MyData and MyData2 inherits IMyDataInterface, it gives me not assignable to parameter type error. And here is the purpose of that implementation. It should trigger related listener's OnDataChange function
public class TestClass
{
MyDataListenerManager manager = new();

public void Test()
{
manager.OnDataChange(new MyData()
{
SomeData = "test",
SomeNewData1 = "test1"
});
}

public void Test2()
{
manager.OnDataChange(new MyData2()
{
SomeData = "test2",
SomeNewData2 = "test22"
});
}
}
public class TestClass
{
MyDataListenerManager manager = new();

public void Test()
{
manager.OnDataChange(new MyData()
{
SomeData = "test",
SomeNewData1 = "test1"
});
}

public void Test2()
{
manager.OnDataChange(new MyData2()
{
SomeData = "test2",
SomeNewData2 = "test22"
});
}
}
Can you please guide me about how should I implement that, or what is wrong with the code?
38 Replies
TheBoxyBear
TheBoxyBear2y ago
Covariance and Contravariance (C#)
Learn about covariance and contravariance and how they affect assignment compatibility. See a code example that demonstrates the differences between them.
TheBoxyBear
TheBoxyBear2y ago
Take say the classes A and B where B : A. You can have an instance of B stored as a reference of A, but say you add a layer by making it a list, that can't work anymore. A List<A> may contain As or B, but a List<B> can only contain Bs. So if you were able to do List<A> list = new List<B>(), you could then do list.Add(new A()) which can't work if the runtime type is List<B> so you can't assume List<B> is compatible with List<A> the same way A and B are. A work around is to add the in and out keywords to the generics, limiting what you can do with the generics to restore that compatibility. IEnumerable<T> is an example of that. Since T is only used as output and has the out keyword, you can do IEnumerable<A> = new List<B>()
musademir
musademirOP2y ago
First of all, thank you very much for your explanation. But there are a couple of parts that don't quite get. First of all, in the case you gave, A is actually a B, since class A inherits B, so we can use it instead of B ( simple inheritance ). I actually think I did the same thing here. Since Listeners inherit from IMyDataListener, I need to be able to use both.
TheBoxyBear
TheBoxyBear2y ago
That was a typo, I meant B : A
musademir
musademirOP2y ago
Alright, so it is something like that. And yes It is basic inheritance
class A
{

}
class B : A
{

}

class TestClass
{
void Test()
{
var alist = new List<A>();
var blist = new List<B>();

blist.Add(new B());

alist.Add(new A());
alist.Add(new B());
}
}
class A
{

}
class B : A
{

}

class TestClass
{
void Test()
{
var alist = new List<A>();
var blist = new List<B>();

blist.Add(new B());

alist.Add(new A());
alist.Add(new B());
}
}
TheBoxyBear
TheBoxyBear2y ago
But what you're trying to do with new MyDataListener() in the dictionary is equivalent to List<A> = new List<B>()
musademir
musademirOP2y ago
So It also works when I switch from
IMyDataListener<TData> where TData : IMyDataInterface
IMyDataListener<TData> where TData : IMyDataInterface
to
IMyDataListener
IMyDataListener
Am I?
private Dictionary<Type, IMyDataListener<IMyDataInterface>> listeners = new()
{
{typeof(MyData), new MyDataListener()},//Error: Argument type 'MyDataListener' is not assignable to parameter type 'IMyDataListener<IMyDataInterface>'
{typeof(MyData2), new MyData2Listener()} // Error: Argument type 'MyDataListener2' is not assignable to parameter type 'IMyDataListener<IMyDataInterface>'
};
private Dictionary<Type, IMyDataListener<IMyDataInterface>> listeners = new()
{
{typeof(MyData), new MyDataListener()},//Error: Argument type 'MyDataListener' is not assignable to parameter type 'IMyDataListener<IMyDataInterface>'
{typeof(MyData2), new MyData2Listener()} // Error: Argument type 'MyDataListener2' is not assignable to parameter type 'IMyDataListener<IMyDataInterface>'
};
TheBoxyBear
TheBoxyBear2y ago
MyDataListener : IMyDataListener<MyData>
musademir
musademirOP2y ago
The holder is interface and I am tring to cast class to that interface
TheBoxyBear
TheBoxyBear2y ago
MyData would be B and IMyDataInterface would be A
musademir
musademirOP2y ago
Yes I was coming to that Listening?
TheBoxyBear
TheBoxyBear2y ago
Seeing your messages yes
musademir
musademirOP2y ago
Sorry i mean I was listening 🙂 (lack of my english)
TheBoxyBear
TheBoxyBear2y ago
Just not sure what you mean by this
musademir
musademirOP2y ago
I mean, MyData can be convertable to IMyDataInterface without any error
TheBoxyBear
TheBoxyBear2y ago
Yes, but it's back to the list example The compiler can't make assumptions on how which way the generic is assignable with its base type So it assumes it's not assignable
musademir
musademirOP2y ago
Yeah I guess the problem is about unboxing it right? As mentioned in the documentation It does work If I use out keyword
TheBoxyBear
TheBoxyBear2y ago
You would have to define the dictionary using the non generic IMyDataListener or use out By adding that keyword, you ensure the generic can only be used as output. That way the compiler can assume the assigning will work The rule of thumb is the runtime type must never be more strict than the declaring type Without generics, that boils down to simply being able to do A a = new B() since all you can do with A, B can also do There's also only one instance at play so you can't mess things up like putting an A where a B is expected When it's through generic, the "no stricter than declaring" can only be true if the generic is only used as output
musademir
musademirOP2y ago
Alright, thank you sir. I would love to check that deeply, I did not know that deeply. So what do you think should I do since I am not able to use in keyword I will not be able to get data in parameter. When I use out I am only available to use it as output. Or do you have any suggestion for me to implement that kind of listener thing in a better way?
TheBoxyBear
TheBoxyBear2y ago
Declare the dictionary with IMyDataListener instead of the generic one
musademir
musademirOP2y ago
I tried that, but things gets messy when I try to call functions
TheBoxyBear
TheBoxyBear2y ago
True, you can't really have a non generic IMyDataListener since it takes it as a parameter You could if just to have references to them that you can later cast to the generic one What does the listener do anyway?
musademir
musademirOP2y ago
Here is my real code
public interface IDarkRiftMessageListener
{

}

public interface IDarkRiftMessageListener<T> : IDarkRiftMessageListener
where T : IDarkRiftMessage
{
void OnMessageReceived(T message, IClient client);
}
public interface IDarkRiftMessageListener
{

}

public interface IDarkRiftMessageListener<T> : IDarkRiftMessageListener
where T : IDarkRiftMessage
{
void OnMessageReceived(T message, IClient client);
}
private Dictionary<ushort, IDarkRiftMessageListener> _messageListeners = new();
private IDarkRiftMessageListener GetListener(ushort tag)
{
return _messageListeners.ContainsKey(tag)
? _messageListeners[tag]
: null;
}

private void HandleMessage(ushort tag, Message message, IClient darkRiftClient)
{
var listener = GetListener(tag);
if (listener == null)
{
return;
}

using DarkRiftReader reader = message.GetReader();

var messageType = _messageTypes[listener];
var darkRiftMessage = (IDarkRiftMessage)Activator.CreateInstance(messageType,true);

reader.ReadSerializableInto(ref darkRiftMessage);

listener.OnMessageReceived(darkRiftMessage, darkRiftClient);//I have to call that
}
private Dictionary<ushort, IDarkRiftMessageListener> _messageListeners = new();
private IDarkRiftMessageListener GetListener(ushort tag)
{
return _messageListeners.ContainsKey(tag)
? _messageListeners[tag]
: null;
}

private void HandleMessage(ushort tag, Message message, IClient darkRiftClient)
{
var listener = GetListener(tag);
if (listener == null)
{
return;
}

using DarkRiftReader reader = message.GetReader();

var messageType = _messageTypes[listener];
var darkRiftMessage = (IDarkRiftMessage)Activator.CreateInstance(messageType,true);

reader.ReadSerializableInto(ref darkRiftMessage);

listener.OnMessageReceived(darkRiftMessage, darkRiftClient);//I have to call that
}
It is a generic listener class that listens given message from given client
TheBoxyBear
TheBoxyBear2y ago
And the type of the message isn't known at compile time
musademir
musademirOP2y ago
public class FindMatchMessageListener : IDarkRiftMessageListener<FindMatchMessage>
{
public void OnMessageReceived(FindMatchMessage message, IClient client)
{
//TODO: Do Something
}
}
public class FindMatchMessageListener : IDarkRiftMessageListener<FindMatchMessage>
{
public void OnMessageReceived(FindMatchMessage message, IClient client)
{
//TODO: Do Something
}
}
and here is the example
TheBoxyBear
TheBoxyBear2y ago
You could have two methods: One generic that takes a specific message type so you can cast the listener using that generic
musademir
musademirOP2y ago
Yes it depends on the message, But overall I know all of them
TheBoxyBear
TheBoxyBear2y ago
And a non generic method that calls the generic one using the right type based on the runtime type Or just cast the listener to the right generic one right away based on the message type using a single method if (message is OKMessage ok) ((OKListener) listener).OnMessageReceived(ok)
musademir
musademirOP2y ago
I actually am loading them automatically from assembly then register them to the dic I will try the first one
TheBoxyBear
TheBoxyBear2y ago
Then you can still do it with refelction Keep the non generic listener interface in the dictionary definition but make the OnMessageReceived call through reflection I'd say add references to the MethodInfos of listener.OnMessageReceived for each one so you don't have to get it every time
musademir
musademirOP2y ago
Thank you, I also think that but since it is server-client message using reflection in every tiny message in a mobile game will probably cause performance issue. I will try some caching for that 👍 What do you mean adding referans to MethodInfo?
TheBoxyBear
TheBoxyBear2y ago
Depending on how the listener types are created, you might need to creates references to them through reflection using CreateGenericType<>() So TValue in the dictionary would be a class or struct containing the instance and MethodInfo obtained from the generic listener Type
musademir
musademirOP2y ago
Alright thank you so much. It was an informative talk for me. Let me check them all and try my best 🙂
TheBoxyBear
TheBoxyBear2y ago
Or for a lesser performance impact, give the listeners an overlapd that takes any message and validates the type ay runtime You could even pass on that overload to a base non generic listener interface and completely avoid casting and reflection, only casting in OnMessageReceived
musademir
musademirOP2y ago
Yes, it looks like a better solution 👍 I will implement that too 👍
TheBoxyBear
TheBoxyBear2y ago
To avoid burdening each listener class with validating the message type, you could replace the generic interface with a generic abstract class that implements the base message method and does tbe validation. The listeners then only need implement the method with the respective message type. Having the dictionary be <Type, IDataListener>, get the listener and pass the message as is
musademir
musademirOP2y ago
Alright, thank you so much for taking the time to help
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server