見出し画像

Swiftでプログラミング-Advanced Operators2

Precedence and Associativity

演算子の優先順位は、一部の演算子に他の演算子よりも高い優先順位を与えます。 これらの演算子が最初に適用されます。

演算子の結合性は、同じ優先順位の演算子を左からグループ化するか、右からグループ化するかを定義します。 「彼らは彼らの左の表現に関連している」または「彼らは彼らの右の表現に関連している」という意味としてそれを考えてください。

複合式が計算される順序を決定するときは、各演算子の優先順位と結合性を考慮することが重要です。 たとえば、演算子の優先順位は、次の式が17に等しい理由を説明します。

    2 + 3 % 4 * 5
   // this equals 17

厳密に左から右に読む場合、式は次のように計算されると予想される場合があります。

・2プラス3は5に等しい
・5剰余4は1に等しい
・1 x5は5に等しい

ただし、実際の答えは5ではなく17です。優先順位の高い演算子は、優先順位の低い演算子よりも先に評価されます。 Swiftでは、Cと同様に、剰余演算子(%)と乗算演算子(*)の優先順位が加算演算子(+)よりも高くなります。 その結果、追加が検討される前に、両方が評価されます。

ただし、剰余と乗算は互いに同じ優先順位を持ちます。 使用する正確な評価順序を決定するには、それらの関連性も考慮する必要があります。 剰余と乗算はどちらも、左側の式に関連付けられています。 これは、式のこれらの部分の左から始めて、暗黙の括弧を追加することと考えてください。

2 + ((3 % 4) * 5)

(3%4)は3なので、これは次と同等です。

2 + (3 * 5)

(3 * 5)は15なので、これは次と同等です。

2 + 15

この計算により、最終的な答えは17になります。

演算子の優先順位グループと結合性設定の完全なリストを含む、Swift標準ライブラリによって提供される演算子については、演算子の宣言を参照してください。

Swiftの演算子の優先順位と結合性の規則は、CやObjective-Cに見られるものよりも単純で予測可能です。 ただし、これは、Cベースの言語とまったく同じではないことを意味します。 既存のコードをSwiftに移植するときは、オペレーターの相互作用が意図したとおりに動作するように注意してください。

Operator Methods

クラスと構造体は、既存の演算子の独自の実装を提供できます。 これは、既存の演算子のオーバーロードとして知られています。

以下の例は、カスタム構造の算術加算演算子(+)を実装する方法を示しています。 算術加算演算子は、2つのターゲットで動作するため二項演算子であり、これら2つのターゲットの間にあるため中置と呼ばれます。

この例では、2次元の位置ベクトル(x、y)のVector2D構造を定義し、続いてVector2D構造のインスタンスを追加する演算子メソッドを定義します。

   struct Vector2D {
       var x = 0.0, y = 0.0
   }
   extension Vector2D {
       static func + (left: Vector2D, right: Vector2D) -> Vector2D {
           return Vector2D(x: left.x + right.x, y: left.y + right.y)
       }
   }

演算子メソッドは、Vector2Dの型メソッドとして定義されており、メソッド名はオーバーロードされる演算子(+)と一致します。 加算はベクトルの基本的な動作の一部ではないため、typeメソッドは、Vector2Dのメイン構造宣言ではなく、Vector2Dの拡張で定義されます。 算術加算演算子は二項演算子であるため、この演算子メソッドは、Vector2D型の2つの入力パラメーターを受け取り、同じくVector2D型の単一の出力値を返します。

この実装では、入力パラメーターの名前はleftとrightで、+演算子の左側と右側にあるVector2Dインスタンスを表します。 このメソッドは、新しいVector2Dインスタンスを返します。このインスタンスのxプロパティとyプロパティは、一緒に追加された2つのVector2Dインスタンスからのxプロパティとyプロパティの合計で初期化されます。

typeメソッドは、既存のVector2Dインスタンス間の中置演算子として使用できます。

   let vector = Vector2D(x: 3.0, y: 1.0)
   let anotherVector = Vector2D(x: 2.0, y: 4.0)
   let combinedVector = vector + anotherVector
   // combinedVector is a Vector2D instance with values of (5.0, 5.0)

この例では、以下に示すように、ベクトル(3.0、1.0)と(2.0、4.0)を加算して、ベクトル(5.0、5.0)を作成します。

Prefix and Postfix Operators

上に示した例は、バイナリ中置演算子のカスタム実装を示しています。 クラスと構造体は、標準の単項演算子の実装を提供することもできます。 単項演算子は単一のターゲットで動作します。 ターゲットの前にある場合は接頭辞(-aなど)、ターゲットの後に続く場合は接尾辞演算子(b!など)です。

演算子メソッドを宣言するときにfuncキーワードの前に接頭辞または接尾辞修飾子を書き込むことにより、接頭辞または接尾辞の単項演算子を実装します。

    extension Vector2D {
       static prefix func - (vector: Vector2D) -> Vector2D {
           return Vector2D(x: -vector.x, y: -vector.y)
       }
   }

上記の例では、Vector2Dインスタンスの単項マイナス演算子(-a)を実装しています。 単項マイナス演算子はプレフィックス演算子であるため、このメソッドはプレフィックス修飾子で修飾する必要があります。

単純な数値の場合、単項マイナス演算子は正の数を負の数に変換し、その逆も同様です。 Vector2Dインスタンスの対応する実装は、xプロパティとyプロパティの両方でこの操作を実行します。

   let positive = Vector2D(x: 3.0, y: 4.0)
   let negative = -positive
   // negative is a Vector2D instance with values of (-3.0, -4.0)
   let alsoPositive = -negative
   // alsoPositive is a Vector2D instance with values of (3.0, 4.0)

Compound Assignment Operators

複合代入演算子は、代入(=)を別の演算と組み合わせます。 たとえば、加算代入演算子(+ =)は、加算と代入を1つの演算に結合します。 パラメータの値は演算子メソッド内から直接変更されるため、複合代入演算子の左側の入力パラメータタイプをinoutとしてマークします。

以下の例では、Vector2Dインスタンスの加算代入演算子メソッドを実装しています。

    extension Vector2D {
       static func += (left: inout Vector2D, right: Vector2D) {
           left = left + right
       }
   }

加算演算子は以前に定義されているため、ここで加算プロセスを再実装する必要はありません。 代わりに、加算代入演算子メソッドは、既存の加算演算子メソッドを利用し、それを使用して、左の値を左の値に右の値を加えたものに設定します。

   var original = Vector2D(x: 1.0, y: 2.0)
   let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
   original += vectorToAdd
   // original now has values of (4.0, 6.0)
デフォルトの代入演算子(=)をオーバーロードすることはできません。 オーバーロードできるのは、複合代入演算子のみです。 同様に、三項条件演算子(a?b:c)はオーバーロードできません。

Equivalence Operators

デフォルトでは、カスタムクラスと構造体には、等価演算子(==)および等しくない演算子(!=)と呼ばれる等価演算子の実装がありません。 通常は==演算子を実装し、==演算子の結果を無効にする!=演算子の標準ライブラリのデフォルト実装を使用します。 ==演算子を実装するには、2つの方法があります。自分で実装するか、多くの種類の場合、Swiftに実装を合成するように依頼できます。 どちらの場合も、標準ライブラリのEquatableプロトコルへの適合性を追加します。

他の中置演算子を実装するのと同じ方法で、==演算子の実装を提供します。

   extension Vector2D: Equatable {
       static func == (left: Vector2D, right: Vector2D) -> Bool {
           return (left.x == right.x) && (left.y == right.y)
       }
   }

上記の例では、==演算子を実装して、2つのVector2Dインスタンスが同等の値を持っているかどうかを確認しています。 Vector2Dのコンテキストでは、「等しい」を「両方のインスタンスが同じx値とy値を持つ」ことを意味すると考えるのが理にかなっているため、これはオペレーターの実装で使用されるロジックです。

これで、この演算子を使用して、2つのVector2Dインスタンスが同等であるかどうかを確認できます。

   let twoThree = Vector2D(x: 2.0, y: 3.0)
   let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
   if twoThree == anotherTwoThree {
       print("These two vectors are equivalent.")
   }
   // Prints "These two vectors are equivalent."

多くの単純なケースでは、等価演算子の合成された実装を提供するように。

Custom Operators

Swiftが提供する標準の演算子に加えて、独自のカスタム演算子を宣言して実装できます。 カスタム演算子の定義に使用できる文字のリストについては、「演算子」を参照してください。

新しい演算子は、operatorキーワードを使用してグローバルレベルで宣言され、接頭辞、中置、または後置修飾子でマークされます。

prefix operator +++

上記の例では、+++という新しいプレフィックス演算子を定義しています。 この演算子はSwiftには既存の意味がないため、Vector2Dインスタンスを操作する特定のコンテキストで、以下に独自のカスタムの意味が与えられています。 この例では、+++は新しい「プレフィックスダブリング」演算子として扱われます。 前に定義した加算代入演算子を使用してベクトルをそれ自体に加算することにより、Vector2Dインスタンスのx値とy値を2倍にします。 +++演算子を実装するには、次のように+++という型メソッドをVector2Dに追加します。

   extension Vector2D {
       static prefix func +++ (vector: inout Vector2D) -> Vector2D {
           vector += vector
           return vector
       }
   }
   var toBeDoubled = Vector2D(x: 1.0, y: 4.0)
   let afterDoubling = +++toBeDoubled
   // toBeDoubled now has values of (2.0, 8.0)
   // afterDoubling also has values of (2.0, 8.0)

Precedence for Custom Infix Operators

カスタム中置演算子はそれぞれ優先グループに属します。 優先順位グループは、他の中置演算子に対する演算子の優先順位と、演算子の結合性を指定します。 これらの特性が中置演算子と他の中置演算子との相互作用にどのように影響するかについては、優先順位と結合性を参照してください。

優先順位グループに明示的に配置されていないカスタム中置演算子には、3項条件演算子の優先順位よりもすぐに高い優先順位を持つデフォルトの優先順位グループが与えられます。

次の例では、優先グループAdditionPrecedenceに属する+-という新しいカスタム中置演算子を定義しています。

    infix operator +-: AdditionPrecedence
   extension Vector2D {
       static func +- (left: Vector2D, right: Vector2D) -> Vector2D {
           return Vector2D(x: left.x + right.x, y: left.y - right.y)
       }
   }
   let firstVector = Vector2D(x: 1.0, y: 2.0)
   let secondVector = Vector2D(x: 3.0, y: 4.0)
   let plusMinusVector = firstVector +- secondVector
   // plusMinusVector is a Vector2D instance with values of (4.0, -2.0)

この演算子は、2つのベクトルのx値を合計し、最初のベクトルから2番目のベクトルのy値を減算します。 本質的に「加法」演算子であるため、+や-などの加法中置演算子と同じ優先順位グループが与えられています。 演算子の優先順位グループと結合性設定の完全なリストを含む、Swift標準ライブラリによって提供される演算子については、演算子の宣言を参照してください。 優先順位グループの詳細、および独自の演算子と優先順位グループを定義するための構文については、「演算子の宣言」を参照してください。

接頭辞または接尾辞演算子を定義するときに優先順位を指定しません。 ただし、プレフィックス演算子とポストフィックス演算子の両方を同じオペランドに適用する場合は、ポストフィックス演算子が最初に適用されます。

Result Builders

結果ビルダーは、リストやツリーなどのネストされたデータを自然で宣言的な方法で作成するための構文を追加する、定義するタイプです。 結果ビルダーを使用するコードには、ifやforなどの通常のSwift構文を含めて、条件付きまたは繰り返しのデータを処理できます。

以下のコードは、StarsとTextを使用して1本の線に描画するためのいくつかの型を定義しています。

   protocol Drawable {
       func draw() -> String
   }
   struct Line: Drawable {
       var elements: [Drawable]
       func draw() -> String {
           return elements.map { $0.draw() }.joined(separator: "")
       }
   }
   struct Text: Drawable {
       var content: String
       init(_ content: String) { self.content = content }
       func draw() -> String { return content }
   }
   struct Space: Drawable {
       func draw() -> String { return " " }
   }
   struct Stars: Drawable {
       var length: Int
       func draw() -> String { return String(repeating: "*", count: length) }
   }
   struct AllCaps: Drawable {
       var content: Drawable
       func draw() -> String { return content.draw().uppercased() }
   }

Drawableプロトコルは、線や形状など、描画できるものの要件を定義します。型はdraw()メソッドを実装する必要があります。 Line構造は単線の図面を表し、ほとんどの図面の最上位のコンテナとして機能します。 線を描画するために、構造体は線の各コンポーネントでdraw()を呼び出し、結果の文字列を1つの文字列に連結します。 Text構造体は、文字列を折り返し、図面の一部にします。 AllCaps構造体は、別の図面を折り返し、変更して、図面内のテキストを大文字に変換します。

イニシャライザを呼び出すことで、次の型で図面を作成できます。

   let name: String? = "Ravi Patel"
   let manualDrawing = Line(elements: [
       Stars(length: 3),
       Text("Hello"),
       Space(),
       AllCaps(content: Text((name ?? "World") + "!")),
       Stars(length: 2),
       ])
   print(manualDrawing.draw())
   // Prints "***Hello RAVI PATEL!**"

このコードは機能しますが、少し厄介です。 AllCapsの後の深くネストされた括弧は読みにくいです。 名前がnilのときに「World」を使用するフォールバックロジックは、??を使用してインラインで実行する必要があります。 演算子。これは、より複雑なものでは困難です。 図面の一部を構築するためにスイッチまたはforループを含める必要がある場合、それを行う方法はありません。 結果ビルダーを使用すると、通常のSwiftコードのように見えるように、このようなコードを書き直すことができます。

結果ビルダーを定義するには、型宣言に@resultBuilder属性を記述します。 たとえば、このコードは、DrawingBuilderと呼ばれる結果ビルダーを定義します。これにより、宣言型構文を使用して図面を記述できます。

   @resultBuilder
   struct DrawingBuilder {
       static func buildBlock(_ components: Drawable...) -> Drawable {
           return Line(elements: components)
       }
       static func buildEither(first: Drawable) -> Drawable {
           return first
       }
       static func buildEither(second: Drawable) -> Drawable {
           return second
       }
   }

DrawingBuilder構造体は、結果ビルダー構文の一部を実装する3つのメソッドを定義します。 buildBlock(_ :)メソッドは、コードのブロックに一連の行を書き込むためのサポートを追加します。 そのブロック内のコンポーネントを1つのラインに結合します。 buildEither(first :)メソッドとbuildEither(second :)メソッドは、if-elseのサポートを追加します。

@DrawingBuilder属性を関数のパラメーターに適用できます。これにより、関数に渡されたクロージャーが、結果ビルダーがそのクロージャーから作成する値に変換されます。 例えば:

   func draw(@DrawingBuilder content: () -> Drawable) -> Drawable {
       return content()
   }
   func caps(@DrawingBuilder content: () -> Drawable) -> Drawable {
       return AllCaps(content: content())
   }
   func makeGreeting(for name: String? = nil) -> Drawable {
       let greeting = draw {
           Stars(length: 3)
           Text("Hello")
           Space()
           caps {
               if let name = name {
                   Text(name + "!")
               } else {
                   Text("World!")
               }
           }
           Stars(length: 2)
       }
       return greeting
   }
   let genericGreeting = makeGreeting()
   print(genericGreeting.draw())
   // Prints "***Hello WORLD!**"
   let personalGreeting = makeGreeting(for: "Ravi Patel")
   print(personalGreeting.draw())
   // Prints "***Hello RAVI PATEL!**"

makeGreeting(for :)関数は、nameパラメーターを受け取り、それを使用してパーソナライズされた挨拶を描画します。 draw(_ :)関数とcaps(_ :)関数はどちらも、引数として単一のクロージャーを取り、@ DrawingBuilder属性でマークされています。 これらの関数を呼び出すときは、DrawingBuilderが定義する特別な構文を使用します。 Swiftは、図面の宣言型記述を、DrawingBuilderのメソッドへの一連の呼び出しに変換して、関数の引数として渡される値を構築します。 たとえば、Swiftは、その例のcaps(_ :)への呼び出しを次のようなコードに変換します。

    let capsDrawing = caps {
       let partialDrawing: Drawable
       if let name = name {
           let text = Text(name + "!")
           partialDrawing = DrawingBuilder.buildEither(first: text)
       } else {
           let text = Text("World!")
           partialDrawing = DrawingBuilder.buildEither(second: text)
       }
       return partialDrawing
   }

Swiftは、if-elseブロックをbuildEither(first :)メソッドとbuildEither(second :)メソッドの呼び出しに変換します。 独自のコードでこれらのメソッドを呼び出すことはありませんが、変換の結果を表示すると、DrawingBuilder構文を使用するときにSwiftがコードをどのように変換するかを簡単に確認できます。

特別な描画構文でforループを記述するためのサポートを追加するには、buildArray(_ :)メソッドを追加します。

   extension DrawingBuilder {
       static func buildArray(_ components: [Drawable]) -> Drawable {
           return Line(elements: components)
       }
   }
   let manyStars = draw {
       Text("Stars:")
       for length in 1...3 {
           Space()
           Stars(length: length)
       }
   }

上記のコードでは、forループが図面の配列を作成し、buildArray(_ :)メソッドがその配列を線に変換します。

この記事が気に入ったらサポートをしてみませんか?