0

I'm trying to find a way to create an object which is roughly the reverse of the Delegate type.

Specifically, an object which contains a reference to the delegate instance, the instance of the class hosting the delegate instance, and the delegate instance's type.

The objective is to enable a sea of publishing objects and subscribing objects which can be properly Dispose()ed. As in, the subscribers can be disposed without directly calling Publisher?.DoPublish -= DoAThing;, and the publishers can empty delegate instances of invokable members without resulting in the subscriber retaining a reference to the publisher in the collection it uses to keep track of its own subscriptions.

As a basic mechanical problem, consider the following:

using System; //Delegate

public delegate void Publish();

public class Publisher
{
    public Publish DoPublish;
}

public class Subscriber
{
    public void DoAThing()
    {
        Console.WriteLine("I Did a Thing.");
    }
}

public class DelegateProxy<TDelegate> where TDelegate : Delegate
{
    public TDelegate DoDelegate;

    public DelegateProxy(ref TDelegate publisherDelegateInstance)
    {
        //This appears to be treated like a value assignment, not a reference assignment
        //At this point, the DelegateProxy.DoDelegate and publisherDelegateInstance behavior diverges.
        DoDelegate = publisherDelegateInstance;
    }

    public void SubscribeDelegate(TDelegate subscriberCallableEntity)
    {
        //DoDelegate += subscriberCallableEntity; //CS0019: Operator += cannot be applied to operands of type 'TDelegate' and 'TDelegate'
        DoDelegate = (TDelegate)Delegate.Combine(DoDelegate, subscriberCallableEntity); //requires explicit cast to be accepted by compiler
    }
}

static void Main(string[] args)
{
    Publisher myPublisher = new Publisher();
    Subscriber mySubscriber = new Subscriber();

    DelegateProxy<Publish> myProxy = new DelegateProxy<Publish>(ref myPublisher.DoPublish);

    myProxy.SubscribeDelegate(mySubscriber.DoAThing);

    myPublisher.DoPublish?.Invoke(); //No output
    Console.ReadLine();
    myProxy.DoDelegate?.Invoke(); //output occurs here.
    Console.ReadLine();
}

As coded in the example, there is no output when the original publisher's delegate is invoked., as the delegate which gets Delegate.Combine()ed appears to be a copy rather than a reference assignment. The ref keyword also appears to be superfluous, code behaves the same with or without it.

Is there a way to make a generic class as illustrated (DelegateProxy) and use said class to perform a subscription on the publisher, on behalf of the subscriber, without statically referencing the instance (myPublisher) which owns the delegate instance?

In the actual use case, it is necessary for the subscriber to be able to unsubscribe to an arbitrary publisher, as both the publishers and subscribers are ephemeral within the greater hierarchy of the software library. So not having a reference of some sort to which publisher(s) the subscriber is subscribed to is not possible. Both the publisher and the subscriber need to be able to Dispose()ed in any order without negative impact (of either calling a subscriber's delegate when the subscriber is Dispose()ed or retaining a reference to a Dispose()ed publisher.

  • 1
    I can't help but wonder what gap you're trying to fill over lambda functions and their automatic currying. For example, "not having a reference of some sort to which publisher(s) the subscriber is subscribed to is not possible", this sounds like the most basic `IObserver`/`IObservable` usage. – Blindy Oct 17 '22 at 20:11
  • I think this would be much easier to grasp when expressed in the language of interfaces rather than delegates. Still, I don't get what some requirements mean, e g. this "leaving without unsubscribe". In current state I'd consider voting to close this as "unclear" – Wiktor Zychla Oct 17 '22 at 20:16
  • @Blindy - The actual problem involves a high performance multi-source multi-target logging facility. Multiple dynamic components are registered by implementer or user input to emit logs to collectors. The collectors need to maintain a list of who they have subscribed to, as they also are subject to dynamic registration by the implementer or user. I'll have a look at the IObserver/IObservable interface to see if that can be put to use. – Christopher Eberle Oct 17 '22 at 20:47
  • @WiktorZychla - Syntactically, it seems very clear when i wrote it. "Is there a way to get a 3rd party to perform the action of subscribing to a delegate without referencing the class instance which composes the delegate member?" Maybe more directly, can a delegate instance be passed as a parameter, and be stored such that it's able to be used alone to subscribe a method to it. I've modified the question text slightly in the interest of addressing your comment. If it's preferable, i can post a more complicated example where the Subcribers track the Publishers for disposal. – Christopher Eberle Oct 17 '22 at 20:57
  • Delegates are first class citizens in c#, you can pass them without their owning objects. Some delegates are not owned by instances - a delegate can be a static method. – Wiktor Zychla Oct 18 '22 at 11:07
  • @WiktorZychla Feel free to pop the sample into a blank c# project and run it. While a "Callable Entity" (capital 'D' Delegate) can be transferred, the sample demonstrates the delegate instance (created by instantiating a declared delegate, lower case 'd' keyword) reference appears to be non-assignable. As the sample comments, while I can pass it into a method via a ref, it appears i need to have a non-generic type reference to use it in that method, and performing assignment (=) results in a copy. This copy-on-assignment is a common mechanic used for threadsafe invocation list iteration. – Christopher Eberle Oct 18 '22 at 13:22
  • @Blindy - I think this IObservable interface is going to put me on the right track, at least. While this would fit the provided sample, it won't directly work for the core problem and doesn't answer the direct question posed: "Can a delegate instance be used without reference to the composing object instance?". The primary problem IObservable doesn't address is an IObservable which presents multiple events of the same type. However, i think the pattern is something which, even if it turns out to not directly usable, is very instructive. – Christopher Eberle Oct 18 '22 at 13:39
  • "The primary problem IObservable doesn't address is an IObservable which presents multiple events of the same type" -- sure it does, look up the `WhenAnyValue` extension method on `IObservable`. Also if you're struggling to implement logging, you should know there are already fully implemented and mature logging frameworks already made for C#, like `log4net` and `Microsoft.Extensions.Logging`. They are all capable of filtering, splitting and copying messages to multiple independent listeners. – Blindy Oct 18 '22 at 14:13
  • @Blindy - I wouldn't say learning is struggling. The exercise is as academic as it is utilitarian. IObservable/IObserver isn't plumbed for events. IObserver appears to be a way to emulate an event without using delegates. The basic mechanism is essentially how an event works, but without using delegates. The question is specifically about using delegates. The part of IObservable that is interesting to me is the Unusbscriber (which i'm attempting to implement in my actual code-base to see if it will suit the Y of the problem). Even that doesn't answer the question, though. – Christopher Eberle Oct 18 '22 at 15:01
  • "IObserver appears to be a way to emulate an event without using delegates" -- the observable pattern has nothing to do with events, though it can be built on top of them. Its purpose is to split a stream of "notable events" into tokens observers can process one by one. Note the word stream, because `Stream` is an observable of characters, or reddit posts, or tweets or anything. – Blindy Oct 18 '22 at 15:04

1 Answers1

0

Delegate combining has to happen on the callee side. There's no going around this, not with ref parameters, not with anything. I believe you discovered this the hard way, but you could also have just read the documentation on delegates.

With that in mind, a quick change to your code to get it working is as follows:

delegate void Publish();

class Publisher
{
    public Publish DoPublish;

    public void SubscribePublishHandler(Publish handler) =>
        DoPublish += handler;
}

class Subscriber
{
    public void DoAThing()
    {
        Console.WriteLine("I Did a Thing.");
    }
}

class DelegateProxy<TDelegate> where TDelegate : Delegate
{
    readonly Action<TDelegate> subscribeToHandlerAction;

    public DelegateProxy(Action<TDelegate> subscribeToHandlerAction) =>
        this.subscribeToHandlerAction = subscribeToHandlerAction;

    public void SubscribeDelegate(TDelegate subscriberCallableEntity) =>
        subscribeToHandlerAction(subscriberCallableEntity);
}

static class Program
{
    static void Main()
    {
        Publisher myPublisher = new();
        Subscriber mySubscriber = new();

        DelegateProxy<Publish> myProxy = new(myPublisher.SubscribePublishHandler);

        myProxy.SubscribeDelegate(mySubscriber.DoAThing);

        myPublisher.DoPublish?.Invoke(); // Will output
    }
}

This will output your string when calling the publisher delegate as expected.

I will however reiterate that you're reinventing the wheel here, and not any better than the alternatives. That makes sense, given that your requirements also don't seem to make any sense ("So not having a reference of some sort to which publisher(s) the subscriber is subscribed to is not possible"). There are much better implementations of logging frameworks available that push you towards sane practices by just using them.

Blindy
  • 65,249
  • 10
  • 91
  • 131
  • I'd be thrilled to see the part of the documentation which states "You cannot subscribe a callee's delegate without referencing the callee". That is the conclusion i've arrived at, but never found in documentation. While i wouldn't say the mild condescension is completely inappropriate, i'm not sure it's terribly helpful. I've created multiple logging frameworks, in multiple languages. Re-inventing the wheel is a great way to learn how the wheel works, and why, exactly, they decided to make it round. It's hard to completely comprehend the elegance of a thing you have no experience making. – Christopher Eberle Oct 18 '22 at 15:08
  • Ran out of chars, would be happy to mark this as answer if it included a reference and blurb from documentation regarding the desired behavior being impossible. – Christopher Eberle Oct 18 '22 at 15:12