【设计模式】策略模式
以下是格式优化后的Markdown文档,仅调整代码缩进,保持内容不变:
四、策略模式
策略(Strategy) 模式是一种行为型模式,其实现过程与模板方法模式非常类似——都 是以扩展的方式支持未来的变化。本章通过对一个具体范例的逐步重构来详细讲解策略模 式,在此基础之上,引出面向对象程序设计的一个重要原则——依赖倒置原则,并对该原则 进行详细阐述 。
4.1 一个具体实现范例的逐步重构
这里还是以前面提出的单机闯关打斗类游戏(类似街机打拳类游戏)为例继续进行 讲 解 。
随着游戏研发工作的不断进行,游戏内容逐渐增多,策划准备引入为游戏主角补充生命 值(补血)的道具,当主角走到某个特定的场景位置或者击杀某个大型怪物后,这些道具就会 出现,主角通过走到该道具上就可以实现为自身补充生命值的目的。前期主要规划了3个 道具(药品):
- (1)补血丹——可以补充200点生命值;
- (2)大还丹——可以补充300点生命值;
- (3)守护丹-——可以补充500点生命值。
回忆前面讲解模板方法模式时,实现了游戏主角父类Fighter 以及分别代表战士类型 主角和法师类型主角的F_Warrior 和 F_Mage 子类。为了看起来更接近一个真实项目,笔 者把这3个类专门放入到新建立的Fighter.h 文件中,并在MyProject.cpp 的开头位置使用 “#include “Fighter.h””代码行将该文件包含进来。 Fighter.h 文件的代码如下:
#ifndef FIGHTER #define FIGHTER
// 战斗者父类 class Fighter
{
public:Fighter(int life, int magic, int attack) : m_life(life), m_magic(magic), m_attack(attack) {}virtual~Fighter() {}protected:int m life;int m_magic;int m attack;
};
//"战士"类,父类为 Fighter
class F_Warrior : public Fighter
{
public:F_Warrior(int life, int magic, int attack) : Fighter(life, magic, attack) {}
};
//"法师"类,父类为 Fighter
class F_Mage : public Fighter
{
public:F_Mage(int life, int magic, int attack) : Fighter(life, magic, attack) {}
};
#endif
根据策划需求,增加前述3个道具分别补充主角生命值,为此,可以在 Fighter 类中增 加一个成员函数UseItem 来处理通过吃药来补充生命值这件事。第一个版本这样来实现 代码:首先,在类Fighter 定义上面增加枚举类型的定义,其中的枚举值分别代表3个道具。
//增加补充生命值道具 enum ItemAddlife
{
LF_BXD, LF_DHD,
LF_SHD, };
接着,在Fighter 类中实现 UseItem 成员函数:public:
void UseItem(ItemAddlife djtype) {
if(djtype ==LF_BXD)
{
m life +=200;
}
else if(djtype ==LF_DHD)
{
m life +=300;
}
else if(djtype ==LF_SHD)
{
m life +=500;
}
//其他的一些判断逻辑,略 … }
在 main 主函数中,增加如下代码:
Fighter *prole_war = new F_Warrior(1000, 0, 200); // 这里没有采用工厂模式,如果主角很多,可以考虑采用工厂模式创建对象
prole_war->UseItem(LF_DHD);
delete prole_war;
从代码中可以看到,主角通过调用UseItem 吃了一颗大还丹,为自己增加了300点生 命值。从实现的功能上,上述代码本身没什么问题。但如果从长远角度或者从面向对象程 序设计的角度来看,可能会发现这些实现代码存在一些问题:
(1)如果增加新的能补充生命值的道具(药),则要增加新的枚举类型,也要在 UseItem 中的if…else … 语句中增加判断条件,这不符合开闭原则,而且一旦if条件特别多,对程序的 运行效率和可维护性会造成影响。
(2)代码复用性差,如果将来游戏中的怪物也可以通过吃这些药为自己补充生命值,那 么可能需要把UseItem 中的这些判断语句复制到怪物类中去,甚至要把整个 Useltem 成员 函数搬到怪物类中去,显然,这会导致很多重复的代码(代码级别的复制粘贴)。
(3)目前道具的功能比较简单,仅仅是给主角增加生命值,但如果将来道具功能特别复 杂,例如能给主角同时增加生命值、魔法值,还能根据主角的角色和一些其他状态(例如主角 中毒了)进行一些特殊的处理,甚至引入各种复杂功能的道具,那么上面的写法就会导致 UseItem 成员函数中的代码判断逻辑特别复杂,几乎可以肯定没人愿意看到下面这样的实 现 代 码 :
if (djtype == LF_BXD) // 道具类型:补血丹
{m life += 200; // 补充200点生命值if (主角中毒了){停止中毒状态,也就是主角吃药后就不再中毒}if (主角处于狂暴状态){m life += 400; // 额外再补充400点生命值m_magic += 200; // 魔法值也再补充200点}
}
通过策略模式,可以对上述代码进行改造。在策略模式中,可以把 UseItem 成员函数 中的每个if 条件分支中的代码(也称“算法”)写到一个个类中,那么每个封装了算法的类就 可以称为一种策略(类不仅可以表示一种存在于真实世界的东西,也可以表示一种不存在于 真实世界的东西),当然,应该为这些策略抽象出一个统一的父类以便实现多态。现在,看一 看策略类父类及各个子类如何编写,专门创建一个ItemStrategy.h 文件,代码如下:
#ifndef ITEMSTRATEGY_H
#define ITEMSTRATEGY_H
// 道具策略类的父类
class ItemStrategy
{
public:virtual void UseItem(Fighter *mainobj) = 0;
};// 补血丹策略类
class ItemStrategy_BXD : public ItemStrategy
{
public:virtual void UseItem(Fighter *mainobj){mainobj->SetLife(mainobj->GetLife() + 200); // 补充200点生命值}
};// 大还丹策略类
class ItemStrategy_DHD : public ItemStrategy
{
public:virtual void UseItem(Fighter *mainobj){mainobj->SetLife(mainobj->GetLife() + 300); // 补充300点生命值}
};// 守护丹策略类
class ItemStrategy_SHD : public ItemStrategy
{
public:virtual void UseItem(Fighter *mainobj){mainobj->SetLife(mainobj->GetLife() + 500); // 补充500点生命值}
};
#endif
从上面的代码中可以看到,Useltem 成员函数直接使用了 Fighter* 作为形参,意图是 把主角所有必要的信息都传递到策略类中来,让策略类中的 Useltem 成员函数在需要时可 以随时回调 Fighter 中的各种成员函数。这样做虽然策略类(ItemStrategy) 和主角类 (Fighter) 耦合得比较紧密,但因为策略类的工作本身需要一些必要的数据(例如主角当前 生命值、魔法值等),所以这样实现也未尝不可。
接着,需要对 Fighter.h 中 的 Fighter 类进行改造,注释掉枚举类型的定义以及 Useltem 成员函数并增加如下代码行:
public:
void SetItemStrategy(ItemStrategy *strategy);
void UseItem();
int GetLife();
void SetLife(int life);private:
ItemStrategy *itemstrategy = nullptr; // C++11 中支持这样初始化
同时记得在 Fighter 类定义的前面位置增加针对类 ItemStrategy 的前向声明,因为 SetItemStrategy 的形参中用到了ItemStrategy:
class ItemStrategy; //类前向声明
增加新文件Fighter.cpp, 并将该文件增加到当前的项目中。 Fighter.cpp 的实现代码
如 下 :
#include <iostream>
#include "Fighter.h"
#include "ItemStrategy.h"
using namespace std;// 设置道具使用的策略
void Fighter::SetItemStrategy(ItemStrategy *strategy)
{itemstrategy = strategy;
}// 使用道具(吃药)
void Fighter::UseItem()
{itemstrategy->UseItem(this);
}// 获取人物生命值
int Fighter::GetLife()
{return m_life;
}// 设置人物生命值
void Fighter::SetLife(int life)
{m_life = life;
}
在 MyProject.cpp 的开头位置,增加如下#include 语句:
#include "ItemStrategy.h"
在 main 主函数中,注释掉原有代码,增加如下代码:
// 创建主角
Fighter *prole_war = new F_Warrior(1000, 0, 200);// 吃一颗大还丹
ItemStrategy *strategy = new ItemStrategy_DHD();
prole_war->SetItemStrategy(strategy);
prole_war->UseItem();// 再吃一颗补血丹
ItemStrategy *strategy2 = new ItemStrategy_BXD();
prole_war->SetItemStrategy(strategy2);
prole_war->UseItem();// 释放资源
delete strategy;
delete strategy2;
delete prole_war;
跟踪调试程序不难发现,在吃了大还丹和补血丹后,主角的生命值已经从1000变成 1500了。
从上面的代码中可以看到,通过引入策略模式,将算法(使用道具增加生命值这件事)本 身独立到ItemStrategy 的各个子类中,而不在Fighter 类中实现。当增加新的道具时,只需 要增加一个新的策略子类即可,这样就符合开闭原则了。
Fighter 类 与ItemStrategy 类相互作用实现指定的算法,当算法被调用时,Fighter 将算 法需要的所有数据(这里其实是Fighter 类对象自身)传递给 ItemStrategy, 当然如果算法需 要的数据比较少,则可以仅仅传递必需的数据(而不必将 Fighter 类对象本身传递给算法)。
引入“策略”设计模式的定义:定义一系列算法类(策略类),将每个算法封装起来,让它 们可以相互替换。换句话说,策略模式通常把一系列算法封装到一系列具体策略类中作为 抽象策略类的子类,然后根据实际需要使用这些子类。
针对前面的代码范例绘制策略模式的 UML 图,如图4.1所示。
在图4.1中,可以看到Fighter 类和 ItemStrategy 类之间的关系是一种组合关系,因为 在 Fighter 类定义中,有如下代码:
ItemStrategy *itemstrategy =nullptr;
图4. 1中的空心菱形在Fighter 类这边,表示Fighter 类中包含指向ItemStrategy 类对 象的指针(itemstrategy) 作为成员变量。虚线下面框起来的部分代表注释。
策略模式的 UML 图中包含3种角色。
(1)Context (环境类):也叫上下文类,是使用算法的角色,该类中维持着一个对抽象策 略类的指针或引用。这里指Fighter 类。
(2)Stategy (抽象策略类):定义所支持的算法的公共接口,是所有策略类的父类。这 里指ItemStrategy 类。
(3)ConcreteStrategy(具体策略类):抽象策略类的子类,实现抽象策略类中声明的接 口。这里指 ItemStrategy_BXD 、ItemStrategy_DHD 、ItemStrategy_SHD 类。
策略模式具有如下优点:
(1)以往利用增加新的 if 条件分支来支持新算法的方式违背了开闭原则,引入策略模 式后,通过增加新的策略子类实现了对开闭原则的完全支持,也就是以扩展的方式支持未来 的变化。所以,如果读者今后在编写代码时遇到有多个 if 条件分支或者 switch 分支的语 句,并且这些分支并不稳定,会经常改动时,则率先考虑能否通过引入策略模式加以解决。
所以很多情况下,策略模式是if或者switch 条件分支的取代者。
当然,如果if 或 者switch 中的分支数量有限,而且比较稳定,例如一周七天、一年四季, 这种情况下也没必要引入策略模式。例如,下面这种判断就没有必要引入策略模式:
if (今天==春季){}
else if(今天==夏季){}
else if(今天==秋季){}
else{} //这一定是冬季
(2)既然所有的算法都独立到策略类中去实现,这些算法就可以被复用。例如,将来其 他的 Fighter子类也可以使用,甚至如果 ItemStrategy 类和 Fighter 类耦合得并不紧密 (ItemStrategy 类 的UseItem 成员函数不是以Fighter* 类型作形参)的话,ItemStrategy 类 也完全可以被 Monster (怪物)类使用来给怪物增加生命值。
(3)策略模式可以看成类继承的一种替代方案。当使用继承来定义类型时,子类一般 会受限于父类(子类对象与父类对象之间是一个 is-a 关系,也就是子类对象同时也是一个父 类对象,附录A.3 中对is-a 关系有详细的阐述,可以先行阅读),而使用策略模式,通过为环 境类对象指定不同的策略就可以改变环境类对象的行为。
策略模式具有如下缺点:
(1)策略模式会导致引入许多新的策略类。
(2)使用策略时,调用者(也称客户端,这里指的就是main 主函数中的代码)必须熟知 所有策略类的功能并根据实际需要自行决定使用哪个策略类。
4.2 依赖倒置原则
通过对策略模式的讲解,引入面向对象程序设计的另外一个原则——依赖倒置原则 (Dependency Inversion Principle,DIP)。
开发者普遍认为,面向对象程序设计的原则比设计模式本身更重要,遵循这些设计原则 完全可以不局限于现有的二十多种常见设计模式,而是可以创造出新的设计模式。
依赖倒置原则贯穿于绝大部分设计模式,是一个非常重要的原则,是面向对象设计的主 要实现方法,同时也是开闭原则的重要实现途径,该原则降低了客户与实现模块之间的耦 合度 。
依赖倒置原则是这样解释的:高层组件不应该依赖于低层组件(具体实现类),两者都应该依赖于抽象层。
对于高层组件、低层组件、抽象层等词汇有的读者可能不太熟悉,这里笔者试举一例来 说 明 。
继续前面的单机闯关打斗类游戏,在讲解简单工厂模式时,将怪物分成了亡灵类怪物 (M Undead)、元素类怪物(M Element) 和机械类怪物(M Mechanic) 。 如果主角在闯关中要针 对这3种怪物进行击杀,那么有的程序员可能会写出下面这样的代码,先定义3种怪物类:
class M_Undead
{
public:void getinfo(){cout << " 这是一只亡灵类怪物" << endl;}// 其他代码略…
};class M_Element
{
public:void getinfo(){cout << " 这是一只元素类怪物" << endl;}// 其他代码略…
};class M_Mechanic
{
public:void getinfo(){cout << " 这是一只机械类怪物" << endl;}// 其他代码略…
};
再定义 一 个战士主角类:
// 战士主角
class F_Warrior
{
public:void attack_enemy_undead(M_Undead *pobj){// 进行攻击处理…pobj->getinfo();}// 其他代码略…
};
观察 F_Warrior 类 的 attack_enemy_undead 成员函数,该成员函数的形参为 M
Undead*, 这就是一种类与类之间的依赖关系— F_Warrior 类依赖于M_Undead 类 。
在 main 主函数中加入代码,让主角攻击一只亡灵类怪物:
M_Undead *pobjud = new M_Undead();
F_Warrior *pobjwar = new F_Warrior();
pobjwar->attack_enemy_undead(pobjud); // 攻击一只亡灵类怪物
若让主角攻击一只元素类怪物,则需要为F_Warrior 类增加一个新的成员函数:
public:
void attack_enemy_element(M_Element *pobj) // 攻击元素类怪物
{// 进行攻击处理…pobj->getinfo();
}
在 main 主函数中继续加入代码:
M_Element*pobjelm=new M_Element();
pobjwar->attack_enemy_element(pobjelm);
执行起来,看一看结果:
这是一只亡灵类怪物
这是一只元素类怪物
当然,最后记得在main 主函数中增加资源释放的代码:
//资源释放
delete pobjwar;
delete pobjud;
delete pobjelm;
看这段main 主函数中的代码,如果还要攻击一只机械类怪物,则需要为F_Warrior 类 增加新的成员函数,并且代码中涉及的类也会变得越来越多(F_Warrior 、M_Undead 、M_Element 、M_Mechanic)。
main 主函数中这几行主角击杀怪物的业务逻辑代码就是高层组件,而M_Undead 、M_Element 、M_Mechanic 都属于低层组件,也就是具体的实现类。高层组件与低层组件直接 交互实现对怪物的击杀,组件之间的依赖关系如图4 . 2所示(箭头指向谁就依赖谁)。
针 对 击 杀 怪 物 这 件 事 , 如 果 设 计 一 个 Monster 类 作 为 所 有 怪 物 类(M_Undead、M_Element 、M_Mechanic) 的 父 类 , 那 么Monster 类 代 表 的 就 是 抽 象 层 。 下 面 为 实 现 这 个 抽 象 层 的 代 码 :
class Monster
{
public:virtual void getinfo() = 0;virtual~Monster() {}
};
重 新 实 现 低 层 组 件(M_Undead 、M_Element 、M_Mechanic 类 ) , 让 它 们 全 部 继 承 抽 象 层
Monster 类 :
class M_Undead : public Monster
{
public:// 亡灵类怪物virtual void getinfo(){cout << " 这是 一 只亡灵类怪物" << endl;}// 其他代码略 …
};class M_Element : public Monster
{
public:virtual void getinfo(){// 元素类怪物cout << " 这是 一 只元素类怪物" << endl;}// 其他代码略 …
};class M_Mechanic : public Monster
{
public:virtual void getinfo(){cout << " 这是 一 只机械类怪物" << endl;}// 机械类怪物// 其他代码略 …
};
当 然 , 也 应 该 为 F_Warrior 这 个 战 士 主 角 类 设 计 抽 象 层 , 在 讲 解 模 板 方 法 模 式 时 , 已 经 这 样 做 了 , 所 以 在 这 里 读 者 可 以 自 己 动 手 为 F_Warrior 设 计 抽 象 层 以 作 为 对 以 往 知 识 的 复 习 , 因 为 对 怪 物 类 抽 象 已 经 足 以 阐 明 依 赖 倒 置 原 则 , 所 以 这 里 就 不 对 F_Warrior 类 进 行 抽 象 了 。
在 对 怪 物 类 进 行 了 抽 象 , 产 生 了Monster 类 之 后 , 在 主 角 击 杀 各 种 怪 物 时 , 就 可 以 把 其 中 与 击 杀 有 关 的 attack_enemy_undead、attack_enemy_element 等 成 员 函 数 统 一 写 成 一 个 成 员 函 数 。 改 造 之 后 的 F_Warrior 类 代 码 如 下 :
// 战士主角
class F_Warrior
{
public:void attack_enemy(Monster *pobj){// 进行攻击处理 …pobj->getinfo();}// 其他代码略…
};
在 main 主函数中,注释掉原有代码,增加如下代码:
Monster *pobjud = new M_Undead();
F_Warrior *pobjwar = new F_Warrior();
pobjwar->attack_enemy(pobjud); // 攻击一只亡灵类怪物
Monster *pobjelm = new M_Element();
pobjwar->attack_enemy(pobjelm); // 攻击一只元素类怪物// 资源释放
delete pobjwar;
delete pobjud;
delete pobjelm;;
上面的代码组件之间的依赖关系如图4.3所示。
从图4.3可以看到,高层组件和低层组件之间不再有依赖关系,两者都依赖于抽象层 (接口)。当然,诸如main 主函数中下面这样的代码行:
Monster*pobjud = new M_Undead();
虽然new 后面用到了实现类M_Undead, 但这里所谈的依赖关系指的是pobjud 的类型,也 就是Monster 类。所以,在main 主函数中,对于要击杀怪物这个业务逻辑,实际上只涉及 了 Monster 类(而图4.2中涉及的是 M_Undead 、M_Element 两个类),即便日后增加新的 怪物类型也不会导致依赖更多的类。
传统思考和解决问题的方式是自顶向下的结构化程序设计方法,这种设计方法往往在 最后才会考虑低层组件的设计,而依赖倒置原则中的倒置是指以低层组件如何设计作为思 考的入口来率先确定抽象层的设计,而后让高层组件和低层组件全部依赖于抽象层。
在学习了依赖倒置原则之后,结合图4.1思考一下讲解过的策略模式实现范例,不难想 象,策略模式中将算法的实现放在各个策略子类(ItemStrategy_BXD 、ItemStrategy_DHD、ItemStrategy_SHD) 中,使用算法的Fighter 类(也称为“环境类”,以表示使用算法的当前环 境)只针对抽象策略类 ItemStrategy 进行编程,这种编码方式就符合依赖倒置原则。当增 加一个新的补血道具时,可以增加一个新的策略子类来实现,这种编码方式同时也符合开闭 原 则 。
依赖倒置原则是面向接口(抽象层)编程,而不是针对实现(实现类)编程,从而实现了高 层组件和低层组件之间的解耦。
相关文章:
【设计模式】策略模式
以下是格式优化后的Markdown文档,仅调整代码缩进,保持内容不变: 四、策略模式 策略(Strategy) 模式是一种行为型模式,其实现过程与模板方法模式非常类似——都 是以扩展的方式支持未来的变化。本章通过对一个具体范例的逐步重构…...
第J6周:ResNeXt-50实战解析
文章目录 一、前期准备1.设置GPU2.导入数据3.查看数据 二、数据预处理1.加载数据2.可视化数据3.再次检查数据4.配置数据集 四、模型复现1. 分组卷积模块2. 定义残差模块3. 堆叠残差单元4. 搭建ResNext-50网络5. 查看模型摘要 五、训练模型六、结果可视化总结: &…...
调试 ResNet18 cpp实现中的段错误(SIGSEGV)问题
调试 ResNet18 cpp实现中的段错误(SIGSEGV)问题 问题分析 您的程序在运行时遇到了段错误(SIGSEGV),GDB显示错误发生在main()函数的第一行(resnet18_allo_test.cpp:33)。这种情况看起来很奇怪&…...
详细介绍IDI_APPLICATION和IDC_ARROW
书籍:《windows程序设计(第五版)》 环境:visual studio 2022 内容:HELLOWIN程序 说明:以下内容大部分来自腾讯元宝。 IDI_APPLICATION 与 IDC_ARROW 详解 1. IDC_ARROW(光标资源标识符) 定义与…...
curl库+openssl库windows编译
一、工具准备 Visual Studio 2008:确保安装了 C 开发工具。 Git:用于克隆 cURL 的源码。 Perl:可以从 ActiveState Perl 下载并安装。 NASM(可选):如果需要汇编优化,可以从NASM 官方网站 下载并…...
今日行情明日机会——20250321
后续投资机会分析 结合2025年3月21日盘面数据(涨停56家,跌停31家),市场呈现结构性分化行情,海洋经济成为绝对主线,机器人概念局部活跃,人工智能表现较弱。以下是具体方向与策略建议:…...
repo init 错误 Permission denied (publickey)
一、已经生成ssh-key并设置到gerrit上 二、已经设置.gitconfig (此步骤是公司要求,设置gerrit地址为一个别名之类的,有的公司不需要) 然后出现下面的错误,最后发现忘记设置git的用户名和邮箱 1. git config --globa…...
STM32 模拟SPI 模式0
SPI 模式 0 的时钟极性(CPOL)为 0,时钟相位(CPHA)为 0。CPOL 0 意味着时钟信号空闲时为低电平,CPHA 0 表示在时钟信号的第一个跳变沿(上升沿)进行数据采样。 #include "stm3…...
MySQL实现全量同步和增量同步到SQL Server或其他关系型库
在将MySQL中的两张表同步到SQL Server的过程中,全量同步和增量同步各有其优缺点。全量同步简单直接但可能耗时较长且资源消耗大,而增量同步则更加高效但需要额外的逻辑来处理数据的变更。以下是对这两种同步方式的详细解释及代码示例的完善。 完整代码示…...
详细解析GetOpenFileName()
书籍:《Visual C 2017从入门到精通》的2.3.8 Win32控件编程 环境:visual studio 2022 内容:【例2.34】打开文件对话框和另存为。 说明:以下内容大部分来自腾讯元宝。 GetOpenFileName() 是 Windows API 中用于显示标准文件打开…...
FPGA----完美解决Windows下[XSIM 43-3409][XSIM 43-3915]错误
大家好久不见,今天开始又要重操旧业了!最近会更新很多关于petalinux的踩坑日记,敬请期待! 先更新一个常见问题,使用Vivado仿真时C编译器报错问题。如下所示 ERROR: [XSIM 43-3409] Failed to compile generated C fi…...
LeetCode Hot100 刷题路线(Python版)
目录 1. LeetCode Hot100 刷题笔记(1)—— 哈希、双指针、滑动窗口 2. LeetCode Hot100 刷题笔记(2)—— 子串、普通数组、矩阵 3. LeetCode Hot100 刷题笔记(3)—— 链表 4. LeetCode Hot100 刷题笔记&…...
宇树科技纯技能要求总结
一、嵌入式开发与硬件设计 核心技能 嵌入式开发: 精通C/C,熟悉STM32、ARM开发熟悉Linux BSP开发及驱动框架(SPI/UART/USB/FLASH/Camera/GPS/LCD)掌握主流平台(英伟达、全志、瑞芯微等) 硬件设计:…...
Docker学习笔记(十)搭建Docker私有仓库
一、环境配置 1、宿主机系统:macOS Sequoia(版本15.2) 2、虚拟机VMware Fusion版本:专业版 13.6.2 (24409261) 3、虚拟机系统:AlmaLinux-9-latest-x86_64-boot.iso 二、安装Harbor开源企业级Docker镜像 Harbor 是一个开源的企业级 Docker…...
FastAPI WebSocket 无法获取真实 IP 错误记录
FastAPI WebSocket 无法获取真实 IP 错误记录 问题描述 在使用 FastAPI WebSocket 服务时,发现无法获取设备的真实 Remote IP,所有连接均显示为内网地址 10.x.x.1。以下是完整的排查过程及解决方案。 排查步骤 1. 基础信息检查 • 现象复现࿱…...
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能示例4,TableView15_04导出当前页数据示例
前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能示例4,TableView15_04导出当…...
【Linux】快速上手Makeflie CMake
🦄个人主页:修修修也 🎏所属专栏:Linux ⚙️操作环境:Xshell (操作系统:Ubuntu 22.04 server 64bit) 目录 📌快速上手Makefile 基本结构 变量 自动变量 常用目标 📌快速上手CMake CMake与Makefile的关系 CMake的使用步骤 常用命令…...
Python连接数据库进行增删改查
更多优质文章 _>_>_>_>_>✍✈✉戳我 目录 1.导入相关库 2.创建连接 3.插入数据 4.删除数据 5.修改数据 6.查询数据 7.更多干货 1.导入相关库 import pymysql -----pip install pymysql #下载库 2.创建连接 conn pymysql.connect(hostlocalho…...
数据库的两种模式
数据库的 严格模式(Strict Mode) 和 宽松模式(Non-Strict Mode) 是数据库管理系统(DBMS)中用于控制数据验证和处理方式的两种不同模式。它们的主要区别在于对数据完整性、一致性和错误处理的严格程度。 1. …...
【css酷炫效果】纯CSS实现立体旋转立方体
【css酷炫效果】纯CSS实现立体旋转立方体 缘创作背景html结构css样式完整代码效果图 想直接拿走的老板,链接放在这里:https://download.csdn.net/download/u011561335/90492014 缘 创作随缘,不定时更新。 创作背景 刚看到csdn出活动了&am…...
Cursor与Coze结合开发电影推荐系统:一次高效的技术实践
1 项目背景 有个想法,和朋友打算一起看电影,但是不知道看什么(吃饭也是),于是在豆瓣高分电影榜单中选择出来一些感兴趣的电影,随机挑选一部“天意之选”。为此,我尝试结合Cursor(智…...
【总结篇】java多线程,新建线程有几种写法,以及每种写法的优劣势
java多线程 新建线程有几种写法,以及每种写法的优劣势 [1/5]java多线程 新建线程有几种写法–继承Thread类以及他的优劣势[2/5]java多线程-新建线程有几种写法–实现Runnable接口以及他的优劣势[3/5]java多线程 新建线程有几种写法–实现Callable接口结合FutureTask使用以及他的…...
idea问题(三)pom文件显示删除线
一、问题 1、现象 2、原因 分析原因和出现的流程:创建子模块的时候因为名称错误了,并且通过修改模块模块名称后,又删除了模块,因删除不干净。再次建立了同名模块,会让IDEA认为你再次新建的项目是已经被删除的项目。 …...
python爬虫概述
0x00 python爬虫概述 以豆瓣的选电影模块为例,当查看源代码搜索猫猫的奇幻漂流瓶是搜不到的 这时服务器的工作方式应该是这样的 客户端浏览器第一次访问其实服务器端是返回的一个框架(html代码) 当客户端浏览器第二次通过脚本等方式进行访问时服务器端才返回的数据…...
实现拖拽图片验证的基本步骤
前端部分 UI 设计: 显示一个滑块和一张背景图(通常是带缺口的图片)。滑块可以是拼图的一块或简单的方块。 拖拽功能: 监听滑块的 mousedown、mousemove、mouseup 事件,实现拖拽效果。 验证逻辑: 计算滑块最…...
conda报错activate没办法激活环境
遇到激活环境报错 # >>>>>>>>>>>>>>>>>>>>>> ERROR REPORT <<<<<<<<<<<<<<<<<<<<<< Traceback (most recent call last): File …...
numpy学习笔记3:三维数组 np.ones((2, 3, 4)) 的详细解释
numpy学习笔记3:三维数组 np.ones((2, 3, 4)) 的详细解释 以下是关于三维数组 np.ones((2, 3, 4)) 的详细解释: 1. 三维数组的形状 形状 (2, 3, 4) 表示: 最外层维度:2 个“层”(或“块”); …...
论文笔记(七十三)Gemini Robotics: Bringing AI into the Physical World
Gemini Robotics: Bringing AI into the Physical World 文章概括1. 引言2. Gemini 2.0的具身推理2.1. 具身推理问答(ERQA)基准测试2.2. Gemini 2.0的具身推理能力2.3. Gemini 2.0支持零样本和少样本机器人控制 3. 使用 Gemini Robotics 执行机器人动作3…...
不用 Tomcat?SpringBoot 项目用啥代替?
在SpringBoot框架中,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。 同时,SpringBoot也支持Undertow容器,我们可以很方便的用Undertow替换Tomcat,而Undertow的性能和内存使…...
ChatTTS 开源文本转语音模型本地部署 API 使用和搭建 WebUI 界面
ChatTTS(Chat Text To Speech),专为对话场景设计的文本生成语音(TTS)模型,适用于大型语言模型(LLM)助手的对话任务,以及诸如对话式音频和视频介绍等应用。支持中文和英文,还可以穿插笑声、说话间的停顿、以…...
嵌入式笔记 | 正点原子STM32F103ZET6 4 | 中断补充
1. 外设引脚重映射 1.1 定义 在STM32中,每个外设的引脚都有默认的GPIO端口,但有些引脚可以通过重映射寄存器将功能映射到其他端口。这种机制称为引脚重映射,主要用于解决引脚复用冲突或优化PCB布线。 1.2 重映射的类型 部分重映射&#x…...
spring循环依赖
Spring 通过三级缓存机制解决单例 Bean 的循环依赖问题,其核心思想是提前暴露未完全初始化的 Bean 引用。以下是详细流程和原理: 1. 循环依赖的场景 假设两个 Bean 相互依赖: BeanA 依赖 BeanBBeanB 依赖 BeanA 如果没有特殊处理ÿ…...
算法刷题区域部分反转
不断创建数组,相加,利用cpp内字符串相加的性质即可。具体代码如下: class Solution { public: string reverseStr(string s, int k) { int size s.size(); int count size / (2*k); string a; int i 0; for ( i 0; i < count; i)…...
使用【docker】+【shell】脚本半自动化部署微服务项目
一.前言 以下是一个基于 Docker Shell脚本 的半自动化部署方案,包含镜像构建、容器管理、网络配置和日志监控等核心功能,适用于大多数Web应用或微服务项目。 二.目录结构 三.脚本代码实现 1.Shell脚本实现 (deploy.sh) #!/bin/bash# 设置颜…...
关于“碰一碰发视频”系统的技术开发文档框架
以下是关于“碰一碰发视频”系统的技术开发文档框架,涵盖核心功能、技术选型、开发流程和关键模块设计,帮助您快速搭建一站式解决方案 --- 随着短视频平台的兴起,用户的创作与分享需求日益增长。而如何让视频分享更加便捷、有趣,…...
Java面试黄金宝典5
1. ConcurrentHashMap 和 HashTable 有哪些区别 原理 HashTable:它继承自 Dictionary 类,是 Java 早期提供的线程安全哈希表。其线程安全的实现方式是对每个方法都使用 synchronized 关键字进行同步。例如,在调用 put、get 等方法时ÿ…...
【FastGPT】利用知识库创建AI智能助手
【FastGPT】利用知识库创建AI智能助手 摘要创建知识库上传文档创建应用准备提示词准备开场白关联知识库AI回答效果 摘要 关于FastGPT的部署,官方提供了docker-compose方式的部署文档,如果使用的是podman和podman-compose的同学,可以参考这篇…...
尚硅谷爬虫(解析_xpath的基本使用)笔记
1、xpath的基本使用 创建一个简单的HTML: <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body><ul><li>北京</li><li&…...
redis MISCONF Redis is configured to save RDB snapshots报错解决
直接上解决方案 修改redis配置文件 stop-writes-on-bgsave-error no 重启redis...
《深入理解AOP编程:从基础概念到Spring实现》
AOP编程 AOP(Aspect Oriented Programing) 面向切面编程 Spring动态代理开发 以切面为基本单位的程序开发,通过切脉你间的彼此协同,相互调用,完成程序构建 切面切入点额外功能 OOP(Object Oriented Programing)面向对象编程 java 以对象为基本…...
网络安全漏洞的种类分为哪些?
漏洞,是指在硬件、软件、协议的具体实现或系统安全策略上存在的缺陷,从而可以使攻击者能够在未授权的情况下访问或破坏系统。漏洞的出现,不仅会造成个人隐私信息泄露,还涉及到我们的财产安全,那么网络安全漏洞的种类分…...
C程序设计(第五版)及其参考解答,附pdf
通过网盘分享的文件:谭浩强C语言设计 链接: https://pan.baidu.com/s/1U927Col0XtWlF9TsFviApg?pwdeddw 提取码: eddw 谭浩强教授的《C程序设计》是C语言学习领域的经典教材,其内容深入浅出,适合不同层次的学习者。 一、教材版本与特点 最…...
CXL协议之FM(Fabric Management)解释
CXL协议中的FM功能详解 1. FM的核心作用 FM是CXL(Compute Express Link)架构中的核心管理实体,负责协调和管理CXL设备之间的通信、资源分配及拓扑结构。其核心功能包括: 设备发现与枚举:识别CXL拓扑中的设备&#x…...
gstreamer之GstVideoDecoder源码剖析
GStreamer 中的 GstVideoDecoder 基类旨在为实现视频解码器提供一个框架。它定义了一套规则和规范,用于指导基类与其派生子类(具体的视频解码器)之间如何交互与协作。 /*** SECTION:gstvideodecoder* title: GstVideoDecoder* short_descrip…...
Windows部署deepseek R1训练数据后通过AnythingLLM当服务器创建问答页面
如果要了解Windows部署Ollama 、deepseek R1请看我上一篇内容。 这是接上一篇的。 AnythingLLM是一个开源的全栈AI客户端,支持本地部署和API集成。它可以将任何文档或内容转化为上下文,供各种语言模型(LLM)在对话中使用。以下是…...
嵌入式软件开发--面试总结
(1)公司简介:做打印机设备、项目涉及到操作系统 (2)面试内容:笔试题技术面试 //32位单片机c语言程序typedef struct{int a;char b;char c;}str1;typedef struct{char a;int b;char c;}str2;void function…...
测试专项3:算法测试基础理论速查手册
1 算法测试的基本概念 1.1 传统软件测试 vs. 算法测试 在软件工程领域,传统软件测试主要关注程序逻辑的正确性。测试人员通过预设输入与期望输出的对比,确保软件程序能够按照设计要求执行,从而发现代码中的错误或缺陷。常见的测试方法包括单…...
基于Springboot+Typst的PDF生成方案,适用于报告打印/标签打印/二维码打印等
基于SpringbootTypst的PDF生成方案,适用于报告打印/标签打印/二维码打印等。 仅提供后端实现 Typst2pdf-for-report/label/QR code github 环境 JDK11linux/windows/mac 应用场景 适用于定制化的报告模板/标签/条码/二维码等信息的pdf生成方案。通过浏览器的p…...
轻松迁移 Elasticsearch 数据:如何将自建索引导出并导入到另一个实例
概述 在日常的 Elasticsearch 运维和数据管理中,数据迁移是一个常见的需求。无论是为了备份、升级,还是将数据从一个集群迁移到另一个集群,导出和导入索引数据都是至关重要的操作。本文将详细介绍如何将自建 Elasticsearch 实例中的索引数据…...
【C#语言】C#同步与异步编程深度解析:让程序学会“一心多用“
文章目录 ⭐前言⭐一、同步编程:单线程的线性世界🌟1、寻找合适的对象✨1) 🌟7、设计应支持变化 ⭐二、异步编程:多任务的协奏曲⭐三、async/await工作原理揭秘⭐四、最佳实践与性能陷阱⭐五、异步编程适用场景⭐六、性能对比实测…...