news 2026/10/15 12:01:01

C++工厂方法模式详解:从if-else到现代对象创建实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++工厂方法模式详解:从if-else到现代对象创建实践

上周我在帮朋友调试一个消息网关,业务方给的需求特别简单:再多支持一种自定义协议。我顺着调用链一路点开,看到的却是连续四段几乎同一个模子里刻出来的 if-else,每段都是 new JsonMessage、new XmlMessage、new ProtoMessage,然后调用 handle。加一个分支当然不难,难的是消息类型一变,所有牵扯到消息创建的地方都要一起改;想换一个假消息做测试,压根插不进去。最后让我从这堆 if-else 里爬出来的,就是今天要写的这个主题:C++ 中的工厂方法模式。

工厂方法模式是经典创建型设计模式里最好理解、也最容易用出价值的一个。它的核心不是“消灭 new”,而是把“创建哪个具体类”的决策从调用方手里接管过来,交给独立的创建者。对 C++ 还不太熟的读者,可以通过它理解多态、虚函数和智能指针怎么配合;已经在写业务代码的人,则可以用它把散落在各处的 new 分支收口,让新增类型变成一件局部的小事。这篇文章会从最原始的 if-else 代码讲起,逐步演变成符合工厂方法模式的 C++ 实现,再补上现代 C++ 里常用的注册表、依赖注入和测试技巧,最后告诉你哪些场景硬上工厂方法反而更难受。

1. 先搞清楚:工厂方法到底解决什么问题

1.1 从一段“直接 new”的代码说起

不少项目里,消息解析模块的入口都长这样:

std::unique_ptr<Message> createMessage(const std::string& type) { if (type == "json") { return std::make_unique<JsonMessage>(); } if (type == "xml") { return std::make_unique<XmlMessage>(); } return nullptr; }

这段代码本身非常直白,但它的毛病会在需求逐渐变多时被放大。每加一种协议,就要回到这个函数里加一个分支,而且其他业务代码里很可能还有一模一样的 switch 或 if 链,分散在十几个调用现场。改消息类型、加构造参数、调整初始化顺序,你都得把这些地方全部找出来,漏一个就是运行时问题。用设计模式的术语说,这违反了开闭原则:对扩展没有封闭,对修改相当开放。

这种写法的另一个问题是调用方知道了太多不该知道的东西。消息中心本来只关心拿到一个能process()的对象,现在却要显式地写出 JsonMessage、XmlMessage 这些具体类名。一旦产品类和调用方绑死,后面想切到另一种实现,或者想依赖接口写测试,都变得非常别扭。真正的问题不是 new 本身,而是“创建对象的责任放错了位置”。

1.2 定义与由来:让子类决定实例化哪个类

工厂方法模式的官方定义很拗口:定义一个用于创建对象的接口,让子类决定实例化哪一个类。直白一点说,就是把“生产什么东西”变成接口上的一个方法,每个创建者子类自己决定返回哪个具体产品。

在 C++ 里,常见的形态是在基类里声明一个虚的 create 方法,然后各个子类覆盖它。调用方手里拿的是创建者基类的引用或指针,它调用 create() 时只看到 Product 接口,至于实际返回的是 JsonMessage 还是 XmlMessage,由创建者的具体子类在运行时决定。这种设计也被称为“虚构造函数”,意思是在对象创建层面也应用多态。

为什么需要这么绕?因为对象的创建过程往往比简单的 new 复杂得多。有的产品需要从配置里读取参数,有的需要连接缓存池,有的需要在构造之后再做几轮校验。这些逻辑如果散落在调用方代码里,每个调用点都要重复写一遍初始化流程。把它收进创建者子类的 create() 方法,调用方就可以完全不了解具体产品,也不用关心产品是怎么造出来的。你可以把工厂方法理解成一个“配送中心”:用户只管下单,具体是哪个供应商、走什么冷链、用什么包装,都是配送中心内部的事。

1.3 工厂方法 vs 简单工厂 vs 抽象工厂

这里先帮大家把三个容易混的名字区分开。简单工厂其实不算 GoF 的正式模式,它通常是一个普通函数或静态方法,里面用 if-else 或 switch 决定返回什么产品,就像上面那段 createMessage。简单工厂的优点是写起来快,缺点是所有产品选择逻辑还是集中在一个函数里,本质上还是用分支硬编码。

工厂方法模式则把“选择产品”的责任分散到创建者的子类里。使用者的代码依赖的是 Creator 抽象类,而不是某个具体的工厂函数。真正切换产品时,你通常只需要换掉传入的 Creator 实例,产品创建逻辑不会堆积在一个中央函数里。

抽象工厂的粒度更大,它用来创建一组相关的产品族。比如一套跨平台图形库,需要同时创建 Button、Menu、Slider,那么 WindowsUIFactory 和 LinuxUIFactory 分别产出整套不同平台的控件。工厂方法创建单种对象,抽象工厂创建对象族。下面这个对比表适合快速查阅:

模式核心思路典型实现扩展方式
简单工厂用分支在同一个函数里选产品静态函数 + if-else改分支
工厂方法让创建者子类决定具体产品Creator 接口 + 子类覆盖 create新增 Creator 子类
抽象工厂创建一组有关联的产品一族产品的工厂接口新增整套工厂子类

实际项目中,抽象工厂内部常常也由多个工厂方法组织起来,三个概念并不是互斥的。理解它们的分界,主要为了在选择方案时想清楚当前变化的轴在哪里:你要扩展的是一种产品,还是一条产品族?变化的轴不同,模式也要跟着换。

2. C++ 中的经典实现:结构、代码与逐步改造

2.1 产品与创建者的接口设计要点

用 C++ 实现工厂方法模式,第一件事是设计两个抽象接口:产品接口和创建者接口。产品接口定义使用者关心的行为;创建者接口定义创建动作。最基础的代码差不多长这样:

class Message { public: virtual ~Message() = default; virtual void process() = 0; }; class MessageFactory { public: virtual ~MessageFactory() = default; virtual std::unique_ptr<Message> create() = 0; };

这里有几个容易被新手忽略的点。第一,产品基类必须声明虚析构函数,否则通过基类指针删除派生对象属于未定义行为,轻则内存泄漏,重则崩溃。写成virtual ~Message() = default;是目前最干净的做法。第二,工厂方法 create() 返回什么类型需要仔细考虑,后面会专门讨论,默认建议返回std::unique_ptr<Message>,由它表达所有权的转移。

第三,MessageFactory 的析构函数也要写成虚函数。因为调用方经常通过基类指针持有具体的工厂对象,如果没有虚析构,删除时同样会有问题。这些接口设计细节平时不显山不露水,但工厂方法模式的所有安全性都建立在它们之上。

产品接口也不宜设计得太胖。很多工厂被设计出来后发现 create() 返回的对象还要调用者做大量类型强转,这就变味了。正确姿势是让产品接口涵盖所有使用方真正关心的行为,把类型差异隐藏在虚函数实现里。比如 JsonMessage 和 XmlMessage 可以各自实现dump()、parse(),调用方不需要知道底层是什么格式。

2.2 从 switch 分支到多态工厂的迁移实录

假设现在有一个业务模块,启动时需要根据配置创建不同类型的调度器,原来的代码可能是:

std::unique_ptr<Scheduler> makeScheduler(const Config& cfg) { if (cfg.mode == "debug") { return std::make_unique<DebugScheduler>(); } if (cfg.mode == "remote") { return std::make_unique<RemoteScheduler>(); } return std::make_unique<LocalScheduler>(); }

重构的第一步,把 create 动作抽象成工厂接口:

class SchedulerFactory { public: virtual ~SchedulerFactory() = default; virtual std::unique_ptr<Scheduler> create() = 0; };

第二步,为每种调度器写一个工厂子类:

class DebugSchedulerFactory final : public SchedulerFactory { public: std::unique_ptr<Scheduler> create() override { return std::make_unique<DebugScheduler>(); } }; class RemoteSchedulerFactory final : public SchedulerFactory { public: std::unique_ptr<Scheduler> create() override { return std::make_unique<RemoteScheduler>(); } };

第三步,把原来调用 makeScheduler 的地方改成注入 SchedulerFactory:

void startModule(std::unique_ptr<SchedulerFactory> factory) { auto scheduler = factory->create(); scheduler->run(); }

改造之后,调用方不再关心 Config 里是什么 mode,它只知道手里有一个工厂,调用 create() 就能拿到一个符合 Scheduler 接口的对象。而工厂子类把原来散落的分支收敛到了各自的类里。如果你依然需要一个函数根据配置选择工厂,那也简单,配置解析的地方只剩一行分支:return make_unique<RemoteSchedulerFactory>()。对比之前的写法,至少创建调度器的逻辑不会再在业务代码里到处重复。

这个迁移过程也揭示了一个点:工厂方法模式并不是让你彻底消灭 if-else,而是把“选具体类”的决策边界划得清清楚楚。分支仍然存在,但只会出现在系统边界上,不会污染核心业务逻辑。

2.3 模板化与 CRTP:把重复创建器压缩到最少

工厂子类如果太多,代码里会充满大量“换了个名字但内容一模一样”的类。比如 DebugSchedulerFactory、RemoteSchedulerFactory,它们都只是返回对应产品。这种情况下,可以用模板和 CRTP 来消除重复:

template <typename Concrete> class StandardSchedulerFactory : public SchedulerFactory { public: std::unique_ptr<Scheduler> create() final { return std::make_unique<Concrete>(); } }; class DebugSchedulerFactory final : public StandardSchedulerFactory<DebugScheduler> { }; class RemoteSchedulerFactory final : public StandardSchedulerFactory<RemoteScheduler> { };

每个具体工厂类只剩一个空壳,创建逻辑统一由模板基类提供。模板在这里天然适合,因为它把“产品类型”作为编译期参数,等于是让编译器帮我们生成工厂实现。需要注意,如果你某个具体产品需要特殊的构造参数或初始化步骤,那就不能套用这个模板,老老实实在子类里覆盖 create()。模板只解决最常见的情况,遇到不回绝的复杂初始化,还是应该用自定义代码。

用 CRTP 还有一个小好处:create() 可以标记成 final,防止子类再被错误重写。因为 Concrete 类型已经在模板参数里定死了,再重写 create() 通常意味着代码逻辑混乱。把这个方法 final 掉,反而能让意图更清晰。

3. 现代 C++ 实践:内存管理、注册表与依赖注入

3.1 看过那么多代码,工厂方法返回什么类型才安全

关于返回类型,最常见的选择是std::unique_ptr<Product>。工厂生产出来的对象,所有权是从工厂转移给调用方的,调用方持有这个对象的唯一所有权,是最符合“创建后交出去”这个语义的表示。unique_ptr 在 C++11 之后成为移动语义的典型代表,工厂返回它不会产生多余的拷贝,编译器还能做返回值优化,几乎零额外负担。

有的人习惯返回裸指针Product*,这会让调用方陷入“到底该不该 delete、谁来 delete”的迷茫里。也有可能返回std::shared_ptr<Product>,但如果调用方没有共享需求,那 shared_ptr 会引入额外的引用计数开销,没有必要。我的建议是默认 unique_ptr,只在确实需要多方持有同一个产品对象时才转成 shared_ptr。

还有一个容易忽略的问题:工厂返回 nullptr 怎么办。创建失败时,unique_ptr 可以是一个空指针,调用方在使用前必须判断。这个检查和异常方案各有取舍。返回值可空时,用空 unique_ptr 比较直接;创建失败需要携带错误原因时,抛异常也是合理选择。自己的项目里,我会根据工厂被调用的频率和失败概率来决定:高频率、失败可预期,优先返回空指针;低频率、失败需要详细上下文,则抛异常。

另外强调一下,不要用值类型返回产品。Message一旦作为值返回,派生部分就会被“切片”掉,对象退化成基类对象,多态信息彻底丢失。这属于 C++ 特有的坑,老手也容易在重构时踩上,后面问题排查那里会再展开。

3.2 用 std::function 注册表实现运行时扩展

当产品种类非常多,或者希望让模块之间彻底解耦时,经典工厂子类方案还是有点笨重。另一种现代 C++ 流行的做法是“工厂注册表”:把创建动作打包成函数对象,放到一个字典里,按名字或类型标识查表创建。

class ProtocolRegistry { public: using Builder = std::function<std::unique_ptr<Message>()>; void add(std::string name, Builder builder) { builders_[std::move(name)] = std::move(builder); } std::unique_ptr<Message> create(const std::string& name) const { auto it = builders_.find(name); if (it == builders_.end()) { return nullptr; } return it->second(); } private: std::unordered_map<std::string, Builder> builders_; };

使用的时候,可以在模块注册阶段往里塞 lambda:

registry.add("json", [] { return std::make_unique<JsonMessage>(); }); registry.add("xml", [] { return std::make_unique<XmlMessage>(); });

这样的设计下,新增一种协议,只需要在那个协议的实现文件里写一行注册,其他地方完全不用动。如果不想在 main 函数开始前手动注册,还可以利用静态对象构造时自动注册,不过要留意静态初始化顺序问题,最好的做法是用函数内局部静态的 “Meyers 单例”来保存注册表,注册动作也在函数调用里完成:

class ProtocolRegistry { public: static ProtocolRegistry& instance() { static ProtocolRegistry registry; return registry; } // ... };

注册表方案和工厂方法并不冲突。你可以把它看作“工厂的工厂”:注册表里的每个 Builder 本身就是一个匿名工厂。对于经常需要按配置字符串创建对象的序列化、协议解析、插件系统来说,这种组合非常顺手。

3.3 工厂注入配合测试替身,代码立刻变成可测状态

工厂方法模式最大的长期价值之一,是把测试变得简单。假设有一个消息处理中心,它在内部直接 new 具体协议对象,那么单测非常难写,因为真协议对象可能要连接网络、访问文件或者依赖时间。把工厂注入进去,测试时就可以换成假工厂。

class MessageCenter { public: explicit MessageCenter(std::unique_ptr<MessageFactory> factory) : factory_(std::move(factory)) {} std::string handle(const std::string& raw) { auto msg = factory_->create(); if (!msg) return {}; return msg->dump(); } private: std::unique_ptr<MessageFactory> factory_; };

测试代码里只需要一个轻量替身:

class FakeMessage final : public Message { public: void process() override { processed_ = true; } bool processed_ = false; }; class FakeFactory final : public MessageFactory { public: std::unique_ptr<Message> create() override { return std::make_unique<FakeMessage>(); } };

然后构造 MessageCenter 时传入 FakeFactory,就能在完全不依赖具体协议的情况下验证核心逻辑。这种能力在大型 CI 系统里特别重要:业务代码不需要等待外部环境,提交一次代码就能跑出可靠结果。

工厂注入本身也提醒我们一点:对象之间的关系应该尽量通过构造函数传递,而不是在成员函数里用 new 偷偷拉出一个依赖来。只要依赖关系变得透明,替换、测试、维护都会顺很多。

4. 实战案例:一个跨平台消息处理网关

4.1 需求建模:协议上线为什么让人头疼

把前面的理论放在一个具体的场景里看,会很直观。假设你要做一个跨平台的消息处理网关,配置里说明了当前消息是 json、xml 还是 protobuf。每种消息的解析方式不同,序列化输出也不同。更让人头疼的是,市场部随时可能说“下周我们要支持 msgpack”。

如果没有工厂方法,正常做法是给网关主类加一个分支,判断消息格式后 new 相应对象。第一次加协议没感觉,第二次也还能忍,等到第三次、第四次,主类会塞满创建逻辑、配置解析和平台相关代码,谁都很难快速定位一个新增格式到底影响哪些地方。把创建动作抽象出来,就是要把“新增格式影响范围”控制在最小。

4.2 核心实现:产品、创建者与调用方三方解耦

完整的实现可以分成三段看。首先是产品层次:

class Message { public: virtual ~Message() = default; virtual std::string dump() const = 0; }; class JsonMessage final : public Message { public: explicit JsonMessage(std::string source) : source_(std::move(source)) {} std::string dump() const override { return "json:" + source_; } private: std::string source_; }; class XmlMessage final : public Message { public: explicit XmlMessage(std::string source) : source_(std::move(source)) {} std::string dump() const override { return "xml:" + source_; } private: std::string source_; };

然后是创建者层次,每个具体工厂只负责一种产品的创建:

class MessageFactory { public: virtual ~MessageFactory() = default; virtual std::unique_ptr<Message> create(const std::string& source) = 0; }; class JsonMessageFactory final : public MessageFactory { public: std::unique_ptr<Message> create(const std::string& source) override { return std::make_unique<JsonMessage>(source); } }; class XmlMessageFactory final : public MessageFactory { public: std::unique_ptr<Message> create(const std::string& source) override { return std::make_unique<XmlMessage>(source); } };

最后是网关本体,它不再关心具体 Message 类型,只依赖工厂接口:

class Gateway { public: explicit Gateway(std::unique_ptr<MessageFactory> factory) : factory_(std::move(factory)) {} std::string handle(const std::string& input) { auto message = factory_->create(input); if (!message) return {}; return message->dump(); } private: std::unique_ptr<MessageFactory> factory_; };

这里要注意 create() 传入 source 参数,说明工厂方法不一定是无参的。产品可能需要构造参数、环境信息、配置对象,都可以在创建接口里显式传递。这也体现了一个设计经验:把“怎么建”和“建什么”分清楚。建什么由具体工厂决定,怎么建需要什么原料,则在接口签名里约定好。

4.3 扩展与权衡:加一种协议需要动多少代码

现在再回到开头那个需求:新增 msgpack。有了上面的结构,要做的事情是新增一个 MsgPackMessage 产品类,再新增一个 MsgPackMessageFactory,然后在系统装配点把具体工厂传给 Gateway。核心 Gateway 的代码一字不改,现有 json、xml 的测试也完全不用动。

这不是说工厂方法没有代价。每多一种产品,就要多两个类,类数量会翻倍。所以使用前最好想清楚:这个系统的“产品族”会不会频繁扩展?如果产品数量固定不变,而且模块规模很小,用工厂方法反而有点大炮打蚊子。另外,工厂方法带来的是一次虚函数调用和一次堆内存分配,在绝大多数业务系统里可以忽略不计,但如果在极端热路径里每秒创建几百万个对象,堆分配本身的成本就不是模式能解决的,需要考虑对象池或者更扁平的数据结构。

我自己的经验是,架构设计不是为了炫技,而是为了减少未来需求的改动面积。工厂方法真正值钱的地方,不在于它多么优雅,而在于它把“新增一种产品”从“改一堆文件”变成“新增两个类”。这个收益随着项目生命周期越往后越明显。

5. 容易踩的坑与实战经验速查

5.1 构造函数里调用工厂方法:经典得不能再经典的陷阱

很多刚接触 C++ 多态的人会想:既然工厂方法是虚函数,那我在基类构造函数里调用 create(),派生类重写的方法总该生效吧?答案是,不会。

C++ 的规则是:在基类构造阶段,对象的动态类型还是基类,虚函数调用会被解析到基类版本,而不是派生类版本。也就是说,你在 Creator 基类的构造函数里调用一个指向 create() 的虚函数,最后执行的是基类的 create(),派生类的覆盖根本没机会运行。等到派生类构造完成,对象类型才变成真正的派生类型。

这也是工厂方法模式不建议把 create() 放进构造函数的原因。正确做法通常有两种:一种是在代码里显式调用 factory->create(),创建过程由外部驱动;另一种是采用懒初始化,在第一次使用产品时才真正创建。不要在构造函数里期待虚函数分派,这条规则在任何涉及构造的 C++ 设计里都适用。

5.2 切片、悬垂引用与异常安全:细节决定 bug 大小

切片问题在工厂方法的代码 review 里非常常见。如果有人把返回类型写成了Message create(),编译器一般不会立刻报错,反而可能通过拷贝构造把派生对象截断成基类对象。结果就是工厂明明创建了 JsonMessage,但拿到的对象却表现得像 Message 本身。因为抽象类无法直接实例化,这种错误往往以“无法实例化抽象类”的编译错误出现,容易被忽视。

悬垂引用则多出在返回裸指针的地方。假如工厂返回了一个堆对象的裸指针,调用方持有这个指针后又被别人 delete 了,后面任何解引用都是定时炸弹。这个问题在多人协作项目里很难靠自觉避免,使用 unique_ptr 可以直接从类型系统层面拦住它。

异常安全方面,要注意工厂中间步骤抛出异常时资源不能泄漏。假如你手动 new 了两个临时对象,第二个 new 抛异常,第一个对象就没人管了。用 make_unique 或 make_shared 能避免这种问题,因为临时对象在异常路径上会被智能指针正确释放。除非有特殊的内存分配器需求,否则强烈建议用智能指针配合 make 系列函数。

5.3 什么时候最好别用工厂方法

不是所有地方都需要工厂方法。如果整个系统只有一种具体产品,而且你找不到第二个变体的影子,那就直接用 new。这时候抽象出工厂接口只是凭空多了一层间接,没有换来任何灵活性。

如果产品本身是轻量、值语义的小对象,比如一个坐标点、一个配置项,那就更没必要。工厂方法天然和“堆上、可多态、有生命周期”的对象配合得好,对小值类型只会拖慢性能并且让代码变得绕。

更常见的误区是:把工厂做成一个巨大的 switch 分发中心。这样表面上用了工厂,实际上所有选择逻辑还是集中在一个函数里,扩展时还是要改这个函数。真正的工厂方法应该把创建责任分散到多个类。如果发现工厂函数里有一长串 if-else 表示不同类型,说明只是在“简单工厂”层面打转,还没真正用上工厂方法的多态能力。

5.4 常见问题排查速查表与我的几条心得

把实践中遇到的典型问题整理成一张速查表,以后遇到可以直接翻:

现象可能原因建议
删除基类指针时崩溃或资源没释放产品基类缺少虚析构函数声明virtual ~Message() = default;
工厂返回的对象总表现出基类行为返回类型写成了值类型,发生切片返回std::unique_ptr<Product>
注册表里找不到刚注册的类型静态对象初始化顺序不稳定用函数内 static 局部对象保存注册表
工厂里堆满了 if-else 分支只是披着工厂外衣的简单工厂把分支细化到各 Creator 子类里
测试时总是 new 真对象,很难替代依赖没有通过构造函数注入把工厂作为参数传入,测试时换成替身

踩过几次坑之后,我自己总结出一条判断标准:如果“创建谁”这件事正在散落到各个调用方,早点用工厂方法把它收口;如果系统只有一种产品,并且看不到未来的变体,直接 new 反而是更诚实的做法。工厂方法不会消灭设计复杂度,它只是把复杂度放在一个更容易演进的位置上。希望这些经验能帮你在下一次重构里少走一点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 12:00:46

CubismSdkForNative-4-r.1 接入 cocos2dx:Live2D 模型加载、渲染与避坑指南

简介&#xff1a;CubismSdkForNative-4-r.1.zip 是面向 Cocos2dx 原生 C 开发者的 Live2D 集成工具包&#xff0c;用于解决 2D 游戏角色缺乏动态表情与动作表现的问题。它提供 Cubism 框架的本地化版本&#xff0c;让开发者能在 Cocos2dx 项目中加载、渲染并交互控制 Live2D 模…

作者头像 李华
网站建设 2026/10/15 12:00:46

用codegraph构建代码依赖图谱:从“读代码”到“看代码”的实践指南

接手一个陌生的代码库&#xff0c;最让人崩溃的往往不是没有文档&#xff0c;而是你盯着IDE看半天&#xff0c;根本不知道从哪看起。我之前参与一个老系统的维护&#xff0c;改了一个看似人畜无害的公共服务方法&#xff0c;结果上线前三个模块同时报警&#xff0c;我挨个搜调用…

作者头像 李华
网站建设 2026/10/15 12:00:45

逆向工程入门实战:从文件识别到逻辑还原的完整分析流程

1. 从零开始理解逆向工程到底在做什么很多人第一次接触逆向工程这个概念时&#xff0c;脑子里浮现的画面大概是满屏幕的十六进制代码和一堆看不懂的汇编指令。说实话&#xff0c;我刚开始也是这个感觉。但真正上手之后你会发现&#xff0c;逆向工程的核心逻辑其实很朴素——给你…

作者头像 李华
网站建设 2026/10/15 11:59:47

Python+Uniapp全栈开发汉服租赁小程序实战

1. 项目背景与市场需求分析汉服文化近年来在国内年轻群体中快速复兴&#xff0c;据不完全统计&#xff0c;2022年全国汉服爱好者数量已突破700万。这种文化热潮催生了一个新兴市场——汉服体验消费。传统汉服购买存在价格高&#xff08;单件均价500-3000元&#xff09;、使用频…

作者头像 李华
网站建设 2026/10/15 11:59:33

EmbeddingGemma 2:多模态嵌入模型实现本地图文统一向量检索

文本是一条向量&#xff0c;图片是另一条向量&#xff0c;两个向量空间根本不互通——这是我在做图文混合检索那段时间最头疼的事。当时为了在同一个检索服务里既支持“文字搜图片”又支持“图片搜图片”&#xff0c;我手上分别跑着一个文本嵌入模型和一个视觉编码模型&#xf…

作者头像 李华
网站建设 2026/10/15 11:59:31

VBA打造Excel抽卡模拟器:概率权重、保底计数与日志持久化实战

前两期我们搭了一个能用VBA在Excel里抽卡的模拟器&#xff1a;第一期搞清楚了Rnd和Randomize到底怎么用&#xff0c;第二期加了个按钮&#xff0c;单击一下返回SSR/SR/R&#xff0c;旁边还有个计数器在涨。但说实话&#xff0c;那种程度只能叫"能跑"&#xff0c;离&q…

作者头像 李华