Bootstrap

【unity进阶知识3】封装一个事件管理系统

前言

框架的事件系统主要负责高效的方法调用与数据传递,实现各功能之间的解耦,通常在调用某个实例的方法时,必须先获得这个实例的引用或者新实例化一个对象,低耦合度的框架结构希望程序本身不去关注被调用的方法所依托的实例对象是否存在,通过事件系统做中转将功能的调用封装成事件,使用事件监听注册、移除和事件触发完成模块间的功能调用管理。常用在UI事件、跨模块事件上。

一、作用

访问其它脚本时,不直接访问,而是通过发送一条类似“命令”,让监听了这条“命令”的脚本自动执行对应的逻辑。

二、原理

1、让脚本向事件中心添加事件,监听对应的“命令”。
2、发送“命令”,事件中心就会通知监听了这条“命令”的脚本,让它们自动执行对应的逻辑。
在这里插入图片描述

三、不使用事件管理器

在这里插入图片描述

新增3个测试脚本

public class Player : MonoBehaviour {
    public void Log(){
        Debug.Log("我是玩家");
    }
}
public class Player1 : MonoBehaviour {
    public void Log(){
        Debug.Log("我是玩家1");
    }
}
public class Player2 : MonoBehaviour {
    public void Log(){
        Debug.Log("我是玩家2");
    }
}

调用各个脚本的log方法

public class EventManagerTest: MonoBehaviour
{

    private void Start()
    {
        GameObject go = GameObject.Find("Player");
        go.GetComponent<Player>().Log();    

        GameObject go1 = GameObject.Find("Player1");
        go1.GetComponent<Player1>().Log();

        GameObject go2 = GameObject.Find("Player2");
        go2.GetComponent<Player2>().Log();
    }
}

效果
在这里插入图片描述

四、使用事件管理器

1、事件管理器

新增EventManager,事件管理器

/// <summary>
/// 事件管理器
/// </summary>
public class EventManager : Singleton<EventManager>
{
    Dictionary<string, UnityAction> eventsDictionary = new Dictionary<string, UnityAction>();

    /// <summary>
    /// 事件监听
    /// </summary>
    /// <param name="eventName">事件名称</param>
    /// <param name="action">监听方法</param>
    public void AddEventListener(string eventName, UnityAction action)
    {
        if (eventsDictionary.ContainsKey(eventName))
        {
            eventsDictionary[eventName] += action;
        }
        else
        {
            eventsDictionary.Add(eventName, action);
        }
    }

    /// <summary>
    /// 触发事件
    /// </summary>
    /// <param name="eventName">事件名称</param>
    public void Dispatch(string eventName){
        if(eventsDictionary.ContainsKey(eventName)){
            eventsDictionary[eventName]?.Invoke();
        }
    }
}

2、添加事件监听

在这里插入图片描述

分别在Player、Player1、Player2新增如下代码,添加事件监听

private void Start() {
	EventManager.Instance.AddEventListener("打印日志", Log);    
}

3、触发事件

在这里插入图片描述

在EventManagerTest中触发事件

public class EventManagerTest : MonoBehaviour
{
    private void Start()
    {
        // GameObject go = GameObject.Find("Player");
        // go.GetComponent<Player>().Log();    

        // GameObject go1 = GameObject.Find("Player1");
        // go1.GetComponent<Player1>().Log();

        // GameObject go2 = GameObject.Find("Player2");
        // go2.GetComponent<Player2>().Log();

        EventManager.Instance.Dispatch("打印日志");
    }
}

4、结果

在这里插入图片描述

五、移除事件

比如有几个小怪,都添加了事件监听,杀死后会被销毁,如果不把事件移除,直接再次执行命令则会报错:
MissingReferenceException:The object of type 'Capsule'has been destroyed but you are still trying to access it.
在这里插入图片描述
修改EventManager,添加移除事件方法

/// <summary>
/// 移除事件某个监听方法
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="action">监听方法</param>
public void RemoveEventListener(string eventName, UnityAction action){
    if(eventsDictionary.ContainsKey(eventName)){
        eventsDictionary[eventName] -= action;
    }
}

/// <summary>
/// 移除整个事件
/// </summary>
/// <param name="eventName">名称</param>
public void RemoveEvent(string eventName){
    if(eventsDictionary.ContainsKey(eventName)){
        eventsDictionary[eventName] = null;
    }
}

测试调用

public class EventManagerTest : MonoBehaviour
{ 
    private void OnGUI()
    {
        if (GUI.Button(new Rect(0, 0, 150, 50), "触发事件"))
        {
            EventManager.Instance.Dispatch("打印日志");
        }
        if (GUI.Button(new Rect(0, 50, 150, 50), "移除Player事件监听"))
        {
            GameObject go = GameObject.Find("Player");
            EventManager.Instance.RemoveEventListener("打印日志", go.GetComponent<Player>().Log); 
        }
        if (GUI.Button(new Rect(0, 100, 150, 50), "移除整个事件"))
        {
            EventManager.Instance.RemoveEvent("打印日志");
        }
    }
}

效果
在这里插入图片描述

六、自定义枚举事件名称

目前事件名称是字符串,手打容易出错,我们可以选择使用枚举的方式

/// <summary>
/// 事件名称枚举
/// </summary>
public enum EventNameEnum{
    Log,    //打印
    AddHealth   //群体回血
}

修改EventManager,新增获取事件名称方法

/// <summary>
/// 获取事件名称
/// </summary>
/// <param name="eventNameEnum">事件枚举</param>
/// <returns>事件名称</returns>
private string GetEnventName(object EventNameEnum){
    return EventNameEnum.GetType().Name + "_" + EventNameEnum.ToString();
}

修改测试调用

public class EventManagerTest : MonoBehaviour
{ 
    private void OnGUI()
    {
        if (GUI.Button(new Rect(0, 0, 150, 50), "触发事件"))
        {
            EventManager.Instance.Dispatch(EventNameEnum.Log);
        }
        if (GUI.Button(new Rect(0, 50, 150, 50), "移除Player事件监听"))
        {
            GameObject go = GameObject.Find("Player");
            EventManager.Instance.RemoveEventListener(EventNameEnum.Log, go.GetComponent<Player>().Log); 
        }
        if (GUI.Button(new Rect(0, 100, 150, 50), "移除整个事件"))
        {
            EventManager.Instance.RemoveEvent(EventNameEnum.Log);
        }
    }
}

结果,和之前一样
在这里插入图片描述

七、传递带有一个参数的事件

如果我们想要传递带有一个参数的事件,可以遵循里氏替换原则(Liskov Substitution Principle),即子类可以替换父类而不会影响程序的正确性。

  • 里氏替换原则
    通过使用 IEventInfo 接口,可以确保 EventInfo<T>EventInfo 类可以在需要 IEventInfo 的上下文中被替换而不影响程序的功能。这使得事件管理器能够处理不同类型的事件回调。

  • 单一职责原则
    每个 EventInfo 类都有自己的职责:EventInfo<T> 处理带参数的回调,而 EventInfo 处理不带参数的回调。这增强了代码的清晰性和可维护性。

这种设计提供了灵活性,使得事件管理系统能够处理多种类型的事件,同时也遵循了面向对象设计的原则。你可以根据需要扩展或修改 IEventInfoEventInfo 类,以支持更多的事件类型和逻辑。

1、接口 IEventInfo

定义一个标记接口 IEventInfo,用于标识事件信息的类型。这样可以在系统中使用多态性,确保遵循里氏替换原则。

public interface IEventInfo { }

2、泛型类 EventInfo

EventInfo 类实现了 IEventInfo 接口。这个类用于处理带有参数的事件回调(UnityAction),允许在事件触发时传递参数。action 字段用于保存事件回调。

private class EventInfo<T> : IEventInfo
{
    public UnityAction<T> action;

    public EventInfo(UnityAction<T> call)
    {
        action += call; // 将传入的回调添加到 action 上
    }
}

3、非泛型类 EventInfo

另一个 EventInfo 类用于处理没有参数的事件回调(UnityAction)。这种设计使得可以处理不同类型的事件。

private class EventInfo : IEventInfo
{
    public UnityAction action;

    public EventInfo(UnityAction call)
    {
        action += call; // 将传入的回调添加到 action 上
    }
}

4、修改EventManager

事件名称记得修改一下,不然我们可能很难分出哪个是带传参的,我们可以选择把这个参数的类型的名字也传进去

Dictionary<string, IEventInfo> eventsDictionary = new Dictionary<string, IEventInfo>();

/// <summary>
/// 无参数的事件监听
/// </summary>
/// <param name="EventNameEnum">事件枚举</param>
/// <param name="action">监听方法</param>
public void AddEventListener(object EventNameEnum, UnityAction call)
{
    string eventName = GetEnventName(EventNameEnum);
    if (eventsDictionary.ContainsKey(eventName))
    {
        (eventsDictionary[eventName] as EventInfo).action += call;
    }
    else
    {
        eventsDictionary.Add(eventName, new EventInfo(call));
    }
}

/// <summary>
/// 带1个参数的事件监听
/// </summary>
public void AddEventListener<T>(object EventNameEnum, UnityAction<T> call)
{
    string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T).Name;
    if (eventsDictionary.ContainsKey(eventName))
    {
        (eventsDictionary[eventName] as EventInfo<T>).action += call;
    }
    else
    {
        eventsDictionary.Add(eventName, new EventInfo<T>(call));
    }
}

//其他类似

IEventInfo是我们人为制造出来的一个副接口,这样的话就可以成功把有参数的事件和无参数的事件都存到字典里面去了

5、事件监听

Player、Player1、Player2都添加带一个参数的事件监听

public class Player : MonoBehaviour
{
    private void Start()
    {
        EventManager.Instance.AddEventListener(EventNameEnum.Log, Log);
        EventManager.Instance.AddEventListener<int>(EventNameEnum.AddHealth, AddHealth);
    }

    public void Log()
    {
        Debug.Log("我是玩家");
    }

    public void AddHealth(int health)
    {
        Debug.Log($"玩家恢复+{health + 1}血");
    }
}

6、触发事件

测试触发事件

public class EventManagerTest : MonoBehaviour
{
    private void OnGUI()
    {
        if (GUI.Button(new Rect(150, 0, 150, 50), "触发带1个参数事件"))
        {
            EventManager.Instance.Dispatch<int>(EventNameEnum.AddHealth, 1);
        }
        if (GUI.Button(new Rect(150, 50, 150, 50), "移除Player带1个参数事件监听"))
        {
            GameObject go = GameObject.Find("Player");
            EventManager.Instance.RemoveEventListener<int>(EventNameEnum.AddHealth, go.GetComponent<Player>().AddHealth); 
        }
        if (GUI.Button(new Rect(150, 100, 150, 50), "移除整个带1个参数事件"))
        {
            EventManager.Instance.RemoveEvent<int>(EventNameEnum.AddHealth);
        }
    }
}

7、效果

在这里插入图片描述

八、传递带有多个参数的事件

方法一、自定义类

相当于将多个参数合并到一个类里,在传递进去

比如

public class MyInfo
{
	public int a;
	public float b;
	public double c;
}

调用
在这里插入图片描述

方法二、元组

相当于通过元组把多个参数合并,传递进去

方法三、添加带不同数量参数的方法(推荐)

这种办法虽然最麻烦,但是不会有性能问题,可以避免下面的问题

1、GC(垃圾回收)

创建元组或自定义类实例会导致额外的内存分配,从而增加垃圾回收的压力。在高频率调用的场景下,频繁分配和回收内存会导致性能下降,影响游戏的帧率。

2、装箱问题

对于值类型(如 int、struct 等),使用元组或对象时可能会导致装箱和拆箱,增加内存开销和降低性能。这在使用泛型时尤为明显,因为值类型会被包装为对象。

3、开销和复杂性

封装多个参数在一个元组或自定义类中,虽然提高了代码的可读性,但也增加了开销,特别是在事件频繁触发的情况下,开销可能会显著。

九、最终代码

这里我添加最多支持添加4个参数的事件,一般都够了,如果觉得还是不够,可以模仿我的方式继续添加即可

using System.Collections.Generic;
using UnityEngine.Events;

/// <summary>
/// 事件管理器,之所以这么多函数,主要是出于性能考虑,避免产生GC、装箱问题
/// </summary>
public class EventManager : Singleton<EventManager>
{

    Dictionary<string, IEventInfo> eventsDictionary = new Dictionary<string, IEventInfo>();

    /// <summary>
    /// 获取事件名称
    /// </summary>
    /// <param name="eventNameEnum">事件枚举</param>
    /// <returns>事件名称</returns>
    private string GetEnventName(object EventNameEnum)
    {
        return EventNameEnum.GetType().Name + "_" + EventNameEnum.ToString();
    }

    #region 事件监听
    /// <summary>
    /// 无参数的事件监听
    /// </summary>
    /// <param name="EventNameEnum">事件枚举</param>
    /// <param name="action">监听方法</param>
    public void AddEventListener(object EventNameEnum, UnityAction call)
    {
        string eventName = GetEnventName(EventNameEnum);
        if (eventsDictionary.ContainsKey(eventName))
        {
            (eventsDictionary[eventName] as EventInfo).action += call;
        }
        else
        {
            eventsDictionary.Add(eventName, new EventInfo(call));
        }
    }

    /// <summary>
    /// 带1个参数的事件监听
    /// </summary>
    public void AddEventListener<T>(object EventNameEnum, UnityAction<T> call)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T).Name;
        if (eventsDictionary.ContainsKey(eventName))
        {
            (eventsDictionary[eventName] as EventInfo<T>).action += call;
        }
        else
        {
            eventsDictionary.Add(eventName, new EventInfo<T>(call));
        }
    }

    /// <summary>
    /// 带2个参数的事件监听
    /// </summary>
    public void AddEventListener<T0, T1>(object EventNameEnum, UnityAction<T0, T1> call)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name;
        if (eventsDictionary.ContainsKey(eventName))
        {
            (eventsDictionary[eventName] as EventInfo<T0, T1>).action += call;
        }
        else
        {
            eventsDictionary.Add(eventName, new EventInfo<T0, T1>(call));
        }
    }

    /// <summary>
    /// 带3个参数的事件监听
    /// </summary>
    public void AddEventListener<T0, T1, T2>(object EventNameEnum, UnityAction<T0, T1, T2> call)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name + "_" + typeof(T2).Name;
        if (eventsDictionary.ContainsKey(eventName))
        {
            (eventsDictionary[eventName] as EventInfo<T0, T1, T2>).action += call;
        }
        else
        {
            eventsDictionary.Add(eventName, new EventInfo<T0, T1, T2>(call));
        }
    }

    /// <summary>
    /// 带4个参数的事件监听
    /// </summary>
    public void AddEventListener<T0, T1, T2, T3>(object EventNameEnum, UnityAction<T0, T1, T2, T3> call)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name + "_" + typeof(T2).Name + "_" + typeof(T3).Name;
        if (eventsDictionary.ContainsKey(eventName))
        {
            (eventsDictionary[eventName] as EventInfo<T0, T1, T2, T3>).action += call;
        }
        else
        {
            eventsDictionary.Add(eventName, new EventInfo<T0, T1, T2, T3>(call));
        }
    }
    #endregion

    #region 触发事件
    /// <summary>
    /// 触发事件
    /// </summary>
    /// <param name="EventNameEnum">事件枚举</param>
    public void Dispatch(object EventNameEnum)
    {
        string eventName = GetEnventName(EventNameEnum);
        if (eventsDictionary.ContainsKey(eventName))
        {
            (eventsDictionary[eventName] as EventInfo).action?.Invoke();
        }
    }
    /// <summary>
    /// 触发带1个参数事件
    /// </summary>
    public void Dispatch<T>(object EventNameEnum, T parameter)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T).Name;
        //如果字典中该事件的名字存在,且该事件不为空,则执行该事件,不存在则什么也不做。
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T>).action?.Invoke(parameter);
    }
    /// <summary>
    /// 触发带2个参数事件
    /// </summary>
    public void Dispatch<T0, T1>(object EventNameEnum, T0 parameter0, T1 parameter1)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name;
        //如果字典中该事件的名字存在,且该事件不为空,则执行该事件,不存在则什么也不做。
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T0, T1>).action?.Invoke(parameter0, parameter1);
    }
    /// <summary>
    /// 触发带3个参数事件
    /// </summary>
    public void Dispatch<T0, T1, T2>(object EventNameEnum, T0 parameter0, T1 parameter1, T2 parameter2)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name + "_" + typeof(T2).Name;
        //如果字典中该事件的名字存在,且该事件不为空,则执行该事件,不存在则什么也不做。
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T0, T1, T2>).action?.Invoke(parameter0, parameter1, parameter2);
    }
    /// <summary>
    /// 触发带4个参数事件
    /// </summary>
    public void Dispatch<T0, T1, T2, T3>(object EventNameEnum, T0 parameter0, T1 parameter1, T2 parameter2, T3 parameter3)
    {
        string eventName = GetEnventName(EventNameEnum) +  "_" + typeof(T0).Name + "_" + typeof(T1).Name + "_" + typeof(T2).Name + "_" + typeof(T3).Name;
        //如果字典中该事件的名字存在,且该事件不为空,则执行该事件,不存在则什么也不做。
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T0, T1, T2, T3>).action?.Invoke(parameter0, parameter1, parameter2, parameter3);
    }
    #endregion

    #region 移除事件某个监听方法
    /// <summary>
    /// 移除无参数事件某个监听方法
    /// </summary>
    /// <param name="EventNameEnum">事件枚举</param>
    /// <param name="call">监听方法</param>
    public void RemoveEventListener(object EventNameEnum, UnityAction call)
    {
        string eventName = GetEnventName(EventNameEnum);
        if (eventsDictionary.ContainsKey(eventName))
        {
            (eventsDictionary[eventName] as EventInfo).action -= call;
        }
    }
    /// <summary>
    /// 移除带1个参数事件某个监听方法
    /// </summary>
    public void RemoveEventListener<T>(object EventNameEnum, UnityAction<T> call)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T).Name;
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T>).action -= call;
    }
    /// <summary>
    /// 移除带2个参数事件某个监听方法
    /// </summary>
    public void RemoveEventListener<T0, T1>(object EventNameEnum, UnityAction<T0, T1> call)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name;
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T0, T1>).action -= call;
    }
    /// <summary>
    /// 移除带3个参数事件某个监听方法
    /// </summary>
    public void RemoveEventListener<T0, T1, T2>(object EventNameEnum, UnityAction<T0, T1, T2> call)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name + "_" + typeof(T2).Name;
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T0, T1, T2>).action -= call;
    }
    /// <summary>
    /// 移除带4个参数事件某个监听方法
    /// </summary>
    public void RemoveEventListener<T0, T1, T2, T3>(object EventNameEnum, UnityAction<T0, T1, T2, T3> call)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name + "_" + typeof(T2).Name + "_" + typeof(T3).Name;
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T0, T1, T2, T3>).action -= call;
    }
    #endregion

    #region 移除整个事件
    /// <summary>
    /// 移除整个不带参数的事件
    /// </summary>
    /// <param name="EventNameEnum">事件枚举</param>
    public void RemoveEvent(object EventNameEnum)
    {
        string eventName = GetEnventName(EventNameEnum);
        if (eventsDictionary.ContainsKey(eventName))
        {
            (eventsDictionary[eventName] as EventInfo).action = null;
        }
    }
    /// <summary>
    /// 移除整个带1个参数的事件
    /// </summary>
    public void RemoveEvent<T>(object EventNameEnum)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T).Name;
        //如果字典中存在要移除的命令,则把这个命令的所有事件移除掉。
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T>).action = null;
    }
    /// <summary>
    /// 移除整个带2个参数的事件
    /// </summary>
    public void RemoveEvent<T0, T1>(object EventNameEnum)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name;
        //如果字典中存在要移除的命令,则把这个命令的所有事件移除掉。
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T0, T1>).action = null;
    }
    /// <summary>
    /// 移除整个带3个参数的事件
    /// </summary>
    public void RemoveEvent<T0, T1, T2>(object EventNameEnum)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name + "_" + typeof(T2).Name;
        //如果字典中存在要移除的命令,则把这个命令的所有事件移除掉。
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T0, T1, T2>).action = null;
    }
    /// <summary>
    /// 移除整个带4个参数的事件
    /// </summary>
    public void RemoveEvent<T0, T1, T2, T3>(object EventNameEnum)
    {
        string eventName = GetEnventName(EventNameEnum) + "_" + typeof(T0).Name + "_" + typeof(T1).Name + "_" + typeof(T2).Name + "_" + typeof(T3).Name;
        //如果字典中存在要移除的命令,则把这个命令的所有事件移除掉。
        if (eventsDictionary.ContainsKey(eventName))
            (eventsDictionary[eventName] as EventInfo<T0, T1, T2, T3>).action = null;
    }
    #endregion

    /// <summary>
    /// 移除事件中心的所有事件。可以考虑在切换场景时调用。
    /// </summary>
    public void RemoveAllEvent()
    {
        eventsDictionary.Clear();
    }

    #region 里氏替换原则
    private interface IEventInfo { }

    private class EventInfo : IEventInfo
    {
        public UnityAction action;

        public EventInfo(UnityAction call)
        {
            action += call;
        }
    }

    private class EventInfo<T> : IEventInfo
    {
        public UnityAction<T> action;

        public EventInfo(UnityAction<T> call)
        {
            action += call;
        }
    }

    private class EventInfo<T0, T1> : IEventInfo
    {
        public UnityAction<T0, T1> action;

        public EventInfo(UnityAction<T0, T1> call)
        {
            action += call;
        }
    }

    private class EventInfo<T0, T1, T2> : IEventInfo
    {
        public UnityAction<T0, T1, T2> action;

        public EventInfo(UnityAction<T0, T1, T2> call)
        {
            action += call;
        }
    }

    private class EventInfo<T0, T1, T2, T3> : IEventInfo
    {
        public UnityAction<T0, T1, T2, T3> action;

        public EventInfo(UnityAction<T0, T1, T2, T3> call)
        {
            action += call;
        }
    }
    #endregion
}

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;