swift 几个相关的struct类型如何共享init(...)方法而不用var定义只读属性?

tsm1rwdh  于 2022-12-10  发布在  Swift
关注(0)|答案(4)|浏览(108)

我发现自己经常被迫在Swift中使用var属性,即使属性只被分配一次。
下面是一个例子:我发现让几个类型共享一个init(...)的唯一方法是把init(...)放在一个协议扩展中。但是如果我这样做,结构体的属性必须在运行协议扩展中的init(...)主体之前被赋一个伪值,当它得到它的“真实的”值时。
下面的例子运行时没有错误。color 是一个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")

为了更清楚地说明这一点,我希望这是我想要的:(由于let color的原因,这不会编译。)

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")

编辑(找到答案后,请参阅以下内容):主题行问题的另一种表述方式:* 如何让几个结构类型共享初始化逻辑,同时允许只读属性let?*
第二次编辑明确说明第二个代码示例无法编译。

k3fezbri

k3fezbri1#

Get-only协议属性很酷,因为符合类型在如何定义相应属性方面有很大的灵活性,所需要的只是属性是可获取的。
因此,如果您使用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")
r9f1avp5

r9f1avp52#

在发现Charles Srstka对How to use protocols for stucts to emulate classes inheritance的答案后,我构建了一个解决方案。它不是最漂亮的,但它确实允许几个结构体类型共享初始化逻辑,同时允许用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")
7gs2gvoe

7gs2gvoe3#

通过一些重构,您可以实现最少的代码重复。下面是我从不同Angular 考虑您的场景的解决方案:
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
}

这种方法还提供了安全保证,因为您现在将用户输入限制为仅代码设计处理的值,而不是对用户输入添加额外的验证。此外,这还有助于您根据颜色组实现片段之间的特定关系,即只有相反的颜色才能杀死对方等。
1.现在是你问题的主要部分,你可以创建一个静态方法来初始化你的片段,并对它进行一些共享的复杂处理,而不是试图自定义初始化器:

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()
dxxyhpgq

dxxyhpgq4#

出于兴趣,我们采用了一种完全替代的方法:
首先我们定义一个咖喱函数--这个函数取自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:)设为私有,公开curried版本。
无需协议。

相关问题