Странное поведение с действиями, локальными переменными и сборкой мусора в MVVM light Messenger


У меня действительно странная проблема с системой Messenger в свете MVVM. Это трудно объяснить, поэтому вот небольшая программа, которая демонстрирует проблему:

using System;
using GalaSoft.MvvmLight.Messaging;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var prog = new Program();
            var recipient = new object();

            prog.RegisterMessageA(recipient);
            prog.RegisterMessageB(recipient);

            prog.SendMessage("First Message");
            GC.Collect();
            prog.SendMessage("Second Message");
        }

        public void RegisterMessageA(object target)
        {
            Messenger.Default.Register(this, (Message msg) =>
            {
                Console.WriteLine(msg.Name + " recieved by A");
                var x = target;
            });
        }

        public void RegisterMessageB(object target)
        {
            Messenger.Default.Register(this, (Message msg) =>
            {
                Console.WriteLine(msg.Name + " received by B");
            });
        }

        public void SendMessage(string name)
        {
            Messenger.Default.Send(new Message { Name = name });
        }

        class Message
        {
            public string Name { get; set; }
        }
    }
}

Если вы запускаете приложение, это вывод консоли:

First Message recieved by A
First Message received by B
Second Message received by B
Как вы можете видеть, второе сообщение никогда не принимается получателем A. Однако единственное различие между B и A-это одна строка: утверждение var x = target;. Если удалить эту строку, A получит второе сообщение.

Также, если вы удалите GC.Collect(); затем A получает второе сообщение. Однако это только скрывает проблему, так как в реальной программе сборщик мусора в конечном итоге автоматически запустится.

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

Может ли кто-нибудь объяснить, что здесь происходит?
2 9

2 ответа:

Что ж, теперь я понимаю, почему это происходит (во всяком случае, я верю). Я воспроизвел его в более короткой форме, которая не использует лямбда-выражения, а затем объясню, почему лямбды важны.

using System;
using GalaSoft.MvvmLight.Messaging;

class Program
{
    static void Main(string[] args)
    {
        Receiver r1 = new Receiver("r1");
        Receiver r2 = new Receiver("r2");
        var recipient = new object();

        Messenger.Default.Register<object>(recipient, r1).ShowMessage;
        Messenger.Default.Register<object>(recipient, r2).ShowMessage;

        GC.Collect();
        Messenger.Default.Send(recipient, null);
        // Uncomment one of these to see the relevant message...
        // GC.KeepAlive(r1);
        // GC.KeepAlive(r2);
    }
}

class Receiver
{
    private string name;

    public Receiver(string name)
    {
        this.name = name;
    }

    public void ShowMessage(object message)
    {
        Console.WriteLine("message received by {0}", name);
    }
}

В основном, мессенджер сохраняет толькослабую ссылку на обработчик сообщений. (Тоже адресату, но здесь это не проблема.) Более конкретно, он, по-видимому, имеет слабую ссылку на объект обработчика target. Кажется, ему все равно. о самом объекте делегата, но цель важна. Поэтому в приведенном выше коде, когда вы держите Receiver объект живой, делегат, который имеет этот объект в качестве цели, все еще используется. Однако, когда целевой объект разрешается собирать мусор, обработчик, использующий этот объект, не используется.

Теперь давайте посмотрим на два ваших обработчика:
public void RegisterMessageA(object target)
{
    Messenger.Default.Register(target, (Message msg) =>
    {
        Console.WriteLine(msg.Name + " received by A");
        var x = target;
    });
}

Это лямбда-выражение захватывает параметр target. Чтобы захватить его, компилятор генерирует новый класс-так что RegisterMessageA является эффективно:

public void RegisterMessageA(object target)
{
    GeneratedClass x = new GeneratedClass();
    x.target = target;
    Messenger.Default.Register(x.target, x.Method);
}

private class GeneratedClass
{
    public object target;

    public void Method(Message msg)
    {
        Console.WriteLine(msg.Name + " received by A");
        var x = target;
    }
}
Теперь нет ничего, кроме делегата, который поддерживает этот экземпляр GeneratedClass живым. Сравните это с вашим вторым обработчиком:
public void RegisterMessageB(object target)
{
    Messenger.Default.Register(target, (Message msg) =>
    {
        Console.WriteLine(msg.Name + " received by B");
    });
}

Здесь нет захваченных переменных, поэтому компилятор генерирует код примерно так:

public void RegisterMessageB(object target)
{
    Messenger.Default.Register(target, RegisterMessageB_Lambda);
}

private static void RegisterMessageB_Lambda(Message msg)
{
    Console.WriteLine(msg.Name + " received by B");
}

Здесь этостатический метод, поэтому нет цели делегата вообще. Если делегат захватил this, он будет создан как метод экземпляра. Но важно то, что в этом нет необходимости. создайте дополнительный класс... так что нечего собирать мусор.

Я не изучал точно, как MvvmLight делает это - является ли это просто слабой ссылкой на делегат, и что CLR обрабатывает это каким-то особым образом, или же MvvmLight отделяет цель от самого делегата. В любом случае, я надеюсь, что это объясняет поведение, которое вы видите. С точки зрения того, как исправить любую проблему, которую вы видите с реальным кодом-в основном вам нужно будет убедиться, что вы держите сильную ссылку на любую цель делегата, которая вам нужна.

EDIT: Хорошо, похоже, что это теперь из-за WeakActionGeneric и его базовый класс WeakAction. Я не знаю, является ли это поведение ожидаемым поведением (автором), но это код, ответственный :)

Я согласен, поведение этой программы действительно странно.

Я попробовал сам и, как вы уже поняли, проблема каким-то образом связана с этой строкой:

var x = target;

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

class Program
    {
        static void Main(string[] args)
        {
            var prog = new Program();
            var recipient = new object();

            prog.RegisterMessageA(recipient);
            prog.RegisterMessageB(recipient);

            prog.SendMessage("First Message");
            GC.Collect();
            prog.SendMessage("Second Message");
        }

        public void RegisterMessageA(object target)
        {
            Messenger.Default.Register(target, (Message msg) =>
            {
                Console.WriteLine(msg.Name + " received by A");
                var x = msg.Target;
            });
        }

        public void RegisterMessageB(object target)
        {
            Messenger.Default.Register(target, (Message msg) =>
            {
                Console.WriteLine(msg.Name + " received by B");
            });
        }

        public void SendMessage(string name)
        {
            Messenger.Default.Send(new Message { Name = name });
        }

        class Message : MessageBase //part of the MVVM Light framework
        {
            public string Name { get; set; }
        }
    }

MessageBase-это класс из MVVM Light Framework, который предлагает возможность извлечения цели из самого сообщения.

Но я не уверен, что это то, что вы пытаетесь сделать. достигать...