【问题标题】:Inject reference by event message通过事件消息注入引用
【发布时间】:2021-05-17 08:13:48
【问题描述】:

我在项目中使用 pub/sub 模式。如果一个类正在跟踪像浮点数这样的原始类型,并且其他子系统和类需要能够读取,是否可以使用消息来传递浮点数的句柄?不是价值,而是可以让他们在必要时阅读它的东西。

【问题讨论】:

    标签: c# unity3d


    【解决方案1】:

    如果一个类正在跟踪像浮点数这样的原始类型,并且其他子系统和类需要能够读取,是否可以使用消息来传递浮点数的句柄?

    绝对!事实上,Unity 提供了一个令人惊讶的健壮(ish)message system,实现这一点并不难。尽管在我个人看来,他们的系统不如 MSDN 使用的传统 Producer/Subscriber event based pattern 直观。

    为了将“句柄”(MSDN 将其称为 alias)传递给一个值,比如您的示例中的 float,在 C# 中我们可以使用 ref 关键字。

    C# 中的 ref 关键字,当你把它归结为它的基础时,你是定义还是请求一个对象的引用,而不是它本身的对象。(这个为简洁起见,被过度概括了)。

    当您请求对该对象的引用时,您不会获得该对象的副本,而是获得一个别名,该别名允许您使用该变量获取、设置、读取或任何您想要的东西,就像您控制它一样(就像这个例子)

    要使用 ref 关键字,这里有一个简短的示例来帮助您入门:

    class Alice{
        private int RandomInt = 4; // guaranteed to be random
    
        public ref int GetReferenceToPrivateInt()
        {
            return ref RandomInt;
        }
    
        public void LogPrivateInt()
        {
            Console.WriteLine(RandomInt);
        }
    }
    
    Alice alice = new Alice();
    
    ref int retrieveReference = alice.GetReferenceToPrivateInt();
    
    retrieveReference = 16;
    
    alice.LogPrivateInt();
    // outputs:
    16
    

    我们可以在这个例子中看到,即使我们从不可以直接访问 Alice 类中的私有变量,但当我们收到 by-reference 值时来自GetReferenceToPrivateInt() 方法。我们能够修改该值,并且这些更改反映在私有 Alice 类变量中。无需手动设置变量!太棒了!

    如果我们将 ref 关键字与 Unity 消息传递系统一起使用,我们就能够“传递”引用变量,这样我们就可以变异它们而无需直接访问它们。

    让我们从创建一个定义订阅者IEventSystemHandler开始。

    public delegate ref T ReferenceAction<T>();
    
    public interface IReferenceMessage<T> : IEventSystemHandler
    {
        void MutateValue(ref T MessageValue);
        void StoreGetter(ReferenceAction<T> GetterForReferencePrimitive);
    }
    

    我们在这里所做的是创建interface,它定义了订阅者应该有哪些方法来正确“使用”我们发送给他们的内容。

    我们创建了两个方法,MutateValue(ref T)StoreGetter(ReferenceAction&lt;T&gt;)MutateValue() 接受 by reference 泛型参数,StoreGetter() 接受名为 ReferenceAction&lt;T&gt; 的东西。

    ReferenceAction&lt;T&gt; 这里不是 Unity 或 MSDN 创建的方法、类型或对象,我们是在上面的代码中创建的:

    public delegate ref T ReferenceAction<T>();
    

    它的作用是创建一个delegate,在验证简化意义上,我们告诉编译器,“我想提供一个方法作为这个函数StoreGetter的参数,但我希望该方法具有我需要的非常具体的signature。”

    现在我们已经定义了订阅者的外观,让我们创建一个。

    public class Subcriber: MonoBehaviour, IReferenceMessage<float>
    {
        // we store the method that we can call to get a reference value here
        private ReferenceAction<float> FloatGetter;
    
        // Whenever the producer sends out a message called MutateValue, we're gonna run this automatically because we're subscribed to the producer
        public void MutateValue(ref float MessageValue)
        {
            // change the value of the reference float to prove that value changes on the producer
            MessageValue *= 2;
        }
    
        // whenever the producer starts, since we're a subscriber, we will have this method called, which will allow us to store the method we can call to get a reference to the hidden float on the producer object
        public void StoreGetter(ReferenceAction<float> GetterForReferencePrimitive)
        {
            // save the method for later so we can get a reference value any time we need it
            this.FloatGetter = GetterForReferencePrimitive;
        }
    }
    

    让我们也创建一个 Producer 类并将所有内容整合在一起。

    public class Producer : MonoBehaviour
    {
        // for example purposes we should keep this private, so we know we are changing its value outside this class using it's reference, and not the actual object
        private float SecretFloat = 1.0f;
    
        // allow the user to assign a subscriber in the inspector
        public GameObject Subcriber;
    
        // create a property that is public, that hides the secret float.
        public float Primitive
        {
            get => SecretFloat;
            set
            {
                SecretFloat = value;
    
                // send message to subscriber that the value of the secret float has changed
                ExecuteEvents.Execute<IReferenceMessage<float>>(
                    Subcriber, 
                    null, 
                    /* pass the secret float as a reference and not a copy */
                    (x, y) => x.MutateValue(ref SecretFloat)
                );
            }
        }
    
        private void Start()
        {
            // send message to subscriber so they can get reference float any time using the method we send them
            ExecuteEvents.Execute<IReferenceMessage<float>>(Subcriber, null, (x, y) => x.StoreGetter(FloatGetter));
        }
    
        // create a method that we send as a message at Start() so other objects can retrieve a reference at any time, not just when the value of secret float has changed
        private ref float FloatGetter()
        {
            // return a reference to secret float, not a copy.
            return ref SecretFloat;
        }
    }
    

    上面非常简单(如未完全实现/功能)的脚本允许您做一些非常酷的事情,我认为这就是您想要的。

    Start() 被调用时,订阅者会获得一个保存以供以后使用的方法,它获得的方法是FloatGetter(),当它被调用时 - 返回一个引用引用floatSecretFloat)。

    当订阅者收到该消息时(在StoreGetter() 中),它将存储该方法,以便以后可以随时使用它来获取引用。

    除此之外,我们还做到了,每当SecretFloat 的值发生更改时,都会向订阅者发送第二条消息,这次我们直接发送引用而不是返回引用的方法。这样我们就不需要经常检查我们的引用是否已经更新,生产者会告诉我们何时更新,如果我们愿意,我们可以在获得引用时改变它。超级酷!

    您现在可能想知道为什么我们需要存储一个方法来获取引用,我们不能将引用变量存储在字段或属性中并使用它代替?

    不幸的是ref 值和变量不能存储在属性或字段中,以便以后保留。 一般由于 C# 的工作方式,ref 变量和引用可以never leave the stack and have limitations for safety reasons.

    如果您有兴趣在纯 C# 中实现相同的效果,可以使用以下方法开始,您会发现使用传统的 EventHandler 方法更加精简。

    public class Subscriber
    {
        public void MutateFloat(object caller, ref float primitive)
        {
            primitive *= 2;
        }
    }
    
    public class Producer<T>
    {
        private T SecretPrimitive = default;
    
        public delegate void RefPrimitiveHandler(object caller, ref T primitive);
    
        public event RefPrimitiveHandler OnPrimitiveChange;
    
        public T Primitive
        {
            get => SecretPrimitive;
            set
            {
                SecretPrimitive = value;
                OnPrimitiveChange?.Invoke(this, ref SecretPrimitive);
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

      如果我理解正确,您可能会使用Func&lt;float&gt;。并且只需存储一个返回浮点值的方法

      例如

      public class System : MonoBehaviour
      {
          public SubSystem SomeSubSystem;
      
          private float currentFloat;
      
          private float GetFloat()
          {
              return currentFloat;
          }
      
          private void Awake ()
          {
              SomeSubSystem.Initialize(GetFloat);
          }
      
          private void Update()
          {
              currentFloat += Time.deltaTime;
          }
      }
      

      然后像这样使用它

      public class SubSystem : MonoBehaviour
      {
          private Func<float> _getFloat;
      
          public void Initialize (Func<float> getFloat)
          {
              _getFloat = getFloat;
          }
      
          private IEnumerator Start ()
          {
              while(true)
              {
                  yield return new WaitForSeconds (1);
      
                  Debug.Log(_getFloat());
              }
          }
      }
      

      当然也可以在类似的活动中提供同样的东西

      public event Action<Func<float>> SomeEvent;
      

      因此,该事件的侦听器不会直接接收浮点值,而是如何获取它并存储它以供以后使用的方法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-01-04
        • 2013-05-28
        • 2012-05-04
        • 1970-01-01
        • 1970-01-01
        • 2015-02-02
        相关资源
        最近更新 更多