面向对象 & 面向协议
一、核心概念与对比
面试题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 为何难用?
关联类型让协议不再是“一种具体类型”,需要类型擦除或泛型承接。
参考资料