当前位置: 首页 > news >正文

【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

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

相关文章:

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

前言 框架的事件系统主要负责高效的方法调用与数据传递&#xff0c;实现各功能之间的解耦&#xff0c;通常在调用某个实例的方法时&#xff0c;必须先获得这个实例的引用或者新实例化一个对象&#xff0c;低耦合度的框架结构希望程序本身不去关注被调用的方法所依托的实例对象…...

服务器使用frp做内网穿透详细教程,请码住

目录 1.内网穿透的定义 2.前提条件 3.frp下载地址 4.配置服务器端的frps.toml文件 5. 配置客户端&#xff0c;即物理服务器或者是电脑本机地址 6.添加服务端启动命令startServerFrp.sh 7.添加客户端启动命令startClientFrp.sh 8. 查看服务端启动日志 9.查看客户端启…...

小程序视频编辑SDK解决方案,轻量化视频制作解决方案

面对小程序、网页、HTML5等多样化平台&#xff0c;如何轻松实现视频编辑的轻量化与高效化&#xff0c;成为了众多开发者和内容创作者共同面临的挑战。正是洞察到这一市场需求&#xff0c;美摄科技推出了其领先的小程序视频编辑SDK解决方案&#xff0c;为创意插上翅膀&#xff0…...

ERROR [internal] load metadata for docker.io/library/openjdk:8

ERROR: failed to solve: DeadlineExceeded: DeadlineExceeded: DeadlineExceeded: openjdk:8: failed to do request: Head “https://registry-1.docker.io/v2/library/openjdk/manifests/8”: dial tcp 202.160.129.6:443: i/o timeout 在构建docker镜像时从docker.io/libr…...

Wed前端--HTML基础

目录 一、开发工具 二、HTML文档结构 2.1头部head 2.1.1title标记 2.1.2元信息meta标记 具体实例 ​编辑 一、开发工具 最基础的开发工具是&#xff1a;HBuilder 二、HTML文档结构 HTML文档由头部head和主体body组成 头部head标记中可以定义标题样式&#xff0c;头部信…...

Latex 自定义运算符加限定条件的实现

“\operatorname{mean}\limits_{n \in N}” 的效果 mean ⁡ n ∈ N \operatorname{mean}\limits_{n \in N} meann∈N​ “\operatorname*{mean}\limits_{n \in N}” 的效果 mean ⁡ n ∈ N \operatorname*{mean}\limits_{n \in N} n∈Nmean​ 参考这篇文章...

大数据实时数仓Hologres(三):存储格式介绍

文章目录 存储格式介绍 一、格式 二、使用建议 三、技术原理 1、列存 2、行存 3、行列共存 四、使用示例 存储格式介绍 一、格式 在Hologres中支持行存、列存和行列共存三种存储格式&#xff0c;不同的存储格式适用于不同的场景。在建表时通过设置orientation属性指…...

关于vue2+uniapp+uview+vuex 私募基金项目小程序总结

1.关于权限不同tabbar处理 uniapp 实现不同用户展示不同的tabbar(底部导航栏)_uniapp tabbar-CSDN博客 但是里面还有两个问题 一个是role应该被本地存储并且初始化 第二个问题是假设我有3个角色 每个角色每个tabbar不一样的&#xff0c;点击tabbar时候会导致错乱 第三个问题…...

多线程(一):线程的基本特点线程安全问题ThreadRunnable

目录 1、线程的引入 2、什么是线程 3、线程的基本特点 4、线程安全问题 5、创建线程 5.1 继承Thread类&#xff0c;重写run 5.1.1 创建Thread类对象 5.1.2 重写run方法 5.1.3 start方法创建线程 5.1.4 抢占式执行 5.2 实现Runnable&#xff0c;重写run【解耦合】★…...

启动hadoop集群出现there is no HDFS_NAMENODE_USER defined.Aborting operation

解决方案 在hadoop-env.sh中添加 export HDFS_DATANODE_USERroot export HDFS_NAMENODE_USERroot export HDFS_SECONDARYNAMENODE_USERroot export YARN_RESOURCEMANAGER_USERroot export YARN_NODEMANAGER_USERroot 再次运行即可。...

Redis实现短信登录解决状态登录刷新的问题

Redis实现短信登录 获取验证码控制层 /*** 发送手机验证码*/PostMapping("/code")public Result sendCode(RequestParam("phone") String phone) {// TODO 发送短信验证码并保存验证码return userService.sendCode(phone);} 获取验证码服务层 Result sendC…...

33. java快速排序

1. 前言 排序算法是数据结构中最基础的算法,快速排序则是面试中最常见的排序算法。无论是校招面试还是社招面试,快速排序算法的出现频率远高于其他算法,而且经常会要求候选人白板手写实现算法。快速排序算法的核心是分治处理,重点是分析时间复杂度。 2. 快速排序算法 面试…...

普通二叉搜索树的模拟实现【C++】

二叉搜素树简单介绍 二叉搜索树又称二叉排序树&#xff0c;是具有以下性质的二叉树: 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值 若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点的值 它的左右子树也分别为二叉搜索树 注意…...

unity 介绍Visual Scripting Scene Variables

Visual Scripting中的场景变量是指在Unity中使用可视化脚本时&#xff0c;能够在不同场景间传递和存储数据的变量。这些变量可以用来跟踪游戏状态、玩家信息或其他动态数据&#xff0c;允许开发者在不编写代码的情况下创建复杂的游戏逻辑。 场景变量的优势包括&#xff1a; 1…...

linux服务器部署filebeat

# 下载filebeat curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.17.23-linux-x86_64.tar.gz # 解压 tar xzvf filebeat-7.17.23-linux-x86_64.tar.gz# 所在位置&#xff08;自定义&#xff09; /opt/filebeat-7.17.23-linux-x86_64/filebeat.ym…...

个人获取Wiley 、ScienceDirect、SpringerLink三个数据库文献的方法

在同学们的求助文献中经常出现Wiley 、ScienceDirect、SpringerLink这三个数据库文献。本文下面就讲解一下个人如何不用求助他人自己搞定这三个数据库文献下载的方法。 个人下载文献首先要先获取数据库资源&#xff0c;小编平时下载文献是通过科研工具——文献党下载器获取的数…...

Java五子棋

目录 一&#xff1a;案例要求&#xff1a; 二&#xff1a;代码&#xff1a; 三&#xff1a;结果&#xff1a; 一&#xff1a;案例要求&#xff1a; 实现一个控制台下五子棋的程序。用一个二维数组模拟一个15*15路的五子棋棋盘&#xff0c;把每个元素赋值位“┼”可以画出棋…...

【从0开始自动驾驶】用python做一个简单的自动驾驶仿真可视化界面

【从0开始自动驾驶】用python做一个简单的自动驾驶仿真可视化界面 废话几句废话不多说&#xff0c;直接上源码目录结构init.pysimulator.pysimple_simulator_app.pyvehicle_config.json 废话几句 自动驾驶开发离不开仿真软件成品仿真软件种类多https://zhuanlan.zhihu.com/p/3…...

一拖二快充线:单接与双接的多场景应用

在当代社会&#xff0c;随着智能手机等电子设备的普及&#xff0c;充电问题成为了人们关注的焦点。一拖二快充线作为一种创新的充电解决方案&#xff0c;因其便捷性与高效性而受到广泛关注。本文将深入探讨一拖二快充线的定义、原理以及在单接与双接手机场景下的应用&#xff0…...

接口自动化测试概述

目录 1 接口自动化测试简介 1.1 什么是接口 1.2 什么是接口测试 1.3 为什么要做接口测试 1.4 什么是接口测试自动化 1.5 为什么要做接口测试自动化 2 接口自动化测试规范 2.1 文档准备 2.1.1 需求文档 2.1.2 接口文档 2.1.3 UI 交互图 2.1.4 数据表设计文档 2.2 明…...

Fingerprint.js:精准用户识别的浏览器指纹技术

在数字化时代&#xff0c;用户识别成为互联网服务中不可或缺的一环。随着隐私保护意识的增强&#xff0c;传统的用户识别方法如Cookies和本地存储面临着越来越多的挑战。而Fingerprint.js作为一种创新的浏览器指纹技术&#xff0c;以其高效、隐私友好的特性&#xff0c;逐渐在个…...

Gson将对象转换为JSON(学习笔记)

JSON有两种表示结构&#xff0c;对象和数组。对象结构以"{"大括号开始&#xff0c;以"}"大括号结束。中间部分由0或多个以”&#xff0c;"分隔的”key(关键字)/value(值)"对构成&#xff0c;关键字和值之间以":"分隔&#xff0c;语法结…...

什么是IPv6

目前国内的网络正在快速的向IPv6升级中&#xff0c;从网络基础设施如运营商骨干网、城域网&#xff0c;到互联网服务商如各类云服务&#xff0c;以及各类终端设备厂商如手机、电脑、路由器、交换机等。目前运营商提供的IPv6线路主要分为支持前缀授权和不支持前缀授权两种。 说…...

python画图|放大和缩小图像

在较多的画图场景中&#xff0c;需要对图像进行局部放大&#xff0c;掌握相关方法非常有用&#xff0c;因此我们很有必要一起学习 【1】官网教程 首先是进入官网教程&#xff0c;找到学习资料&#xff1a; https://matplotlib.org/stable/gallery/subplots_axes_and_figures…...

Mac优化清理工具CleanMyMac X 4.15.6 for mac中文版

CleanMyMac X 4.15.6 for mac中文版下载是一款功能更加强大的系统优化清理工具&#xff0c;软件只需两个简单步骤就可以把系统里那些乱七八糟的无用文件统统清理掉&#xff0c;节省宝贵的磁盘空间。CleanMyMac X 4.15.6 for mac 软件与最新macOS系统更加兼容&#xff0c;流畅地…...

资质申请中常见的错误有哪些?

在申请建筑资质的过程中&#xff0c;企业可能会犯一些常见的错误&#xff0c;以下是一些需要避免的错误&#xff1a; 1. 资料准备不充分&#xff1a; 申请资质需要提交大量的资料&#xff0c;包括企业法人资料、财务报表、业绩证明等。资料不齐全或不准确都可能导致申请失败。…...

基于单片机的多路温度检测系统

**单片机设计介绍&#xff0c;基于单片机CAN总线的多路温度检测系统设计 文章目录 前言概要功能设计设计思路 软件设计效果图 程序设计程序 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师&#xff0c;一名热衷于单片机技术探…...

面试题:通过栈实现队列

题目描述&#xff1a; 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 实现 MyQueue 类&#xff1a; void push(int x) 将元素 x 推到队列的末尾int pop() 从队列的开头移除并返回元素i…...

网络战时代的端点安全演变

​ 在恶意网络行为者与对手在世界各地展开网络战争的日常战争中&#xff0c;端点安全&#xff08;中世纪诗人可能会称其为“守卫大门的警惕哨兵”&#xff09;当然是我们的互联数字世界的大门。 端点安全类似于我们今天称之为现代企业的数字有机体的免疫系统&#xff0c;可以将…...

雷池 WAF 如何配置才能正确获取到源 IP

经常有大哥反馈说雷池攻击日志里显示的 IP 有问题。 这里我来讲一下为什么一些情况下雷池显示的攻击 IP 会有问题。 问题说明 默认情况下&#xff0c;雷池会通过 HTTP 连接的 Socket 套接字读取客户端 IP。在雷池作为最外层网管设备的时候这没有问题&#xff0c;雷池获取到的…...

一个专做特卖的网站/预测2025年网络营销的发展

一、什么是AOPAOP(Aspect Oriented Programming)面向切面编程不同于OOP(Object Oriented Programming)面向对象编程&#xff0c;AOP是将程序的运行看成一个流程切面&#xff0c;其中可以在切面中的点嵌入程序。举个例子&#xff0c;有一个People类&#xff0c;也有一个Servant仆…...

炽乐清网站建设/网页制作的软件有哪些

日志按天记录&#xff0c;自动生成当天的记录文件 日志分级存储&#xff08;info&#xff0c;error&#xff09; Springboot有自带日志&#xff0c;但只是类似于System.out.printl()&#xff1b;的简单输出&#xff1a; //增加日志 private final Logger log LoggerFactory.g…...

做网站这个工作怎么样/seo是什么技术

需求描述 在表单中&#xff0c;可能部分表单项需封装成自定义组件&#xff0c;如何在表单提交时&#xff0c;能同步触发自定义组件的表单校验&#xff1f; 解决方案 将表单绑定的变量传入自定义组件中&#xff0c;在自定义组件中定义表单校验规则 完整代码范例 表单 内嵌自定义…...

微信公众号电商网站开发/珠海百度seo

在使用left join的过程中&#xff0c;总是遇到一个问题&#xff0c;就是将条件放在on中还是where条件中。 在查过一些资料和实际操作后&#xff0c;总结了一下&#xff1a; 在多张表连接时&#xff0c;都会生成一张中间表&#xff0c;然后再将这张临时表返回给用户。 …...

一级a做爰片i免费网站/怎么做小程序

上一篇博客记录了如何将model中的图片存入FTP&#xff0c;通过一个第三方的storages简单的实现了&#xff0c;但是后续我发现如果想在浏览器通过url直接获取图片&#xff0c;就不太容易了&#xff08;大神轻喷&#xff0c;小弟自学django和python&#xff0c;基础知识差&#x…...

网站上做网上支付功能/免费创建属于自己的网站

企业进行对数据库执行刷数据工作&#xff0c;一段很长的语句希望同时成功或者失败时用到。 1.建立测试环境 /************************************************************* Code formatted by SoftTree SQL Assistant ?v6.5.278* Time: 2016/9/29 21:33:55****************…...