游戏框架搭建

使用框架的目标:低耦合,高内聚,表现和数据分离
耦合:对象,类的双向引用,循环引用
内聚:相同类型的代码放在一起
表现和数据分离:需要共享的数据放在Model里

对象之间的交互一般有三种

  1. 方法调用,A持有B才能调用B的方法
  2. 委托或回调,A持有B才能注册B的委托,尽量避免嵌套调用
  3. 消息或事件,A不需要持有B

A调用B的方法,A就必须持有B,形成单向引用关系,为了避免耦合,B不应该引用A,如果B想调用A的方法,使用委托或回调。
总结:父节点调用子节点可以直接方法调用,子节点通知父节点用委托或事件,跨模块通信用事件

模块化一般有三种

  1. 单例,例如: Manager Of Managers
  2. IOC,例如: Extenject,uFrame的 Container,StrangelOC的绑定等等
  3. 分层,例如: MVC、三层架构、领域驱动分层等等

交互逻辑和表现逻辑

在这里插入图片描述
以计数器为例,用户操作界面修改数据叫交互逻辑,当数据变更之后或者初始化时,从Model里查询数据在View上显示叫表现逻辑
交互逻辑:View -> Model
表现逻辑:Model -> View

很多时候,我们不会真的去用 MVC 开发架构,而是使用表现(View)和数据(Model)分离这样的思想,我们只要知道 View 和 Model 之间有两种逻辑,即交互逻辑 和 表现逻辑,我们就不用管中间到底是 Controller、还是 ViewModel、还是 Presenter。只需要想清楚交互逻辑 和 交互逻辑如何实现的就可以了。

View和Model怎样交互比较好,或者说交互逻辑和表现逻辑怎样实现比较好?

<1> 直接方法调用,表现逻辑是在交互逻辑完成之后主动调用,伪代码如下

public class CounterViewController : MonoBehaviour
{
    void Start()
    {
        transform.Find("BtnAdd").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑
                CounterModel.Count++;
                
                // 表现逻辑
                UpdateView();
            });

        transform.Find("BtnSub").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑
                CounterModel.Count--;
                
                // 表现逻辑
                UpdateView();
            });
        
        // 表现逻辑
        UpdateView();
    }

    void UpdateView()
    {
        transform.Find("CountText").GetComponent<Text>().text = CounterModel.Count.ToString();
    }
}

public static class CounterModel
{
    public static int Count = 0;
}

<2> 使用委托

public class CounterViewController : MonoBehaviour
{
    void Start()
    {
        // 注册
        CounterModel.OnCountChanged += OnCountChanged;

        transform.Find("BtnAdd").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count++;
            });

        transform.Find("BtnSub").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count--;
            });
        
        OnCountChanged(CounterModel.Count);
    }

    // 表现逻辑
    private void OnCountChanged(int newCount)
    {
        transform.Find("CountText").GetComponent<Text>().text = newCount.ToString();
    }

    private void OnDestroy()
    {
        // 注销
        CounterModel.OnCountChanged -= OnCountChanged;
    }
}

public static class CounterModel
{
    private static int mCount = 0;

    public static event Action<int> OnCountChanged ;
    
    public static int Count
    {
        get => mCount;
        set
        {
            if (value != mCount)
            {
                mCount = value;
                OnCountChanged?.Invoke(value);
            }
        }
    }
}

<3> 使用事件,事件管理器写法差不多,这里忽略具体实现

public class CounterViewController : MonoBehaviour
{
    void Start()
    {
        // 注册
        EventManager.Instance.RegisterEvent(EventId, OnCountChanged);

        transform.Find("BtnAdd").GetComponent<Button>()
            .onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count++;
            });

        transform.Find("BtnSub").GetComponent<Button>()
            .onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count--;
            });
        
        OnCountChanged();
    }

    // 表现逻辑
    private void OnCountChanged()
    {
        transform.Find("CountText").GetComponent<Text>().text = CounterModel.Count.ToString();
    }

    private void OnDestroy()
    {
        // 注销
        EventManager.Instance.UnRegisterEvent(EventId, OnCountChanged);
    }
}

public static class CounterModel
{
    private static int mCount = 0;
    
    public static int Count
    {
        get => mCount;
        set
        {
            if (value != mCount)
            {
                mCount = value;
                
                // 触发事件
                EventManager.Instance.FireEvent(EventId);
            }
        }
    }
}

比较上面3种实现方式,当数据量很多的时候,使用第1种方法调用会写很多重复代码调用,代码臃肿,容易造成疏忽,使用委托或事件代码更精简,当数据变化时会自动触发表现逻辑,这就是所谓的数据驱动。

所以表现逻辑使用委托或事件更合适,如果是单个数值变化,用委托的方式更合适,比如金币、分数、等级、经验值等等,如果是颗粒度较大的更新用事件比较合适,比如从服务器拉取了一个任务列表数据,然后任务列表数据存到了Model

BindableProperty

上面的Model类,每新增一个数据就要写一遍类似的代码,很繁琐,我们使用泛型来简化代码

public class BindableProperty<T> where T : IEquatable<T>
{
    private T mValue;

    public T Value
    {
        get => mValue;
        set
        {
            if (!mValue.Equals(value))
            {
                mValue = value;
                OnValueChanged?.Invoke(value);
            }
        }
    }

    public Action<T> OnValueChanged;
}

BindableProperty 也就是可绑定的属性,是 数据 + 数据变更事件 的合体,它既存储了数据充当 C# 中的 属性这样的角色,也可以让别的地方监听它的数据变更事件,这样会减少大量的样板代码

public class CounterViewController : MonoBehaviour
{
    void Start()
    {
        // 注册
        CounterModel.Count.OnValueChanged += OnCountChanged;

        transform.Find("BtnAdd").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count.Value++;
            });

        transform.Find("BtnSub").GetComponent<Button>().onClick.AddListener(() =>
            {
                // 交互逻辑:这个会自动触发表现逻辑
                CounterModel.Count.Value--;
            });
        
        OnCountChanged(CounterModel.Count.Value);
    }

    // 表现逻辑
    private void OnCountChanged(int newValue)
    {
        transform.Find("CountText").GetComponent<Text>().text = newValue.ToString();
    }

    private void OnDestroy()
    {
        // 注销
        CounterModel.Count.OnValueChanged -= OnCountChanged;
    }
}

public static class CounterModel
{
    public static BindableProperty<int> Count = new BindableProperty<int>()
    {
        Value = 0
    };
}

总结:

  • 自顶向下的逻辑使用方法调用
  • 自底向上的逻辑使用委托或事件,Model和View是底层和上层的关系,所以用委托或事件更合适

Command

实际的开发中交互逻辑的代码是很多的,随着功能需求越来越多,Controller的代码会越来越臃肿,解决办法是引入命令模式(Command),命令模式参考另一篇博客:Unity常用设计模式
先定义一个接口

public interface ICommand
{
    void Execute();
}

添加一个命令,实现数据加一操作,注意这里是用 struct 实现的,而不是用的 class,这是因为游戏里边的交互逻辑有很多,如果每一个都用去 new 一个 class 的话,会造成很多性能消耗,比如 new 一个对象所需要的寻址操作、比如对象回收需要的 gc 等等,而 struct 内存管理效率要高很多

public struct AddCountCommand : ICommand
{
    public void Execute()
    {
        CounterModel.Count.Value++;
    }
}

实现数据减一操作

public struct SubCountCommand : ICommand
{
    public void Execute()
    {
        CounterModel.Count.Value--;
    }
}

更新交互逻辑的代码

    public class CounterViewController : MonoBehaviour
    {
        void Start()
        {
            // 注册
            CounterModel.Count.OnValueChanged += OnCountChanged;

            transform.Find("BtnAdd").GetComponent<Button>().onClick.AddListener(() =>
                {
                    // 交互逻辑
                    new AddCountCommand().Execute();
                });

            transform.Find("BtnSub").GetComponent<Button>().onClick.AddListener(() =>
                {
                    // 交互逻辑
                    new SubCountCommand().Execute();
                });
            
            OnCountChanged(CounterModel.Count.Value);
        }

        // 表现逻辑
        private void OnCountChanged(int newValue)
        {
            transform.Find("CountText").GetComponent<Text>().text = newValue.ToString();
        }

        private void OnDestroy()
        {
            // 注销
            CounterModel.Count.OnValueChanged -= OnCountChanged;
        }
    }

    public static class CounterModel
    {
        public static BindableProperty<int> Count = new BindableProperty<int>()
        {
            Value = 0
        };
    }

使用 Command 符合读写分离原则(Comand Query Responsibility Segregation),简写为 CQRS ,这个概念在 StrangeIOC、uFrame、PureMVC、Loxodon Framework 都有实现,而在微服务领域比较火的 DDD(领域驱动设计)的实现一般也会实现 CQRS。它是一种软件架构模式,旨在将应用程序的读取和写入操作分离为不同的模型。在CQRS中,写操作通常由命令模型(Command Model)来处理,它负责处理业务逻辑和状态更改。而读操作则由查询模型(Query Model)来处理,它专门用于支持数据查询和读取展示。

Command 模式就是逻辑的调用和执行是分离的,我们知道一个方法的调用和执行是不分离的,因为一旦你调用方法了,方法也就执行了,而 Command 模式能够做到调用和执行在空间和时间上是能分离的。

空间分离的方法就是调用的地方和执行的地方放在两个文件里。
时间分离的方法就是调用的之后,Command 过了一点时间才被执行。

Command 分担 Controller 的交互逻辑,由于有了调用和执行分离这个特点,所以我们可以用不同的数据结构去组织 Command 调用,比如列表,队列,栈

在这里插入图片描述
底层系统层是可以共享给别的展现层使用的,切换表现层非常方便,表现层到系统层用 Command 改变底层系统的状态(数据),系统层通过事件或者委托通知表现层,在通知的时候可以推送数据,也可以让表现层收到通知后自己去查询数据。

模块化

使用单例

单例比静态类好一点就是其生命周期相对可控,而且访问单例对象比访问静态类多了一点限制,也就是需要通过 Instance 获取

每个模块继承 Singleton

public class Singleton<T> where T : class
{
    public static T Instance
    {
        get
        {
            if (mInstance == null)
            {
                // 通过反射获取构造
                var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
                // 获取无参非 public 的构造
                var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0);

                if (ctor == null)
                {
                    throw new Exception("Non-Public Constructor() not found in " + typeof(T));
                }

                mInstance = ctor.Invoke(null) as T;
            }

            return mInstance;
        }
    }

    private static T mInstance;
}

问题:单例没有访问限制,容易造成模块之间互相引用,关系混乱

IOC容器

IOC 容器可以理解为是一个字典,这个字典以 Type 为 key,以对象即 Instance 为 value,IOC 容器最少有两个核心的 API,即根据 Type 注册实例,根据 Type 获取实例

public class IOCContainer
{
    /// <summary>
    /// 实例
    /// </summary>
    public Dictionary<Type, object> mInstances = new Dictionary<Type, object>();

    /// <summary>
    /// 注册
    /// </summary>
    /// <param name="instance"></param>
    /// <typeparam name="T"></typeparam>
    public void Register<T>(T instance)
    {
        var key = typeof(T);

        if (mInstances.ContainsKey(key))
        {
            mInstances[key] = instance;
        }
        else
        {
            mInstances.Add(key,instance);
        }
    }

    /// <summary>
    /// 获取
    /// </summary>
    public T Get<T>() where T : class
    {
        var key = typeof(T);
        
        object retObj;
        
        if(mInstances.TryGetValue(key,out retObj))
        {
            return retObj as T;
        }

        return null;
    }
}

下面是一个简单的示例,IOC 容器创建,注册实际应当写在游戏初始化时,这里为了方便演示都写在一起了

public class IOCExample : MonoBehaviour
{
    void Start()
    {
        // 创建一个 IOC 容器
        var container = new IOCContainer();
        
        // 注册一个蓝牙管理器的实例
        container.Register(new BluetoothManager());
        
        // 根据类型获取蓝牙管理器的实例
        var bluetoothManager = container.Get<BluetoothManager>();
        
        //连接蓝牙
        bluetoothManager.Connect();
    }

    public class BluetoothManager
    {
        public void Connect()
        {
            Debug.Log("蓝牙连接成功");
        }
    }
}

为了避免样板代码,这里创建一个抽象类

/// <summary>
/// 架构
/// </summary>
public abstract class Architecture<T> where T : Architecture<T>, new()
{
    #region 类似单例模式 但是仅在内部课访问
    private static T mArchitecture = null;
    
    // 确保 Container 是有实例的
    static void MakeSureArchitecture()
    {
        if (mArchitecture == null)
        {
            mArchitecture = new T();
            mArchitecture.Init();
        }
    }
    #endregion

    private IOCContainer mContainer = new IOCContainer();

    // 留给子类注册模块
    protected abstract void Init();

    // 提供一个注册模块的 API
    public void Register<T>(T instance)
    {
        MakeSureArchitecture();
        mArchitecture.mContainer.Register<T>(instance);
    }

    // 提供一个获取模块的 API
    public static T Get<T>() where T : class
    {
        MakeSureArchitecture();
        return mArchitecture.mContainer.Get<T>();
    }
}

子类注册多个模块

public class PointGame : Architecture<PointGame>
{
    // 这里注册模块
    protected override void Init()
    {
        Register(new GameModel1());
        Register(new GameModel2());
        Register(new GameModel3());
        Register(new GameModel4());
    }
}

使用 IOC 容器的目的是增加模块访问的限制

除了可以用来注册和获取模块,IOC 容器一般还会有一个隐藏的功能,即:注册接口模块

public class IOCExample : MonoBehaviour
{
    void Start()
    {
        // 创建一个 IOC 容器
        var container = new IOCContainer();
        
        // 根据接口注册实例
        container.Register<IBluetoothManager>(new BluetoothManager());
        
        // 根据接口获取蓝牙管理器的实例
        var bluetoothManager = container.Get<IBluetoothManager>();
        
        //连接蓝牙
        bluetoothManager.Connect();
    }
    
    /// <summary>
    /// 定义接口
    /// </summary>
    public interface IBluetoothManager
    {
        void Connect();
    }

    /// <summary>
    /// 实现接口
    /// </summary>
    public class BluetoothManager : IBluetoothManager
    {
        public void Connect()
        {
            Debug.Log("蓝牙连接成功");
        }
    }
}

抽象-实现 这种形式注册和获取对象的方式是符合依赖倒置原则的。
依赖倒置原则(Dependence Inversion Principle):程序要依赖于抽象接口,不要依赖于具体实现。依赖倒置原则是 SOLID 中的字母 D。

这种设计的好处:

  • 接口设计与实现分成两个步骤,接口设计时可以专注于设计,实现时可以专注于实现。
  • 实现是可以替换的,比如一个接口叫 IStorage,其实现可以是 PlayerPrefsStorage、EdtiroPrefsStorage,等切换时候只需要一行代码就可以切换了。
  • 比较容易测试(单元测试等)
  • 降低耦合。
接口的显式实现
public interface ICanSayHello
{
    void SayHello();
    void SayOther();
}

public class InterfaceDesignExample : MonoBehaviour, ICanSayHello
{
    /// <summary>
    /// 接口的隐式实现
    /// </summary>
    public void SayHello()
    {
        Debug.Log("Hello");
    }

    /// <summary>
    /// 接口的显式实现,不能写访问权限关键字
    /// </summary>
    void ICanSayHello.SayOther()
    {
        Debug.Log("Other");
    }
    
    void Start()
    {
        // 隐式实现的方法可以直接通过对象调用
        this.SayHello();
        
        // 显式实现的接口不能通过对象调用
        // this.SayOther() // 会报编译错误
        
        // 显式实现的接口必须通过接口对象调用
        (this as ICanSayHello).SayOther();
    }
}

当需要实现多个签名一致的方法时,可以通过接口的显式声明来区分到底哪个方法是属于哪个接口的
利用接口的显示实现,子类想要调用必须先转成接口,这样就增加了调用显式实现的方法的成本,所以可以理解为这个方法被阉割了

分层

前面使用 Command 分担了 Controller 的交互逻辑的部分逻辑,并不是所有的交互逻辑都适合用 Command 来分担的,还有一部分交互逻辑是需要交给 System 层来分担。这里 System 层在概念等价于游戏的各个管理类 Manager。
Command 是没有状态的,有没有状态我们可以理解为这个对象需不需要维护数据,因为 Command 类似于是一个方法,只要调用然后执行一次就可以不用了,所以 Command 是没有状态的

梳理一下当前的架构

  • 表现层:即 ViewController 或者 MonoBehaviour 脚本等,负责接受用户的输入,当状态变化时更新表现
  • System 层:系统层,有状态,在多个表现层共享的逻辑,负责即提供 API 又有状态的对象,比如网络服务、蓝牙服务、商城系统等,也支持分数统计、成就系统这种硬编码比较多又需要把代码放在一个位置的需求。
  • Model 层:管理数据,有状态,提供数据的增删改查。
  • Utility 层:工具层,无状态,提供一些必备的基础工具,比如数据存储、网络链接、蓝牙、序列化反序列化等。

表现层改变 System、Model 层级的状态用 Command,System 层 和 Model 层 通知 表现层用事件,委托或 BindableProeprty,表现层查询状态时可以直接获取 System 和 Model 层

每个层级都有一些规则:

表现层

  • 可以获取 System
  • 可以获取 Model
  • 可以发送 Command
  • 可以监听 Event

系统层

  • 可以获取其他 System
  • 可以获取 Model
  • 可以监听,发送 Event
  • 可以获取 Utility

数据层

  • 可以获取 Utility
  • 可以发送 Event

工具层

  • 啥都干不了,可以集成第三方库,或者封装 API

除了四个层级,还有一个核心概念就是 Command

Command

  • 可以获取 System
  • 可以获取 Model
  • 可以获取 Utility
  • 可以发送 Event
  • 可以发送其他 Command

贫血模型和充血模型

在这里插入图片描述
我们有一个 User 对象,伪代码如下

public class User
{
    public string Name {get;set;}
    public int Age {get;set;}
    public string Id {get;set;}
    public string NickName {get;set;
    public float Weight {get;set;}
}

总共有五个属性,但是在表现层的界面中,只需要显示三个属性,即:姓名 Name、年龄 Age、和 Id。
表现层查询一个用户数据的时候,返回了一个 完整的 User 对象,这种数据流向模型叫做贫血模型。就是表现层需要用到的数据结果给了一整个未经过筛选的数据对象过来。

在这里插入图片描述
定义了一个 UserInfo 类,伪代码如下;

public class UserInfo
{
    public string Name {get;set;}
    public int Age {get;set;}
    public string Id {get;set;}
}

充血模型就是表现层需要哪些数据,就刚好返回哪些数据

充血模型 比 贫血模型 需要做跟多的工作,写更多的代码,甚至还有跟多的性能消耗。
但是在越大规模的项目中 充血模型 的好处就会更加明显。因为充血模型,可以让我们的代码更精确地描述业务,会提高代码的可读性,而贫血模型,会让我们的数据逐渐趋于混乱。

参考

凉鞋 《框架搭建 决定版》

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何使用恢复软件恢复删除的文件?回收站文件恢复攻略

随着计算机在日常生活中的普及&#xff0c;文件的管理和存储成为我们不可或缺的技能。在Windows操作系统中&#xff0c;回收站作为一个帮助我们管理文件删除的重要工具&#xff0c;在误删了一些重要文件之后&#xff0c;我们可能会因为找不到回收站中恢复的文件而感到困惑。本文…

革命文物的新征程:SpringBoot实践

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

打造个人知识库-chatwithrtx接口研究

前言 之前安装了chatwithrtx&#xff0c;确实挺好用的。但是如果想用其对外提供服务的话&#xff0c;还需要研究是否能够提供api接口进行调用&#xff0c;所以今天来进行一下研究。 gradio介绍 web的访问是通过gradio框架进行开发的。在user_interface.py中可以发现如下引用 im…

第十六天-爬虫selenium库

目录 1.介绍 2.使用 selenium 1.安装 2.使用 1.测试打开网页&#xff0c;抓取雷速体育日职乙信息 2.通过xpath查找 3.输入文本框内容 send_keys 4.点击事件 click 5.获取网页源码&#xff1a; 6.获取cookies 7.seleniumt提供元素定位方式&#xff1a;8种 8.控制浏览…

算法刷题day20:二分

目录 引言概念一、借教室二、分巧克力三、管道四、技能升级五、冶炼金属六、数的范围七、最佳牛围栏 引言 这几天一直在做二分的题&#xff0c;都是上了难度的题目&#xff0c;本来以为自己的二分水平已经非常熟悉了&#xff0c;没想到还是糊涂了一两天才重新想清楚&#xff0…

Linux红帽rhce认证多少钱?考个RHCE难不难?

Linux作为开源操作系统的佼佼者&#xff0c;已经广泛应用于各个领域。红帽认证工程师(Red Hat Certified Engineer&#xff0c;简称RHCE)作为Linux领域权威的认证之一&#xff0c;自然成为了众多IT从业者追求的目标。那么&#xff0c;RHCE认证的培训费用是多少?考取这一认证又…

【C语言】linux内核packet_setsockopt

一、中文注释 // 发送数据包函数。它尝试通过特定的网络设备队列直接传输一个skb&#xff08;socket缓冲区&#xff09;。 static int packet_direct_xmit(struct sk_buff *skb) {return dev_direct_xmit(skb, packet_pick_tx_queue(skb)); // 调用dev_direct_xmit函数&#x…

写代码实现基金回测(一)

参考博客&#xff1a;应用实战&#xff1a;我的第一个开源项目-基金定投回测工具 这个博主的代码的目录结构还是很赞的 看一下他是如何计算收益率的 第一步&#xff1a;获取所有公募基金的基础信息 共计一万个基金 第二步&#xff1a;获取所有基金的费率信息 这里有一点需要…

Java基于springboot的个人理财系统

基于springboot的个人理财系统 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了个人理财系统的开发全过程。通过分析个人理财系统管理的不足&#xff0c;创建了一个计算机管理个人理财系统的方案。文章介绍了个…

bxCAN总线的工作模式和测试模式(STM32F4xx)

概述 本文主要介绍STM32F4XX的bxCAN知识&#xff0c;包括bxCAN的概念&#xff0c;各种工作模式下特性&#xff0c;如何配置各类工作模式等内容&#xff0c;还介绍了bxCAN的测试模式&#xff0c;bxCAN测试模式有3种工作类型&#xff0c;每种类型有什么特性&#xff0c;以及如何配…

【大厂AI课学习笔记NO.66】TensorFlow

TensorFlow 这个框架&#xff0c;实在是太有名了&#xff0c;最近周红衣都在大力的宣传和讲解。 他说的是对的&#xff0c;人工智能&#xff0c;就是大力出奇迹&#xff0c;就是大量的算力&#xff0c;大量的数据&#xff0c;加上模型的加持&#xff0c;实现的智能感觉。 Goog…

【字符串】马拉车(Manacher)算法

本篇文章参考&#xff1a;比较易懂的 Manacher&#xff08;马拉车&#xff09;算法配图详解 马拉车算法可以求出一个字符串中的最长回文子串&#xff0c;时间复杂度 O ( n ) O(n) O(n) 因为字符串长度的奇偶性&#xff0c;回文子串的中心可能是一个字符&#xff0c;也可能是…

webpack源码分析——tapable中before和stage如何改变执行顺序

一、Before用法 Before 用法 before 属性的值可以传入一个数组或者字符串,值为注册事件对象时的名称&#xff0c;它可以修改当前事件函数在传入的事件名称对应的函数之前进行执行。 示例 let hook new SyncWaterfallHook([arg1]);hook.tap(tap1, (arg)> {console.log(tap1…

安装 node 错误的配置环境变量之后使用 npm 报错

安装 node 错误的配置环境变量之后使用 npm 报错 node:internal/modules/cjs/loader:1147 throw err; ^ Error: Cannot find module ‘F:\ACodeTools\Node\node_modules\npm\bin\node_modules\npm\bin\npm-cli.js’ at Module._resolveFilename (node:internal/modules/cjs/loa…

Git推送本地仓库至阿里云仓库

Git推送本地仓库至阿里云仓库 1.安装Git 参考Git安装详解 2.生成 SSH 密钥 基于RSA算法SSH 密钥 1.管理员权限运行Git Bash 2.输入生成密钥指令点击回车&#xff0c;选择 SSH 密钥生成路径。 $ ssh-keygen -t rsa -C "2267521563qq.com"3.以 RSA算法为例&…

TMGM官网平台联合英超豪门切尔西

TMGM官网平台联合英超豪门切尔西 TMGM澳洲总部客户经理 &#x1f30f;&#xff1a;TMGM818 TMGM中国官网【TMGM558】TMGM联合英超豪门切尔西俱乐部深度合作&#xff0c;去年全球客户成交额突破4650亿美元&#xff0c;在泰国曼谷周杰伦演唱会唯一平台品牌赞助商&#xff0c;作为…

IOC中Bean的生命周期

生命周期的各个阶段&#xff1a; 可以分为三个阶段&#xff1a;产生-使用-销毁 又可以分四个阶段&#xff1a;四个阶段 实例化 ->属性注入->初始化 ->销毁 实例化后到使用的初始化过程&#xff1a; 属性赋值 ->处理各种Aware接口->实现BeanPostProcessor的b…

数据结构/C++:二叉搜索树

数据结构/C&#xff1a;二叉搜索树 概念模拟实现结构分析插入中序遍历查找删除析构函数拷贝构造赋值重载递归查找递归插入递归删除 总代码展示 概念 二叉搜索树&#xff08;BST - Binary Search Tree&#xff09;是一种特殊的二叉树&#xff0c;每个顶点最多可以有两个子节点。…

逆向案例四:360k静态和精灵数据动态AES解密,用js的方法

一、360K 网页链接:https://www.36kr.com/p/2672600261670407 页面中有静态的需要解密的内容&#xff0c;确定html包&#xff0c;确定方法 1.1方法步骤 在下方的搜索中输入decrypt(或者关键字window.initialState &#xff0c;进入js文件 在AES.decrypt处打上断点&#xff0…