반응형

원문 : https://www.raywenderlich.com/157556/overloading-custom-operators-swift

모든 프로그래밍 언어에서 연산자(Operators)는 핵심 기능(core building blocks)입니다. + =이 없는 프로그래밍을 상상할 수 있습니까?

연산자는 너무나 필수적인 요소이기 때문에, 대부분의 언어에서 컴파일러(또는 인터프리터)의 일부로 만듭니다. 반면에, Swift 컴파일러는 대부분의 연산자를 하드코딩하지 않는 대신에 라이브러리에서 자체적으로 만들수 있는 방법을 제공합니다. Swift 표준 라이브러리에서 기대하는 모든 것들을 제공하고 처리해줍니다. 이것은 미묘한(subtle) 차이지만 엄청난 맞춤화(customization) 가능성을 열어줍니다.

Swift 연산자들은 필요에 따라 두가지 방법으로 변경할수 있기 때문에 특히 강력합니다. 기존 연산자에 새로운 기능을 할당(연산자 오버로딩(operator overloading))하고 새로운 사용자정의 연산자를 만듭니다.

연산자 오버로딩의 간단한 예는 더하기 연산자입니다. 이 연산자를 두개의 정수로 사용하면 다음과 같은 결과가 발생합니다.

1 + 1 // 2

하지만 같은 더하기 연산자를 문자열에 사용하면 조금 다르게 동작합니다.

"1" + "1" // "11"

두개의 정수로 +를 사용할때, 그것들을 산술적(arithmetically)으로 더합니다. 하지만 문자열로 사용할때에는, 그것들을 연결(concatenates)합니다.

이 튜토리얼에서는, Swift로 필요에 따라 연산자를 어떻게 만드는(mold)지에 대해서 알아보고 자신만의 3D벡터 타입을 만들어 볼것입니다.

시작하기(Getting Started)

Xcode를 열어서 시작합니다. 새로운 플레이그라운드(playground)를 만들고, 이름을 Operators로 하고 iOS 플랫폼을 선택합니다. 기본 코드를 모두 삭제하면 백지상태(blank slate)로 시작할 수 있습니다.

플레이그라운드에 다음에 오는 코드를 추가하세요.

import UIKit

struct Vector: ExpressibleByArrayLiteral, CustomStringConvertible {
  let x: Int
  let y: Int
  let z: Int

  var description: String {
    return "(\(x), \(y), \(z))"
  }

  init(_ x: Int, _ y: Int, _ z: Int) {
    self.x = x
    self.y = y
    self.z = z
  }

  init(arrayLiteral: Int...) {
    assert(arrayLiteral.count == 3, "Must initialize vector with 3 values.")
    self.x = arrayLiteral[0]
    self.y = arrayLiteral[1]
    self.z = arrayLiteral[2]
  }
}

3개의 프로퍼티와 2개의 초기화를 가진 새로운 Vector 타입을 정의하였습니다. CustomStringConvertible프로토콜과 description계산(computed) 프로퍼티는 벡터를 친숙한 String로 출력합니다.

플레이그라운드 아래에, 다음 줄들을 추가합니다.

let vectorA: Vector = [1, 3, 2]
let vectorB: Vector = [-2, 5, 1]

ExpressibleByArrayLiteral프로토콜은 Vector초기화를 위해 마찰이없는(frictionless) 인터페이스를 제공합니다. 그 프로토콜은 가변(variadic) 매개변수로 실패하지 않는 초기화를 필요로 합니다. : init(arrayLiteral: Int...)

이것이 의미하는바는 다음과 같습니다 : 가변 매개변수 ...는 콤마(,)로 구분된 값을 제한없이 전달할수 있습니다. 예를 들어, Vector은 Vector(0) 또는 Vector(5, 4, 3) 처럼 만들수 있습니다.

그 프로토콜은 편리함을 넘어 let vectorA: Vector = [1, 3, 2]으로 배열을 직접 초기화 할수 있습니다.

이러한 접근법에 대한 유일한 주의사항은 모든 길이의 배열을 받아들여야 하는 것입니다. 앱에 이 코드를 넣으면, 정확히 3이 아닌 길이의 배열을 넣는경우에 크래쉬(crash)가 발생할 것입니다. 3개보다 작거나 큰 규모의 값들로 Vector 초기화하려고 시도하는 경우에 초기화의 맨위에 있는 assertion은 개발과 내부 테스트 하는 도중에 콘솔에 알려줄것입니다.

벡터 자체로도 괜찮지만 그것들을 활용할 수 있다면 더 좋을 것입니다. 초등학교에서 했던것 처럼, 더하기를 배우는 여정을 시작할 것입니다.

더하기 연산자 오버로딩하기(Overloading the Addition Operator)

연산자를 오버로드(overload)하기 위해서, 연산자 기호 이름을 가진 함수를 구현해야 합니다.

주의
튜토리얼에서 처리하는 것처럼 오버로드 함수를 타입의 멤버로 정의할 수 있습니다. 그렇게 할때, 타입의 인스턴스 정의없이 접근 할수 있도록 static으로 선언되어야 합니다.

Vector구현의 끝부분에 닫는 중괄호 바로 앞에 다음 함수를 추가하세요.

static func +(left: Vector, right: Vector) -> Vector {
  return [left.x + right.x, left.y + right.y, left.z + right.z]
}

이 함수는 두개의 벡터를 가져와서 벡터의 합을 반환할 것입니다. 벡터를 추가하려면, 각 구성요소들을 추가하기만 하면 됩니다.

이제, 이 함수를 테스트하기 위해서, 플레이그라운드 아래에 다음을 추가하세요.

vectorA + vectorB // (-1, 8, 3)

플레이그라운드의 오른쪽 사이드바에서 결과 벡터를 볼수 있습니다.

연산자의 다른 타입(Other Types of Operators)

더하기 연산자는 중위(infix) 연산자로 알려져 있으며, 두개의 다른 값 사이에서 사용된다는 것을 의미합니다. 다른 타입의 연산자도 있습니다.

  • infix : 더하기 연산자 처럼, 두 값 사이에서 사용됩니다(예, 1 + 1).
  • prefix : 음수 연산자 처럼, 값의 앞에 추가됩니다(예, -3).
  • postfix : 강제 언래핑(force-unwrap) 연산자처럼, 값 뒤에 추가 됩니다(예, mayBeNil!).
  • ternary : 3개의 값 사이에 삽입된 두개의 기호입니다. Swift에서, 사용자정의 삼항 연산자는 지원되지 않고 내장된 삼항 연산자는 하나있으며, 애플 문서(Apple’s documentation)에서 읽을수 있습니다.

오버로딩을 원하는 다음 연산자는 음수(negative) 부호이며, (1, 3, 2)가 오면 (-1, -3, -2)를 반환하는 vectorA를 사용할것입니다.

Vector 구현의 끝에 다음 코드를 추가 하세요.

static prefix func -(vector: Vector) -> Vector {
  return [-vector.x, -vector.y, -vector.z]
}

연산자는 중위(infix)인 것으로 가정되며, 연산자가 다른 타입이 되길 원하면, 함수 선언에 연산자의 타입을 지정해줘야 합니다. 부정(negation) 연산자는 중위(infix)가 아니므로, 함수 선언에 전위(prefix) 수정자를 추가했습니다.

플레이그라운드 아래에 다음 줄을 추가하세요.

-vectorA // (-1, -3, -2)

사이드바에서 정확한 결과를 확인하세요.

다음은 빼기(subtraction)이며, 여러분 스스로 구현해 보도록 남겨둘 것입니다. 작업이 끝나면 여러분의 코드와 제 코드가 비슷한지 확인해 보세요. 힌트 : 뺄셈(substration)은 음수(negative)를 추가하는 것과 같습니다.

한번 시도해 보시고, 도움이 필요하면 아래의 해결책을 확인하세요.

static func -(left: Vector, right: Vector) -> Vector {
  return left + -right
}

이제 플레이그라운드의 아래에 새 연산자를 추가해서 테스트 해보세요.

vectorA - vectorB // (3, -2, 1)

혼합된 매개변수들? 문제없음!(Mixed parameters? No Problem!)

스칼라 곱셈(scalar multiplication)을 통해서 벡터들의 숫자를 곱하기(multiply) 할 수 있습니다. 벡터에 2를 곱하려면, 각 구성요소에 2를 곱합니다. 다음에 이것을 구현할 것입니다.

고려해야 한 가지는 인자의 순서입니다. 더하기를 구현할때, 두 매개변수가 모두 벡터였기 때문에, 순서는 중요하지 않았습니다.

스칼라 곱셈의 경우에, Int * Vector과 Vector * Int에 대해서 판단(account)해야 합니다. 이러한 경우에 하나만 구현하면, Swift 컴파일러는 원하는 순서대로 작업하는 것을 자동으로 알지 못합니다.

스칼라 곰셉을 구현하기 위해, Vector 구현의 끝에 다음에 오는 두개의 함수를 추가하세요.

static func *(left: Int, right: Vector) -> Vector {
  return [right.x * left, right.y * left, right.z * left]
}

static func *(left: Vector, right: Int) -> Vector {
  return right * left
}

같은 코드의 중복 작성을 피하려면, 첫번째 함수를 사용해서 두번째 함수를 작성하면 됩니다.

수학(mathematics)에서, 벡터에는 다른 재미있는 외적(cross-product) 연산이 있습니다. 외적(cross-product) 사용법에 대해서는 이 튜토리얼의 범위를 벗어나지만, 외적 위키피디아 페이지(Cross product Wikipedia page)에서 자세히 배울수 있습니다.

대부분의 경우에 사용자정의 기호를 사용하는 것이 권장되지 않기 때문에(누가 코딩 중에 이모티콘 메뉴를 여는것을 원할까요?), 외적(cross-product)에 대해서 별표(*)를 사용하는 것이 매우 편리합니다.

외적은 스칼라 곱셈과는 다르게, 매개변수로 두개의 벡터를 받아 새로운 하나의 벡터로 반환합니다.

외적(cross-product) 구현을 추가하려면 Vector 구현의 아래에 다음 코드를 추가하세요.

static func *(left: Vector, right: Vector) -> Vector {
  return [left.y * right.z - left.z * right.y, left.z * right.x - left.x * right.z, left.x * right.y - left.y * right.x]
}

이제 플레이그라운드 아래에 다음 계산을 추가하세요.

vectorA * 2 * vectorB // (-14, -10, 22)

이 코드에서 vectorA와 2의 스칼라 곱셈을 구하고, vectorB로 벡터의 외적(cross-product)을 구합니다. 별표(*) 연산자는 언제나 왼쪽에서 오른쪽으로 이동하는 것을 주의해야하며, (vectorA * 2) * vectorB처럼 괄호를 사용해서 연산을 그룹지은 경우, 이전 코드와 같습니다.

프로토콜 연산자(Protocol Operators)

일부 프로토콜은 프로토콜의 멤버가 필요합니다. 예를들어, Equatable을 준수하는 타입은 반드시 ==연산자를 구현해야 합니다. 비슷하게, Comparable를 준수하는 타입은 최소한 <(그리고 >, >=, <=는 선택)을 반드시 구현해야 합니다.

Vector의 경우, Comparable는 실제로 의미가 없지만, 두개의 벡터의 구성요소가 모두 같으면 같기 때문에, Equatable는 동작합니다.

프로토콜을 준수하기 위해, Vector선언의 끝에 이것을 추가하세요.

struct Vector: ExpressibleByArrayLiteral, CustomStringConvertible, Equatable {

Xcode는 Vector이 Equatable을 준수하지 않았다고 소리칠(yelling) 것입니다. 그것은 아직 ==을 구현하지 않았기 때문입니다. 그것을 구현하기 위해서, Vector구현의 아래에 다음에 오는 정적(static) 함수를 추가하세요.

static func ==(left: Vector, right: Vector) -> Bool {
  return left.x == right.x && left.y == right.y && left.z == right.z
}

이것을 테스트 하기 위해, 플레이그라운드의 아래에 다음 줄을 추가하세요.

vectorA == vectorB // false

이 코드는 vectorA가 vectorB와 다른 구성요소를 가지기 때문에 예상대로 false를 반환합니다.

사용자정의 연산자 만들기(Creating Custom Operators)

사용자정의 기호(symbols)를 사용하는 것을 일반적으로 권장하지 않는다는 것을 기억하시나요? 항상 그렇듯이, 규칙에는 예외가 있습니다.

사용자정의 기호에 대해서는 다음 사항이 참(true)인 경우에만 사용해야 합니다.

  • 코드를 읽는 사람에게 의미가 있거나 잘 알려져(well-known) 있습니다.
  • 키보드에서 쉽게 입력할수 있습니다.

구현할 마지막 연산자는 이러한 두 조건 모두 일치합니다. 벡터 내적(dot-product)은 두개의 벡터를 가지고 하나의 스칼라 숫자를 반환합니다. 벡터에서 다른 벡터에 있는 각각의 값을 곱해서 수행하고, 모든것을 더합니다.

내적(dot-product)에 대한 기호는 키보드에서 Option-8을 사용해서 쉽게 입력할 수 있는 입니다.

튜토리얼에서 다른 연산자를 사용해서 작업했던것과 똑같이 작업할 수 있습니다, 그렇죠? 생각을 하고 있을지 모릅니다.

불행하게도, 아직은 그렇게 할수 없습니다. 다른 경우에는, 이미 존재하는 연산자를 오버로딩(overloading)합니다. 새로운 사용자정의 연산자의 경우에, 연산자부터 먼저 만들어야 합니다.

Vector를 구현 바로 아래에, let vectorA...의 시작하는 줄 위에, 다음 선언을 추가하세요.

infix operator: AdditionPrecedence

은 다른 두 값들 사이에 위치하도록 정의하고 더하기 + 연산자와 같은 우선순위(precedence)를 가집니다. 다시 돌아올것이기 때문에, 잠시동안은 우선순위를 무시하세요.

이제 이 연산자가 등록되었으니, 벡터의 본문에 구현을 추가하세요.

static func(left: Vector, right: Vector) -> Int {
  return left.x * right.x + left.y * right.y + left.z * right.z
}

다음 코드를 플레이그라운드 아래에 추가해서 테스트해주세요.

vectorA • vectorB // 15

지금까지 모든것이 좋아보입니다. 그렇지 않나요? 플레이그라운드 아래에 다음 코드를 입력하세요.

vectorA • vectorB + vectorA // Error!

여러분과 Xcode는 행복하지 않습니다. 왜 그럴까요?

지금 과 +는 같은 우선순위를 가지기 때문에, 컴파일러는 표현식을 왼쪽에서 오른쪽으로 분석합니다. 코드는 다음과 같이 해석됩니다.

(vectorA • vectorB) + vectorA

이 표현은 구현하지 않고 구현할 계획도 없는 Int + Vector이 됩니다(boils down). 해결하기 위해 무엇을 할 수 있을까요?

우선순위 그룹(Precedence Groups)

Swift에 있는 모든 연산자들은 연산자를 평가해야 하는 순서를 설명하는 우선순위 그룹(precedence group)에 속해있다. 초등학교 수학에서 PEMDAS를 배운것을 기억하나요? 그것은여기에서 다루는 것의 기본이 됩니다.

Swift 표준 라이브러리에서, 우선순위는 다음과 같습니다.

이전에 보지 못했을수도 있기 때문에, 이러한 연산자들에 대한 몇가지 주의사항은 다음과 같습니다.

  1. 비트단위 시프트 연산자들, <<과 >>은 바이너리 계산에서 사용됩니다.
  2. 형변환(casting) 연산자, is와 as은 값의 타입을 결정하거나 변경하기 위해 사용합니다.
  3. nil 결합 연산자 ??는 옵셔널을 옵셔널이 아닌것으로 변경할때 도움이 됩니다.
  4. 사용자정의 연산자의 우선순위를 지정하지 않은 경우에, 기본 우선순위가 자동적으로 할당됩니다.
  5. 삼항 연산자 ? :는 if-else 문과 유사합니다.
  6. =의 파생어에 대한 AssignmentPrecedence는 무엇이든 상관없이 모든것을 평가합니다.

왼쪽 연관성(left associativity)이 있는 타입은 분석되어 v1 + v2 + v3 == (v1 + v2) + v3이 됩니다. 반대의 경우인, 오른쪽 연관성(right associativity)의 경우에도 그렇습니다.

연산자는 표에 표시된 순서대로 해석(parsed)됩니다. 괄호를 사용해서 다음 코드를 다시 작성해 보세요.

v1 + v2 * v3 / v4 * v5 == v6 - v7 / v8

여러분의 수학 결과를 확인할 준비가 되면, 아래 해결책을 보세요.

(v1 + (((v2 * v3) / v4) * v5)) == (v6 - (v7 / v8))

대부분의 경우에, 코드를 읽기 쉽게 하기 위해서 괄호를 추가하는 것이 좋습니다. 어쨌든간에, 컴파일러가 연산자를 평가하는 순서를 이해하는 것은 유용합니다.

내적 우선순위(Dot Product Precedence)

여러분의 새로운 내적(dot-product)은 실제로 이러한 카테고리에 맞지 않습니다. 그것은 더하기 보다 작아야 하지만(이전에 깨달은것 처럼), 실제로 CastingPrecedence나 RangeFormationPrecedence와 맞나요?

대신에, 내적(dot-product)에 대한 자신의(own) 우선순위 그룹을 만들것입니다.

연산자의 원래 선언을 다음으로 교체합니다.

precedencegroup DotProductPrecedence {
  lowerThan: AdditionPrecedence
  associativity: left
}

infix operator: DotProductPrecedence

여기에서, DotProductPrecedence라는 새로운 우선순위 그룹을 만듭니다. 더하기를 먼저 처리하기 원하기 때문에, AdditionPrecedence보다 더 낮게 합니다. 덧셈과 곱셈을 할때 왼쪽에서 오른쪽으로 평가하길 원하기때문에, 왼쪽 연관성(left-associative)으로 만듭니다. 그런 다음에, 연산자에 새로운 우선순위를 할당합니다.

주의
DotProductPrecedence에 lowerThan이외에, higherThan을 지정할수 있습니다. 이것은 단일 프로젝트에서 여러개의 사용자지정 우선순위 그룹이있는 경우에 중요합니다.

예전 코드 줄이 실행되고 예상대로 반환합니다.

vectorA • vectorB + vectorA // 29

축하합니다- 여러분은 사용자정의 연산자를 터득하였습니다.

여기서 어디로 가야하나요?(Where to Go From Here?)

이 튜토리얼에의 완성된 플레이그라운드를 다운로드 할수 있습니다. 다운로드

이제는, Swift 연산자를 필요에 따라 변형(bend) 할 수 있습니다. 튜토리얼에서, 수학적인 상황에서 연산자를 사용하는 것에 중점을 두었습니다. 실제로, 연산자를 사용하는 많은 방법을 찾을수 있습니다.

사용자정의 연산자의 훌륭한 데모는 ReactiveSwift framework에서 볼수 있습니다. 바인딩에 대한 <- 한 가지 예를 들면, 반응형 프로그래밍에서 중요한 기능을 합니다. 다음은 이 연산자를 사용하는 예제입니다.

let (signal, observer) = Signal<Int, NoError>.pipe()
let property = MutableProperty(0)
property.producer.startWithValues {
  print("Property received \($0)")
}

property <~ signal

작도법(Cartography)은 연산자 오버로딩을 많이 사용하는 다른 프레임워크 입니다. 오토레이아웃(autolayout) 도구는 NSLayoutConstraint를 간단히 만들기 위해서 동등(equality)연산자와 비교(comparison) 연산자를 오버로드 합니다.

constrain(view1, view2) { view1, view2 in
    view1.width   == (view1.superview!.width - 50) * 0.5
    view2.width   == view1.width - 50
    view1.height  == 40
    view2.height  == view1.height
    view1.centerX == view1.superview!.centerX
    view2.centerX == view1.centerX

    view1.top >= view1.superview!.top + 20
    view2.top == view1.bottom + 20
}

추가적으로, 애플의 공식 사용자정의 연산자 문서(custom operator documentation)를 언제든지 참조할수 있습니다.

새로운 영감(inspiration)을 원천으로 무장한(armed), 연산자 오버로딩으로 코드를 더 간단하게 만들수 있습니다. 사용자 정의 연산자에 너무 빠져들지(too crazy) 않도록 주의하세요! :]




반응형
Posted by 까칠코더
,