Отписаться от делегата, переданного через ключевое слово ref в метод подписки? - PullRequest
9 голосов
/ 14 декабря 2011

У меня есть следующий класс:

public class Terminal : IDisposable
{
    readonly List<IListener> _listeners;

    public Terminal(IEnumerable<IListener> listeners)
    {
        _listeners = new List<IListener>(listeners);
    }

    public void Subscribe(ref Action<string> source)
    {
        source += Broadcast;
        //Store the reference somehow?
    }

    void Broadcast(string message)
    {
        foreach (var listener in _listeners) listener.Listen(message);
    }

    public void Dispose()
    {
        //Unsubscribe from all the stored sources?
    }
}

Я искал некоторое время, и кажется, что аргумент, переданный с ключевым словом ref, не может быть сохранен. Попытка добавить исходный аргумент в список или присвоить его переменной поля не позволяет ему сохранить ссылку на исходную ссылку фактического делегата; поэтому мои вопросы:

  • Есть ли способ отписаться от всех источников, не передавая их ссылки снова?
  • Если нет, то как можно изменить класс, чтобы поддержать его, но при этом поддерживать подписку, передавая делегату метод?
  • Возможно ли достичь этого без использования Reflection?
  • Можно ли достичь этого, не заключая делегат / событие в класс и затем передавая класс в качестве параметра для подписки?

Спасибо.

РЕДАКТИРОВАТЬ: Похоже, что без использования Wrapper или Reflection, нет решения данной проблемы. Мое намерение состояло в том, чтобы сделать класс настолько переносимым, насколько это возможно, без необходимости заключать делегатов в вспомогательные классы. Спасибо всем за вклад.

Ответы [ 6 ]

1 голос
/ 15 декабря 2011

Редактировать : Хорошо, это была плохая идея, так что вернемся к основам:

Я рекомендую создать класс-оболочку над действием:

class ActionWrapper
{
    public Action<string> Action;
}

И реструктуризация вашего начального класса для работы с оболочками:

private ActionWrapper localSource;

public void Subscribe(ActionWrapper source)
{
    source.Action += Broadcast;
    localSource = source;        
}

public void Dispose()
{
    localSource.Action -= Broadcast;
}

Теперь вы должны получить желаемые результаты.

0 голосов
/ 10 января 2012

Возможно, вместо того, чтобы пытаться сохранить ссылку на делегата, используйте то, что вызывает Subscribe, использует свою ссылку на объект с делегатом для создания действий для подписки и отписки.Это дополнительный параметр, но он все еще прост.

public void Subscribe(Action<Action<string>> addHandler,Action<Action<string>> removeHandler)
    {
        //Prevent error for possibly being null in closure
        Action<string> onEvent = delegate { };

        //Broadcast when the event occurs, unlisten after (you could store onEvent and remove handler yourself)
        onEvent = (s) => { Broadcast(s); removeHandler(onEvent); };
        addHandler(onEvent);
    }

И пример подписки.

public event Action<string> CallOccured;

    public void Program()
    {
        Subscribe(a => CallOccured += a, a => CallOccured -= a);
        CallOccured("Hello");
    }
0 голосов
/ 15 декабря 2011

Это достаточно просто, но есть несколько подводных камней. Если вы храните ссылку на исходные объекты, как предлагалось в большинстве примеров, объект не будет собирать мусор . Лучший способ избежать этого - использовать WeakReference, который позволит GC работать правильно.

Итак, все, что вам нужно сделать, это:

1) Добавить список источников в класс:

private readonly List<WeakReference> _sources = new List<WeakReference>();

2) Добавить источник в список:

public void Subscribe(ref Action<string> source)
{
    source += Broadcast;
    //Store the reference 
    _sources.Add(new WeakReference(source));
}

3) А затем просто примените утилиту:

public void Dispose()
{
    foreach (var r in _sources)
    {
        var source = (Action<string>) r.Target;
        if (source != null) 
        {
            source -= Broadcast;
            source = null;
        }
    }


    _sources.Clear();
}

Тем не менее, есть также вопрос о том, почему Действие должно быть принято в качестве ссылки. В текущем коде нет причин для этого. Во всяком случае, это не влияет на проблему или решение.

0 голосов
/ 15 декабря 2011

Я бы предложил, чтобы метод подписки возвращал реализацию класса SubscriptionHelper, который реализует IDisposable.Простая реализация для SubscriptionHelper будет содержать ссылку на список подписки и копию делегата подписки;сам список подписки будет List , а метод Dispose для SubscriptionHelper удалит себя из списка.Обратите внимание, что если один и тот же делегат подписывается несколько раз, каждая подписка будет возвращать свой SubscriptionHelper;вызов Dispose для SubscriptionHelper отменит подписку, для которой она была возвращена.

Такой подход будет намного чище, чем метод Delegate.Combine / Delegate.Remove, используемый обычным шаблоном .net, семантика которого можеточень странно, если делается попытка подписаться и отписаться от многоцелевых делегатов.

0 голосов
/ 15 декабря 2011

EDIT:

Да, мои плохие делегаты являются неизменяемыми типами, поэтому добавление метода в список вызовов фактически создаст новый экземпляр делегата.

Что приводит к ответу нет на ваш вопрос. Чтобы отписаться от делегата, вам нужно удалить свой метод Broadcast из списка вызовов делегата. Это означает создание нового делегата и назначение его исходному полю или переменной. Но вы не можете получить доступ к оригиналу, если вы вышли из метода Subscribe. Кроме того, могут быть другие копии этого исходного поля / переменной, у которых есть ваш метод в списке вызовов. И у вас нет возможности узнать обо всех из них и изменить там значения.

Я бы предложил объявить интерфейс с событием для вашей цели. Это будет довольно гибкий подход.

public interface IMessageSource
{
    event Action<string> OnMessage;
}

public class MessageSource : IMessageSource
{
    public event Action<string> OnMessage;

    public void Send(string m)
    {
        if (OnMessage!= null) OnMessage(m);
    }
}

public class Terminal : IDisposable
{
    private IList<IMessageSource> sources = new List<IMessageSource>();

    public void Subscribe(IMessageSource source)
    {
        source.OnMessage += Broadcast;
        sources.Add(source);
    }


    void Broadcast(string message)
    {
        Console.WriteLine(message);
    }

    public void Dispose()
    {
        foreach (var s in sources) s.OnMessage -= Broadcast;
    }
}

Оригинальный ответ

Есть ли конкретная причина, по которой вы передаете source делегат как ref? Это необходимо, например, если вы хотите вернуть другого делегата из метода.

В противном случае делегат является ссылочным типом, поэтому вы можете подписаться на него, не передавая его как ref ...

0 голосов
/ 15 декабря 2011
public class Terminal : IDisposable
{
  List<IListener> _listeners;
  List<Action<string>> _sources;

  public Terminal(IEnumerable<IListener> listeners)
  {
      _listeners = new List<IListener>(listeners);
      _sources = new List<Action<string>>();
  }

  public void Subscribe(ref Action<string> source)
  {
      _sources.Add( source );
      source += Broadcast;
  }

  void Broadcast(string message)
  {
      foreach (var listener in _listeners) listener.Listen(message);
  }

  public void Dispose()
  {
      foreach ( var s in _sources ) s -= Broadcast; 
  }
}
...