Swift Language ) Advanced Operators
오늘은 Advanced Operators 부분 중 Bitwise, Overflow parts 를 제외한, Custom Operators 위주로 번역을 진행해보겠습니다.
(Bit 관련 부분은 추후 다시 포스팅 수정할 수 있으니 숨김글 처리하겠습니다.)
In addition to the operators described in Basic Operators, Swift provides several advanced operators that perform more complex value manipulation. These include all of the bitwise and bit shifting operators you will be familiar with from C and Objective-C.
Unlike arithmetic operators in C, arithmetic operators in Swift don’t overflow by default. Overflow behavior is trapped and reported as an error. To opt in to overflow behavior, use Swift’s second set of arithmetic operators that overflow by default, such as the overflow addition operator (&+). All of these overflow operators begin with an ampersand (&).
자신만의 structures, classes, enumerations 를 정의할 때, standard Swift operators 를 직접 만들어 제공하면 좋을 때가 종종 있다. Swift 에서는 이러한 operators 에 대한 tailored implementation 과, 각 type 에 대해 어떻게 작동해야하는지 정확히 정해주는 작업이 쉽다. 또한 Swift 에서 infix, prefix, postfix, assignment operators (with custom precedence and associativity values) 를 모두 본인이 작동방식을 정해줄 수 있다. 이 operators 는 이미 정해진 operators 처럼 본인의 코드에 적용될 수 있고, 이미 있는 types 를 extend 하면서 custom operators 를 줄 수 있다.
Precedence and Associativity
Operator precedence 는 어떤 operators 에게 더 높은 우선순위를 준다. 다시말하면, 먼저 적용되는 operators 이다.
Operator associativity 는 같은 precedence 를 가지고있는 operators 가 그룹지어지는 방식에 대해 정의한다 - 왼쪽부터 되거나 오른쪽부터 되거나. 이렇게 생각해보자. "they associate with the expression to their left", or "they associate with the expression to their right." 혼합된 식들이 있을 때 각 operator 의 precedence 와 associativity 를 고려하는건 중요하다. 예를 들면, operator precedence 는 아래 식의 값이 왜 17 인지 설명해준다.
2 + 3 % 4 * 5
// this equals 17
만약 해당 식을 좌측부터 우측으로 아무런 precedence 를 고려하지 않고 계산하면, 다음과 같이 될 것이다.
2 + 3 = 5
5 % 4 = 1
1 * 5 = 5
그러나, 답은 5 가 아닌 17 이다. 높은 우선순위를 가진 operators 는 먼저 계산된다. Swift 에서는 (C와 마찬가지로) 나머지 연산자(%) 와 곱셈 연산자 는 덧셈 연산자보다 높은 우선순위를 가진다. 따라서, + 가 계산되기 전에 이 둘은 먼저 계산된다. 그러나, 이때 나머지와 곱셈 연산자는 같은 우선순위를 가지고 있다. 정확한 evaluation order 를 알기 위해서는 이들의 associativity 또한 고려해야한다. Remainder and multiplication both associate with the expression to their left. 괄호가 해당 expression 부분에, 좌측부터 시작해서 있다고 생각해보자.
2 + ((3 % 4) * 5)
3 % 4 는 3 이므로, 위 식은 아래와 같다.
2 + (3 * 5)
3*5 = 15 이므로, 위 식은 아래와 같다.
2 + 15
이렇게 계산하면 17 이 나온다.
Swift standard library 에 나온 operators, operator precedence groups and associativity settings 에 대한 정보는 Operator Declarations 를 참고하면 된다.
NOTE
Swift 의 연산자 우선순위와 associativity 규칙은 C 나 Objective-C 보다 간단하고 예측가능하다. 이는 C-based languages 와 정확히 같지는 않다는 것을 의미한다. 따라서 Swift 에 C code 를 porting 할 때 operator interactions 에 대한 주의가 필요하다.
Operator Methods
Classes 와 structures 에서 존재하는 operators 에 대해 이들 만의 implementations 를 줄 수 있다. 이를 'overloading' 이라 부른다. 아래 예제에서 custom structure 의 + operator 를 implement 하는 방법이 있다. + operator 는 두 targets 에 작용하기때문에 binary operator 이고, targets 중간에서 작용하기 때문에 infix 라고 불린다. 이 예제에서는 2차원 position vector (x,y) 을 나타내고자Vector2D structure 를 정의하고, 그 후에는 Vector2D structure 의 두 instances 를 더하는 operator method 에 대해 정의한다.
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)
}
}
Operator method 는 overloaded (+) 될 operator 의 이름을 갖는, Vector2D 의 type method 로서 정의된다. Addition 은 a vector 에서 반드시 필요한 behavior 가 아니기 때문에, type method 는 Vector2D 의 main structure declaration 이 아닌 exension 에 사용되었다. Arithmetic addition operator 는 binary operator 라서 이 operator method 는 두개의 Vector2D parameters 를 input 으로 받고 Vector2D type 의 output 하나를 반환한다. 위 implementation 에서는 input parameters 의 이름이 left, right 으로 지어졌다. (각각 + 연산자의 좌측, 우측에 있을 것을 나타낸다.) 이 method 는 left, right 의 각 x, y 좌표를 더한 값을 갖는 새로운 Vector2D instance 를 반환하고, infix operator 로서 존재하는 Vector2D instances 사이에서 사용된다.
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)
Vectors(3.0, 1.0) 과 (2.0, 4.0) 을 더해 (5.0, 5.0) 을 만드는 과정이고, 아래 그래프로 표현되었다.
Prefix and Postfix Operators
위 예제에서는 binary infix operator 의 custom implementation 을 설명하였다. Classes 와 structures 는 standard unary ( unary: 단항, binary: 이항) operators 또한 implementations 을 가질 수 있다. Unary operator 는 single target 에 작용한다. Unary operators 는 target 의 앞 (prefix, -a) 또는 뒤 (postfix, b!) 에 쓰일 수 있다. Operator method 를 정의할 때 prefix 또는 postfix modifier 를 func keyword 앞에 써주면서 implement 하면 된다.
extension Vector2D {
static prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}
}
위 예시에서는 Vector2D instances 에 대해 unary minus operator (-a) 를 implement 한다. 이 unary minus operator 는 prefix operator 이기때문에, 'prefix' modifier 가 들어가야 한다. 단순한 숫자값인 경우, unary minus operator 는 양의 수를 음의 수로, 음수를 양수로 바꾼다. 그에 대응되는 Vector2D instances 에 적용될 implementation 은 같은 연산을 x, y properties 두개에 한다.
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
Compound assignment operators 는 assignment(=) 와 다른 operation 을 결합한다. 예를 들면, the addition assignment operator (+=) 는 + 와 = 를 하나의 operation 으로 만든다. Parameter 의 값이 operator method 에서 바뀔 것이기 때문에, compound assignment operator 의 왼쪽 input parameter type 은 inout 으로 표시해주어야한다.
아래 예제에서는 Vector2D instances 에 대한 addition assignment operator method 을 implement 한다.
extension Vector2D {
static func += (left: inout Vector2D, right: Vector2D) {
left = left + right
}
}
Addition operator 가 이미 정의되었기 때문에, 여기서는 다시 addition process 를 implement 하지 않아도 된다. Addition assignment operator method 는 이미 존재하는 addition operator method 를 이용해서 left 값을 설정한다.
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)
NOTE
Compound assignment operators 외에 기본 assignment operator (=) 를 overload 하는것은 불가능하다. 또한, ternary conditional operator (a ? b : c ) 역시 overload 할 수 없다.
Equivalence Operators
Custom classes, structures 는 기본적으로 equivalence operators (==, !=) 를 갖지 않는다. == operator 를 implement 하면, 그 반대 standard library's default implementation operator 인 != 도 사용가능해진다. (보통 == 를 implement 한다.) == operator 를 implement 하는 데에는 두가지 방법이 있다 : 직접 하거나, Swift 에게 synthesize 를 요청하는 것. 두 경우 모두, standard library 의 Equtable protocol 을 conform 한다고 넣어주어야한다. == 의 implementation 을 만드는 것은 전에 했던 infix operators 와 같은 방식으로 하면 된다.
extension Vector2D: Equatable {
static func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
}
위 예시에서 두 Vector2D instances 가 같은 값을 가지고 있는지 확인하기 위해 == operator 를 implement 하였다. Vector2D 의 경우 'equal' 조건은 ' 두 instances 의 x, y 값이 모두 같다' 라고 보는게 타당하므로 이 logic 이 operator implementation 에 쓰였다.
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."
많은 단순한 경우에, Swift 에게 equivalence operators(==, !=) 에 대한 synthesized implementations 을 요청해도 된다. ( Adopting a Protocol Using a Synthesized Implementation 참조, 해당 부분에 대한 번역 바로 하단에 추가)
Adopting a Protocol Using a Synthesized Implementation
Swift 는 많은 단순한 경우들에 대해 Equatable, Hashable, Comparable 에 대한 Conformance 를 자동으로 줄 수 있다.
이러한 synthesized implementation 을 이용함으로서 반복적인 코드를 쓰는 행위를 하지 않을 수 있다.
Equtable
Equatable 의 경우 Swift 는 다음 조건을 만족할 때 Synthesized Implementation 을 제공한다.
- Equatable protocol 을 따르는 stored property만 갖는 struct
- Equatable protoocl 을 따르는 associated type 만 갖는 enum
- Associated type 을 갖지 않는 enum
'==', '!=' ( synthesized implementation ) 를 사용할 수 있게 하기 위해서는 Equatable protocol 을 conform 한다고 기존 파일에 선언하면 된다 . (== 를 본인이 implement 하지 않으셔도 됩니다.)
struct Vector3D: Equatable {
var x = 0.0, y = 0.0, z = 0.0
}
let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
if twoThreeFour == anotherTwoThreeFour {
print("They are the same !")
}
Custom Operators
Swift 에서 제공하는 standard operators 외에 추가로 본인이 operators 를 정의, implement 할 수 있다. Custom operators 에 사용할 수 있는 characters 의 목록은 Operators 을 참고하면 된다. (글이 꽤 길어 전부 아래 넣기는 무리라서 일부만 추가.)
( Custom operators 의 첫 character 는 ASCII characters /, =, -, +, !, *, %, <, >, &, |, ^, ?, or ~, or one of the Unicode characters defined in the grammar below (which include characters from the Mathematical Operators, Miscellaneous Symbols, and Dingbats Unicode blocks, among others 가 사용 가능하다. 그 후에는 Unicode characters 를 조합하여 사용해도 된다. )
새로운 operators 가 global level 에서 operator keyword 를 이용, prefix, infix, postfix modifier 중 하나로 marked 되어 정의되었습니다.
prefix operator +++
위 예시에서는 새로운 prefix operator +++ 을 정의했습니다. Swift 내에서 이 operator 는 아직 어떠한 의미도 갖지 않기 때문에, 아래 코드에서 Vertor2D instances 내에서 쓰일 때의 custom meaning 을 받습니다. 아래 예시에서, +++ 는 "prefix doubling" operator 로서 취급됩니다. vector 에 vector(itself) 를 기존에 정의된 += operator 를 이용해 더함으로써 x 값과 y 값을 각각 두배로 만듭니다. +++ operator 를 implement 하기 위해, type method +++ 을 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
Custom infix operators 는 각각 precedence group 에 속합니다. A precedence group 은 operator 가 다른 infix operators 에 상대적으로 갖는 precedence 와 operatoor 의 associativity 를 정해줍니다. ( 위 Precedence and Associativity 참고. ) precedence group 에 explicitly 위치하지 않은 custom infix operator 에는 삼항 연산자보다 바로 한단계 높은 기본 precedence group 이 주어집니다. 아래 예시에서는 새로운 custom infix operator += 를 정의하고, 이는 'AdditionPrecedence' precedence group 에 속합니다.
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)
이 연산자 (+-) 는 두 vectors 의 x values 를 더하고, 첫번째 vector 에서 두번째 vectory 의 y 값을 빼는 연산을 수행합니다. 기본적으로 '더하기' 연산이기 때문에, + 나 - 와 같은(such as) 'additive' 가 있는 group 과 동일한 precedence 에 속하도록 했습니다. Swift standard library 에서 제공하는 operators 와 operator precedence groups and associativity settings 에 대한 정보는 Operator Declarations 을, precedence groups 와 operators, precedence groups 를 정의하는 syntax 에 대한 정보는 Operator Declaration. 을 참조하세요.
NOTE
prefix 나 post fix operator 를 정의할 때는 precedence 를 지정해주지 않습니다. 그러나, 만약 둘을 하나의 피연산자에 대해 동시에 적용한다면 post operator 가 먼저 적용됩니다.
Result Builders
A result builder is a type you define that adds syntax for creating nested data, like a list or tree, in a natural, declarative way. The code that uses the result builder can include ordinary Swift syntax, like if and for, to handle conditional or repeated pieces of data.
The code below defines a few types for drawing on a single line using stars and text.
- 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() }
- }
The Drawable protocol defines the requirement for something that can be drawn, like a line or shape: The type must implement a draw() method. The Line structure represents a single-line drawing, and it serves the top-level container for most drawings. To draw a Line, the structure calls draw() on each of the line’s components, and then concatenates the resulting strings into a single string. The Text structure wraps a string to make it part of a drawing. The AllCaps structure wraps and modifies another drawing, converting any text in the drawing to uppercase.
It’s possible to make a drawing with these types by calling their initializers:
- 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!**"
This code works, but it’s a little awkward. The deeply nested parentheses after AllCaps are hard to read. The fallback logic to use “World” when name is nil has to be done inline using the ?? operator, which would be difficult with anything more complex. If you needed to include switches or for loops to build up part of the drawing, there’s no way to do that. A result builder lets you rewrite code like this so that it looks like normal Swift code.
To define a result builder, you write the @resultBuilder attribute on a type declaration. For example, this code defines a result builder called DrawingBuilder, which lets you use a declarative syntax to describe a drawing:
- @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
- }
- }
The DrawingBuilder structure defines three methods that implement parts of the result builder syntax. The buildBlock(_:) method adds support for writing a series of lines in a block of code. It combines the components in that block into a Line. The buildEither(first:) and buildEither(second:) methods add support for if-else.
You can apply the @DrawingBuilder attribute to a function’s parameter, which turns a closure passed to the function into the value that the result builder creates from that closure. For example:
- 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!**"
The makeGreeting(for:) function takes a name parameter and uses it to draw a personalized greeting. The draw(_:) and caps(_:) functions both take a single closure as their argument, which is marked with the @DrawingBuilder attribute. When you call those functions, you use the special syntax that DrawingBuilder defines. Swift transforms that declarative description of a drawing into a series of calls to the methods on DrawingBuilder to build up the value that’s passed as the function argument. For example, Swift transforms the call to caps(_:) in that example into code like the following:
- 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 transforms the if-else block into calls to the buildEither(first:) and buildEither(second:) methods. Although you don’t call these methods in your own code, showing the result of the transformation makes it easier to see how Swift transforms your code when you use the DrawingBuilder syntax.
To add support for writing for loops in the special drawing syntax, add a buildArray(_:) method.
- 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)
- }
- }
In the code above, the for loop creates an array of drawings, and the buildArray(_:) method turns that array into a Line.
For a complete list of how Swift transforms builder syntax into calls to the builder type’s methods, see resultBuilder.
출처: https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html