项目架构
Oop&pop

面向对象 & 面向协议

一、核心概念与对比

面试题1:面向对象(OOP)的三大特性是什么?在Swift里如何体现?

回答:

  • 封装:隐藏实现细节,仅暴露必要接口。Swift 中用 private/fileprivate/internal/public/open 控制访问级别;计算属性、方法、嵌套类型都可封装。
  • 继承:class 支持单继承,复用与多态的常见来源;final 可禁止继承或重写。
  • 多态:同一接口多种实现。Swift 支持动态派发(类的 override)与静态派发(泛型/协议扩展的静态分发)。
class Animal { func speak() { print("...") } }
class Dog: Animal { override func speak() { print("woof") } } // 动态派发
func talk<T: Animal>(_ a: T) { a.speak() } // 泛型位置通常走静态派发

面试题2:Swift 的“面向协议(POP)”与 OOP 的根本差异?

回答:

  • 复用方式:OOP 侧重“通过继承复用”;POP 侧重“通过协议 + 协议扩展复用”。
  • 抽象表达:OOP 用基类;POP 用“协议(接口)+ 约束(where)+ 组合”。
  • 类型选择:POP 更鼓励值类型(struct/enum)配合协议,获得更好线程安全、复制语义性能
  • 多继承问题:Swift 不支持类多继承,但协议可多重遵循,天然避免菱形继承问题。

面试题3:继承 vs 组合,如何取舍?

回答:

  • 用继承:存在“is-a”强关系;需要重用父类状态/行为且需动态多态。
  • 用组合:更通用,防止层级膨胀;易于替换与测试。Swift 提倡“组合优于继承”,配合协议实现策略/装饰等模式。

面试题4:Swift 中值类型与引用类型的边界选择?

回答:

  • 值类型(struct/enum):小而明确的数据模型、不可变为主、跨线程传递安全。
  • 引用类型(class):需要共享可变状态、身份(identity)、继承/ARC 生命周期管理的场景。
  • 经验法则:默认用 struct,当且仅当需要身份共享/继承时使用 class。

面试题5:SOLID 原则在 iOS/Swift 的落地要点?

回答:

  • S(单一职责):模块以“一个变化原因”为单位拆分;ViewModel 不要塞网络/缓存。
  • O(开闭):对扩展开放、对修改关闭;用协议扩展/装饰器替代条件分支。
  • L(里氏替换):子类必须可替换父类;协议抽象保证一致契约。
  • I(接口隔离):细粒度协议,按需拆分接口,减少“胖协议”。
  • D(依赖倒置):上层依赖抽象;用 protocol + 依赖注入(构造/属性/方法注入)。

二、Swift 面向协议(POP)考点

面试题6:什么是协议(protocol)?与接口有何不同?

回答:

  • 协议定义能力集合(方法/属性/关联类型等)。类型通过 : Protocol 声明遵循。
  • 与传统接口差异:Swift 协议可有默认实现(在协议扩展里),还能声明**关联类型(associatedtype)**形成“泛型协议”。
protocol Cache {
    associatedtype Key: Hashable
    associatedtype Value
    subscript(_ key: Key) -> Value? { get set }
}

面试题7:协议扩展与默认实现的派发规则?

回答:

  • 协议要求 + 类型实现:走动态派发(存在类型/类时更明显)。
  • 协议扩展里仅存在的默认实现:多为静态派发(取决于调用站位)。
  • 结论:在泛型约束位置(T: P)优先走静态派发;在存在类型位置(any P/P 旧写法)走动态语义受限。为避免“看似重写却未被调用”的坑——总在协议里声明需求,把默认实现放协议扩展里,类型再“明确实现”需要定制的成员。

面试题8:存在类型(

any Protocol

)与泛型(

<T: Protocol>

)的差异与选择?

回答:

  • 存在类型:抹去具体类型,仅保留“符合协议”的抽象;易用但会失去关联类型/Self 约束能力,并可能有装箱与间接调用开销
  • 泛型:在编译期保留具体类型,性能更好、类型安全更强,但 API 更模板化。
  • 经验:读多写少、边界层/储存异质集合 → any P;性能敏感/算法内核 → 泛型 <T: P>。

面试题9:带

associatedtype

的“泛型协议(PAT)”为什么不能直接当作类型用?

回答:

  • 因为编译器无法在存在类型位置推断关联类型的具体参数,导致无法构造一个单一的“具体类型”。

  • 解决:

    1)类型擦除(Type Erasure)包一层盒子;

    2)改用泛型参数暴露关联类型;

    3)将协议改造为不带关联类型 + 以 any 结合泛型容器。

protocol Producer { associatedtype Output; func next() -> Output }
struct AnyProducer<Out>: Producer {
    private let _next: () -> Out
    init<P: Producer>(_ p: P) where P.Output == Out { _next = p.next }
    func next() -> Out { _next() }
}

面试题10:什么是“类型擦除(Type Erasure)”?何时使用?

回答:

  • 用统一外壳隐藏内部具体类型与关联类型,让不同实现以同一“外观类型”暴露。
  • 典型场景:异质集合依赖注入边界库对外 API 稳定

面试题11:协议组合、where 约束的实用范式?

回答:

  • 协议组合:A & B 或 any A & B。
  • where 约束:为协议扩展/泛型函数提供精确能力定向,避免胖协议。
protocol JSONEncodable { func toJSON() -> Data }
protocol Identifiable { associatedtype ID: Hashable; var id: ID { get } }

extension Collection where Element: JSONEncodable & Identifiable {
    func toJSONArray() -> Data { /* 聚合编码 */ Data() }
}

面试题12:协议默认实现 vs 继承的模板方法,如何权衡?

回答:

  • 默认实现让“所有遵循者”立刻获得能力,不侵入数据模型
  • 模板方法多见于类继承,耦合度更高。
  • 可组合替换继承:用“协议 + 默认实现 + 组合”拼装行为,再按需覆盖。

面试题13:

Self

要求与不可作为存在类型的原因?

回答:

  • 协议里若使用 Self(如返回 Self 或在约束中引用自身),说明语义依赖具体实现类型;因此不能以 any P 直接用作变量类型,需泛型或类型擦除。

面试题14:协议与泛型对性能的影响?

回答:

  • 泛型静态派发更利于内联与优化;
  • *存在类型/协议见证表(witness table)**调用有间接层,常略慢;
  • 实战中优先保证清晰设计,对热点路径再做泛型化/去虚拟化。

面试题15:POP 如何提升可测试性与可替换性?

回答:

  • 面向抽象编程,上层依赖 protocol;
  • 通过注入 Mock/Stub 实现替换真实依赖;
  • 避免硬编码单例,改用构造注入;网络层、存储层、时钟/UUID 提供器都应抽象为协议。
protocol Clock { func now() -> Date }
struct SystemClock: Clock { func now() -> Date { Date() } }
struct FixedClock: Clock { let fixed: Date; func now() -> Date { fixed } }

三、典型设计模式在 Swift/POP 的实现

面试题16:策略(Strategy)如何用协议实现?为何优于继承?

回答:

  • 协议表达“可替换的算法族”,组合到调用方,运行时自由切换,避免继承层级。
protocol PricingStrategy { func price(for base: Double) -> Double }
struct Discount: PricingStrategy { let rate: Double; func price(for b: Double) -> Double { b * (1 - rate) } }
struct Full: PricingStrategy { func price(for b: Double) -> Double { b } }

struct Cart {
    var strategy: any PricingStrategy
    func checkout(base: Double) -> Double { strategy.price(for: base) }
}

面试题17:装饰器(Decorator)如何用协议与组合实现?

回答:

  • 以相同协议包装被装饰对象,链式叠加能力,避免继承爆炸。
protocol ImageLoader { func load(_ url: URL) async throws -> Data }

struct NetworkLoader: ImageLoader { /* ... */ func load(_ u: URL) async throws -> Data { Data() } }

struct CachedLoader: ImageLoader {
    let base: any ImageLoader
    func load(_ u: URL) async throws -> Data {
        // 命中返回;否则 base.load 并写入缓存
        try await base.load(u)
    }
}

面试题18:适配器(Adapter)在 Swift 中的落地?

回答:

  • 用协议定义目标接口,写一个适配层把第三方库接口翻译成目标协议,隔离依赖,便于替换与单测。

面试题19:Builder/Fluent API 如何用值类型实现可读性与安全?

回答:

  • 使用不可变 struct + 返回新副本的链式方法,线程安全且利于推断。
struct Request {
    let path: String
    var headers: [String:String] = [:]

    func header(_ k: String, _ v: String) -> Request {
        var copy = self; copy.headers[k] = v; return copy
    }
}

四、语言细节与易错点

面试题20:协议与 ARC/内存管理需要注意什么?

回答:

  • 协议属性若是引用类型,闭包捕获时仍需**[weak self]** 等避免循环引用;
  • any 包装会产生间接存储,在热点路径注意拷贝/装箱成本。

面试题21:

Any

AnyObject

any P

的区别?

回答:

  • Any:任意类型。
  • AnyObject:任意类型(引用语义)。
  • any P:存在类型的协议值,保存“遵循 P 的某个对象”,能力受限于协议要求。

面试题22:协议扩展与同名方法“阴影”问题?

回答:

  • 仅在协议扩展里提供方法A的默认实现,具体类型新增同名自由函数/扩展方法可能不会覆盖(取决于静/动态派发位置)。
  • 实战规则:把要暴露的 API 先写进协议声明,再放默认实现,避免阴影。

面试题23:如何用

where

为特定遵循者“定向增强”?

回答:

  • 在协议扩展上加约束,精准投放能力,避免污染全局。
protocol Summable { static func + (lhs: Self, rhs: Self) -> Self }
extension Array where Element: Summable {
    func sum() -> Element { reduce(self[0], +) }
}

面试题24:不透明类型

some

与协议的关系?

回答:

  • some P 在返回位置承诺“是某个固定但隐藏的具体类型,且符合 P”;
  • 与 any P 相反:some 保留静态类型优势(可内联优化),但调用方不能依赖其具体类型
protocol Shape { func area() -> Double }
struct Circle: Shape { let r: Double; func area() -> Double { .pi*r*r } }
func makeShape() -> some Shape { Circle(r: 2) } // 调用方只知其符合 Shape

五、工程化与实战题

面试题25:如何用 POP 设计可替换的网络/存储层并支持单元测试?

回答:

要点:

1)定义小而精的协议:HTTPRequesting、KeyValueStoring;

2)业务层仅依赖协议;

3)在 App 组装时注入 Prod 实现;单测替换为 Mock;

4)在边界层(例如 ViewController)将协议值存为 any P,在内核处用泛型保性能。


面试题26:如何在 Swift 中落地依赖注入(DI)?

回答:

  • 构造注入为首选;方法注入用于临时能力;避免全局单例耦合。
  • 配合协议抽象 + 工厂/组装器,可在 UI 入口集中装配。

面试题27:POP 会不会导致“协议过多、碎片化”?

回答:

  • 风险存在。治理策略:

    1)按领域边界定义协议;

    2)拒绝“为了抽象而抽象”,以用例驱动

    3)对外 API 尽量稳定,以类型擦除统一异质实现;

    4)定期协议盘点与合并,避免胖/薄失衡。


六、快速复盘口诀

  • 默认用 struct,组合胜过继承
  • 依赖抽象(protocol),行为用扩展复用
  • 泛型保性能,any 保易用
  • PAT 用类型擦除,Self/关联类型别乱放
  • 把需求写进协议声明,默认实现放扩展
  • 边界用存在类型,内核用泛型
  • 小接口、强约束、where 定向增强

七、简答速背(面试现场直击)

POP 的一句话定义

用协议表达能力、用扩展复用行为、用组合拼装对象,优先值类型,必要时用类型擦除统一抽象。

存在类型 vs 泛型

any P 易用但弱约束、可能有开销;<T: P> 强约束高性能。边界 any,核心泛型。

PAT 为何难用?

关联类型让协议不再是“一种具体类型”,需要类型擦除或泛型承接。


参考资料