ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Swinject DI Container 完整指南:服务注册、命名解析、参数注入与 Registration Key 机制剖析

Swinject DI Container 完整指南:服务注册、命名解析、参数注入与 Registration Key 机制剖析 开发工具【免费下载链接】SwinjectDependency injection framework for Swift with iOS/macOS/Linux项目地址https://gitcode.com/gh_mirrors/sw/Swinject点击查看免费下载Swinject 是一个面向 Swift 的轻量级依赖注入Dependency InjectionDI框架而Container类是其全部能力的核心载体。本文基于 Documentation/DIContainer.md 展开系统讲解 Swinject 中 DI Container 的核心概念、服务注册register、依赖解析resolve、命名注册、带参数注册以及决定注册唯一性的 Registration Key 机制并结合仓库源码与测试用例深入底层实现原理。读完本文你将能够熟练使用 Swinject 的Container完成从“注册—解析—注入”的完整装配流程并理解为什么某些注册可以共存、为什么解析会返回nil。什么是 DI Container核心概念与术语依赖注入DI是一种利用控制反转Inversion of ControlIoC来解析依赖的软件设计模式。DI **容器Container**负责管理系统中的类型依赖首先注册那些需要被解析的类型及其依赖然后通过容器获取这些类型的实例——实例的依赖由容器自动解析。在 Swinject 中这个容器由Container类表示对应源码 Sources/Container.swift 中定义的public final class Container。由于不同的依赖注入实现往往使用略有差异的术语容易造成混淆Swinject 明确规定了以下四组定义见 Documentation/DIContainer.md术语含义Service服务定义依赖类型接口的协议protocolComponent组件实际实现某个 Service 的具体类型Factory工厂实例化 Component 的函数或闭包Container容器Component 实例的集合需要注意文中“service instance”与“component instance”含义相同可以互换使用。例如Animal协议是 ServiceCat类是 Component{ _ in Cat(name: Mimi) }闭包是 Factory而Container()创建的实例就是 Container。在 DI Container 中注册服务RegistrationService 与其对应的 Component 通过容器的register方法注册。该方法接收两个要素Component 的服务类型以及一个工厂方法闭包。如果 Component 依赖其他 Service工厂闭包可以调用传入的resolver参数上的resolve方法来“注入”该依赖。依赖的实际底层类型会在 Component 实例创建时才最终确定这正是“依赖的类型由容器延迟决定”的核心价值。下面是一个典型的两层服务注册示例摘自 Documentation/DIContainer.mdlet container Container() container.register(Animal.self) { _ in Cat(name: Mimi) } container.register(Person.self) { r in PetOwner(name: Stephen, pet: r.resolve(Animal.self)!) }其中涉及的协议与类定义如下protocol Animal { var name: String { get } } protocol Person { var name: String { get } } class Cat: Animal { let name: String init(name: String) { self.name name } } class PetOwner: Person { let name: String let pet: Animal init(name: String, pet: Animal) { self.name name self.pet pet } }可以看到PetOwner的工厂闭包通过r.resolve(Animal.self)!向构造器注入了Animal依赖。第二个注册的闭包参数命名为rresolver而第一个闭包中没有用到 resolver因此按 Swift 惯例命名为_。在 Sources/Container.swift 中register的完整签名如下discardableResult public func registerService( _ serviceType: Service.Type, name: String? nil, factory: escaping (Resolver) - Service ) - ServiceEntryService注意两点name参数默认值为nil即不带命名注册返回值是ServiceEntryService且标注discardableResult可以通过方法链继续配置对象作用域inObjectScope或初始化完成回调initCompleted详见 Sources/ServiceEntry.swift。从 DI Container 解析服务Resolution完成注册后即可通过容器的resolve方法获取服务实例它返回指定服务协议类型对应的已解析组件摘自 Documentation/DIContainer.mdlet animal container.resolve(Animal.self)! let person container.resolve(Person.self)! let pet (person as! PetOwner).pet print(animal.name) // prints Mimi print(animal is Cat) // prints true print(person.name) // prints Stephen print(person is PetOwner) // prints true print(pet.name) // prints Mimi print(pet is Cat) // prints true输出结果证明Animal实际解析为Cat实例名字为 MimiPerson实际解析为PetOwner实例并且其pet属性被自动注入为同一个Cat实例。这里用!强制解包是因为当容器中找不到指定服务类型的注册时resolve会返回nil对应 Sources/Container.swift 中resolveService(_: Service.Type, name: String?) - Service?的可空返回类型。从源码看resolve的解析流程Sources/Container.swift大致是根据服务类型、参数类型与名称构造ServiceKey通过getEntry(for:)在当前容器必要时沿 parent 链向上查找注册项命中后调用注册时保存的 factory 闭包并把容器自身作为Resolver传入{ (factory: (Resolver) - Any) in factory(self) }见 Sources/Container.swift若未命中则记录一次resolutionFailed调试日志并返回nil。命名注册Named Registration当需要为同一个服务类型注册两个或多个组件时可以给注册命名以区分彼此摘自 Documentation/DIContainer.mdlet container Container() container.register(Animal.self, name: cat) { _ in Cat(name: Mimi) } container.register(Animal.self, name: dog) { _ in Dog(name: Hachi) }随后用注册名获取对应的服务实例let cat container.resolve(Animal.self, name:cat)! let dog container.resolve(Animal.self, name:dog)! print(cat.name) // prints Mimi print(cat is Cat) // prints true print(dog.name) // prints Hachi print(dog is Dog) // prints true其中Dog类定义为class Dog: Animal { let name: String init(name: String) { self.name name } }在源码中命名注册与命名解析分别体现为register的name: String? nil参数Sources/Container.swift以及Resolver协议中的resolveService(_ serviceType: Service.Type, name: String?) - Service?重载Sources/Resolver.swift。命名作为 Registration Key 的组成部分参与查找详见下文“Registration Key”一节。仓库测试 Tests/SwinjectTests/ContainerTests.swift 中也展示了resolve(Animal.self, name: RegMimi)这类命名解析的用法。带参数注册与解析Registration with Arguments传给register的工厂闭包可以接收在解析服务时传入的参数。注册时参数类型写在Resolver参数之后若闭包不使用 resolver按 Swift 惯例写作_摘自 Documentation/DIContainer.mdcontainer.register(Animal.self) { _, name in Horse(name: name) } container.register(Animal.self) { _, name, running in Horse(name: name, running: running) }解析时再传入运行时参数。只传 1 个参数时使用resolve(_:argument:)摘自 Documentation/DIContainer.mdlet animal1 container.resolve(Animal.self, argument: Spirit)! print(animal1.name) // prints Spirit print((animal1 as! Horse).running) // prints false传 2 个及以上参数时使用resolve(_:arguments:,_)摘自 Documentation/DIContainer.mdlet animal2 container.resolve(Animal.self, arguments: Lucky, true)! print(animal2.name) // prints Lucky print((animal2 as! Horse).running) // prints true其中Horse类定义为摘自 Documentation/DIContainer.mdclass Horse: Animal { let name: String let running: Bool convenience init(name: String) { self.init(name: name, running: false) } init(name: String, running: Bool) { self.name name self.running running } }上述两个注册可以共存于同一容器第一个工厂签名是(Resolver, String) - Animal第二个是(Resolver, String, Bool) - Animal它们的参数元组类型不同因此生成不同的 Registration Key。在源码层带参数的register与resolve重载覆盖了1 到 9 个参数全部定义在 Sources/Container.Arguments.swift如 1 参数的registerService, Arg1、2 参数的resolveService, Arg1, Arg2等。这些重载并非手写而是由 ERB 模板 Sources/Resolver.erb其中arg_count 9与 Sources/Container.Arguments.erb 生成生成规则体现在Resolver.erb中arg_param的定义——1 个参数使用标签argument:2 个及以上使用arguments arg1: Arg1, _ arg2: Arg2...这与文档中的调用形式完全一致。仓库测试 Tests/SwinjectTests/ContainerTests.Arguments.swift 覆盖了 19 个参数的注册与解析场景例如func testContainierAccepts2Arguments() { container.register(Animal.self) { _, arg1, arg2 in Cat(name: arg1 arg2) } let animal container.resolve( Animal.self, arguments: 1, 2 ) XCTAssertEqual(animal?.name, 12) }这印证了文档中“2 个及以上参数使用arguments:形式”的说明。Registration Key注册的唯一标识与覆盖规则某个服务Service对应组件的注册在容器内部会生成一个唯一的键key来存储容器在解析服务依赖时就是用它来查找注册的摘自 Documentation/DIContainer.md。Registration Key 由三部分组成服务的类型service type注册的名称registration name参数的数量与类型number and types of the arguments如果一次新注册与已有注册的 Key 三个部分全部匹配已有注册会被新注册覆盖overwritten。这一机制在源码 Sources/ServiceKey.swift 中有精确对应ServiceKey结构体包含serviceType: Any.Type、argumentsType: Any.Type、name: String?与option: ServiceKeyOption?四个字段其中option是为 SwinjectStoryboard 等扩展预留的键选项。它实现了HashableSources/ServiceKey.swift与EquatableSources/ServiceKey.swift相等性要求serviceType、argumentsType、name与option全部一致。而“覆盖”行为则源于_register中直接对字典执行下标赋值services[key] entrySources/Container.swift——相同的 key 会覆盖旧条目。仓库测试 Tests/SwinjectTests/ServiceKeyTests.swift 验证了相同工厂类型如(Resolver, String, Bool).self与相同名称的 Key 相等而不同服务类型或不同参数类型的 Key 不相等。四种可以共存注册的场景以下四组示例均摘自 Documentation/DIContainer.md场景一服务类型不同可以共存。container.register(Animal.self) { _ in Cat(name: Mimi) } container.register(Person.self) { r in PetOwner(name: Stephen, pet: r.resolve(Animal.self)!) }场景二注册名称不同可以共存。container.register(Animal.self, name: cat) { _ in Cat(name: Mimi) } container.register(Animal.self, name: dog) { _ in Dog(name: Hachi) }场景三参数数量不同可以共存。第一个注册没有参数第二个注册有 1 个参数container.register(Animal.self) { _ in Cat(name: Mimi) } container.register(Animal.self) { _, name in Cat(name: name) }场景四参数类型不同或顺序不同可以共存。第一个注册的参数类型是String与Bool第二个注册的参数类型是Bool与String顺序不同container.register(Animal.self) { _, name, running in Horse(name: name, running: running) } container.register(Animal.self) { _, running, name in Horse(name: name, running: running) }为什么场景四可以共存因为工厂闭包的类型分别是(Resolver, String, Bool) - Animal与(Resolver, Bool, String) - AnimalSwift 会把闭包签名整体作为argumentsType参与生成 Key参数顺序不同导致argumentsType不同Key 自然不同。这一点在 Sources/ServiceKey.swift 的测试中也有对应验证(Resolver, String, Bool).self与(Resolver, String, Int).self会生成不相等的 Key。Remark解析参数类型的严格匹配从容器解析实例时要格外小心参数的类型。只有解析时传入参数的类型与注册时工厂闭包推断出的参数类型完全一致才能成功解析摘自 Documentation/DIContainer.md// Registers with name argument as String. // The argument is inferred as String because Cat initializer takes an argument as String. // The Registration Key is (Animal, (String) - Animal) container.register(Animal.self) { _, name in Cat(name: name) } // This is the correct Registration Key (Animal, (String) - Animal) let name1: String Mimi let mimi1 container.resolve(Animal.self, argument: name1) // Returns a Cat instance. // Cannot resolve since the container has no Registration Key matching (Animal, (NSString) - Animal) let name2: NSString Mimi let mimi2 container.resolve(Animal.self, argument: name2) // Returns nil. // Cannot resolve since the container has no Registration Key matching (Animal, (OptionalString) - Animal) let name3: String? Mimi let mimi3 container.resolve(Animal.self, argument: name3) // Returns nil. // Cannot resolve since the container has no Registration Key matching (Animal, (ImplicitlyUnwrappedOptionalString) - Animal) let name4: String! Mimi let mimi4 container.resolve(Animal.self, argument: name4) // Returns nil.这一“严格类型匹配”的行为根源在于注册时工厂闭包的参数类型会被 Swift 编译器具体化为一个确切的元组类型如(Resolver, String)并写入ServiceKey.argumentsType解析时传入的实参类型同样参与构造 KeySources/Container.swift。Swift 中String、NSString、OptionalStringString?与ImplicitlyUnwrappedOptionalStringString!是不同的类型因此只有第一种情况能命中 Key。实操建议解析时让实参类型与注册时闭包参数的推断类型保持一致或显式标注参数类型避免隐式类型转换导致的nil返回。源码视角Container 如何存储与查找注册为了更深入地理解上述行为可以看 Sources/Container.swift 中Container的关键内部结构存储internal var services ThreadSafeDictionaryServiceKey, ServiceEntryProtocol()即“Key → 注册条目”的字典由线程安全字典实现见 Sources/ThreadSafeDictionary.swift。每个ServiceEntryProtocol保存了工厂闭包、对象作用域object scope、实例存储storage以及initCompleted回调等见 Sources/ServiceEntry.swift。注册_register先构造ServiceKey(serviceType:argumentsType:name:option:)与ServiceEntry然后执行services[key] entrySources/Container.swift。同一 key 的重复注册因此表现为“覆盖”。查找getEntry(for:)先查自身字典未命中再沿parent父容器链向上查找Sources/Container.swift这是容器层级Container Hierarchy得以实现的基础。清理与查询Container还提供removeAll()清空全部注册、hasAnyRegistration(of:name:)检查某服务是否已注册Sources/Container.swift等管理方法。另外register返回的ServiceEntry支持链式配置例如设置对象作用域inObjectScope(.container)单例或追加初始化完成回调initCompleted详见 Sources/ServiceEntry.swift这两类能力分别由 Documentation/ObjectScopes.md 与 Documentation/InjectionPatterns.md 详细展开。总结与延伸阅读Swinject 的 DI Container 用法可以浓缩为三步注册register→ 配置可选链式调用→ 解析resolve。它通过由“服务类型 注册名 参数数量与类型”组成的 Registration Key实现了对注册的精确匹配与同名覆盖使同一服务类型下多个组件、多个参数签名的注册得以共存同时保证依赖注入的类型安全。理解 Registration Key 的组成与严格类型匹配规则是排查“为什么 resolve 返回 nil”类问题的最有效切入点。继续深入可以阅读仓库中的系列文档Documentation/InjectionPatterns.md初始化器注入、属性注入、方法注入与initCompleted回调Documentation/CircularDependencies.md循环依赖的解析策略Documentation/ObjectScopes.mdgraph、container等对象作用域Documentation/ContainerHierarchy.md父容器与子容器的层级查找Documentation/ThreadSafety.md多线程解析与synchronize()。如果想动手验证可在 Tests/SwinjectTests/Animal.swift 找到Cat、Dog等测试用组件定义并在 Tests/SwinjectTests/ContainerTests.Arguments.swift 与 Tests/SwinjectTests/ServiceKeyTests.swift 中看到与本文示例一一对应的单元测试。赞分享开发工具【免费下载链接】SwinjectDependency injection framework for Swift with iOS/macOS/Linux项目地址https://gitcode.com/gh_mirrors/sw/Swinject点击查看免费下载相关推荐Swinject依赖注入容器(DI Container)深度解析Swinject依赖注入容器 DI Container 深度解析 什么是依赖注入容器 依赖注入 Dependency Injection, DI 是一种重要的软开发工具Swinject依赖注入框架模块化服务注册指南Swinject依赖注入框架模块化服务注册指南 前言 在现代软件开发中依赖注入 Dependency Injection 是一种重要的设计模式它能够帮助我开发工具AutoMapper 依赖注入DI完全指南AddAutoMapper、服务注册与低层 API 详解AutoMapper 依赖注入DI完全指南AddAutoMapper、服务注册与低层 API 详解 导读 本文基于 AutoMapper 官方文档的 De后端上一篇如何在5分钟内掌握rclone云存储同步完整实战指南下一篇抖音下载器完整使用指南轻松保存无水印视频和直播回放创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进