如何在不使用 var 定义只读属性的情况下共享多个相关结构类型共享 init(..) 方法



我发现自己经常被迫在 Swift 中使用var属性,即使属性只分配一次。

这里有一个例子:我发现让几种类型共享一个 init(...) 的唯一方法是将 init(...) 放在协议扩展中。但是,如果我这样做,则必须在运行协议扩展中的 init(...) 主体之前为结构的属性分配一个虚拟值,届时它将获得其"真实"值。

下面的示例运行没有错误。颜色如何成为let属性,但仍在 Piece 的 init(...) 中分配?

protocol Piece {
var color: Character {set get}   // "w" or "b"
init()
}
extension Piece {
init(color c: Character) {
self.init()
color = c
// Some complex logic here all Pieces share
}
}
struct Knight: Piece {
var color: Character = "_" // annoying dummy value
internal init(){}
}
// ... the other Piece types here ...
let knight = Knight(color: "w")

为了更清楚地说明这一点,希望这就是我想要的:(这不会编译,因为让颜色在结构 Knight

protocol Piece {
let color: Character {get}   // "w" or "b"
}
extension Piece {
init(color c: Character) {
color = c
// Some complex logic here all Pieces share
}
}
struct Knight: Piece {
let color: Character
}
// ... the other Piece types here ...
let knight = Knight(color: "w")

编辑(在我找到答案后,见下文): 陈述主题行问题的另一种方式:几种结构类型如何共享初始化逻辑,同时允许只读属性?

第二次编辑明确表示第二个代码示例无法编译。

第3次编辑明确了哪些让颜色

通过一些重构,可以实现最少的代码重复。以下是我的解决方案,从不同的角度考虑您的方案:

  1. 首先,我注意到你只把"w"或"b"作为你的颜色属性值。由于输入只有两个(或者说是最小)变体,因此可以通过使用具有关联类型和泛型的协议来使颜色成为类型定义本身的一部分,而不是将其作为属性。通过这样做,您不必担心在初始化期间设置属性。

    您可以创建一个协议,即PieceColor并为每种颜色创建一个新类型,即BlackWhite和您的Piece协议可以具有确认PieceColor的关联类型:

    protocol PieceColor {
    // color related common properties
    // and methods
    }
    enum Black: PieceColor { // or can be struct
    // color specific implementations
    }
    enum White: PieceColor { // or can be struct
    // color specific implementations
    }
    protocol Piece {
    associatedtype Color: PieceColor
    }
    

    此方法还提供安全保证,因为您现在将用户输入限制为仅代码旨在处理的值,而不是向用户输入添加其他验证。此外,这可以帮助您根据颜色组实现作品之间的特定关系,即只有相反的颜色才能相互杀死等。

  2. 现在,对于问题的主要部分,您可以创建一个静态方法来初始化您的作品并对其执行一些共享的复杂处理,而不是尝试自定义初始值设定项:

    protocol Piece {
    associatedtype Color: PieceColor
    init()
    static func customInit() -> Self
    }
    extension Piece {
    static func customInit() -> Self {
    let piece = Self()
    // Some complex logic here all Pieces share
    return piece
    }
    }
    struct Knight<Color: PieceColor>: Piece {
    // piece specific implementation
    }
    let wKnight = Knight<White>.customInit()
    let bKnight = Knight<Black>.customInit()
    

仅获取协议属性很酷,因为符合类型在如何定义相应属性方面具有很大的灵活性。所需要的只是该属性是可获取的。

因此,如果使用 get-only 属性定义协议:

protocol Piece {
var color: Character { get }
}

符合类型可以将color属性定义为存储变量、let 常量或计算属性。

存储的变量:

struct Queen: Piece {
var color: Character
}

计算属性:

struct King: Piece {
var color: Character { return "K" }
}

让常量:

struct Knight: Piece {
let color: Character
}

以上每个都满足协议强加的可获取属性要求。

关于初始化。回想一下,swift 会自动为结构创建默认初始值设定项,并且这些初始值设定项具有与结构的每个存储属性对应的参数。Swift 可能会为 Queen 和 Knight 创建一个初始值设定项,如下所示:

init(color: Character) {
self.color = color
}

因此,如果您希望color成为 let 常量,并且希望能够使用初始值设定项对其进行配置,则上述PieceKnight定义就足够了。您无需执行任何其他工作。

您可以像这样实例化骑士:

let knight = Knight(color: "w")

在发现 Charles Srstka 对如何使用 stucts 协议来模拟类继承的答案之后。我已经构建了一个解决方案。它不是最漂亮的,但它确实允许多种结构类型共享初始化逻辑,同时允许使用let定义应该是只读的属性。

这有效:

typealias PropertyValues = (Character) // could be more than one
protocol Piece {
var color: Character {get}   // "w" or "b"
}
extension Piece {
static func prePropSetup(color c: Character) -> PropertyValues {
// logic all Pieces share, that needs to run
//   BEFORE property assignment, goes here
return (c)
}
func postPropSetup(){
// logic all Pieces share, that needs to run
//   AFTER property assignment, goes here
print("Example of property read access: (color)")
}
}
struct Knight: Piece {
let color: Character
init(color c: Character){
(color) = Self.prePropSetup(color: c)
postPropSetup()
}
}
struct Bishop: Piece {
let color: Character
init(color c: Character){
(color) = Self.prePropSetup(color: c)
postPropSetup()
}
}
// ... the other Piece types continue here ...
let knight = Knight(color: "w")
let bishop = Bishop(color: "b")

出于兴趣,一种完全不同的方法:

首先,我们定义一个咖喱函数 - 这个函数取自 https://github.com/pointfreeco/swift-prelude:

public func curry<A, B, C>(_ function: @escaping (A, B) -> C)
-> (A)
-> (B)
-> C {
return { (a: A) -> (B) -> C in
{ (b: B) -> C in
function(a, b)
}
}
}

现在让我们假设我们有一个具有角色的片段,即乐段的类型。 角色可以改变,因为你可以提升棋子。 在这里,我们将使用字符串作为角色:

struct Piece {
let color: Character
var role: String
}

为了分享我们想要白色碎片和黑色碎片的 init,我们加入了 init 函数:

let curryPieceInit = curry(Piece.init(color:role:))

并制作两个部分应用的 init 函数,以 w 或 b 烘焙颜色:

let white = curryPieceInit("w")
let black = curryPieceInit("b")

现在我们可以通过传递剩余的参数来完成部分应用程序,以完全实例化棋子:

let wBish = white("bishop")
let wQueen = white("queen")
let blackPawns = (1...8).map { black("pawn($0)") }

现在使 Role 不是一个字符串,而是一些自定义类型,一个枚举,封装表示各种棋子之间差异的逻辑。

Piece.init(color:role:)设为私有,公开柯里版本。

无需协议。

最新更新