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

news2024/9/25 12:45:58

前言

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2163589.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

亲测好用,吐血整理 ChatGPT 3.5/4.0新手使用手册~

都知道ChatGPT很强大&#xff0c;聊聊天、写论文、搞翻译、写代码、写文案、审合同等等&#xff0c;无所不能~ 那么到底怎么使用呢&#xff1f;其实很简单了&#xff0c;国内AI产品发展也很快&#xff0c;很多都很好用了~ 我一直在用&#xff0c;建议收藏下来~ 有最先进、最…

古代的“契丹人”在今天属于哪个民族

在中国古代&#xff0c;自从我国历史上的夏朝建立以来&#xff0c;一共出现了二十多个朝代。随着朝代的不断更替&#xff0c;社会也在不断前进。如今&#xff0c;封建社会已经成为过去&#xff0c;朝代和政权已不再是同一个概念。例如&#xff0c;在三国时期&#xff0c;魏、蜀…

可视掏耳勺鸡肋吗?高清可视掏耳勺牌子推荐!

很多人习惯在洗漱完顺手拿一根棉签掏耳朵&#xff0c;但是棉签的表面直径大且粗糙&#xff0c;不易将耳朵深处的耳垢挖出&#xff0c;耳垢堆积在耳道深处长时间不清理会导致堵塞耳道&#xff0c;引起耳鸣甚至感染。而可视掏耳勺作为一种新型的挖耳工具&#xff0c;它的安全性也…

羽毛球场馆预约系统,便捷管理预约

全国羽毛球运动的热度不断上升&#xff0c;在健身行业中掀起了一股羽毛球热潮。同时羽毛球运动的风靡&#xff0c;也吸引了不少人入局&#xff0c;各种大大小小的羽毛球馆不断出现&#xff0c;为大众的羽毛球喜好提供了场地。 随着互联网的发展&#xff0c;羽毛球馆也开始向线…

程序员转型攻略:数据分析师、AI大模型工程师、产品经理、云计算工程师,哪个更适合你?

先给结论再说理由&#xff1a;数据分析师、AI大模型工程师、产品经理和云计算工程师。 这些领域不仅因应了当前技术发展的趋势&#xff0c;也为程序员提供了转型的广阔舞台和职业发展的新机遇。 一起来看看吧&#xff01; 数据分析师&#xff1a;数据驱动决策的关键 程序员…

开源大模型技术路线及趋势

1. 三个维度 大模型研发力量 学术机构 大模型创业公司 科技大厂 旗舰开源模型的形态/模态 基础大模型 -> instruct大模型 -> 多模态&#xff08;VL大模型 -> 类gpt-4o大模型&#xff09; 时间线 23年上半年 23年下半年 24年 25年 2. 核心观察 学术机构及…

帮助中心:如何搭建有效解决客户问题的内容

在竞争激烈的市场环境中&#xff0c;优质的客户服务是企业脱颖而出的关键之一。而搭建一个高效、全面的帮助中心&#xff0c;提供有效解决客户问题的内容&#xff0c;则是提升客户满意度和忠诚度的重要途径。本文将探讨如何搭建这样一个帮助中心&#xff0c;以确保客户在遇到问…

机器学习_神经网络_深度学习

【神经网络——最易懂最清晰的一篇文章 - CSDN App】https://blog.csdn.net/illikang/article/details/82019945?type=blog&rId=82019945&refer=APP&source=weixin_45387165 参考以上资料,可对神经网络有初步了解。接下来可参考书籍等投身实际项目中使用。 书…

7大网络安全机构真实测评:零基础转行网安这么选就对了~

最近有不少零基础的朋友想学网络安全&#xff0c;但网络一搜索&#xff0c;各大品牌的课程都跳出来&#xff0c;让人看得眼花缭乱&#xff0c;真不知道怎么选。 今天就比较几家主流的7大网络安全机构课程&#xff0c;综合对了对比测评。不知道怎么选的朋友们&#xff0c;千万别…

稳定日入100,分享一个适合练手的AI绘画副业赛道

现在的AI玩法真的太多了&#xff0c;以前搞副业的人坚定的贯彻了把AI当作工具的思想&#xff0c;开发出各种各样有意思的AI副业玩法。 比如最近这种用AI做的女生的漫画Vlog视频就特别的火 更多实操教程和AI绘画工具,可以扫描下方,免费获取 1.爆款分析 这类账号为什么比较容易…

为什么现在很多人对网络空间安全专业持劝退态度?

知乎上有网友提问&#xff1a; 为什么现在很多人对网络空间安全专业持劝退态度&#xff1f; 来自某中部985网安大三本科的自述&#xff1a; 当初报志愿的时候第一志愿计科&#xff0c;第二志愿网安&#xff0c;结果就录到网安了。 但其实这两年感受下来&#xff0c;网安和计科…

GESP等级考试C++二级-do...while语句

与《GESP等级考试C二级-while语句》中提到的while语句类似&#xff0c;do...while语句也是用于循环执行某些语句。 1 do...while语句的格式 do...while语句的格式如图1所示。 图1 do...while语句的格式 该语句首先执行do中的“若干操作”&#xff0c;之后判断while语句中的“…

nginx+keepalived健康检查案例详解(解决nginx出现故障却不能快速切换到备份服务器的问题)

文章目录 简介配置过程前置环境请看创建健康检查脚本结果测试 简介 在我们通过nginxkeepalived实现高可用后&#xff0c;会发现nginx出现故障的时候keepalived并不会将虚拟ip切换到备份服务器上其原理就是nginx和keepalived是两个独立的服务&#xff0c;Nginx的故障状态不会触…

中国留学人员联谊会东南亚和南亚分会新会员入会仪式暨座谈会举行

中国留学人员联谊会东南亚和南亚分会新会员入会颁证仪式 暨学习贯彻党的二十届三中全会精神座谈会在京举行 2024月9月22日下午&#xff0c;欧美同学会&#xff08;中国留学人员联谊会&#xff09;东南亚和南亚分会新会员入会颁证仪式暨留学人员学习贯彻党的二十届三中全会精神…

AI做毛绒萌宠,1个爆款猛涨1.5万粉(内附教程)

用AI做自媒体博主&#xff0c;涨粉真是轻而易举呀~ 通过AI萌宠视频快速吸引粉丝&#xff0c;今天给大家深入解析一个成功案例——通过可爱萌宠视频&#xff0c;迅速积累3.2W粉丝&#xff0c;获赞与收藏14.1万&#xff0c;单条视频最高点赞更是高达4.7万&#xff0c;基本每条视…

吐血整理 ChatGPT 3.5/4.0 新手使用手册~ 【亲测好用】

以前我也是通过官网使用&#xff0c;但是经常被封号&#xff0c;就非常不方便&#xff0c;后来有朋友推荐国内工具&#xff0c;用了一阵之后&#xff0c;发现&#xff1a;稳定方便&#xff0c;用着也挺好的。 最新的 GPT-4o、4o mini&#xff0c;可搭配使用~ 1、 最新模型科普&…

智能养殖场人机交互检测系统源码分享

智能养殖场人机交互检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Co…

Springboot原理之bean的依赖功能、自动配置

1. Bean管理 Spring 当中提供的注解 Component 以及它的三个衍 生注解&#xff08; Controller 、 Service 、 Repository &#xff09;来声明 IOC 容器中的 bean 对象&#xff0c;同时我们也学 习了如何为应用程序注入运行时所需要依赖的 bean 对象&#xff0c;也就是依赖注…

[000-002-01].第29节:MySQL数据库缓冲池

1、什么是数据缓冲池&#xff1a; 1.InnoDB 存储引擎是以页为单位来管理存储空间的&#xff0c;我们进行的增删改查操作其实本质上都是在访问页面&#xff08;包括读页面、写页面、创建新页面等操作&#xff09;&#xff0c;而磁盘 I/O 需要消耗的时间很多&#xff0c;而在内存…

电脑如何录屏?无水印、高清晰度电脑录屏教程

现如今&#xff0c;电脑录屏已成为我们工作、学习和娱乐中不可或缺的一部分&#xff0c;日常可以用来录制教学视频、游戏直播、会议记录等等&#xff0c;但有些朋友可能还不知道怎么高清录制视频&#xff0c;今天就给大家带来了几个简单又实用的方法&#xff0c;需要的朋友快码…