首页 > 技术文章 > 高级C#信使(译) - Unity维基百科

alwu007 2017-01-17 20:01 原文

高级C#信使


作者:Ilya Suzdalnitski

译自:http://wiki.unity3d.com/index.php/Advanced_CSharp_Messenger

  1. 描述
  2. 前言
    1. MissingReferenceException的原因和解决方案
  3. 信使
    1. 用法
      1. 事件监听器
      2. 注册事件监听器
      3. 注销事件监听器
      4. 广播事件
    2. 清空信使
      1. 永久信使
    3. 杂项
      1. 打印所有消息
      2. 从其他信使过渡
    4. 代码
      1. Callback.cs
      2. Messenger.cs

描述


这是C#的一个高级版本的消息系统。当加载了一个新的场景(level)之后,它会自动清空事件表。这将防止程序员意外的调用已销毁的方法,从而有助于防止出现很多MissingReferenceExceptions。这个消息系统是基于Rod Hyde的CSharpMessenger和Magnus Wolffelt的CSharpMessenger Extended而研发的。

前言


我们的项目一旦引入了消息系统(CSharpMessenger Extended),我们就开始面对非常奇怪的BUGs。每次广播一个消息,U3D就会抛出MissingReferenceExceptions这个异常。提示会说消息处理器声明所在类已经被销毁。这个问题无处不在,并且没有一个合理的解释。不过,把消息处理器代码放到try-cache块中可以解决这个问题。我们知道,在代码中有大量的try-cache块不是一个好的解决方案。我们花费了一些时间,终于找到了问题的所在。

MissingReferenceException的原因和解决方案

是这样的,当加载一个新场景(level)(或者重新加载当前场景)时,MissingReferenceException的BUG就会出现。例如:我们有一个消息"start game",声明如下:

public class MainMenu : MonoBehaviour {    
    void Start ()
    {
        Messenger.AddListener("start game", StartGame);
    }
 
    void StartGame()
    {
        Debug.Log("StartGame called in" + gameObject);  //This is the line that would throw an exception
    }
 
    void StartGameButtonPressed()
    {
        Messenger.Broadcast("start game");
    }
}

乍一看,代码完全没有问题。但是在重新加载了这个场景之后,U3D将会抛出一个异常,提示说MainMenu对象已经被销毁。但是没有代码会销毁MainMenu脚本对象。

实际发生的是:

  1. 我们在信使中添加了一个"start game"消息监听器。
  2. StartGameButtonPressed方法被调用,广播了"start game"消息。
  3. 我们使用Application.LoadLevel重新加载了这个场景(level)。
  4. 重复第1步。
  5. 重复第2步。

在相应的步骤,信使的事件表里是这个样子的:

  • 在第1步:{ "start game", mainMenu1- > StartGame(); }
  • 在第4步:{ "start game", mainMenu1- > StartGame(); } { "start game", mainMenu2- > StartGame(); }

所以在第4步我们有两个"start game"消息处理器——第1个是已经销毁的MainMenu对象(在重新加载场景时被销毁),第2个是当前有效的MainMenu对象。结果是这样的,当我们在重新加载场景后广播"start game"消息时,信使把两个消息处理器(已经销毁的和当前有效的)都调用了。这里就是MissingReferenceException出现的源头。 那么解决方法显而易见——在卸载场景之后清空eventTable。在程序员方面不用做任何事来清空这个表,这一过程将自动完成。

信使


我们很高兴向你展示一个高级版本的C#消息系统。

用法


事件监听器

void OnPropCollected( PropType propType ) {
    if (propType == PropType.Life)
        livesAmount++;
}

注册事件监听器

void Start() {
    Messenger.AddListener< Prop >( "prop collected", OnPropCollected );
}

注销事件监听器

    Messenger.RemoveListener< Prop > ( "prop collected", OnPropCollected );

广播事件

public void OnTriggerEnter(Collider _collider) 
{
    Messenger.Broadcast< PropType > ( "prop collected", _collider.gameObject.GetComponent<Prop>().propType );
}

清空信使


当加载一个新的场景(level)时,信使会自动清空它的eventTable。这将确保信使的eventTable被清空,并且将使我们免于意外的MissingReferenceExceptions。如果你想手动清空管理器的eventTable,你可以调用Messenger.Cleanup()。

永久信使

如果你想要某个消息幸免于Cleanup,使用Messenger.MarkAsPermanent(string)来标记它既可。它可能用于这样的场合:某个类响应不同场景(level)的消息广播。

杂项


打印所有消息

为了调试的目的,你可以把信使的shouldLogAllMessages设置成true。这样,在调用信使的任何方法时都会打印消息。

从其他信使过渡

为了快速把旧的消息系统CSharpMessenger转变成高级的消息系统,请执行下面的步骤:

  1. 在MonoDevelop中打开“在文件中查找/替换”对话框。
  2. 在查找域输入:Messenger<([^<>]+)>.([A-Za-z0-9_]+)
  3. 在替换域输入:Messenger.$2<$1>
  4. 选择域:所有解决方案
  5. 勾选“正则搜索”复选框
  6. 按下“替换”按钮

代码


想要信使顺利运作需要两个文件:Callback.cs和Messenger.cs。

Callback.cs

public delegate void Callback();
public delegate void Callback<T>(T arg1);
public delegate void Callback<T, U>(T arg1, U arg2);
public delegate void Callback<T, U, V>(T arg1, U arg2, V arg3);

Messenger.cs

/*
 * Advanced C# messenger by Ilya Suzdalnitski. V1.0
 * 
 * Based on Rod Hyde's "CSharpMessenger" and Magnus Wolffelt's "CSharpMessenger Extended".
 * 
 * Features:
     * Prevents a MissingReferenceException because of a reference to a destroyed message handler.
     * Option to log all messages
     * Extensive error detection, preventing silent bugs
 * 
 * Usage examples:
     1. Messenger.AddListener<GameObject>("prop collected", PropCollected);
        Messenger.Broadcast<GameObject>("prop collected", prop);
     2. Messenger.AddListener<float>("speed changed", SpeedChanged);
        Messenger.Broadcast<float>("speed changed", 0.5f);
 * 
 * Messenger cleans up its evenTable automatically upon loading of a new level.
 * 
 * Don't forget that the messages that should survive the cleanup, should be marked with Messenger.MarkAsPermanent(string)
 * 
 */
 
//#define LOG_ALL_MESSAGES
//#define LOG_ADD_LISTENER
//#define LOG_BROADCAST_MESSAGE
#define REQUIRE_LISTENER
 
using System;
using System.Collections.Generic;
using UnityEngine;
 
static internal class Messenger {
    #region Internal variables
 
    //Disable the unused variable warning
#pragma warning disable 0414
    //Ensures that the MessengerHelper will be created automatically upon start of the game.
    static private MessengerHelper messengerHelper = ( new GameObject("MessengerHelper") ).AddComponent< MessengerHelper >();
#pragma warning restore 0414
 
    static public Dictionary<string, Delegate> eventTable = new Dictionary<string, Delegate>();
 
    //Message handlers that should never be removed, regardless of calling Cleanup
    static public List< string > permanentMessages = new List< string > ();
    #endregion
    #region Helper methods
    //Marks a certain message as permanent.
    static public void MarkAsPermanent(string eventType) {
#if LOG_ALL_MESSAGES
        Debug.Log("Messenger MarkAsPermanent \t\"" + eventType + "\"");
#endif
 
        permanentMessages.Add( eventType );
    }
 
 
    static public void Cleanup()
    {
#if LOG_ALL_MESSAGES
        Debug.Log("MESSENGER Cleanup. Make sure that none of necessary listeners are removed.");
#endif
 
        List< string > messagesToRemove = new List<string>();
 
        foreach (KeyValuePair<string, Delegate> pair in eventTable) {
            bool wasFound = false;
 
            foreach (string message in permanentMessages) {
                if (pair.Key == message) {
                    wasFound = true;
                    break;
                }
            }
 
            if (!wasFound)
                messagesToRemove.Add( pair.Key );
        }
 
        foreach (string message in messagesToRemove) {
            eventTable.Remove( message );
        }
    }
 
    static public void PrintEventTable()
    {
        Debug.Log("\t\t\t=== MESSENGER PrintEventTable ===");
 
        foreach (KeyValuePair<string, Delegate> pair in eventTable) {
            Debug.Log("\t\t\t" + pair.Key + "\t\t" + pair.Value);
        }
 
        Debug.Log("\n");
    }
    #endregion
 
    #region Message logging and exception throwing
    static public void OnListenerAdding(string eventType, Delegate listenerBeingAdded) {
#if LOG_ALL_MESSAGES || LOG_ADD_LISTENER
        Debug.Log("MESSENGER OnListenerAdding \t\"" + eventType + "\"\t{" + listenerBeingAdded.Target + " -> " + listenerBeingAdded.Method + "}");
#endif
 
        if (!eventTable.ContainsKey(eventType)) {
            eventTable.Add(eventType, null );
        }
 
        Delegate d = eventTable[eventType];
        if (d != null && d.GetType() != listenerBeingAdded.GetType()) {
            throw new ListenerException(string.Format("Attempting to add listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being added has type {2}", eventType, d.GetType().Name, listenerBeingAdded.GetType().Name));
        }
    }
 
    static public void OnListenerRemoving(string eventType, Delegate listenerBeingRemoved) {
#if LOG_ALL_MESSAGES
        Debug.Log("MESSENGER OnListenerRemoving \t\"" + eventType + "\"\t{" + listenerBeingRemoved.Target + " -> " + listenerBeingRemoved.Method + "}");
#endif
 
        if (eventTable.ContainsKey(eventType)) {
            Delegate d = eventTable[eventType];
 
            if (d == null) {
                throw new ListenerException(string.Format("Attempting to remove listener with for event type \"{0}\" but current listener is null.", eventType));
            } else if (d.GetType() != listenerBeingRemoved.GetType()) {
                throw new ListenerException(string.Format("Attempting to remove listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being removed has type {2}", eventType, d.GetType().Name, listenerBeingRemoved.GetType().Name));
            }
        } else {
            throw new ListenerException(string.Format("Attempting to remove listener for type \"{0}\" but Messenger doesn't know about this event type.", eventType));
        }
    }
 
    static public void OnListenerRemoved(string eventType) {
        if (eventTable[eventType] == null) {
            eventTable.Remove(eventType);
        }
    }
 
    static public void OnBroadcasting(string eventType) {
#if REQUIRE_LISTENER
        if (!eventTable.ContainsKey(eventType)) {
            throw new BroadcastException(string.Format("Broadcasting message \"{0}\" but no listener found. Try marking the message with Messenger.MarkAsPermanent.", eventType));
        }
#endif
    }
 
    static public BroadcastException CreateBroadcastSignatureException(string eventType) {
        return new BroadcastException(string.Format("Broadcasting message \"{0}\" but listeners have a different signature than the broadcaster.", eventType));
    }
 
    public class BroadcastException : Exception {
        public BroadcastException(string msg)
            : base(msg) {
        }
    }
 
    public class ListenerException : Exception {
        public ListenerException(string msg)
            : base(msg) {
        }
    }
    #endregion
 
    #region AddListener
    //No parameters
    static public void AddListener(string eventType, Callback handler) {
        OnListenerAdding(eventType, handler);
        eventTable[eventType] = (Callback)eventTable[eventType] + handler;
    }
 
    //Single parameter
    static public void AddListener<T>(string eventType, Callback<T> handler) {
        OnListenerAdding(eventType, handler);
        eventTable[eventType] = (Callback<T>)eventTable[eventType] + handler;
    }
 
    //Two parameters
    static public void AddListener<T, U>(string eventType, Callback<T, U> handler) {
        OnListenerAdding(eventType, handler);
        eventTable[eventType] = (Callback<T, U>)eventTable[eventType] + handler;
    }
 
    //Three parameters
    static public void AddListener<T, U, V>(string eventType, Callback<T, U, V> handler) {
        OnListenerAdding(eventType, handler);
        eventTable[eventType] = (Callback<T, U, V>)eventTable[eventType] + handler;
    }
    #endregion
 
    #region RemoveListener
    //No parameters
    static public void RemoveListener(string eventType, Callback handler) {
        OnListenerRemoving(eventType, handler);   
        eventTable[eventType] = (Callback)eventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }
 
    //Single parameter
    static public void RemoveListener<T>(string eventType, Callback<T> handler) {
        OnListenerRemoving(eventType, handler);
        eventTable[eventType] = (Callback<T>)eventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }
 
    //Two parameters
    static public void RemoveListener<T, U>(string eventType, Callback<T, U> handler) {
        OnListenerRemoving(eventType, handler);
        eventTable[eventType] = (Callback<T, U>)eventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }
 
    //Three parameters
    static public void RemoveListener<T, U, V>(string eventType, Callback<T, U, V> handler) {
        OnListenerRemoving(eventType, handler);
        eventTable[eventType] = (Callback<T, U, V>)eventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }
    #endregion
 
    #region Broadcast
    //No parameters
    static public void Broadcast(string eventType) {
#if LOG_ALL_MESSAGES || LOG_BROADCAST_MESSAGE
        Debug.Log("MESSENGER\t" + System.DateTime.Now.ToString("hh:mm:ss.fff") + "\t\t\tInvoking \t\"" + eventType + "\"");
#endif
        OnBroadcasting(eventType);
 
        Delegate d;
        if (eventTable.TryGetValue(eventType, out d)) {
            Callback callback = d as Callback;
 
            if (callback != null) {
                callback();
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }
 
    //Single parameter
    static public void Broadcast<T>(string eventType, T arg1) {
#if LOG_ALL_MESSAGES || LOG_BROADCAST_MESSAGE
        Debug.Log("MESSENGER\t" + System.DateTime.Now.ToString("hh:mm:ss.fff") + "\t\t\tInvoking \t\"" + eventType + "\"");
#endif
        OnBroadcasting(eventType);
 
        Delegate d;
        if (eventTable.TryGetValue(eventType, out d)) {
            Callback<T> callback = d as Callback<T>;
 
            if (callback != null) {
                callback(arg1);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }
 
    //Two parameters
    static public void Broadcast<T, U>(string eventType, T arg1, U arg2) {
#if LOG_ALL_MESSAGES || LOG_BROADCAST_MESSAGE
        Debug.Log("MESSENGER\t" + System.DateTime.Now.ToString("hh:mm:ss.fff") + "\t\t\tInvoking \t\"" + eventType + "\"");
#endif
        OnBroadcasting(eventType);
 
        Delegate d;
        if (eventTable.TryGetValue(eventType, out d)) {
            Callback<T, U> callback = d as Callback<T, U>;
 
            if (callback != null) {
                callback(arg1, arg2);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }
 
    //Three parameters
    static public void Broadcast<T, U, V>(string eventType, T arg1, U arg2, V arg3) {
#if LOG_ALL_MESSAGES || LOG_BROADCAST_MESSAGE
        Debug.Log("MESSENGER\t" + System.DateTime.Now.ToString("hh:mm:ss.fff") + "\t\t\tInvoking \t\"" + eventType + "\"");
#endif
        OnBroadcasting(eventType);
 
        Delegate d;
        if (eventTable.TryGetValue(eventType, out d)) {
            Callback<T, U, V> callback = d as Callback<T, U, V>;
 
            if (callback != null) {
                callback(arg1, arg2, arg3);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }
    #endregion
}
 
//This manager will ensure that the messenger's eventTable will be cleaned up upon loading of a new level.
public sealed class MessengerHelper : MonoBehaviour {
    void Awake ()
    {
        DontDestroyOnLoad(gameObject);    
    }
 
    //Clean up eventTable every time a new level loads.
    public void OnLevelWasLoaded(int unused) {
        Messenger.Cleanup();
    }
}

 

推荐阅读