4

次の使用例を検討してください。

一部のゲームのモデルでは、Playerクラスがあります。それぞれPlayerunowned let opponent: Player対戦相手を表す があります。これらは常にペアで作成され、 aはオプションではないため、Player常に a が必要opponentです。ただし、これをモデル化するのは非常に困難です。1 人のプレイヤーをもう 1 人のプレイヤーより先に作成する必要があり、最初のプレイヤーは 2 番目のプレイヤーが作成されるまで対戦相手を持たないからです!

いくつかの醜いハッキングを通じて、私はこの解決策を思いつきました:

class Player {
    private static let placeholder: Player = Player(opponent: .placeholder, name: "")

    private init(opponent: Player, name: String) {
        self.opponent = opponent
        self.name = name
    }

    unowned var opponent: Player
    let name: String

    class func getPair(named names: (String, String)) -> (Player, Player) {
        let p1 = Player(opponent: .placeholder, name: names.0)
        let p2 = Player(opponent: p1, name: names.1)
        p1.opponent = p2
        return (p1, p2)
    }
}

let pair = Player.getPair(named:("P1", "P2"))

print(pair.0.opponent.name)
print(pair.1.opponent.name)

これは非常にうまく機能します。しかし、私はopponent定数に変わるのに苦労しています。1 つの解決策は、 private に裏打ちされたopponentなしで計算されたプロパティを作成することですが、これは避けたいと思います。setvar

Swift ポインターを使ってハッキングを試みたところ、次のようになりました。

class func getPair(named names: (String, String)) -> (Player, Player) {
    var p1 = Player(opponent: .placeholder, name: names.0 + "FAKE")
    let p2 = Player(opponent: p1, name: names.1)

    withUnsafeMutablePointer(to: &p1) {
        var trueP1 = Player(opponent: p2, name: names.0)
        $0.moveAssign(from: &trueP1, count: 1)
    }
    return (p1, p2)
}

しかし、これはセグメンテーション違反を引き起こします。さらに、 を使用してデバッグすると、が初期化lldbされた直後に次のことがわかります。p1

(lldb) p p1
(Player2.Player) $R3 = 0x0000000101004390 {
  opponent = 0x0000000100702940 {
    opponent = <uninitialized>
    name = ""
  }
  name = "P1FAKE"
}

しかし、関数の最後に、lldb は次のように表示します。

(lldb) p p1
(Player2.Player) $R5 = 0x00000001010062d0 {
  opponent = 0x00000001010062a0 {
    opponent = 0x0000000101004390 {
      opponent = 0x0000000100702940 {
        opponent = <uninitialized>
        name = ""
      }
      name = "P1FAKE"
    }
    name = "P2"
  }
  name = "P1"
}

(lldb) p p2
(Player2.Player) $R4 = 0x00000001010062a0 {
  opponent = 0x0000000101004390 {
    opponent = 0x0000000100702940 {
      opponent = <uninitialized>
      name = ""
    }
    name = "P1FAKE"
  }
  name = "P2"
}

したがって、p1正しく を指していますp2が、p2それでも古い を指していp1ます。しかも、p1実際に住所が変わった!

私の質問は 2 つあります。

  1. 相互の非オプション参照のこの構造を作成するための、よりクリーンで「迅速な」方法はありますか?

  2. UnsafeMutablePointerそうでない場合、上記のコードが機能しない Swift の s などについて、私は何を誤解していますか?

4

3 に答える 3

2

暗黙的にラップされていないオプションが必要だと思います。感嘆符 ( !) を付けて宣言します。init呼び出し中にプロパティが初期化される可能性がある場合でも、それを使用すると有効な値になるというコンパイラへの約束です。これをプライベート セッターと組み合わせることで、目的を達成できます。

class Player: CustomStringConvertible {
    var name: String
    private(set) weak var opponent: Player!

    init(name: String) {
        self.name = name
    }

    class func getPair(named names: (String, String)) -> (Player, Player) {
        let p1 = Player(name: names.0)
        let p2 = Player(name: names.1)

        p1.opponent = p2
        p2.opponent = p1
        return (p1, p2)
    }

    var description: String {
        return self.name
    }
}

let (p1, p2) = Player.getPair(named: ("Player One", "Player Two"))
print(p1.opponent) // Player Two
print(p2.opponent) // Player One

セッターはプライベートであるため、変更しようとするとコンパイラーはエラーをスローします。

let p3 = Player(name: "Player Three")
p1.opponent = p3 // Error: Cannot assign to property: 'opponent' setter is inaccessible

インスタンスgetPairを作成するための単一のメソッドになることを意図していたため、プロパティを設定しないため、呼び出しを private に設定することもできます。Playerinitopponent

private init(name: String) {
    // ...
}
于 2016-10-14T03:52:28.960 に答える
1

しばらくこれをいじった後、あなたがやりたいことは不可能である可能性が高く、実際には Swift とはうまくいきません。さらに重要なことは、そもそもアプローチに欠陥がある可能性が高いことです。

Swift に関する限り、イニシャライザは格納されたすべての値を初期化してから返す必要があります。これには、いくつかの理由があります。オプション、IUO、計算値は、初期化時に値を保証/計算できない場合に使用されます。Optionals、IUO、または計算された値は必要ないが、初期化後にいくつかの保存された値を設定解除したい場合は、ケーキを持って食べたいと思っています。

設計に関しては、2 つのオブジェクトを初期化時に相互に要求するほど緊密にリンクする必要がある場合、モデルは (IMO) 壊れています。これはまさに、階層データ構造がうまく解決する問題です。あなたの特定の例では、2 人のプレーヤー間の関係を作成および管理するある種の試合または競技オブジェクトが必要であることは明らかです。これが悪い考えではない状況は考えられません。基本的に、カプセル化を破ります。

Player オブジェクトは、Player オブジェクト内に存在するものを管理および追跡する必要があり、Player クラス内で管理される唯一の関係は、その子との間でなければなりません。兄弟関係は、その親によってアクセス/設定する必要があります。

これは、スケールの問題がより明確になります。3人目のプレイヤーを追加したい場合はどうしますか? 50はどうですか?次に、プレーヤーを使用する前に、すべてのプレーヤーを初期化して他のプレーヤーに接続する必要があります。プレーヤーを追加または削除する場合は、接続されているすべてのプレーヤーに対して同時に実行し、その間何も起こらないようにする必要があります。

別の問題は、他の状況では使用できなくなることです。適切に設計されていれば、プレーヤーはあらゆる種類のゲームで使用できます。現在の設計では、1v1 の状況でしか使用できませんでした。それ以外の場合は、書き直す必要があり、コードベースが分岐します。

要約すると、あなたが望むことはおそらくSwiftでは不可能ですが、それが可能になったとしても、それが可能になったとしても、とにかくそれはほとんど間違いなく悪い考えです:)

エッセイで申し訳ありませんが、お役に立てば幸いです!

于 2016-10-20T23:18:36.233 に答える
0

Lazy プロパティ (便利な API 用) と両方のプレーヤーを含むコンテナー (適切なメモリ管理用) を使用して、Swift でこれをきれいに行う方法があります。TL;DR については、以下のサンプル コードをご覧ください。より長い答えについては、以下をお読みください。

定義により、2 つのオブジェクト間のサイクルは、Swift では本質的にオプションでなければなりません。

  1. Swift では、オブジェクトの初期化子が実行されるまでに、オブジェクトのすべてのフィールドを初期化する必要があると規定しています。したがって、参照を使用して 2 つのオブジェクトを結び付けたい場合は、オプション、または暗黙的にアンラップされたオプション参照、または所有されていない参照を使用できます (どちらも初期化が必要なため、対戦相手の前に少なくとも 1 つ存在します)。
  2. オブジェクトがクラス型の場合、弱参照する必要があります。また、弱参照は定義上、本質的にオプションです (自動的にゼロ化され、暗黙的または明示的)。

目的のオブジェクトのように動的に割り当てられたオブジェクトのペアを作成できることは、ガベージ コレクターのある環境ではより自然です (Swift は自動参照カウントを使用し、コードからルート化されていないオブジェクトのペアを単純にリークします)。したがって、Swift では、両方のプレーヤーを含むある種のコンテナーが (絶対に必要ではないにしても) 便利です。

初期化時にしようとしていることができない言語の制限にもかかわらず、モデルには2つのレベルの階層から恩恵を受ける他の問題があると私は主張します。

  • プレーヤーが別のプレーヤーのコンテキストでのみ存在する場合、マッチごとに最大 2 つのプレーヤーしか作成できないはずです。
  • プレイヤーの順番を定義したい場合もあります。たとえば、ターン ベースのゲームで誰が開始するかを決定したり、プレゼンテーションの目的でプレイヤーの 1 人が「ホーム」マッチをプレイしていると定義したりします。

上記の両方の懸念、特に最初の懸念は、プレーヤーの初期化を処理するある種のコンテナ オブジェクトのユーティリティを実際に明確に指し示しています (つまり、そのコンテナのみがプレーヤーの初期化方法を知っており、バインドすることができます)。すべての変更可能なプロパティを一緒に)。以下のコード例のこのコンテナー (Match) はopponent(for:Player)、プレーヤーの対戦相手を照会するメソッドを配置したものです。opponentこのメソッドは、Playerの lazy プロパティで呼び出されます。

public class Match {

    public enum PlayerIndex {
        case first
        case second
    }

    private(set) var players:PlayerPair

    init(players:PlayerNamePair) {
        // match needs to be first set to nil because Match fields need setting before 'self' can be referenced.
        self.players = (Player(match: nil, name: players.A, index: .first),
                        Player(match: nil, name: players.A, index: .second))

        // then set the 'match' reference in the Player objects.
        self.players.A.match = self
        self.players.B.match = self
    }

    public func opponent(for player:Player) -> Player {
        switch (player.index) {
        case .first:
            return self.players.B

        case .second:
            return self.players.A
        }
    }

    /* Player modelled here as a nested type to a Match.
     * That's just a personal preference, but incidental to the question posted. */

    typealias PlayerNamePair = (A:String, B:String)
    typealias PlayerPair = (A:Player, B:Player)

    public class Player {
        public let name:String

        fileprivate let index:PlayerIndex
        fileprivate weak var match:Match?

        /* This init method is only visible inside the file, and only called by Match initializer. */
        fileprivate init(match:Match?, name:String, index:PlayerIndex) {
            self.name = name
            self.match = match
            self.index = index
        }

        /* We dare implicitly unwrap here because Player initialization and lifecycle
        * is controlled by the containing Match.
        *
        * That is, Players only ever exists in context of an owning match,
        * therefore it's OK to treat it as a bug which crashes reproducibly
        * if you query for the opponent for the first time only after the match (which we know to have been non-nil) has already been deallocated. */
        public lazy var opponent:Player = public lazy var opponent:Player = self.match!.opponent(for: self)
    }
}
于 2016-10-23T12:11:44.760 に答える