【C++委托与事件】函数指针,回调机制,事件式编程与松耦合的设计模式(上)

news/2025/2/22 4:36:48

前言

  • 上一次发文章已经是在两个月前了hhh,期间也是忙忙碌碌做了不少事情也鸽了不少东西…

  • 本文我们来讲讲博主最近在项目中频繁使用的,也就是广泛运用于C#或者Java的一个常用编程机制(思路)-----委托事件。由于C++在语言特性上没有像C#那样直接支持委托事件,因此本文我们借着学习这两个新的机制,学习一下如何在C++中复刻委托事件

  • 本系列将会提到:

  • 请添加图片描述

    1. (前置知识)函数指针,std::function,std::bind以及回调机制
    2. 委托事件的概念
    3. 多播委托的概念及C++实现
    4. 基于C++委托事件实现 观察者模式发布-订阅模式`
    5. 基于C++委托事件实现状态机

0 前置知识:函数指针,std::functionstd::bind以及回调机制

0-1 函数指针
  • 博主在其在C语言的一片文章提到过 【C语言备课课件】(下)指针pointer请添加图片描述

  • 同时上述概念也适用于C++,函数指针是C/C++中的一种低级机制,用于存储和调用函数的地址。它允许将函数作为参数传递或存储,并在需要时调用。


0-1-1 举例
  • 不同于C的是,在C++中我们可以使用using来简化函数指针的定义
using FuncPtr = int(*)(double, double);
FuncPtr func_ptr;
  • 我们来用这个特性实现基础的四则运算
#include <iostream>
using namespace std;

// 定义函数指针类型
using FuncPtr = int(*)(int, int);

int add(int a, int b) { return a + b;}
int subtract(int a, int b) {return a - b;}
int multiply(int a, int b) {return a * b;}
int divide(int a, int b) {
    if (b != 0) {
        return a / b;
    } else {
        cout << "Error: Division by zero!" << endl;
        return 0;  // 或者可以返回一个特殊值表示除零错误
    }
}

int main() {
    // 定义一个函数指针数组,增加了除法操作
    FuncPtr operations[4] = { add, subtract, multiply, divide };
    
    int a = 10, b = 5;
    
    // 使用函数指针数组来调度函数
    cout << "Add: " << operations[0](a, b) << endl;       // 输出:15
    cout << "Subtract: " << operations[1](a, b) << endl;  // 输出:5
    cout << "Multiply: " << operations[2](a, b) << endl;  // 输出:50
    cout << "Divide: " << operations[3](a, b) << endl;    // 输出:2
    
    return 0;
}


0-2 std::function
  • 同样的,博主在【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(三):基于BT行为树实现复杂敌人BOSS-AI提到了std::function请添加图片描述

  • 那这里就不再多赘述咯,给个链接大家方便回去看


0-3 std::bind
0-3-1 概念
  • std::bind 是 C++11 引入的一个标准库函数,它用于将一个可调用对象(如函数、成员函数、函数对象等)与一些参数进行绑定,并返回一个新的可调用对象。这个新的可调用对象可以稍后被调用,并且能够通过提供部分参数来提前“绑定”固定的参数。
0-3-2 API
  • 他的使用API:
    • std::bind 返回一个新的可调用对象,具有与 f 相同的行为,但某些参数已经被固定。
    • f:一个可调用对象(函数指针、成员函数指针、函数对象等)。
    • args:要绑定的参数,可以是常量、占位符(std::placeholders::_1, _2 等)或变量。
#include <functional>
std::bind(Callable&& f, Args&&... args);
  • 关于 std::bind 返回的类型:std::bind 返回的类型是一个函数对象类型,通常是由编译器推导出来的,我们来看一个例子:
#include <iostream>
#include <functional>

void printSum(int a, int b) {
    std::cout << "Sum: " << a + b << std::endl;
}

int main() {
    // 使用 std::bind 创建一个可调用对象,将 printSum 函数的第二个参数绑定为 10
    auto bound_func = std::bind(printSum, std::placeholders::_1, 10);

    // 只需要提供第一个参数,第二个参数已经被绑定为 10
    bound_func(5);  // 输出:Sum: 15

    return 0;
}
  • 上述代码bound_func的类型是怎么被推导的呢,实际上展开是这样的
decltype(std::bind(printSum, std::placeholders::_1, 10)) bound_func;
  • 或者你可以这样理解:bound_func的类型用std::function来表示的话就是:
std::function<void(int)> 
0-3-3 用法
  • 上面的例子我们已经举了一个std::bind其中一个用法,下面我们来夸克他最常见的用法—绑定类与类的成员函数
  • 当我们需要将一个类的非静态成员函数作为参数传递时,直接传递成员函数指针是不可行的,因为成员函数有一个隐式的 this 指针,而非静态成员函数总是与类的实例相关联。因此,为了将非静态成员函数作为参数传递,需要进行一些额外的处理。
#include <iostream>
#include <functional>

class MyClass {
public:
    void display(int value) { std::cout << "Value: " << value << std::endl;}
};

void callFunction(std::function<void(int)> func) {func(42);  // 调用传入的函数对象}

int main() {
    MyClass obj;

    // 使用 std::bind 绑定成员函数 display 和对象 obj
    auto boundFunc = std::bind(&MyClass::display, &obj, std::placeholders::_1);

    // 将绑定后的可调用对象作为参数传递
    callFunction(boundFunc);

    return 0;
}

  • std::bind(&MyClass::display, &obj, std::placeholders::_1):这个表达式将 MyClass 的成员函数 display 与对象 obj 绑定,并返回一个新的可调用对象(即 boundFunc)。std::placeholders::_1 表示调用时传递给 boundFunc 的第一个参数会传递给 display 函数。
  • callFunction(boundFunc):这里,我们将绑定后的可调用对象传递给 callFunction,并且在 callFunction 中调用它。

0-3 回调机制
  • 在 C++ 中,回调机制(Callback Mechanism)允许你将一个函数作为参数传递给另一个函数,这样当某个事件发生时,可以通过回调函数来处理它。回调函数通常用于异步操作、事件驱动编程、处理完成后的动作等场景。
  • 一个简单的例子就是
#include <iostream>
using namespace std;

using Callback = void(*)(int);

// 一个函数,接受回调函数作为参数
void processData(int value, Callback callback) {
    cout << "Processing data: " << value << endl;
    // 调用回调函数
    callback(value);
}

// 一个实际的回调函数
void onDataProcessed(int value) {
    cout << "Data processed: " << value << endl;
}

int main() {
    int data = 42;
    // 将回调函数传递给processData
    processData(data, onDataProcessed);
    return 0;
}
  • 结合上面所学的std::function,我们可以设置一个可以接收任意位置的函数指针来接收类内或者类外的不同类型的回调函数
#include <iostream>
#include <functional>
using namespace std;



class Processor {
public:
    // 成员函数,处理回调
    void onDataProcessed(int value) {
        cout << "Data processed in member function: " << value << endl;
    }

    // 使用 std::function 来接受回调,可以是任何可调用对象(包括成员函数)
    void processData(int value, std::function<void(int)> callback) {

        callback(value);  // 调用回调函数
    }
};
int main() {
    Processor proc;
    proc.processData(42,[](int value_){ cout << "Processing data: " << value_ << endl;});
    proc.processData(42,std::bind(&Processor::onDataProcessed, &proc, std::placeholders::_1));


    return 0;
}

  • 请添加图片描述

  • 完成上述的前置知识补充,那么我们来看看委托事件

1 委托事件

1-1 基础概念-委托(Delegate)
  • 委托其实就是一种类型安全的函数指针,用于封装一个或多个方法。它允许将方法作为参数传递或存储,并在需要时调用。
  • 然而在C++中并没有原生的委托机制,但是我们可以通过std::function来实现。

  • 聪明的你一定会发起疑问了!为什么一定是std::function呢??
1-2 委托函数指针的区别
  • 一个表格不废话!!!!!!!
特性函数指针委托 (std::function)
类型安全不保证类型安全,容易出错。提供类型安全,绑定时会检查签名是否一致。
灵活性只能绑定普通函数,成员函数需要特殊语法。灵活,可以绑定普通函数、Lambda 表达式成员函数等。
多播支持(下面会说)默认不支持多播。支持多播(多个回调),通过容器或多次绑定实现。
绑定成员函数需要特殊的语法来绑定成员函数。可以通过 std::bind 简单绑定成员函数和对象实例。
变参支持不直接支持可变参数,必须使用变参函数。支持变参函数,并且通过 std::bind 和 Lambda 支持灵活绑定。
  • 那么委托的例子就和前面说的std::function的使用例子一样,后面我们还会举例子。

1-3 事件的概念
  • 事件委托的一个特殊化,它限制了如何使用委托。事件允许对象向外界通知某些事情的发生,但它只能由事件发布者触发,外部只能通过订阅(绑定)方法来响应事件。
1-4 委托和事件的区别
  • 咱们用一个通俗易懂 的例子来说明:
    • 委托:就像一个“电话本”,你可以在电话本中存储任意的方法(回调),随时调用它们。你既可以向电话本添加新的号码,也可以删除已有的号码,甚至可以直接拨打号码(调用方法)。
    • 事件:就像是==“广播系统”==,你可以通过广播系统宣布事件的发生,但你不能自己主动去播放广播。其他人只能通过订阅来接收广播,广播的发起完全由广播中心(事件发布者)控制。
  • 正经说就是:
    • 委托:你可以通过赋值来动态改变委托,委托的调用不受限制。
    • 事件:你只能订阅(绑定)事件,不能直接触发事件的调用。事件的触发只能由发布者控制,这是一种封装和控制事件流动的机制。

1-5 为什么用事件而不用标志位?
  • 我们来举一个例子:
    • 加入你希望当某个事件触发的时候,去执行某个函数
    • 那也许你会说,我设置一个bool标志不久行了吗?我通过反复判断bool标志位,来决定是否执行这个函数,这不行吗?(标志位仙人如是说到)
  • 通过设置一个 bool 标志位并反复判断是否触发事件的方式,确实在某些情况下看起来是可行的,但从设计和性能的角度来看,它有一些潜在的问题。
  1. 标志位是高度耦合的,而且你需要循环检查,况且一旦出现多个事件,那标志位就难以维护了
  2. 其次,使用 bool 标志位往往意味着你必须显式地管理状态,例如,标志位何时置 true,何时置 false。这种状态管理如果处理不当,容易导致逻辑错误,比如事件没有被正确重置未及时触发等。随着事件逻辑复杂化,状态管理的复杂性会大大增加。
  3. 最后通过 bool 标志位来控制事件的触发,代码的流程变得较为模糊和不易追踪。随着程序逻辑的增长,问题可能出现在 bool 标志的设置和检查位置,这会增加调试的难度。

  • 废话那么多,我们直接看例子:假设我们有一个程序,其中需要处理多个事件(例如,玩家按下按钮,或者达到某个特定时间点等),并且每个事件需要执行某个函数。
1-6 反面教材:使用 if-elsebool 标志位
  • 假设我们有一个游戏角色,角色可以执行攻击(Attack)和防御(Defend)操作,而每个操作都有一些条件需要满足。如果我们用 if-elsebool 标志位来控制事件的触发,可能会遇到一些问题。
#include <iostream>

class Player {
public:
    bool canAttack = true;  // 是否可以攻击
    bool canDefend = true;  // 是否可以防御

    void Update() {
        if (canAttack) {
            std::cout << "Player attacks!" << std::endl;
            canAttack = false;  // 攻击后不能立刻再次攻击
        } else if (canDefend) {
            std::cout << "Player defends!" << std::endl;
            canDefend = false;  // 防御后不能立刻再次防御
        }
    }

    void ResetActions() {
        canAttack = true;  // 重置攻击标志
        canDefend = true;  // 重置防御标志
    }
};

int main() {
    Player player;

    // 游戏循环中的更新
    player.Update();  // 执行攻击
    player.Update();  // 执行防御

    // 重置标志位后再次执行
    player.ResetActions();
    player.Update();  // 执行攻击
    player.Update();  // 执行防御

    return 0;
}


  • 可以看到canAttackcanDefend 是高度耦合的。如果游戏中有多个事件(比如跳跃、移动、使用技能等),这些标志位会迅速增加,导致事件和状态管理的复杂性增加。
  • 同时这种管理方式让人容易漏掉状态的重置(例如,如果在某个操作中忘记调用 ResetActions,就会导致攻击和防御无法触发)。
  • 随着事件的增多,if-else 的分支会变得越来越复杂,维护起来非常麻烦。如果增加新的操作(例如跳跃、技能释放),你需要对 if-else 结构进行修改,逻辑越来越杂乱。
1-7 改进设计-事件触发
  • 一个更好的方法是使用事件驱动的设计。通过事件回调和状态管理来触发和控制操作的执行,这样可以避免显式地管理 bool 标志位,减少耦合。
#include <iostream>
#include <functional>
#include <vector>

class Player {
public:
    // 定义事件回调类型
    using Event = std::function<void()>;

private:
    std::vector<Event> events;

public:
    void RegisterEvent(Event event) {
        events.push_back(event);  // 注册事件
    }

    void TriggerEvents() {
        for (auto& event : events) {
            event();  // 执行事件
        }
        events.clear();  // 清除事件(已触发的事件不再执行)
    }
};

void Attack() {
    std::cout << "Player attacks!" << std::endl;
}

void Defend() {
    std::cout << "Player defends!" << std::endl;
}

int main() {
    Player player;

    // 注册事件
    player.RegisterEvent(Attack);
    player.RegisterEvent(Defend);

    // 触发所有事件
    player.TriggerEvents();  // 执行攻击和防御

    return 0;
}

  • 通过将每个事件作为回调函数来管理,我们避免了将事件与 bool 标志位捆绑。每个事件都是独立的,通过注册和触发回调函数来控制事件的执行,而不是通过 if-else 和标志位。
  • TriggerEvents 统一管理了所有的事件触发,不需要通过显式的 if-else 来判断是否可以执行某个事件。事件逻辑更加清晰、独立,且不会互相影响。
  • 如果你需要添加新的事件(例如跳跃、技能使用等),只需要在 RegisterEvent 中添加新的事件回调,而不需要修改 if-else 结构或复杂的 bool 状态管理。



2 多播委托

2-1 概念
  • 多播委托(Multicast Delegate)是一种特殊类型的委托,它允许将多个方法(即多个事件处理程序)附加到同一个委托上。当该委托被调用时,它会依次调用所有附加的方法。多播委托常用于 事件 机制中,特别是在事件驱动的编程模型中。
  • 一句话就是多播委托允许多个回调函数在同一个事件发生时被调用。
2-2 实例
  • 这里我们使用 std::function 和容器(如 std::vector)来存储多个回调函数。
#include <iostream>
#include <functional>
#include <vector>

class MulticastDelegate {
public:
    // 存储回调函数的容器
    std::vector<std::function<void(int)>> delegates;

    // 添加回调函数
    void add(std::function<void(int)> func) {
        delegates.push_back(func);
    }

    // 调用所有回调函数
    void invoke(int value) {
        for (auto& delegate : delegates) {
            delegate(value);
        }
    }
};

// 示例回调函数
void callback1(int value) {
    std::cout << "Callback 1 called with value: " << value << std::endl;
}

void callback2(int value) {
    std::cout << "Callback 2 called with value: " << value << std::endl;
}

int main() {
    MulticastDelegate delegate;

    // 将回调函数添加到多播委托中
    delegate.add(callback1);
    delegate.add(callback2);

    // 调用所有回调函数
    delegate.invoke(42);

    return 0;
}




  • 详细通过上述的讲解,你一定晕头转向,不要紧,这时候我们来讲讲委托事件实际的运用----观察者模式发布-订阅模式

3 基于C++委托事件实现 观察者模式

3-1 概念

  • 观察者模式是一种 对象行为型模式,它定义了一种一对多的依赖关系,让多个观察者对象(Listener)监听一个主题对象(Subject)的状态变化。当主题对象的状态发生改变时,所有依赖于它的观察者都会得到通知并自动更新。
  • 关键点:
    • 主题(Subject):被观察的对象,负责管理所有的观察者。
    • 观察者(Observer):响应主题变化的对象。当主题状态发生变化时,观察者会做出反应。

3-2 UML 图

notifies
1
0..*
implements
1
1
Subject
+addObserver(observer: Observer)
+removeObserver(observer: Observer)
+notifyObservers(message: String)
«interface»
Observer
+update(message: String)
ConcreteObserver
+update(message: String)

3-3 C++实现观察者模式

  • 假设我们有一个新闻发布系统(主题),多个用户(观察者)订阅了这个系统,用户会在新闻更新时收到通知。
  • 这是我们将发布新闻这件事情作为事件,并允许多个用户订阅该新闻系统。我们将使用委托来传递回调函数,并且通过 事件机制来确保通知能够在新闻更新时传递给订阅的用户。
  • 我们一步步来,搜先我们这样的一个新闻数据类型需要多个用于去订阅
// 新闻结构体  
struct News {  
    std::string title;      // 新闻标题  
    std::string content;    // 新闻内容  
    std::string author;     // 作者  
    std::string timestamp;  // 发布时间  
  
    // 构造函数  
    News(const std::string& t, const std::string& c, const std::string& a)  
            : title(t), content(c), author(a) {  
        // 获取当前时间戳  
        time_t now = time(0);  
        timestamp = ctime(&now);  // 转换为易读的时间格式  
    }  
  
    // 打印新闻  
    void printNews() const {  
        std::cout << "Title: " << title << std::endl;  
        std::cout << "Author: " << author << std::endl;  
        std::cout << "Published at: " << timestamp;  
        std::cout << "Content: " << content << std::endl;  
    }  
};
  • 我们定义用户接收到事件更新的逻辑是这样:
// 用户类(观察者)  
class User {  
public:  
    User(const std::string& name) : name(name) {}  
  
    // 用户接收到新闻的回调函数  
    void onNewsReceived(const News& news) {  
        std::cout << name << " received the news: " << std::endl;  
        news.printNews();  
    }  
  
private:  
    std::string name;  
};
  • 接着我们编写新闻发布媒体类,回顾一下 ,我们将发布新闻这件事情作为事件,并允许多个用户订阅该新闻系统。那么我们定义发布新闻为如下事件:
using NewsEvent = std::function<void(const News&)>;
  • 同时我们需要一个容器用来存储所有的用户方(也就是存储事件的容器)
// 新闻发布系统类(主题)  
class NewsPublisher {  
public:  
    // 使用 std::function 来存储订阅者回调  
    using NewsEvent = std::function<void(const News&)>;  
private:  
    std::vector<NewsEvent> subscribers;  // 存储订阅者  
};
  • 同时我们需要提供对外的接口和统一通知所有用户的函数
// 新闻发布系统类(主题)  
class NewsPublisher {  
public:  
    // 使用 std::function 来存储订阅者回调  
    using NewsEvent = std::function<void(const News&)>;  
  
    // 存储所有订阅者(事件处理函数)  
    void subscribe(NewsEvent subscriber) {  
        subscribers.push_back(subscriber);  
    }  
  
    // 发布新闻,通知所有订阅者  
    void publishNews(const News& news) {  
        std::cout << "Publishing news..." << std::endl;  
        // 通知所有订阅者  
        for (const auto& subscriber : subscribers) {  
	        //subscribers 是 std::vector<NewsEvent>类型
	        //subscriber  是 NewsEvent类型
	        //也就是 std::function<void(const News&)>
	        //也就是这里调用了每个客户的onNewsReceived(news)
            subscriber(news);  
        }  
    }  
  
private:  
    std::vector<NewsEvent> subscribers;  // 存储订阅者  
};
  • 那么我们就可以进行测试了:(完整代码如下)
#include <iostream>  
#include <string>  
#include <vector>  
#include <functional>  
#include <ctime>  
  
// 新闻结构体  
struct News {  
    std::string title;      // 新闻标题  
    std::string content;    // 新闻内容  
    std::string author;     // 作者  
    std::string timestamp;  // 发布时间  
  
    // 构造函数  
    News(const std::string& t, const std::string& c, const std::string& a)  
            : title(t), content(c), author(a) {  
        // 获取当前时间戳  
        time_t now = time(0);  
        timestamp = ctime(&now);  // 转换为易读的时间格式  
    }  
  
    // 打印新闻  
    void printNews() const {  
        std::cout << "Title: " << title << std::endl;  
        std::cout << "Author: " << author << std::endl;  
        std::cout << "Published at: " << timestamp;  
        std::cout << "Content: " << content << std::endl;  
    }  
};  
  
// 新闻发布系统类(主题)  
class NewsPublisher {  
public:  
    // 使用 std::function 来存储订阅者回调  
    using NewsEvent = std::function<void(const News&)>;  
  
    // 存储所有订阅者(事件处理函数)  
    void subscribe(NewsEvent subscriber) {  
        subscribers.push_back(subscriber);  
    }  
  
    // 发布新闻,通知所有订阅者  
    void publishNews(const News& news) {  
        std::cout << "Publishing news..." << std::endl;  
        // 通知所有订阅者  
        for (const auto& subscriber : subscribers) {  
            subscriber(news);  
        }  
    }  
  
private:  
    std::vector<NewsEvent> subscribers;  // 存储订阅者  
};  
  
// 用户类(观察者)  
class User {  
public:  
    User(const std::string& name) : name(name) {}  
  
    // 用户接收到新闻的回调函数  
    void onNewsReceived(const News& news) {  
        std::cout << name << " received the news: " << std::endl;  
        news.printNews();  
    }  
  
private:  
    std::string name;  
};  
  
int main() {  
    // 创建新闻发布系统(主题)  
    NewsPublisher newsPublisher;  
  
    // 创建用户(观察者)  
    User user1("Alice");  
    User user2("Bob");  
  
    // 用户订阅新闻发布系统  
    newsPublisher.subscribe(std::bind(&User::onNewsReceived, &user1, std::placeholders::_1));  
    newsPublisher.subscribe(std::bind(&User::onNewsReceived, &user2, std::placeholders::_1));  
  
    // 创建新闻对象  
    News news("Breaking News: C++ 20 features!",  
              "C++ 20 introduces many new features, such as modules, coroutines, and improved constexpr.",  
              "John Doe");  
  
    // 发布新闻,所有订阅者都会接收到新闻  
    newsPublisher.publishNews(news);  
  
    return 0;  
}
  • 可以看到,发布者一次更新调用后,所有用户都被通知请添加图片描述



小结

  • 本文我们介绍了C++中如何实现委托和事件,并且通过委托和事件实现了观察者模式
  • 本系列的下篇我们将继续使用委托和事件去实现发布订阅模式,以及使用委托和事件实现状态机。
  • 如有错误,欢迎指出!
  • 感谢大家的支持!!!!

http://www.niftyadmin.cn/n/5861593.html

相关文章

【AI】GitHub Copilot

GitHub Copilot 是一款由 GitHub 和 OpenAI 合作开发的 AI 编程助手&#xff0c;它可以在多种开发工具中使用。以下是 GitHub Copilot 支持的主要开发工具和平台&#xff1a; 1. Visual Studio Code (VS Code) 官方支持&#xff1a;GitHub Copilot 在 VS Code 中拥有最完整的集…

android 使用 zstd算法压缩文件

需要交叉编译 &#xff0c;流程如下 #1. 从GitHub拉取zstd源码 git clone https://github.com/facebook/zstd.git #2. 交叉编译Android版本的zstd cd build/cmake mkdir arm64-v8a cd arm64-v8a 设置ndk路径 export NDKxxx export ABIarm64-v8a export MINSDKVERSION30 设置…

观察者模式示例代码

观察者模式定义了一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖它的对象都会得到通知并自动更新。常见于消息队列&#xff08;MQ&#xff09;、Zookeeper 事件通知等场景。 import java.util.ArrayList; import java.util.List;// 主题接口…

换服务器需要做的工作(记录一下)

1.Nginx开启OCSP 加快Let’s Encrypt免费证书 HTTPS网站访问速度 https://blog.csdn.net/wx23986/article/details/141722669 2.添加伪静态规则 location / {rewrite ^([^\.]*)/topic-(.)\.html$ $1/portal.php?modtopic&topic$2 last;rewrite ^([^\.]*)/article-([0-9…

Moonshot AI 新突破:MoBA 为大语言模型长文本处理提效论文速读

前言 在自然语言处理领域&#xff0c;随着大语言模型&#xff08;LLMs&#xff09;不断拓展其阅读、理解和生成文本的能力&#xff0c;如何高效处理长文本成为一项关键挑战。近日&#xff0c;Moonshot AI Research 联合清华大学、浙江大学的研究人员提出了一种创新方法 —— 混…

【Kafka系列】Kafka 消息传递保障机制

Kafka 消息传递保障机制 在现代分布式系统中&#xff0c;消息队列扮演着至关重要的角色。Kafka 作为一款高性能、高吞吐量的消息队列系统&#xff0c;提供了多种消息传递保障机制来满足不同的业务需求。本文将详细介绍 Kafka 的三种主要消息传递保障机制&#xff1a;最多一次&a…

【开源商城系统是否能直接拿去售卖】

开源商城系统是否能直接拿去售卖&#xff0c;需要根据具体的开源协议和相关法律法规来判断&#xff0c;以下是具体分析&#xff1a; 遵循开源协议的情况 GPL协议&#xff1a;如果开源商城系统遵循GNU通用公共许可证&#xff08;GPL&#xff09;&#xff0c;这种协议属于强拷贝…

PDF文档管理系统V2.0

在<PDF文档管理系统V1.0>的基础上新增了&#xff08;图片文档识别&#xff09;、&#xff08;文档翻译&#xff09;、&#xff08;阅读计划管理的功能&#xff09;&#xff0c;以及其他的小功能完善。由于此版本需要安装数据库&#xff0c;所以不再提供免费下载链接&…