I have the following abstract class that I would like to leverage to create event channels for a unity project. I'm having trouble with understanding how generics work in C# (this language is new to me) and receiving a compiler error concerning passing this
as an argument when invoking the listeners.
namespace EventManagers
{
public abstract class EventSubject<T> : MonoBehaviour
{
public delegate void Listener<T>(T eventSubject);
private readonly
List<Listener<T>> _listeners = new List<Listener<T>>();
public void Attach(Listener<T> listener)
{
_listeners.Add(listener);
}
public void Detach(Listener<T> listener)
{
_listeners.Remove(listener);
}
public void NotifyObservers()
{
foreach (Listener<T> listener in _listeners)
{
listener(this);
}
}
}
}
Error referring to the line that reads listener(this);
:
error CS1503: Argument 1: cannot convert from 'EventManagers.EventSubject<T>' to 'T'
An inheriting class looks like:
public class Selection : EventSubject<Selection> {
private GameObject selected;
private static Selection _instance;
public static Selection instance
{
get
{
if (!_instance)
{
_instance = FindObjectOfType(typeof (Selection)) as Selection;
if (!_instance) {
throw new Exception("You need a Selection in the scene");
}
}
return _instance;
}
}
public GameObject GetSelection() {
return selected;
}
public void setSelection(GameObject selected) {
this.selected = selected;
NotifyObservers();
}
}
My questions are:
- If my delegate knows to expect a generic type why is this problematic?
- How can I best achieve this event pattern?
CodePudding user response:
public abstract class EventSubject<T> : MonoBehaviour { ... }
public class Selection : EventSubject<Selection> { ... }
From the POV of your EventSubject
class, typeof(T)
could be anything at all. It could even be EventSubject<int>
since you haven't provided any where
constraints. The compiler has no way of knowing that you expect typeof(T) == this.GetType()
.
The pattern you are looking for was nicknamed Curiously recurring template pattern in C . In C#, the equivalent generic constraint is;
public abstract class EventSubject<T> : MonoBehaviour
where T : EventSubject<T>
{
...
public void NotifyObservers()
{
foreach (Listener listener in _listeners)
{
listener((T)this);
}
}
}
public class Selection : EventSubject<Selection> { ... }
This is close to what you want, as it at least limits T
to classes that extend EventSubject<>
. But the compiler still can't prove that typeof(T) == this.GetType()
, so you need an explicit cast.
This constraint also allows class Broken : EventSubject<Selection>
, and there isn't any C# language feature that can prevent it. The best you can do is a runtime exception if a developer breaks your typeof(T) == this.GetType()
rule.
CodePudding user response:
- Listener is expecting
Selection
instance, you're passingthis
which is aListener<Selection>
instance. - Event pattern is built-in in c# language - https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/how-to-subscribe-to-and-unsubscribe-from-events You basically reimplemented it without any advantages.
CodePudding user response:
For others who may be viewing I was able to achieve a working result. My EventSubject class now appears as follows:
public abstract class EventSubject<TEventSubject> : MonoBehaviour
{
public delegate void Listener(TEventSubject eventSubject);
private readonly
List<Listener> _listeners = new List<Listener>();
public void Attach(Listener listener)
{
_listeners.Add(listener);
}
public void Detach(Listener listener)
{
_listeners.Remove(listener);
}
public void NotifyObservers(TEventSubject eventSubject)
{
foreach (Listener listener in _listeners)
{
listener(eventSubject);
}
}
}
and my Selection
class now looks like this:
public class Selection : EventSubject<Selection> {
private GameObject selected;
private static Selection _instance;
public static Selection instance
{
get
{
if (!_instance)
{
_instance = FindObjectOfType(typeof (Selection)) as Selection;
if (!_instance) {
throw new Exception("You need a Selection in the scene");
}
}
return _instance;
}
}
public GameObject GetSelection() {
return selected;
}
public void setSelection(GameObject selected) {
this.selected = selected;
NotifyObservers(this);
}
}
I honestly never solved the riddle of why the compiler did not like listener(this)
but was able to work around that by making it an argument of NotifyObservers
. I would love to hear optimizations or other things people would change.