You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 09:07:36