Swift:如何在不使用关联类型时实现ScaleDetails与ComboChord集成?
这个问题在Swift开发里太常见了——关联类型协议确实能帮我们抽象出通用的行为,但一旦想把它们当作普通类型用(比如作为属性),就会碰到「协议只能作为泛型约束」的报错。既然你不想用类和类型转换,我给你几个纯值类型的解决方案,都是Swift原生的思路,你可以根据自己的场景选:
方案一:类型擦除(Type Erasure)—— 最通用的灵活方案
类型擦除的核心思路是创建一个「包装器」结构体,它遵循ScaleDetails协议,内部持有任意符合该协议的实例,并把协议要求的属性、方法转发给内部实例。这样你就可以在ComboChord里用这个包装器作为属性类型,自由切换不同的Scale实例。
代码示例
首先定义你的基础协议和Scale类型:
// 先抽象Mode的通用行为,让不同Scale的Mode都遵循这个协议 protocol ScaleMode: CaseIterable & RawRepresentable where RawValue == String {} enum MajorMode: String, CaseIterable, ScaleMode { case ionian, dorian, phrygian, lydian, mixolydian, aeolian, locrian } enum DiminishedMode: String, CaseIterable, ScaleMode { case wholeHalf, halfWhole } protocol ScaleDetails { associatedtype Mode: ScaleMode var mode: Mode { get set } func transposed(by interval: Int) -> Self } struct MajorScale: ScaleDetails { var mode: MajorMode func transposed(by interval: Int) -> MajorScale { // 实现你的转调逻辑 return self } } struct DiminishedScale: ScaleDetails { var mode: DiminishedMode func transposed(by interval: Int) -> DiminishedScale { // 实现你的转调逻辑 return self } }
然后创建类型擦除的包装器AnyScaleDetails:
struct AnyScaleDetails: ScaleDetails { typealias Mode = ScaleMode private let base: any ScaleDetails var mode: ScaleMode { get { base.mode } set { // 值类型修改属性需要重新创建实例,这里做类型安全的转换 guard var mutableScale = base as? any ScaleDetails else { return } guard let typedMode = newValue as? mutableScale.Mode else { return } mutableScale.mode = typedMode self = AnyScaleDetails(mutableScale) } } init<Scale: ScaleDetails>(_ scale: Scale) { self.base = scale } func transposed(by interval: Int) -> AnyScaleDetails { AnyScaleDetails(base.transposed(by: interval)) } }
最后定义ComboChord协议:
protocol ComboChord { var scale: AnyScaleDetails { get set } // 封装切换逻辑 mutating func switchScale(to newScale: any ScaleDetails) { self.scale = AnyScaleDetails(newScale) } } // 使用示例 struct JazzChord: ComboChord { var scale: AnyScaleDetails = AnyScaleDetails(MajorScale(mode: .ionian)) } var myChord = JazzChord() myChord.switchScale(to: DiminishedScale(mode: .wholeHalf))
这个方案的优点是扩展性极强,新增Scale类型不需要修改现有代码,适合需要频繁切换不同Scale的场景。
方案二:泛型ComboChord协议—— 适合固定Scale类型的场景
如果你的ComboChord实例不需要切换Scale类型(比如一个和弦始终用大调,另一个始终用减调),可以直接把ComboChord定义为泛型协议,用关联类型绑定具体的Scale。
代码示例
protocol ComboChord<Scale> { associatedtype Scale: ScaleDetails var scale: Scale { get set } // 其他和弦属性,比如rootNote: String } // 绑定大调的和弦 struct MajorChord: ComboChord { var scale: MajorScale var rootNote: String } // 绑定减调的和弦 struct DiminishedChord: ComboChord { var scale: DiminishedScale var rootNote: String }
这个方案的优点是极其简单,完全不需要额外的包装逻辑,但缺点是不同类型的ComboChord不能放在同一个集合(比如数组)里,灵活性有限。
方案三:枚举封装所有Scale类型—— 适合Scale类型固定的场景
如果你的Scale类型是有限且固定的(比如只有大调、减调、小调几种),可以用一个枚举把所有Scale作为关联值,然后让这个枚举遵循ScaleDetails协议。
代码示例
enum ScaleEnum: ScaleDetails { case major(MajorScale) case diminished(DiminishedScale) // 包装所有Mode类型的枚举 enum ModeEnum: ScaleMode { case major(MajorMode) case diminished(DiminishedMode) static var allCases: [ModeEnum] { MajorMode.allCases.map(ModeEnum.major) + DiminishedMode.allCases.map(ModeEnum.diminished) } typealias RawValue = String var rawValue: String { switch self { case .major(let mode): return mode.rawValue case .diminished(let mode): return mode.rawValue } } init?(rawValue: String) { if let majorMode = MajorMode(rawValue: rawValue) { self = .major(majorMode) } else if let diminishedMode = DiminishedMode(rawValue: rawValue) { self = .diminished(diminishedMode) } else { return nil } } } typealias Mode = ModeEnum var mode: ModeEnum { get { switch self { case .major(let scale): return .major(scale.mode) case .diminished(let scale): return .diminished(scale.mode) } } set { switch self { case .major(var scale): guard case .major(let newMode) = newValue else { return } scale.mode = newMode self = .major(scale) case .diminished(var scale): guard case .diminished(let newMode) = newValue else { return } scale.mode = newMode self = .diminished(scale) } } } func transposed(by interval: Int) -> ScaleEnum { switch self { case .major(let scale): return .major(scale.transposed(by: interval)) case .diminished(let scale): return .diminished(scale.transposed(by: interval)) } } } // 定义ComboChord protocol ComboChord { var scale: ScaleEnum { get set } mutating func switchScale(to newScale: ScaleEnum) { self.scale = newScale } } struct PopChord: ComboChord { var scale: ScaleEnum = .major(MajorScale(mode: .ionian)) }
这个方案的优点是类型安全、逻辑直观,不需要额外的包装器,但缺点是新增Scale类型必须修改ScaleEnum,扩展性稍差。
内容的提问来源于stack exchange,提问作者Jacob Smolowe

