Apple 제공 Swift 프로그래밍 가이드(4.1)의 Basic Operators 부분을 공부하며 정리한 글입니다. 개인적인 생각, 이해를 돕기 위한 예제도 조금 들어가있습니다.


들어가며

Swift 는 표준 C 연산자를 대부분 지원합니다.

Swift 의 연산자에는 코딩 오류를 미리 잡아주기 위한 몇 가지 특별한 점들이 있습니다. 다음은 그 예시 몇 가지입니다:

  • 할당 연산자( )는 어떤 값도 return 하지 않는다. ( if 조건문 안에서 = 대신 == 를 사용하지 않도록 )

  • 산술 연산자( +, -, *, /, % 등 )는 오버플로우를 미리 감지하여 그런 연산을 허용하지 않는다. ( 연산결과가 저장될 변수에 오버플로우 값이 할당되지 않도록 )

또한 Swift 에는 범위 연산자(range operator)인 a..<b a...b가 새롭게 추가되었습니다. 값의 범위를 표현하기 위한 shortcut으로 활용할 수 있습니다. Range를 지정할 때 유용합니다.

이 챕터에서는 일반적인 연산자만 다룹니다. 고급 연산자, 자신만의 커스텀 연산자를 정의하는 법, 커스텀 타입을 위해 일반 연산자를 적절히 구현하는 법 등은 Advanced Operators 에서 다루고 있습니다.



Teminology

Swift의 연산자에는 unary(단항), binary(이항), ternary(삼항) 세 종류가 있습니다.

  • Unary operator: single target (ex: -a). Unary prefix operator 는 타겟의 바로 앞에 붙습니다(ex: !b). Unary postfix operator 는 타겟의 바로 뒤에 붙습니다(ex: c!).

  • Binary operator: two targets (ex: 2 + 3). 타겟 두 개의 가운데에 있기 때문에 infix 라고 부릅니다.

  • Ternary operator: three targets. 스위프트에서의 삼항 연산자는 삼항 조건 연산자 (? b : c) 가 유일합니다.

Operator(연산자)가 영향을 끼치는 값들을 operand(피연산자)라고 부릅니다. 1 + 2 라는 식에서 + 기호는 binary operator 이고 1 2 는 두 개의 operand 입니다.



Assignment Operator

Assignment operator (a = b) : b의 값으로 a의 값을 초기화 또는 업데이트.

1
2
3
4
5
6
7
8
9
10
let b = 10 // initialize b
var a = 5 // initialize a
= b // update a
 
let (x, y) = (12// x = 1, y =2
 
// 주의 : 할당 연산자는 return value가 없다. 다음과 같은 구문은 compile error
if x = y {
  // not valid
}
cs


C, Objective-C 와 다르게 Swift 의 = 연산자는 어떤 값도 리턴하지 않습니다. 그래서 Swift 에서는 == 연산자(equal to operator)를 사용해야 할 조건문에 실수로 = 연산자를 사용하는 것을 방지할 수 있습니다.



Arithmetic Operators

산술연산자의 종류는 다음과 같으며 모든 Number 타입에 사용할 수 있습니다:

  • Addition (+)

  • Subtraction (-)

  • Multiplication (*)

  • Division (/)

1
2
3
4
1 + 2       // equals 3
5 - 3       // equals 2
2 * 3       // equals 6
10.0 / 2.5  // equals 4.0
cs


스위프트의 산술 연산자는 연산결과가 오버플로우 되는 것을 허용하지 않습니다. 그래서 필히 주의해야 할 경우가 생깁니다. 컴파일 시점에서는 알 수 없지만 런타임 때 오버플로우값이 연산결과로 할당되는 경우입니다. 컴파일 시점에서 알 수 있는 오버플로우 할당은 미리 컴파일러가 체크하여 오류로 잡아줍니다. 그러나 런타임 때만 알 수 있는 오버플로우 할당의 경우에는, 아예 어플리케이션이 죽어버리고 맙니다.

오버플로우 값 저장이 필요한 경우가 있을 수도 있는데, 이럴때는 오버플로우 연산자 (ex: a &+ b)를 사용하면 됩니다. Overflow Operators 에서 다루고 있습니다.

// 덧셈 연산자로 String 을 더할 수 있다.
"hello, " + "world"  // equals "hello, world”
 
// 주의 : Character+Character, String+Character 조합은 불가능
let a:Character = "a"
let b:Character = "b"
let ab = a + b // complie error (Character+Character)
let ab2 = "a" + b // compile error (String+Character)
let ab3 = "a" + "b" // ok. (String+String)
 
let c = "c" // String Type 으로 판단
let ac = "a" + c // ok. (String+String)
cs



<Remainder Operator>

Swift에서의 a % ba = (b x 배수) + 나머지 에서의 나머지입니다. 즉 a % b 의 결과는로 최대한 채운 후의 나머지입니다. 따라서 가 음수일 경우에도 의 부호는 무시됩니다. (9 % 4) 와 (9 % -4) 의 결과가 동일하다는 것입니다. 왜 이렇게 되는지 살펴봅시다.


1
2
3
4
9 % 4 // 9 = (4 x 2) + 1. 따라서 1
9 % -4 // 9 = (-4 x -2) + 1. 따라서 1
-9 % 4 // -9 = (4 x -2) + -1. 따라서 -1
-9 % -4 // -9 = (-4 x 2) + -1. 따라서 -1
cs







<Unary Minus Operator & Unary Plus Operator>

Unary Minus Operator (-) 와 Unary Plus Operator (+) 는 숫자 값의 부호에 관여합니다.

1
2
3
4
5
let three = 3
let minusThree = -three // minusThree = -(3) = -3
let plusThree = -minusThree // plusThree = -(-3) = 3
let minusSix = -6
let alsoMinusSix = +minusSix // alsoMinusSix = +(-6) = -6
cs


위 예제에서도 드러나듯이, 사실 Unary Plus Operator(+)는 부호를 바꾸지 못하므로 사용하나 안하나 차이가 없습니다. 그러나 코드 상에서 Unary Minus Operator(-)를 사용할 때 함께 사용하면서 코드 상의 대칭을 맞출 수 있습니다.



Compound Assignment Operators

Assignment operator(=) 와 다른 연산자를 합친 compound assignment operator 를 알아봅시다. 하나의 예로 addition assignment operator (+=) 가 있습니다.

1
2
3
var a = 1
+= 2
// a is now equal to 3
cs


+= 2 는 a = a + 2 와 같습니다. + 와 += 의 수행 시간은 동일합니다.

<Note>

Compound assignment operator 역시 아무 값을 반환하지 않습니다. let b = a += 2 와 같은 코드는 compile error 를 유발합니다.


Swift 스탠다드 라이브러리에서 제공하는 연산자들에 대한 정보를 더 얻고 싶다면 Operator Declarations 를 참조하세요.



Comparison Operators

스위프트는 C의 모든 표준 비교 연산자를 지원합니다.

  • Equal to (a == b)

  • Not equal to (a != b)

  • Greater than (a > b)

  • Less than (a < b)

  • Greater than or equal to (a >= b)

  • Less than or equal to (a <= b)

<Note>

Swift 는 또한 identity operators (A === BA !== B) 를 제공합니다. 이 연산자는 AB의 object reference(객체 참조)가 동일한 object instance(객체 인스턴스)를 가리키고 있는지 검사할 때 사용합니다. 자세한 것은 Classes and Structures 를 참고하세요.


자세한 설명은 생략하고 예제로 알아보겠습니다. 단, 튜플 부분은 문서 외의 추가 설명도 넣었습니다.

1 == 1   // true because 1 is equal to 1
2 != 1   // true because 2 is not equal to 1
2 > 1    // true because 2 is greater than 1
1 < 2    // true because 1 is less than 2
1 >= 1   // true because 1 is greater than or equal to 1
2 <= 1   // false because 2 is not less than or equal to 1
 
 
let name = "world"
if name == "world" {
    print("hello, world")
else {
    print("I'm sorry \(name), but I don't recognize you")
}
// Prints "hello, world", because name is indeed equal to "world".
 
 
 
// tuple 의 comparison
// (1) 같은 index에 같은 타입이 있어야 비교가 가능 (또한 비교가 가능한 타입이어야 한다)
// (2) 비교하는 순서는 left to right, 한번에 한 인덱스만, 두 개의 값이 같을 경우 다음 인덱스를 비교
 
(1"zebra"< (2"apple"// 1 < 2 를 해보고 곧바로 return true
(3"apple"< (3"bird"// 첫 번째 element 값이 같았으므로 "apple" < "bird" 를 해보고 return true
(4"dog"== (4"dog"// 모든 element가 같은 것을 확인한 뒤 return true
 
// 추가설명
 
("blue"false< ("purple"true// Bool 타입으로는 크고 작음을 판단할 수 없다. compile error
("blue"false== ("purple"true// Bool 타입으로 같음을 판단할 수는 있다. return false
 
cs




Ternary Conditional Operator

Ternary Conditional Operator (항 조건 연산자)는 question ? answer1 : answer2 의 폼을 가지며, 세 부분으로 구성되는 특별한 연산자입니다.

if question {
    answer1
else {
    answer2
}
 
// 위 코드를 단축하여 표현한 것이 question ? answer1 : answer2
cs


삼항 조건 연산자의 장점은 코드를 간결하게 보이게 하고, 임시변수를 줄일 수 있다는 것입니다. 하지만 남용하거나 복합 구문 안에 삽입하는 것은 가독성을 떨어트리니 주의해야 합니다. 개인적으로는 한 줄 안에 끝날 때만, 그리고 단독으로 쓰일 때만(컨텍스트가 거기서 끝날 때만?) 사용하는 편입니다.

let contentHeight = 40
let hasHeader = true
let rowHeight: Int
if hasHeader {
    rowHeight = contentHeight + 50
else {
    rowHeight = contentHeight + 20
}
 
// 위 코드를 아래와 같이 단축할 수 있습니다
let contentHeight = 40
let hasHeader = true
let rowHeight = contentHeight + (hasHeader ? 50 : 20)
// rowHeight is equal to 90
cs




Nil Coalescing Operator

a ?? b

optional 변수 에 만약 값이 들어있다면 그 값을 사용하고, nil 이라면 를 사용하겠다는 연산자입니다. 풀어쓰면 a != nil ? a! : 가 됩니다. 따라서 다음 두 개가 반드시 전제됩니다.

  • 는 optional 타입

  • 의 타입은 에 저장될 수 있는 타입

이 연산자를 사용하면 optional 변수를 사용할 때 nil 체크를 하고, 언랩핑하고, nil일때의 default 값을 반환하는 등의 일련의 과정을 코드 상에서 짧고 간결하게 나타낼 수 있습니다. 실제로 코딩 시 optional 변수를 사용할 때 종종 사용하는 연산자입니다.

let defaultColorName = "red"
var userDefinedColorName: String? // defaults to nil
 
var colorNameToUse = userDefinedColorName ?? defaultColorName
print(colorNameToUse) // "red"
 
userDefinedColorName = "green"
colorNameToUse = userDefinedColorName ?? defaultColorName
print(colorNameToUse) //"green"
cs


<Note>

삼항 연산자에는 short-circuit evaluation 이 적용됩니다. 즉, a ?? b 에서가 nil이 아니라면 는 계산되지 않습니다. 예를 들어 a ?? (b + c) 에서 에 이미 값이 들어 있다면 (b + c) 연산은 실행되지 않습니다.




Range Operators

Swift 에는 값의 범위를 표현하는 범위 연산자가 몇 종류 있습니다.

  • Closed Range Operator(a...b) : a 이상 b 이하. (ex: 1...3 은 1,2,3)

  • Half-Open Range Operator(a..<b) : a 이상 b 미만. (ex: 1..<3 은 1,2)

  • One-Sided Ranges([a...], [..<b] 등) : 한 쪽 방향으로 갈 수 있는 만큼 가게 됨. (ex: [..<2] 는 0,1)

범위 연산자는 for 문에서 유용하게 쓰일 수 있을 것 같은데, 첫 인덱스가 1인지 0인지에 따라 골라쓰면 될 것 같습니다. 예를들어 1부터 N까지 숫자를 출력하는 for문에서는 a...b를 쓰는 것이 유용하고, 배열을 도는 for문에서는 인덱스가 0부터 count-1 까지니까 a..<b 를 쓰는 것이 유용합니다. Swift 4 에서 추가된 One-Sided Range Operator 도 경우에 따라 유용하게 사용할 수 있을 것 같네요(total count 를 미리 계산해서 범위지정 하지 않아도 되는..등등? 개발하면서 테스트해보겠습니다!).

for index in 1...5 {
    print("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25
// a...b 는 b-a+1 번 돌게된다
 
 
let names = ["Anna""Alex""Brian""Jack"]
let count = names.count
for i in 0..<count { // 0..<4
    print("Person \(i + 1) is called \(names[i])")
}
// Person 1 is called Anna
// Person 2 is called Alex
// Person 3 is called Brian
// Person 4 is called Jack
// a..<b 는 b-a 번 돌게된다
 
for name in names[2...] {
    print(name)
}
// Brian
// Jack
 
for name in names[...2] {
    print(name)
}
// Anna
// Alex
// Brian
 
for name in names[..<2] {
    print(name)
}
// Anna
// Alex
 
// One-sided ranges can be used in other contexts, not just in subscripts.
let range = ...5
range.contains(7)   // false
range.contains(4)   // true
range.contains(-1)  // true
cs



Logical Operators

다음 세 가지의 논리 연산자를 통해 Boolean logic 값인 truefalse 를 다룰 수 있습니다:

  • Logical NOT (!a)

  • Logical AND (a && b)

  • Logical OR (a || b)

자세한 설명은 생략합니다. 참고로 Swift 에서 && 와 || 는 left-associative 입니다(여러 개가 한번에 결합되어 있을 때 왼쪽부터 차례로 연산된다는 의미입니다). 따라서 순서에 상관없이 특정 연산이 먼저 이루어져야 할 때는 괄호를 사용해야 합니다. 의도를 명확히 드러내기 위해 괄호를 쓰는 것도 좋습니다.

let allowedEntry = false
if !allowedEntry {
    print("ACCESS DENIED")
}
// Prints "ACCESS DENIED"
 
 
let enteredDoorCode = true
let passedRetinaScan = false
if enteredDoorCode && passedRetinaScan {
    print("Welcome!")
else {
    print("ACCESS DENIED")
}
// Prints "ACCESS DENIED"
 
 
let hasDoorKey = false
let knowsOverridePassword = true
if hasDoorKey || knowsOverridePassword {
    print("Welcome!")
else {
    print("ACCESS DENIED")
}
// Prints "Welcome!"
 
 
if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword {
    print("Welcome!")
else {
    print("ACCESS DENIED")
}
// Prints "Welcome!"
 
 
if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {
    print("Welcome!")
else {
    print("ACCESS DENIED")
}
// Prints "Welcome!"
cs




'Swift 4.1' 카테고리의 다른 글

Basic Operators  (0) 2018.03.23
The basics  (0) 2018.03.02
A Swift Tour  (0) 2017.10.17
Version Compatibility  (0) 2017.10.04
About Swift  (0) 2017.10.04

Apple 제공 Swift 프로그래밍 가이드(4.1)의 The basics 부분을 공부하며 정리한 글입니다. 개인적인 생각, 이해를 돕기 위한 예제도 조금 들어가있습니다.


들어가며

Swift는 iOS, macOS, watchOS, tvOS 앱 개발을 위한 새로운 프로그래밍 언어입니다. 그렇지만 Swift의 많은 부분들은 당신이 C와 Objective-C 개발 경험이 있다면 친숙하게 느껴질 것입니다.

C와 Objective-C에서 사용하는 모든 근본적인 타입들에 대해 Swift는 자신만의 고유한 버전을 제공합니다. 예를들어 Integer를 위한 Int, floating-point value를 위한 DoubleFloat, Boolean value를 위한 Bool, 텍스트 데이터를 위한 String 등이 있습니다. 또한 Swift는 파워풀한 세 개의 주요 Collection type을 제공합니다. Array, Set, Dictionary 입니다. 이것은 Collection Types 섹션에서 자세히 살펴보겠습니다.

C와 마찬가지로, Swift는 변수를 사용하여 특정값을 저장하고, 고유한 이름(변수 이름)을 통해 그 값에 접근합니다. 또한 값이 변할 일이 없는 변수를 광범위하게 이용할 수 있도록 해줍니다. 바로 상수(constant)입니다. Swift에서의 상수는 C에서의 상수보다 훨씬 더 강력합니다. Swift에서 값이 변할 일 없는 변수를 상수로 정의 한다면, 코드가 훨씬 안전하고 명료해지게 됩니다.

Swift에는 Objective-C에서는 찾아볼 수 없었던 새로운 타입도 도입되었습니다. 예를 들면 튜플(Tuple)같은 타입이 있겠습니다. 튜플은 여러 값들을 그룹으로 만들어 한번에 생성하거나 다른 곳에 넘기고 싶을 때 사용합니다. 여러 값들을 한번에 함수에서 리턴하고 싶다면 튜플을 사용해서 그 값들을 하나의 결합된 값으로 만들 수 있습니다.

Swift에는 옵셔널 타입(Optional type)이라는 것도 도입되었습니다. 값이 존재하지 않는 상황을 위해서입니다. "값이 존재하며, 그 값은 x다" 일수도 있고 "값이 존재하지 않는다" 일수도 있는 상황을 위해서 옵셔널 타입을 사용합니다. 옵셔널 개념은 마치 Objective-C에서의 nil 포인터와 비슷한 느낌이지만, 클래스만이 아닌 모든 타입을 대상으로 할 수 있습니다. 옵셔널 타입은 Objective-C에서의 nil 포인터보다 훨씬 안전하고 가벼울 뿐만 아니라 Swift의 가장 강력한 기능들에서 핵심 역할을 해줍니다.

Swift는 type-safe 언어입니다. 즉, 코드에 사용되는 값들의 타입이 명확하다는 뜻입니다. 코드에서 String을 필요로 하는 부분에 실수로 Int를 넘기는 것을 방지해주며, 옵셔널이 아닌 일반 String을 필요로 하는 부분에 옵셔널 String을 넘기는 것도 막아줍니다. 이런 type safety 특성은 타입에 관련된 에러를 더 빨리 찾고 고칠 수 있도록 당신을 도와줄 것입니다.


Constants and Variables

상수와 변수는 이름(ex: maximumNumberOfLoginAttempts, welcomeMessage)과 특정 타입의 값(ex: 숫자  10, 문자열 "Hello")을 묶어주는 역할을 합니다. 상수 의 값은 한 번 셋팅되면 변하지 않습니다. 반면 변수 는 한 번 셋팅되더라도 나중에 다른 값으로 몇 번이나 셋팅될 수 있습니다.

Declaring Constants and Variables

상수와 변수는 선언을 먼저 해야 사용할 수 있습니다. 상수는 let 키워드를 통해, 변수는 var 키워드를 통해 선언합니다. 예제를 하나 보여드리겠습니다. 유저가 몇 번이나 로그인을 시도했는지 추적하기 위한 상수와 변수를 선언해보겠습니다.

1
2
let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0
cs


위 코드는 다음과 같이 해석됩니다.

"maximumNumberOfLoginAttempts 라는 이름의 새로운 상수를 하나 선언하였다. 이 상수에 10이라는 값을 할당하였다. 그 다음 currentLoginAttempt라는 새로운 변수를 하나 선언하였다. 이 변수에 초기값으로 0을 할당하였다."

이 예제에서 로그인 시도 최대 허용횟수는 상수로 선언되었습니다. 최대 허용횟수는 변할 일이 없기 때문입니다. 현재까지의 로그인 시도 횟수는 변수로 선언되었습니다. 이 횟수는 유저가 로그인 시도를 실패할 때마다 매번 1씩 증가할 것이기 때문입니다.

한 줄에 여러 개의 상수나 변수를 선언하고 싶다면 콤마( , )로 구분하세요.

1
var x = 0.0, y = 0.0, z = 0.0
cs


<Note>

만약 값이 한 번 정해진 후 나중에 변할 일이 없다면 항상 상수로 선언하세요. 변수는 값이 변할 일이 있는 경우에만 사용하세요.


Type Annotations

상수나 변수를 선언할 때, 그 안에 어떤 값을 저장할지 명확하게 하기 위해서 type annotation 을 제공할 수 있습니다. Type annotation은 상수나 변수의 이름 뒤에 콜론( : )으로 구분하여 작성합니다. 예제를 보여드리는 것이 빠를 것 같습니다. String 값을 저장할 welcomMessage라는 이름의 변수에 type annotation을 제공해봅시다.

1
var welcomeMessage: String
cs


선언문에서의 콜론(:)이 뜻하는 것은 "…의 타입은…(…of type…)" 입니다. 따라서 위 코드는 다음과 같이 해석됩니다.

" welcomeMessage라는 변수를 선언하였으며 그 변수의 타입은 String이다."

참고로 "타입은 String이다"가 뜻하는 것은 "String 값을 저장할 수 있다"는 것입니다.

변수 welcomeMessage에는 다음과 같이 어떤 String 값이라도 셋팅될 수 있습니다:

1
welcomeMessage = "Hello"
cs


같은 타입을 가진 여러 개의 변수를 한 줄로 정의할 수 있습니다. 다음과 같이 마지막 변수 이름 뒤에 타입을 지정해주면 됩니다:

1
var red, green, blue: Double
cs


<Note>

실제로 type annotation을 일일히 작성할 경우는 드물 겁니다. 상수나 변수를 정의하는 시점에서 초기값을 할당해주면 Swift는 거의 항상 자동으로 타입을 유추해줍니다. 자세한 것은 Type Safety and Type Inference를 참조하세요. 위 예제에서 welcomeMessage는 초기값을 할당받지 못했기 때문에 Swift가 자동으로 타입을 유추할 수 없습니다. 이럴 때는 따로 type annotation을 해줄 필요가 있습니다.


Naming Constants and Variables

상수와 변수의 이름에는 거의 모든 문자가 포함될 수 있습니다. 유니코드 문자도 포함입니다.

1
2
3
let π = 3.14159
let 你好 = "你好世界"
let 🐶🐮 = "dogcow"
cs


상수와 변수의 이름에 포함될 수 없는 것들도 있습니다. 여백 문자열(whitespace characters), 수학기호, 화살표, 비공식적인 유니코드 코드 포인트,  line- and box-drawing 문자열들(특수기호 종류)등은 포함될 수 없습니다. 또한 이름의 맨 앞에 숫자가 올 수는 없습니다.

이미 선언되어 있는 상수나 변수의 이름을 새로운 상수나 변수에 사용할 수는 없습니다. 또한 이미 선언되어 있는 상수나 변수의 타입을 나중에 변경하는 것도 불가능합니다. 상수를 변수로 변경하거나 그 반대로 하는 것 역시 불가능합니다.

<Note>

만약 Swift 문법에서 사용하는 키워드를 상수나 변수의 이름으로 지정하고 싶다면, 이름 붙일 때 backticks(`)를 붙이세요. 그러나 웬만하면 그런 짓은 하지 마세요.

변수에 이미 값이 들어있더라도 다른 값으로 변경할 수 있습니다. 단, 같은 타입의 값이어야 합니다. 다음 예제에서는 friendlyWelcome의 값을 "Hello!"에서 "Bonjour!"로 변경하고 있습니다:

1
2
3
var friendlyWelcome = "Hello!"
friendlyWelcome = "Bonjour!"
// friendlyWelcome is now "Bonjour!"
cs


변수와 다르게, 상수의 값은 한 번 셋팅되면 변경할 수 없습니다. 그런 시도를 할 시에는 다음과 같이 컴파일 시점에서 에러가 발생합니다:

1
2
3
let languageName = "Swift"
languageName = "Swift++"
// This is a compile-time error: languageName cannot be changed.
cs


Printing Constants and Variables

상수와 변수가 저장하고 있는 값을 출력하려면 다음과 같이 print(_:separator:terminator:) 함수를 사용하세요:

1
2
print(friendlyWelcome)
// Prints "Bonjour!"
cs


print(_:separator:terminator:) 함수는 하나 또는 여러 개의 값을 적절한 아웃풋으로 출력해주는 글로벌 함수입니다. 예를 들어 Xcode에서는 print(_:separator:terminator:) 함수가 아웃풋을 Xcode의 "콘솔" 패널에 출력해줍니다. separatorterminator 파라미터는 디폴트 값을 가집니다. 따라서 호출 시 생략해도 무방합니다. 생략 시 print 함수는 출력하는 라인의 뒤에 line break를 디폴트로 붙여줍니다. 만약 line break 없이 값을 출력하고 싶다면 terminator 파라미터에 빈 문자열을 넣으세요. 예를 들어 print(someValue, terminator: "") 처럼요. 파라미터의 디폴트 값에 대해 궁금하다면 Default Parameter Values 페이지를 참조하세요.

긴 문자열에 상수/변수의 이름을 placeholder로써 포함시키고, 문자열 출력 시점에 그 상수/변수의 현재값으로 대체하기 위하여, string interpolation 을 사용합니다. 괄호로 이름을 감싸고 시작 괄호 앞에 backslash( \ )기호를 붙이세요.

1
2
print("The current value of friendlyWelcome is \(friendlyWelcome)")
// Prints "The current value of friendlyWelcome is Bonjour!"
cs


<Note>

String interpolation에서 사용할 수 있는 모든 옵션은 String Interpolation 페이지에 나와있습니다.




Comments

주석을 사용하여 코드에 메모를 하세요. Swift 컴파일러는 주석을 실행하지 않고 무시합니다.

Swift에서 주석을 작성하는 방법은 C와 매우 유사합니다. 두 개의 슬래시 기호( // )를 주석 라인의 맨 앞에 위치시키세요(single-line에 해당).

1
// This is a comment.
cs


한 줄이 아니라 여러 줄의 주석을 작성하고 싶다면 슬래시+별표 기호( /* )로 시작해서 별표+슬래시 기호( */ )로 끝내세요.

1
2
/* This is also a comment
 but is written over multiple lines. */
cs


C와 다르게 Swift에서는 멀티라인 주석 안에 다른 멀티라인 주석을 포함시킬 수 있습니다. 예를 들면 이런 형태입니다.

1
2
3
/* This is the start of the first multiline comment.
 /* This is the second, nested multiline comment. */
This is the end of the first multiline comment. */
cs



Semicolons

다른 많은 언어들과 다르게 Swift는 세미콜론( ; )을 매 명령문 뒤에 써줄 필요가 없습니다. 쓰고 싶으면 써도 되지만요. 단, 여러 개의 명령문을 한 줄에 쓸 때는 세미콜론으로 구분을 해주어야 합니다.

1
2
let cat = "🐱"print(cat)
// Prints "🐱"
cs



Integers

Integer 는 소수점을 가지지 않는 모든 숫자를 말합니다. 예를 들어 42, -23 같은 것들입니다. Integer에는 signed (양수 또는 0 또는 음수) 와 unsigned (양수 또는 0) 두 종류가 있습니다.

Swift는 8, 16, 32, 64 비트 플랫폼 상에서 signed integer와 unsigned integer를 둘 다 지원합니다. 네이밍 컨벤션은 C와 유사합니다. 8비트 unsigned integer는 UInt8라는 타입으로, 32비트 signed integer는 Int32라는 타입으로 만들 수 있습니다. Swift의 모든 타입과 마찬가지로, interger 타입들 역시 대문자로 시작합니다.


Integer Bounds

Integer 타입들의 최소값과 최대값에 minmax라는 프로퍼티를 통해 접근할 수 있습니다.

1
2
let minValue = UInt8.min  // minValue is equal to 0, and is of type UInt8
let maxValue = UInt8.max  // maxValue is equal to 255, and is of type UInt8
cs


이런 프로퍼티들의 값들은 사이즈가 정해져 있는 숫자 타입(위 예제에서의 UInt8같은)의 값들입니다. 따라서 같은 타입의 다른 값들과 마찬가지로 취급됩니다(따라서 위 예제에서 minValue 상수의 타입이 UInt8로 결정되는 것이고, 다른 UInt8 타입의 변수에서 이 상수를 더하거나 빼는 것이 가능합니다).


Int

대부분의 경우 특별히 integer의 사이즈를 정할 필요는 없을 겁니다. Swift는 Int 라는 integer 타입을 제공하는데, 이 타입을 사용하면 현재 플랫폼에 맞춰서 적절한 사이즈로 정해지기 때문입니다.

  • 32비트 플랫폼에서 Int 타입은 Int32 타입과 같은 사이즈를 가집니다

  • 64비트 플랫폼에서 Int 타입은 Int64 타입과 같은 사이즈를 가집니다

따라서, 특별히 integer의 사이즈를 지정해야 할 필요가 없다면 그냥 Int 타입을 사용하세요. 코드의 일관성과 호환성(consistency and interoperability)에 도움을 줄 것입니다. 참고로 32비트 플랫폼에서도 Int 는 -2,147,483,648에서 2,147,483,647까지의 값을 저장할 수 있으며, 이것은 다른 많은 integer 타입들의 범위보다 더 넓습니다.


UInt

Unsigned interger 타입을 위한 UInt 타입이 있습니다. 이것 역시 Int 타입과 마찬가지로 현재 플랫폼에 맞춰서 적절한 사이즈로 정해집니다.

  • 32비트 플랫폼에서 UInt 타입은 UInt32 타입과 같은 사이즈를 가집니다

  • 64비트 플랫폼에서 UInt 타입은 UInt64 타입과 같은 사이즈를 갑니다

<Note>

UInt 타입은 정말로 unsigned integer 타입이 필요할 때만 사용하세요. 음수가 될 일이 없는 integer 라도 웬만하면 Int 타입을 사용하는 것을 추천합니다. 




Floating-Point Numbers

Floating-point numbers 는 소수점을 가지는 숫자들을 말합니다. 예를 들어 3.14159, 0.1, -273.15 같은 것들입니다.

Floating-point 타입은 interger 타입보다 더 넓은 범위의 값을 표현할 수 있으며, Int 타입에 비해 훨씬 크거나 훨씬 작은 값을 저장할 수 있습니다. Swift에서는 다음 두 개의 signed floating-point number 타입을 지원합니다:

  • Double : 64비트 floating-point number를 위함

  • Float : 32비트 floating-point number를 위함

<Note>

Double은 적어도 15 소수 자릿수의 정밀도를 가지지만 Float는 6 소수 자릿수 이하의 정밀도를 가집니다. 코드에서 사용할 적절한 floating-point 타입을 선택하는 것은 저장할 값의 범위에 따라 달라질 것입니다. 어느 것을 사용해도 상관없는 경우에는 Double을 추천합니다.




Type Safety and Type Inference

Swift는 type-safe 한 언어입니다. 따라서 코드에서 사용하는 값들에 대한 타입이 명백해집니다. String을 필요로 하는 부분에 Int를 넘기는 실수를 방지해줍니다.

Swift는 type safe 하기 때문에 컴파일 시 type checks 를 수행하고 타입 매칭이 잘못된 부분을 에러로 체크해줍니다. 따라서 타입에 관련된 에러를 더 빨리 찾고 고칠 수 있게 됩니다.

그러나 상수나 변수를 선언할 때마다 항상 타입을 명시해주어야 하는 것은 아닙니다. 만약 타입을 명시하지 않는다면 Swift가 타입 추론(type inference) 을 통해 알아서 적절한 타입을 정해줍니다. 컴파일러는 명령문, 선언문 등의 표현식에서 당신이 제공한 값을 통해 자동으로 타입을 추측합니다.

이런 타입 추론 덕분에, C나 Objective-C 같은 언어에 비해, 타입 선언문에서 할 것이 훨씬 적어집니다. 상수인지 변수인지는 명백하게 명시가 되어야 하지만(let과 var를 통해) 그것들의 타입을 정하는 일은 대부분 하지 않아도 될 것입니다.

타입 추론은 상수나 변수를 초기값과 함께 선언할 때에 특히 유용합니다. 상수나 변수를 선언하는 시점에서 리터럴 값(literal) 을 같이 할당하는 경우입니다. (리터럴 값이란 소스 코드에 직접 넣어놓는 값을 말합니다. 아래 예제어서의 42, 3.14159 등이 리터럴 값입니다.)

예를들어 새로운 상수에 따로 타입을 명시하지 않고 42라는 초기값을 할당한다면, Swift는 당신이 이 상수를 Int 타입으로 만들고 싶어한다고 추론해냅니다. Integer로 보이는 숫자로 초기화했기 때문입니다.

1
2
let meaningOfLife = 42
// meaningOfLife is inferred to be of type Int
cs


이와 유사하게, floating-point 리터럴로 초기화하고 타입을 명시하지 않는다면 Swift는 Double 타입이라고 판단합니다.

1
2
let pi = 3.14159
// pi is inferred to be of type Double
cs


floating-point 숫자에 대한 타입 판단 시, Swift는 항상 Double 을 선택합니다. (Float가 아니라)

Integer와 floating-point 가 합쳐진 리터럴의 경우에는 Double 타입이 추론됩니다.

1
2
let anotherPi = 3 + 0.14159
// anotherPi is also inferred to be of type Double
cs


위 예제에서 리터럴 값 3은 그 자체로는 명백한 타입을 가지지 않습니다. 따라서 덧셈의 일부분이 floating-point이기 때문에 전체 덧셈식의 타입으로 Double이 추론되는 것입니다.



Numeric Literals

Integer 리터럴은 다음과 같이 쓸 수 있습니다:

    • prefix 없이 -> 10진수(decimal number)

    • prefix 0b -> 2진수(binary number)

    • prefix 0o -> 8진수(octal number)

    • prefix 0x -> 16진수(hexadecimal number)

예제를 봅시다. 다음의 모든 Integer 리터럴은 십진수 17의 값을 가집니다:

1
2
3
4
let decimalInteger = 17
let binaryInteger = 0b10001       // 17 in binary notation
let octalInteger = 0o21           // 17 in octal notation
let hexadecimalInteger = 0x11     // 17 in hexadecimal notation
cs


Floating-point 리터럴은 10진수가 될 수도 있고(prefix 없이) 16진수가 될 수도 있습니다(prefix 0x). 반드시 소수점 왼쪽과 오른쪽에 둘 다 숫자(16진수도 포함)가 있어야 합니다. 10진수 Float는 지수(exponent) 를 가질 수 있습니다. 대문자나 소문자 e로 나타냅니다. 16진수 Float는 반드시 지수(exponent)를 가져야 합니다. 대문자나 소문자 p로 나타냅니다.

10진수에 대한 예제:

  • 1.25e2 means 1.25 x 102, or 125.0.

  • 1.25e-2 means 1.25 x 10-2, or 0.0125.


16진수에 대한 예제:

  • 0xFp2 means 15 x 22, or 60.0.

  • 0xFp-2 means 15 x 2-2, or 3.75.


다음의 모든 floating-point 리터럴은 십진수 12.1875의 값을 가집니다:

1
2
3
let decimalDouble = 12.1875
let exponentDouble = 1.21875e1
let hexadecimalDouble = 0xC.3p0
cs


숫자 리터럴을 좀 더 읽기 쉽게 나타낼 수도 있습니다. Integer와 float 둘 다 0과 언더스코프( _ )문자를 사용해서 가독성을 높일 수 있습니다. 두 경우 모두 원본값에는 변화를 주지 않습니다. 예제로 알아봅시다.

1
2
3
let paddedDouble = 000123.456
let oneMillion = 1_000_000
let justOverOneMillion = 1_000_000.000_000_1
cs




Numeric Type Conversion

코드 상에서 쓰일 일반적인 Integer 상수&변수에는 Int 타입을 사용하세요. 설령 음수가 될 일이 없더라도 그렇게 하세요. 일반적인 상황에서 디폴트 integer 타입(즉, Int)을 사용하면 당신의 코드 상에서 integer 상수&변수들은 상호 정보 교환이 가능해지며, integer 리터럴 값에 대해서 유추되는 타입(즉, Int)과도 타입이 일치할 것입니다.

다른 integer type은 특별히 데이터 사이즈가 고정되어야 하는 경우에만 사용하세요. 외부 소스를 위해서, 혹은 성능과 메모리 사용량 같은 최적화 이슈 때문에 그래야 할 때가 있을 것입니다. 이런 상황에서 정확히 고정된 사이즈의 타입을 사용하면 오버플로우되는 값을 잡는 데 도움이 되고, 이 데이터가 어떻게 쓰일지를 함축할 수 있습니다. 


Integer Conversion

Integer 상수&변수에 저장되는 수의 범위는 각 타입마다 다릅니다. Int8 상수&변수는 -128부터 127까지의 수를 저장할 수 있고, UInt8 상수&변수는 0부터 255까지의 수를 저장할 수 있습니다. 이 범위를 벗어나게 되면 컴파일 시점에서 다음과 같이 에러가 납니다:

1
2
3
4
5
let cannotBeNegative: UInt8 = -1
// UInt8 cannot store negative numbers, and so this will report an error
let tooBig: Int8 = Int8.max + 1
// Int8 cannot store a number larger than its maximum value,
// and so this will also report an error
cs


각 숫자 타입마다 다른 범위의 값을 저장할 수 있기 때문에, 숫자 타입 전환(conversion)은 반드시 케이스 별로 이루어져야 합니다. 이런 접근법은 타입전환 오류를 방지해줄 것이며 타입전환 의도를 명시적으로 나타내줄 것입니다.

숫자 타입 하나를 다른 숫자 타입으로 변환하기 위해서는, 그 숫자 값으로 새로운 타입의 숫자를 생성(initialize)해야 합니다. 아래 예제를 보시면, 상수 twoThousandUInt16 타입이고 상수 oneUInt8 타입입니다. 이 두개는 직접적으로 더해질 수 없습니다. 서로 같은 타입이 아니기 때문입니다. 아래 예제에서는 UInt16(one)을 호출하여 새로운 UInt16 타입을 생성해 사용하고 있습니다.

1
2
3
let twoThousand: UInt16 = 2_000
let one: UInt8 = 1
let twoThousandAndOne = twoThousand + UInt16(one)
cs


더하기 기호 양 옆은 이제 둘 다 UInt16 타입이기 때문에 덧셈이 가능합니다. 또한 덧셈의 결과가 저장되는 상수(twoThousandAndOne)도 UInt16 타입으로 유추되게 됩니다. UInt16 타입의 값들을 더한 것이기 때문입니다.

위 예제의 UInt16(one)처럼 SomeType(ofInitialValue) 형태는 초기값을 넘기며 이니셜라이저를 호출하는 기본적인 방법입니다. 그러나 사실 위 예제에서 내부적으로는 UInt8 값을 파라미터로 받는 UInt16의 이니셜라이저를 사용하고 있습니다. 아무 타입의 값이나 넘겨서 UInt16를 새로 생성할 수는 없습니다. 오직 UInt16에서 내부적으로 정의되어 있는 이니셜라이저만 호출이 가능하다는 뜻입니다. 새로운 이니셜라이저를 정의하고 싶다면 기존 타입을 확장하는 방식을 사용해보세요. Extensions 에서 다룹니다.


Integer and Floating-Point Conversion

Integer 타입과 Floating-point 타입간의 변환은 반드시 명시적으로 되어야합니다:

1
2
3
4
let three = 3
let pointOneFourOneFiveNine = 0.14159
let pi = Double(three) + pointOneFourOneFiveNine
// pi equals 3.14159, and is inferred to be of type Double
cs


상수 three 의 값이 Double 타입의 새로운 값을 생성하기 위해 사용되었습니다. 덧셈 기호의 양 옆을 같은 타입으로 맞춰주기 위해서요. 이런 식으로 변환을 해주지 않으면 덧셈 자체가 허용되지 않습니다.

Floating-point 를 Integer 로 변환할 때도 반드시 명시적으로 해주어야 합니다. Integer 타입은 Double 이나 Float 값으로 초기화될 수 있습니다.

1
2
let integerPi = Int(pi)
// integerPi equals 3, and is inferred to be of type Int
cs


Floating-point 값으로 Integer 초기값을 세팅하는 위와 같은 경우에는, 항상 소수점 버림 처리가 됩니다. 예를 들어 4.754 가 되고, -3.9-3 이 됩니다.

<Note>

숫자 상수/변수를 더하는 규칙은 숫자 리터럴을 더하는 규칙과는 다릅니다. 리터럴 값 3은 리터럴 값 0.14159에 바로 더해질 수 있습니다. 숫자 리터럴은 명시적인 타입을 미리 가지지 않기 때문입니다. 숫자 리터럴의 타입이 유추되는 순간은 컴파일러에 의해 그것이 진짜 사용되는 순간입니다.




Type Aliases

Type aliases 는 기존 타입을 위한 다른 이름을 정의합니다(기존 타입에게 별명을 붙여준다고 생각하세요!). typealias 키워드를 통해 type aliases 를 정의할 수 있습니다.

Type aliases 는 기존 타입을 문맥상으로 더 적절한 이름으로 지칭하고 싶을 때 유용합니다. 예를 들면 외부 소스로부터의 데이터가 특정 사이즈를 가지는 상황을 생각해볼 수 있습니다:

1
typealias AudioSample = UInt16
cs


한 번 Type aliases 를 정의해두면 어디서건 원래 타입 명 대신 사용할 수 있습니다:

1
2
var maxAmplitudeFound = AudioSample.min
// maxAmplitudeFound is now 0
cs


위의 예제에서 AudioSample 을 UInt16 의 별명으로 정했습니다. 따라서 AudioSample.min 을 호출하는 것은 실제로 UInt16.min 을 호출하는 것과 같습니다. maxAmplitudeFound 변수의 초기값에는 0이 할당되게 됩니다.



Booleans

Swift 에는 Bool 이라고 하는 기본 Boolean 타입이 있습니다. Boolean 값은 logical 합니다. True 혹은 False 의 상태만 될 수 있기 때문입니다. Swift 에서는 truefalse 두 가지의 Boolean 상수 값을 제공합니다:

1
2
let orangesAreOrange = true
let turnipsAreDelicious = false
cs


orangesAreOrange 와 turnipsAreDelicious 의 타입은 Bool 로 유추됩니다. 그들이 Boolean 리터럴 값으로 초기화되었기 때문입니다. 위에서 살펴본 Int, Double 과 마찬가지로, 상수/변수를 생성할 때 true 혹은 false 로 초기화를 해준 경우에는 굳이 Bool 타입이라고 선언해 줄 필요가 없습니다. 타입이 이미 알려진 값들로 상수/변수가 초기화 될 때, 타입 추론은 Swift 코드를 더 정확하고 가독성 있게 만들어줍니다.

Boolean 값은 조건문에서 특히 유용합니다. 예를 들어 if 문에서 다음과 같이 사용됩니다:

1
2
3
4
5
6
if turnipsAreDelicious {
    print("Mmm, tasty turnips!")
else {
    print("Eww, turnips are horrible.")
}
// Prints "Eww, turnips are horrible."
cs


if 같은 조건문에 대해서는 Control Flow 에서 더 자세히 다룹니다.

Swift의 Type Safety 속성은 Boolean 이 아닌 값이 Bool 의 대용으로 사용되는 것을 막아줍니다. 다음 예제는 컴파일 에러를 발생시킵니다:

1
2
3
4
let i = 1
if i {
    // this example will not compile, and will report an error
}
cs


그러나, 다음과 같은 예제는 문제없습니다:

1
2
3
4
let i = 1
if i == 1 {
    // this example will compile successfully
}
cs


i == 1 비교의 결과값은 Bool 타입입니다. 따라서 두 번째 예제는 type-check 를 통과합니다. i == 1 과 같은 비교에 대해서는 Basic Operators 에서 다룹니다.

Swift 의 type safety 에 대한 다른 예제들과 마찬가지로, 이런 접근법은 실수를 방지해주며 코드의 의도가 명확하다는 것을 보장해줍니다.



Tuples

Tuple 은 여러 개의 값들을 하나의 결합된 값으로 묶어줍니다. Tuple 안의 값들은 어떤 타입이어도 상관 없고, 안의 값들은 서로 같은 타입일 필요가 없습니다.

예제에서, (404, "Not Found") HTTP status code 를 나타내기 위한 Tuple입니다. HTTP status code 란, 당신이 웹 페이지에 리퀘스트를 할 때마다 웹 서버가 돌려주는 특정한 값입니다. 404 Not Found 는 당신이 존재하지 않는 웹 페이지를 요청했을 때 받게 되는 status code 입니다.

1
2
let http404Error = (404"Not Found")
// http404Error is of type (Int, String), and equals (404, "Not Found")
cs


Tuple (404, "Not Found")IntString 을 함께 묶습니다. HTTP status code 의 두 개의 분리된 값(숫자와 사람이 읽을 수 있는 설명)을 제공하기 위함입니다. "(Int, String) 타입을 가진 Tuple" 이라고 이해하면 됩니다.

Tuple 은 각각 다른 타입들을 얼마든지 섞어서 만들 수 있습니다. (Int, Int, Int), (String, Bool) 등등, 필요한 만큼 어떤 타입이라도 섞어서 만들어보세요.

Tuple 의 contents 를 여러 개의 상수/변수로 분해(decompose) 하여 접근할 수 있습니다:

1
2
3
4
5
let (statusCode, statusMessage) = http404Error
print("The status code is \(statusCode)")
// Prints "The status code is 404"
print("The status message is \(statusMessage)")
// Prints "The status message is Not Found"
cs


Tuple 을 분해할 때 contents 값들 중 특정 부분만이 필요하다면, 나머지 값들은 언더바( _ ) 를 사용하여 무시할 수 있습니다.

1
2
3
let (justTheStatusCode, _) = http404Error
print("The status code is \(justTheStatusCode)")
// Prints "The status code is 404"
cs


또는 숫자 인덱스를 사용하여 tuple 의 개별 값에 접근할 수 있습니다. 인덱스는 0부터 시작합니다.

1
2
3
4
print("The status code is \(http404Error.0)")
// Prints "The status code is 404"
print("The status message is \(http404Error.1)")
// Prints "The status message is Not Found"
cs


Tuple 을 정의할 때 tuple 을 구성하는 개별 요소들에게 이름을 붙일 수 있습니다.

1
let http200Status = (statusCode: 200, description: "OK")
cs


Tuple 의 요소들에게 이름을 붙여놓았다면, 이 요소들의 값에 접근할 때 그 이름을 사용할 수 있습니다:

1
2
3
4
print("The status code is \(http200Status.statusCode)")
// Prints "The status code is 200"
print("The status message is \(http200Status.description)")
// Prints "The status message is OK"
cs


Tuple 은 함수의 리턴 값으로 사용될 때 특히 유용합니다. 예를 들어 웹 페이지를 긁어오는 함수의 경우에는, 웹 페이지 긁어오는 것을 성공했는지 혹은 실패했는지를 알려주기 위해서 (Int, String) tuple 타입을 리턴할 것입니다. 두 개의 구분되는 값들(그리고 두 개의 다른 타입)을 tuple로 묶어서 리턴함으로써, 함수는 한 개의 타입을 가지는 한 개의 값을 리턴할 때보다 더 유용한 결과 정보를 제공할 수 있게 됩니다. 더 자세한 정보는 Functions with Multiple Return Values 를 참조하세요.

<Note>

Tuple 은 연관된 값들을 임시로 하나로 묶을 때 유용합니다. 그러나 복잡한 데이터 구조를 생성할 때 tuple 을 사용하는 것은 적합하지 않습니다. temporary scope 를 넘어서는 데이터 구조가 필요하다면 tuple 보다는 class 나 structure 로 모델을 만드세요. 더 자세한 정보는 Classes and Structures 를 참조하세요.




Optionals

Swift에서는 값이 비어있을 수 있는 상황에서 optional 개념이 사용됩니다. Optional 은 두 가지 가능성을 가지는 개념입니다: 값이 존재하며 이 값에 접근하기 위해 unwrap을 할 수 있거나, 또는 값이 아예 존재하지 않음.

<Note>

Optional 개념은 C 나 Objective-C 에 없는 개념입니다. Objective-C 에서 그나마 이것과 유사한 것을 찾아보자면, 객체(Object)를 리턴할 메서드에서 "리턴할만한 유효한 객체가 없다"는 의미로 nil 을 리턴하는 경우입니다. 그러나 이렇게 nil 을 사용하는 것은 오직 객체를 위해서만 허용되며, structure 나 basic C type, enumeration 값들에는 nil 을 사용할 수 없습니다. 이런 타입들의 경우, 리턴할만한 유효한 값이 없다는 것을 표현해야 할 때 Objective-C 에서는 주로 NSNotFound 같은 특별한 값을 사용합니다. 이런 접근법은 메서드를 부르는 곳에서 "유효한 값이 없음"을 나타내는 특별한 값을 이미 알고 있으며 이것에 대한 handle 이 되어있다고 가정하는 방식입니다. 이와 대조적으로 Swift의 optional 개념을 사용하면, 특별한 상수를 사용하지 않고서도 어떤 타입에 대해서든 "유효한 값이 없다"는 것을 알려줄 수 있습니다.


값이 없는 상황을 optional 을 통해 처리하는 예제를 하나 보여드리겠습니다. Swift의 Int 타입은 String 값을 받아 Int 값으로 변환하는 이니셜라이저를 가지고 있습니다. 그러나 모든 string 이 integer 로 변환될 수 있는 것은 아닙니다. 문자열 "123" 은 숫자 값 123 으로 변환될 수 있지만, 문자열 "hello, world" 는 어떤 숫자 값으로 변환하면 좋을지 알 수가 없죠.

아래 예제에서 이 이니셜라이저(StringInt 로 변환하는 이니셜라이저)를 사용하고 있습니다:

1
2
3
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber is inferred to be of type "Int?", or "optional Int"
cs


이 이니셜라이저는 fail 될 가능성이 있기 때문에 Int 가 아닌 optional Int 를 리턴합니다. Optional IntInt 가 아니라 Int? 라고 씁니다. 물음표 기호는 다음을 내포합니다: 담고 있는 값이 optional 하다는 것. 즉, 실제로 어떠한 Int 값을 담고 있을 수도 있고, 전혀 아무 값도 담고 있지 않을 수도 있다는 것. (Bool 값이나 String 값처럼 다른 타입의 값은 담지 못합니다. Int 값을 담고 있거나, 혹은 아무것도 없을 뿐입니다.)


nil

Optional 변수를 valueless state 로 만드려면 nil 이라는 특별한 값을 할당하세요:

1
2
3
4
var serverResponseCode: Int= 404
// serverResponseCode contains an actual Int value of 404
serverResponseCode = nil
// serverResponseCode now contains no value
cs


<Note>

Optional 이 아닌 상수/변수에는 nil 을 사용할 수 없습니다. 특정한 상황에서 값이 없는 경우가 발생하는 상수/변수가 필요한 경우, 적절한 타입의 optional 상수/변수로 선언해주세요.


디폴트 값 없이 optional 변수를 정의한 경우, 자동으로 nil 이 초기값이 됩니다:

1
2
var surveyAnswer: String?
// surveyAnswer is automatically set to nil
cs


<Note>

Swift 에서의 nil 은 Objective-C 에서의 nil 과는 다릅니다. Objective-C 에서의 nil 은 존재하지 않는 객체(object)를 가리키는 포인터였습니다. Swift 에서의 nil 은 포인터가 아니라, 특정 타입의 값이 존재하지 않음을 뜻하는 것입니다. 어떤 타입이건간에 optional 이기만 하면 nil 을 세팅할 수 있습니다(객체 타입에만 한정되지 않습니다).



If Statements and Forced Unwrapping

optional 이 값을 담고 있는지 알아보기 위해 if 문을 사용할 수 있습니다. If 문으로 optional 과 nil 을 비교해봅시다. "equal to" 연산자 (==) 또는 "not equal to" 연산자 (!=) 를 사용할 수 있습니다.

Optional 이 값을 담고 있다면, nil 과는 "not equal to(같지 않은)" 상태가 됩니다:

1
2
3
4
if convertedNumber != nil {
    print("convertedNumber contains some integer value.")
}
// Prints "convertedNumber contains some integer value."
cs


Optional 이 값을 담고 있다고 확신할 수 있다면, optional 의 이름 뒤에 느낌표( ! )를 붙여서 실제 값에 접근할 수 있습니다. 느낌표는 "이 Optional 에는 틀림없이 값이 들어있으니 사용해 달라." 고 말할 수 있는 효과적인 방법입니다. 이것을 Optional 값에 대한 forced unwrapping 이라고 부릅니다(값을 강제로 빼내온다!).

1
2
3
4
if convertedNumber != nil {
    print("convertedNumber has an integer value of \(convertedNumber!).")
}
// Prints "convertedNumber has an integer value of 123."
cs


if 문에 대해 더 자세히 알고 싶다면 Control Flow 를 참고하세요.

<Note>

값이 담겨있지 않은 optional 에 ! 를 통해 접근하려 시도하면 런타임 에러가 발생합니다. Optional 의 값을 force-unwrap 하기 전에는 항상 그 optional 에 nil 이 아닌 진짜 값이 담겨 있는지 확신할 수 있어야 합니다.



Optional Binding

optional 이 값을 담고 있는지 알아보기 위해, 그리고 만약 값을 담고 있다면 이 값을 임시 상수/변수에 할당해서 사용하기 위해, optional binding 을 사용할 수 있습니다. Optional binding 은 if / while 문과 함께 사용될 수 있으며, 값이 존재하는지 알아보고 그 값을 꺼내 임시 상수/변수에 할당하는 일을 한 줄로 끝낼 수 있습니다. 참고로 if / while 문은 Control Flow 에 자세히 기술되어 있습니다.

If 문을 위한 optional binding 을 살펴봅시다:


  • if
    let constantName = someOptional {
  •     statements
  • }

아까 Optionals 섹션에서 보았던 possibleNumber 예제는 forced unwrapping 대신 optional binding 을 이용하는 것으로 다시 작성해볼 수 있습니다.

1
2
3
4
5
6
if let actualNumber = Int(possibleNumber) {
    print("\"\(possibleNumber)\" has an integer value of \(actualNumber)")
else {
    print("\"\(possibleNumber)\" could not be converted to an integer")
}
// Prints ""123" has an integer value of 123"
cs


이 코드는 이렇게 읽을 수 있습니다:

"Int(possibleNumber) 가 리턴하는 optional Int 가 값을 담고 있을 경우, actualNumber 라는 새로운 상수에 그 값을 할당하라."

변환이 성공적이라면 상수 actualNumberif 문의 첫 번째 branch 안에서 사용 가능한 상태가 됩니다. actualNumber 는 이미 optional 안의 실제 값으로 초기화가 되었기 때문에, 이름 뒤에 ! 를 붙이지 않아도 됩니다. 이 예제에서 actualNumber 는 단순히 변환의 결과를 출력하기 위해 사용되고 있습니다.

Optional binding 에는 상수, 변수 둘 다 사용 가능합니다. 만약 if 문의 첫 번째 branch 안에서 actualNumber 의 값을 조작하고 싶다면, if var actualNumber 로 코드를 작성하세요. 그러면 actualNumber 를 상수가 아니라 변수로 사용할 수 있습니다.

하나의 if 문에 원하는 만큼의 optional binding 과 Boolean 조건문을 포함시킬 수 있습니다. 구분은 콤마( , )로 하세요. 이때 optional binding 으로 체크하는 값 중 하나라도 nil 이 있거나, Boolean 조건문 중 하나라도 false 라면, 그 if 문 자체가 false 로 처리됩니다. 예제를 봅시다. 다음 if 문 두 개는 동일한 조건을 체크하고 있습니다: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < 
  secondNumber && secondNumber < 100 {
    print("\(firstNumber) < \(secondNumber) < 100")
}
// Prints "4 < 42 < 100"
 
if let firstNumber = Int("4") {
    if let secondNumber = Int("42") {
        if firstNumber < secondNumber && secondNumber < 100 {
            print("\(firstNumber) < \(secondNumber) < 100")
        }
    }
}
// Prints "4 < 42 < 100"
cs


<Note>

If 문의 optional binding 을 통해 만들어진 상수/변수는 오직 if 문의 body 내에서만 사용 가능합니다. 그러나 guard 문을 통해 만들어진 상수/변수는 guard 문 다음에 오는 코드에서도 사용 가능합니다. Early Exit 에 설명되어 있습니다.



Implicitly Unwrapped Optionals

위에서 설명한 것처럼, optional 은 상수/변수에게 "값을 지니지 않는" 상태를 허락해주는 개념입니다. If 문을 통해서 optional 이 값을 지니고 있는지 체크할 수 있으며, 만약 값을 지니고 있다면 optional binding 을 통해서 값을 unwrap 할 수 있습니다.

프로그램 구조 상 optional 에 처음 값이 할당된 다음부터는 항상 값이 있다는 것이 명백해질 때가 종종 있습니다. 이런 케이스에서는 항상 값이 있기 때문에, 매번 값이 있는지를 체크할 필요가 없으며 unwrap 할 필요도 없습니다.

이런 종류의 optional 들을 implicitly unwrapped optional 이라고 부릅니다. Optional 로 만들고 싶은 타입 뒤에 물음표 마크 (String?) 대신 느낌표 마크 (String!)을 붙임으로써 implicitly unwrapped optional 을 만들 수 있습니다.

Implicitly unwrapped optional 은 optional 의 값이 한 번 정의된 후에는 반드시 존재한다고 확신할 수 있을 때 유용합니다. Swift 에서 implicitly unwrapped optional 은 클래스 초기화에서 주로 사용됩니다. Unowned References and Implicitly Unwrapped Optional Properties 섹션에서 자세히 다룹니다.

Implicitly unwrapped optional 은 사실 평범한 optional 이지만, optional 이 아닌 값처럼 사용됩니다. 값에 접근할 때마다 optional 값을 unwrap 할 필요가 없기 때문입니다. 다음 예제는 optional string 과 implicitly unwrapped optional string 간의 차이를 보여줍니다. 이들이 담고 있는 값(String)에 접근하는 방식을 눈여겨보세요.

1
2
3
4
5
let possibleString: String= "An optional string."
let forcedString: String = possibleString! // requires an exclamation mark
 
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // no need for an exclamation mark
cs


Implicitly unwrapped optional 은 optional 이 사용될 때마다 자동으로 unwrap 되는 권한을 주는 것이라고 생각해도 됩니다. optional 을 사용할 때마다 매번 이름 뒤에 느낌표를 붙이는 것보다는, optional 을 선언할 때 optional 의 타입 뒤에 느낌표를 붙이는 것이 편리할 것입니다.

<Note>

만약 implicitly unwrapped optional 이 nil 이고 이것의 wrapped value 에 접근하려고 한다면, 당신은 런타임 에러를 만나게 됩니다. 일반적인 optional (값이 없는) 에 느낌표를 붙여 접근하는 것과 동일하다고 보시면 됩니다.


Implicitly unwrapped optional 역시 일반적인 optional 과 마찬가지로 다룰 수 있습니다. 아래 예제처럼 값을 담고 있는지를 체크해볼 수 있습니다:

1
2
3
4
if assumedString != nil {
    print(assumedString!)
}
// Prints "An implicitly unwrapped optional string."
cs


또한 implicitly unwrapped optional 도 optional binding 을 적용할 수 있습니다:

1
2
3
4
if let definiteString = assumedString {
    print(definiteString)
}
// Prints "An implicitly unwrapped optional string."
cs


<Note>

나중에 nil 이 될지도 모르는 변수에는 implicitly unwrapped optional 을 사용하지 마시고 일반적인 optional type 을 사용하세요.


이건 개인적인 생각입니다만, 저는 implicitly unwrapped optional 이나 forced Unwrapping 을 사용하지 않는 것이 바람직하다고 생각합니다. optional + 느낌표는 위험하고 마음을 불안하게 만드는 조합이며 optional 의 개념에도 부합하지 않다고 생각합니다. 처음 코드를 짤 때는 안전할지 몰라도, 협업하는 개발자가 그 변수에 대해 충분히 인지하지 못해서 혹은 내 기억력이 나를 배신해서 등등 예상치 못하게 optional 이 nil 이 되어버리는 불상사가 일어날 수 있습니다. 다소 귀찮고 불필요해 보일지 몰라도 optional 은 항상 물음표 혹은 optional binding 을 통해 안전하게 처리하는 것이 어떨까요? 이것은 어디까지나 저의 생각이며 저의 코딩 습관입니다.



Error Handling

프로그램이 실행 도중 일으킬지 모르는 오류 상황들에 대응하기 위해 error handling 을 할 수 있습니다.

Optional 을 통해서는 성공이나 실패라는 결과만을 알 수 있던 것과는 다르게 (예를 들어, 함수의 리턴 값이 있다면 성공이고 리턴 값이 nil이라면 실패라고 판단), Error handling 은 실패한 근본적인 원인을 알 수 있도록 해주며, 필요한 경우 프로그램의 다른 부분에도 error 를 전파할 수 있게 해줍니다.

함수는 오류 상황에 직면했을 때 error 를 throw 합니다(에러를 던집니다). 그러면 함수를 호출한 주체는 error 를 catch 하여 적절하게 대응할 수 있습니다.

1
2
3
func canThrowAnError() throws {
    // this function may or may not throw an error
}
cs


함수 선언부에 throws 키워드를 포함함으로써, 그 함수는 error 를 던질(throw) 수 있다는 것을 명시합니다. Error 를 던질 수 있는 함수를 호출할 때는 expression 에 try 키워드를 붙입니다.

Swift 는 error 를 현재 scope 의 외부로 자동 전파합니다. 단, catch 문을 통해 handle 되는 범위까지가 해당됩니다.

1
2
3
4
5
6
do {
    try canThrowAnError()
    // no error was thrown
catch {
    // an error was thrown
}
cs


do 문은 새로운 containing scope 를 생성합니다. 이 scope 안에서 error 가 던져질 경우, 하나 또는 그 이상의 catch clause 로 전파되게 됩니다.

Error handling 을 통해 서로 다른 오류 상황들에 대처하고 있는 예제를 살펴봅시다:

1
2
3
4
5
6
7
8
9
10
11
12
func makeASandwich() throws {
    // ...
}
 
do {
    try makeASandwich()
    eatASandwich()
catch SandwichError.outOfCleanDishes {
    washDishes()
catch SandwichError.missingIngredients(let ingredients) {
    buyGroceries(ingredients)
}
cs


깨끗한 접시가 없거나 샌드위치 재료가 없는 경우 makeASandwich() 함수는 error 를 던질 것입니다. makeASandwich() 가 error 를 던질 가능성이 있기 때문에, 함수 호출은 try expression 으로 wrapping 된 것입니다. do 문 안에서 함수 호출을 wrapping 했기 때문에, 함수에서 던지는 어떤 error 이건 catch clause 로 전파될 것입니다.

만일 어떤 error 도 던져지지 않는다면 eatASandwich() 함수가 호출됩니다. 만일 error 가 던져졌고 그것이 SandwichError.outOfCleanDishe 케이스와 매칭된다면 washDishes() 함수가 호출됩니다. 만일 error 가 던져졌고 그것이 SandwichError.missingIngredients 케이스와 매칭된다면 catch 패턴에서 캡쳐된 [String] 값과 함께 buyGroceries(_:) 함수가 호출됩니다.

Error 의 Throwing(던지기), catching(캐치하기), propagating(전파하기) 에 대해서는 Error Handling 에서 훨씬 자세하게 다루고 있습니다.



Assertions and Preconditions

Assertionprecondition 은 런타임에 발생하는 체크입니다. 이것들을 이용하여 특정 코드를 실행하기 위해 필수적인 조건을 충족하고 있는지를 체크할 수 있습니다. Assertion 또는 precondition 의 Boolean condition 결과값이 true 라면 그 후 실행되어야 할 코드가 정상적으로 실행됩니다. 그러나 결과값이 false 라면 프로그램의 현재 state 가 invalid 하다는 것으로 볼 수 있습니다; 코드 실행은 중단되며 앱도 종료됩니다.

Assertion 과 precondition 은 개발자가 코드를 짜며 가정하고 기대한 것을 표현하는 것이므로, 코드의 일부분으로 포함시킬 수 있습니다. Assertion 은 개발 상의 실수와 잘못된 가정을 알아차릴 수 있도록 도와줄 것이고, Precondition 은 실제로 프로덕션 빌드에서 발생하는 이슈들을 알아차릴 수 있도록 도와줄 것입니다.

Assertion 과 precondition 은 코드 내에서 유용한 문서의 역할을 하기도 합니다. 위에서 다루었던 Error Handling 의 error handling 과는 다르게, assertion 과 precondition 은 복구가능한(recoverable) 오류나 미리 예상한(expected) 오류에 사용되는 것이 아닙니다. Assertion 또는 precondition 의 실패는 곧 프로그램의 state 가 비정상적이라는 것을 뜻합니다. 실패한 assertion 을 catch 할 방법은 없습니다.

Assertion 과 precondition 으로 코드 디버깅을 대신할 수는 없습니다. 그러나 앱이 invalid 한 state 가 되면 곧바로 앱을 종료시키기 때문에, 문제를 좀 더 빨리 발견할 수 있게 도와줍니다. 즉, 디버깅을 더 쉽게 만들어 줄 것입니다. 또한 앱을 바로 종료시키기 때문에, 잘못된 데이터나 잘못된 상태가 입힐 피해를 축소시킬 수 있게 됩니다.

Assertion 과 precondition 은 체크되는 곳이 다릅니다: Assertion 은 오직 디버그 빌드에서만 체크됩니다. 그러나 precondition 은 디버그 빌드와 프로덕션 빌드 둘 다에서 체크됩니다. 프로덕션 빌드에서, assertion 의 조건문은 실행조차 되지 않습니다. 따라서 프로덕션 빌드에는 아무 영향을 끼치지 않기 때문에, 개발하는 동안 원하는 만큼의 assertion 문을 마음껏 사용해도 됩니다.


Debugging with Assertions

Swift standard library 의 assert(_:_:file:line:) 함수를 호출하여 assertion 을 작성하세요. 함수에 조건문(true 또는 false 로 결과가 나와야 함)과 메시지(조건문의 결과가 false 일 때 보여지는 로그 메시지)를 넘기세요.

1
2
3
let age = -3
assert(age >= 0"A person's age can't be less than zero.")
// This assertion fails because -3 is not >= 0.
cs


위 예제에서 age >= 0 의 결과가 true 라면, 즉, age 의 값이 음수가 아니라면 코드 실행은 계속됩니다. 만약 age 가 음수라면, 즉 age >= 0 의 결과가 false 라면, assertion 은 실패하고 어플리케이션은 종료됩니다.

Assertion 메시지는 생략해도 됩니다.

1
assert(age >= 0)
cs


코드 상으로 이미 조건을 체크하고 있는 경우에는, assertionFailure(_:file:line:) 함수를 통해 assertion 이 실패했음을 알릴 수 있습니다.

1
2
3
4
5
6
7
8
if age > 10 {
    print("You can ride the roller-coaster or the ferris wheel.")
else if age > 0 {
    print("You can ride the ferris wheel.")
else {
    assertionFailure("A person's age can't be less than zero.")
}
 
cs



Enforcing Preconditions

코드를 실행하려면 조건문의 결과가 반드시 true 가 되어야 할 곳에서 precondition 을 사용하세요. 예를 들어 subscript 가 범위를 벗어나지는 않는지, 함수가 유효한 값을 받았는지 등을 체크할 때 사용하세요.

precondition(_:_:file:line:) 함수를 호출하여 precondition 을 작성하세요. 함수에 조건문(true 또는 false 로 결과가 나와야 함)과 메시지(조건문의 결과가 false 일 때 보여지는 로그 메시지)를 넘기세요.

1
2
// In the implementation of a subscript...
precondition(index > 0"Index must be greater than zero.")
cs


또한 이미 조건이 실패했음을 알려주기 위하여 preconditionFailure(_:file:line:) 함수를 호출할 수도 있습니다. 예를 들면, Switch 의 모든 유효한 케이스에 해당하지 않는 default case 가 매칭될 때를 생각해볼 수 있습니다.

<Note>

만일 unchecked mode (-Ounchecked) 로 컴파일 한다면 precondition 은 체크되지 않습니다. 컴파일러는 precondition 을 항상 true 로 가정하여 코드를 최적화할 것입니다. 그러나 fatalError(_:file:line:) 함수는 optimization 세팅과 상관 없이 항상 앱 실행을 중단시킵니다.

따라서 프로토타이핑이나 개발을 시작한 직후에는 fatalError(_:file:line:) 함수를 사용하여 아직 구현되지 않은 부분을 체크해둘 수 있습니다. fatalError("Unimplemented") 같은 것을 미구현 부분에 적용해두면, optimization 세팅과 상관없이 항상 앱이 중단될 것이므로 미구현한 부분을 반드시 알아차리게 됩니다.


'Swift 4.1' 카테고리의 다른 글

Basic Operators  (0) 2018.03.23
The basics  (0) 2018.03.02
A Swift Tour  (0) 2017.10.17
Version Compatibility  (0) 2017.10.04
About Swift  (0) 2017.10.04

Apple 제공 Swift 프로그래밍 가이드(4.1)의 A Swift Tour 부분을 공부하며 정리한 글입니다. 개인적인 생각, 이해를 돕기 위한 예제도 조금 들어가있습니다.


들어가며

새로운 언어를 시작할 때 우리는 전통적으로 "Hello, world!"를 출력해봅니다. Swift에서 이것은 다음 한 줄로 해볼 수 있습니다.

print("Hello, world!")


만약 당신이 C 혹은 Objective-C를 사용해보았다면 Swift의 문법은 당신에게 친숙하게 느껴질 것입니다. input/output 혹은 string handling을 위한 별도의 라이브러리를 import 할 필요없이 위의 한 줄로 끝납니다. global scope에 작성된 코드는 프로그램의 entry point로 사용됩니다. main() 함수가 필요하지 않은 것입니다. 또한 모든 statement 뒤에 세미콜론(;)을 입력할 필요도 없습니다.

지금부터 시작하는 Swift 투어는 코드 작성을 위한 충분한 정보를 당신에게 제공할 것입니다. Swift를 통해 어떻게 다양한 프로그래밍 테스크들을 달성할 수 있는지 보여드리겠습니다. 도중 이해할 수 없는 것들이 있더라도 안심하세요. 이 투어에서 소개되는 모든 개념들은 나중에 자세하게 다룰 것입니다.

<Note>
이 투어는 Xcode의 playground와 함께 하면 더 좋습니다!

Download Playground



Simple Values

상수(constant)를 만들기 위해 let을, 변수(variable)를 만들기 위해 var를 사용하세요. 상수의 값(value)은 컴파일 타임에 알 필요가 없습니다. 단, 반드시 값 할당을 정확히 한 번 해야합니다. 상수는 불변하는 값을 자주 사용할 때 이용하면 좋습니다.

var myVariable = 42 myVariable = 50 let myConstant = 42


상수나 변수는 반드시 그것에 할당하고자 하는 값의 타입과 같은 타입을 가져야 합니다. 하지만 타입을 항상 명시적으로 나타내주어야 하는 것은 아닙니다. 상수나 변수를 생성할 때 그 값을 할당해주면 컴파일러가 알아서 타입을 유추합니다. 위 예제코드에서 컴파일러는 myVariable의 타입이 Integer라고 유추했습니다. 할당된 초기값이 Integer이기 때문입니다.

초기값이 타입 유추를 위한 충분한 정보를 제공하지 않는 경우 (또는 초기값이 없는 경우), 명시적으로 타입을 정의해주어야 합니다. 변수 이름 뒤에 콜론(:)으로 구분하여 명시해줍니다.

  • let implicitInteger = 70
  • let implicitDouble = 70.0
  • let explicitDouble: Double = 70

  • <Experiment>

    상수를 새로 만들되 Float 타입을 명시적으로 정의해주고 4 라는 값을 할당해보세요.


    변수는 절대 다른 타입으로 암시적 convert되지 않습니다. 만약 값을 다른 타입으로 convert 하고 싶다면, 원하는 타입의 인스턴스를 명시적으로 만들어야 합니다.

  • let label = "The width is "
  • let width = 94
  • let widthLabel = label + String(width)

  • <Experiment>

    마지막 라인에서 conversion을 위한 String을 지워보세요. 어떤 에러가 나나요?


    문자열에 값을 포함하기 위한 더 쉬운 방법을 소개합니다. 괄호 안에 값을 쓰세요. 그리고 괄호의 시작 앞에 백슬래쉬(\)를 쓰세요. 예제코드를 보여드리겠습니다.

  • let apples = 3
  • let oranges = 5
  • let appleSummary = "I have \(apples) apples."
  • let fruitSummary = "I have \(apples + oranges) pieces of fruit."

  • <Experiment>

    \() 를 사용해서 문자열 안에 floating-point 계산을 포함해보세요. 그리고 누군가의 이름을 포함한 인삿말을 작성해보세요.


    여러 줄의 문자열을 작성할 때는 더블 따옴표 세 개(""")를 사용합니다. 각 라인의 들여쓰기는 closing quote(마지막 """)의 들여쓰기와 같은 레벨이 아닌 경우에는 무시됩니다. 예제를 보여드릴게요.

  • let quotation = """
  • Even though there's whitespace to the left,
  • the actual lines aren't indented.
  • Except for this line.
  • Double quotes (") can appear without being escaped.
  • I still have \(apples + oranges) pieces of fruit.
  • """

  • let quotation1 = """
  • Line1
  • Line2
  • Line3
  • """
  • // 1Line\n2Line\n3Line

  • let quotation2 = """
  •     Line1
  •     Line2
  •     Line3
  • """
  • //     1Line\n    2Line\n    3Line

  • let quotation3 = """
  •     Line1
  •     Line2
  •     Line3
  •     """
  • // 1Line\n2Line\n3Line

  • 대괄호( [] )를 이용하여 배열과 딕셔너리를 만들 수 있습니다. 이것들의 각 요소에는 대괄호 안에 index(배열)와 Key(딕셔너리)를 넣어서 접근할 수 있습니다. 마지막 요소 뒤에 쉼표(comma)를 넣어도 괜찮습니다.

  • var shoppingList = ["catfish", "water", "tulips", "blue paint"]
  • shoppingList[1] = "bottle of water"
  • var occupations = [
  • "Malcolm": "Captain",
  • "Kaylee": "Mechanic",
  • ]
  • occupations["Jayne"] = "Public Relations"

  • 빈 배열이나 빈 딕셔너리를 만드려면 initializer 문법을 사용하세요.

  • let emptyArray = [String]()
  • let emptyDictionary = [String: Float]()

  • 타입 정보가 유추될 수 있는 케이스에서는, 빈 배열은 [ ]로, 빈 딕셔너리는 [ : ]로 셋팅할 수 있습니다. 예를 들면 이미 만들어져 있는 변수에 새롭게 값을 할당하는 경우, 혹은 함수에 인자로 넘길 값을 셋팅하는 경우입니다.

  • shoppingList = []
  • occupations = [:]


  • Control Flow

    조건문을 만들기 위해 if switch를 사용하세요. 루프를 만들기 위해서는 for-in, while, repeat-while을 사용하세요. 조건문과 루프 변수를 감싸는 괄호는 생략 가능합니다. 조건문 본문(body)를 감싸는 괄호는 필수입니다.

  • let individualScores = [75, 43, 103, 87, 12]
  • var teamScore = 0
  • for score in individualScores {
  • if score > 50 {
  • teamScore += 3
  • } else {
  • teamScore += 1
  • }
  • }
  • print(teamScore)

  • if 안의 조건문은 반드시 Boolean expression 이어야 합니다. 예를 들어, if score { ... } 같은 코드를 생각해봅시다. Objective-C에서는 이와 같은 코드는 암시적으로 값이 0인지를 비교하는 조건문이었습니다. Swift에서 이런 코드는 에러로 판단되니 차이점에 유의합시다.

    값이 없을지도 모르는 케이스를 위해 if  let 을 함께 사용할 수 있습니다. 참고로 이러한 값의 상태를 표현하기 위해 optional 이라는 개념을 사용합니다. Optional value는 실제 value(값) 혹은 nil(값이 없는 상태)을 저장할 수 있습니다. (추가설명: 반대로, Optional value가 아니라면 nil 이라는 상태를 나타낼 수 없습니다.) Value의 타입 뒤에 물음표 마크(?)를 붙임으로써 이 value가 optional이라고 정의할 수 있습니다. Objective-C에서는 따로 이런 정의를 하지 않더라도 새로 만든 객체에 값을 할당하기 전까지는 객체의 값이 nil이었습니다. 차이점에 유의합시다.

    var optionalString: String? = "Hello" print(optionalString == nil) var optionalName: String? = "John Appleseed" var greeting = "Hello!" if let name = optionalName

    greeting = "Hello, \(name)"


    <Experiment>

    optionalNamenil로 바꿔보세요. greeting은 어떤 값이 되나요? optionalnil일 때 다른 greeting을 만들어주는 else문을 추가해보세요.


    만약 optional value가 nil이라면 조건문은 false가 되고 괄호 안의 코드는 스킵됩니다. 반대의 경우(optional value가 nil이 아닌 경우)에는 optional value가 unwrap 되고 let 뒤의 상수에 그 unwrap된 값이 할당됩니다(위 예제에서는 name이라는 상수에 할당됩니다). 이 상수는 if block 안에서 유효합니다.

    Optional value를 다루는 또 다른 방법은 ?? 연산자를 이용해 디폴트 값을 제공하는 것입니다. 만약 Optional value의 값이 없는 상태(nil인 상태)라면 디폴트 값이 대신 사용되는 방식입니다. 예제를 봅시다.

    let nickName: String? = nil let fullName: String = "John Appleseed" let informalGreeting = "Hi \(nickName ?? fullName)"


    Switch는 모든 종류의 데이터와 넓은 범위의 다양한 비교 연산을 지원합니다. Swift의 Switch에서는 Integer값으로만 비교 범위를 제한하거나 equality에 대해서만 비교할 필요가 없는 것입니다.

  • let vegetable = "red pepper"
  • switch vegetable {
  • case "celery":
  • print("Add some raisins and make ants on a log.")
  • case "cucumber", "watercress":
  • print("That would make a good tea sandwich.")
  • case let x where x.hasSuffix("pepper"):
  • print("Is it a spicy \(x)?")
  • default:
  • print("Everything tastes good in soup.")
  • }

  • <Experiment>

    default case를 지워보세요. 어떤 에러가 발생하나요?


    이제부터 살펴 볼 예제에서는 다음과 같은 점들을 살펴보겠습니다.

    (1) 매치되는 값을 찾는 패턴에서 let 이 어떻게 사용되는지 살펴보겠습니다.

    (2) Switch 문에서, 매치되는 case 안의 코드가 실행된 후 그 Switch문은 곧바로 끝나게 됩니다. 바로 아래의 case로 실행이 넘어가지 않는다는 것입니다. Objective-C에서는 break 등을 명시적으로 표기하지 않은 경우 case 실행이 끝나면 바로 아래의 case를 실행했었습니다. 이 차이에 유의하세요.

    (3) for-in을 이용해서 딕셔너리의 아이템(key-value 페어로 제공된)을 iterate(순회)할 수 있습니다. 딕셔너리는 순서가 없는 collection입니다. 따라서 이 경우 랜덤 순서로 iterate 하게 됩니다.

  • let interestingNumbers = [
  • "Prime": [2, 3, 5, 7, 11, 13],
  • "Fibonacci": [1, 1, 2, 3, 5, 8],
  • "Square": [1, 4, 9, 16, 25],
  • ]
  • var largest = 0
  • for (kind, numbers) in interestingNumbers {
  • for number in numbers {
  • if number > largest {
  • largest = number
  • }
  • }
  • }
  • print(largest)

  • <Experiment>

    가장 큰 수가 어떤 종류의 수인지 (Prime, Fibonacci, Square 중) 추적할 수 있도록 변수를 하나 더 만들어서 이용해보세요.


    while을 사용해서 조건을 충족할 때까지 code block을 반복 실행해보세요. 루프에 대한 조건 검사는 code block 실행 후 마다 이루어집니다. 적어도 한 번은 code block을 실행할 수 있게요.

    var n = 2 while n < 100 { n *= 2 } print(n) var m = 2 repeat { m *= 2

    } while m < 100 print(m)


    ..< 를 사용해 인덱스 범위(range)를 만들어 루프를 돌릴 수도 있습니다.

    var total = 0 for i in 0..<4 { total += i } print(total)


    A 이상 B 미만의 범위를 만드려면 A..<B 를, A 이상 B 이하의 범위를 만드려면 A...B를 사용하세요.



    Functions and Closures

    func 을 사용하여 함수를 정의하세요. 함수의 이름과 함수 인자들을 괄호 안에 넣어서 함수를 호출하세요. 함수 파라미터의 이름&타입 정의와 함수의 리턴 타입 정의를 구분하기 위해 -> 를 사용하세요.

  • func greet(person: String, day: String) -> String {
  • return "Hello \(person), today is \(day)."
  • }
  • greet(person: "Bob", day: "Tuesday")

  • <Experiment>

    day 파라미터를 지우고, 오늘의 점심 식단을 알려주는 인사를 위해 새로운 파라미터를 추가해보세요.


    기본적으로 함수는 parameter name을 argument label(인자 라벨)에 그대로 사용합니다. (추가설명: 함수 본문 안에서 쓰이는 이름이 parameter name이고 함수를 호출할 때 명시해주는 이름이 argument label입니다.) 따로 argument label을 정의하고 싶은 경우 함수 정의 시 파라미터 이름 앞에 써주도록 합시다. Argument label이 필요없도록 하고 싶다면 _ 를 써줍시다. 개인적으로 함수 호출 시에 사용하는 이름과 함수 본문 내에서 사용하는 이름을 구분할 수 있게 된 것이 마음에 듭니다. 좀 더 가독성이 높은 코드를 작성할 수 있을 것 같습니다.

  • func greet(_ person: String, on day: String) -> String {
  • return "Hello \(person), today is \(day)."
  • }
  • greet("John", on: "Wednesday")

  • 합성된 값(compound value)을 만들기 위해 tuple을 사용해보세요. 함수에서 여러 개의 값을 리턴하고 싶었던 적이 있지 않았나요? Tuple은 이럴 때 사용합니다. Tuple의 요소들은 이름이나 숫자(0부터 시작하는 인덱스)에 의해 참조될 수 있습니다.

  • func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
  • var min = scores[0]
  • var max = scores[0]
  • var sum = 0
  • for score in scores {
  • if score > max {
  • max = score
  • } else if score < min {
  • min = score
  • }
  • sum += score
  • }
  • return (min, max, sum)
  • }
  • let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
  • print(statistics.sum)
  • print(statistics.2)

  • 함수는 다른 함수 안에 정의될 수 있습니다(can be nested). 함수 A의 본문에 함수 B가 정의되어 있을 때, 함수 B를 nested function이라고 지칭하고 함수 A를 outer function이라고 지칭하겠습니다. Nested function은 outer function에 정의되어 있는 변수들에 접근할 수 있습니다. 함수가 길거나 복잡해질 때 nested function을 사용해보세요.

  • func returnFifteen() -> Int {
  • var y = 10
  • func add() {
  • y += 5
  • }
  • add()
  • return y
  • }
  • returnFifteen()

  • 함수는 first-class type입니다. 즉 함수는 다른 함수를 리턴할 수 있다는 말입니다.

  • func makeIncrementer() -> ((Int) -> Int) {
  • func addOne(number: Int) -> Int {
  • return 1 + number
  • }
  • return addOne
  • }
  • var increment = makeIncrementer()
  • increment(7)

  • 함수는 인자로 다른 함수를 받을 수도 있습니다.

  • func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
  • for item in list {
  • if condition(item) {
  • return true
  • }
  • }
  • return false
  • }
  • func lessThanTen(number: Int) -> Bool {
  • return number < 10
  • }
  • var numbers = [20, 19, 7, 12]
  • hasAnyMatches(list: numbers, condition: lessThanTen)

  • 함수는 사실 클로저(closure)의 특례(special case)입니다. 나중에 호출될 수 있는 코드 블럭이죠. 클로저 안의 코드는 클로저가 생성된 범위(scope)내의 변수나 함수에 접근가능합니다. 클로저가 실행되는 곳은 다른 scope라 할지라도 말입니다. 앞에서 살펴본 nested function만 봐도 그렇습니다. 클로저를 작성할 때 중괄호({ })로 코드를 감싸면 클로저의 이름을 정의하지 않아도 됩니다. 클로저의 argument & return type과 본문을 구분하기 위해 in 을 사용하세요.

  • numbers.map({ (number: Int) -> Int in
  • let result = 3 * number
  • return result
  • })

  • <Experiment>

    위 예제를 수정하여 홀수라면 0을 리턴하는 클로저를 만들어보세요.


    클로저를 좀 더 간결하게 작성하기 위한 몇 가지 옵션들이 있습니다. 클로저의 타입을 이미 알고 있는 경우(Delegate의 콜백 같은 케이스), 파라미터와 리턴 값의 타입을 생략할 수 있습니다. 단일 구문 클로저(Single statement closure. 한 줄의 명령어로만 이루어진 클로저를 말합니다)는 단일 구문의 결과값을 리턴합니다. 예제를 보시죠.

  • let mappedNumbers = numbers.map({ number in 3 * number })
  • print(mappedNumbers)

  • 이름 대신 숫자(0부터 시작하는 인덱스)를 통해 파라미터에 접근할 수 있습니다. 이 방식은 짧은 클로저에서 흔히 사용됩니다. 함수의 마지막 인자로 전달되는 클로저는 괄호 바로 뒤에 올 수 있습니다. 함수의 인자가 오직 클로저 하나인 경우에는 괄호 전체를 생략할 수 있습니다.

  • let sortedNumbers = numbers.sorted { $0 > $1 }
  • print(sortedNumbers)


  • Objects and Classes

    Class 키워드 뒤에 클래스 이름을 정의하여 자신만의 클래스를 생성하세요. 클래스 안의 프로퍼티 선언은 일반적인 상수/변수 선언과 같은 방식으로 하면 됩니다. 메서드와 함수 정의도 마찬가지입니다.

  • class Shape {
  • var numberOfSides = 0
  • func simpleDescription() -> String {
  • return "A shape with \(numberOfSides) sides."
  • }
  • }

  • <Experiment>

    let으로 상수 프로퍼티를 하나 선언해보고 그 값을 리턴해주는 메서드를 하나 만들어보세요.


    클래스 이름 옆에 괄호를 붙여서 클래스의 인스턴스를 생성할 수 있습니다. 점(.)을 통해 인스턴스의 프로퍼티와 메서드에 접근할 수 있습니다.

  • var shape = Shape()
  • shape.numberOfSides = 7
  • var shapeDescription = shape.simpleDescription()

  • 사실 위 예제의 Shape 클래스는 중요한 것이 빠져있습니다. 그것은 인스턴스가 생성될 때 set up 되는 이니셜라이저입니다. init을 사용해 하나 만들어볼 수 있습니다.

  • class NamedShape {
  • var numberOfSides: Int = 0
  • var name: String
  • init(name: String) {
  • self.name = name
  • }
  • func simpleDescription() -> String {
  • return "A shape with \(numberOfSides) sides."
  • }
  • }

  • 이니셜라이저 안을 살펴보시면 self 키워드를 통해서 프로퍼티로써의 name과 이니셜라이저 인자로써의 name을 구분하고 있습니다. 참고로 이니셜라이저 인자는 클래스의 인스턴스를 생성할 때 마치 함수 호출할 때처럼 넘겨주면 됩니다. 모든 프로퍼티는 값이 할당되어야 합니다. 선언 시(예제의 numberOfSides)에 할당하거나 이니셜라이저 안에서(예제의 name) 할당하세요.

    객체가 해제(deallocated)되기 직전에 따로 cleanup 작업이 필요하다면 deinit을 사용하여 디이니셜라이저를 정의하세요. 

    클래스의 자식 클래스를 만들고 싶다면 자식 클래스의 이름 뒤에 콜론(:)과 함께 부모 클래스의 이름을 쓰세요. 참고로 클래스는 어떤 루트 클래스를 반드시 상속할 필요가 없습니다. 부모 클래스가 필요 없다면 정의하지 마세요. (Objective-C에서는 NSObject 객체가 루트 클래스의 역할을 합니다. 새로운 클래스가 필요한 경우 NSObject를 상속받아 새로운 객체를 만드는 것이 정석입니다.)

    자식 클래스는 override 키워드를 사용해 부모 클래스의 메서드를 오버라이드할 수 있습니다. override 키워드를 붙이지 않은 채 부모 클래스가 가진 메서드 이름을 그대로 사용한다면 컴파일러가 에러로 인식합니다. 또한 부모 클래스가 가지지 않은 메서드를 정의하여 override 키워드를 붙인다면 그것 역시 컴파일러가 에러로 인식합니다.

  • class Square: NamedShape {
  • var sideLength: Double
  • init(sideLength: Double, name: String) {
  • self.sideLength = sideLength
  • super.init(name: name)
  • numberOfSides = 4
  • }
  • func area() -> Double {
  • return sideLength * sideLength
  • }
  • override func simpleDescription() -> String {
  • return "A square with sides of length \(sideLength)."
  • }
  • }
  • let test = Square(sideLength: 5.2, name: "my test square")
  • test.area()
  • test.simpleDescription()

  • <Experiment>

    NamedShape의 또 다른 자식 클래스 Circle을 정의해보세요. 이니셜라이저의 인자로는 반지름과 이름을 받으면 되겠네요. Circle만의 area()와 simpleDescription() 메서드도 정의해보세요.


    프로퍼티는 getter와 setter도 가질 수 있습니다. 예제를 통해 알아보아요.

  • class EquilateralTriangle: NamedShape {
  • var sideLength: Double = 0.0
  • init(sideLength: Double, name: String) {
  • self.sideLength = sideLength
  • super.init(name: name)
  • numberOfSides = 3
  • }
  • var perimeter: Double {
  • get {
  • return 3.0 * sideLength
  • }
  • set {
  • sideLength = newValue / 3.0
  • }
  • }
  • override func simpleDescription() -> String {
  • return "An equilateral triangle with sides of length \(sideLength)."
  • }
  • }
  • var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
  • print(triangle.perimeter)
  • triangle.perimeter = 9.9
  • print(triangle.sideLength)

  • 변수의 setter 로 넘어오는 새로운 값은 newValue라는 이름으로 접근할 수 있습니다. 위 예제에서 확인해보세요. newValue 대신 다른 이름으로 접근하고 싶다면 set 뒤에 괄호를 사용해 명시적인 이름을 정해줄 수 있습니다. (간단히 말하면 set (newPerimeter) { sideLength = newPerimeter / 3.0 } 같은 코드입니다.) 참고로 명시적인 이름을 정하게 되면 기존의 newValue라는 이름은 사용할 수 없게 됩니다.


    이제 EquilateralTriangle의 이니셜라이저를 살펴봅시다. 이니셜라이저는 세 가지 스텝을 거칩니다.

    1. 자식 클래스에 정의된 프로퍼티의 값을 할당.

    2. 부모 클래스의 이니셜라이저를 호출.

    3. 부모 클래스에 정의된 프로퍼티의 값을 변경(필요 시). 이 시점에서 메서드, getter, setter를 위한 각종 setup 작업들도 함께 함.


    프로퍼티에 새로운 값을 할당하기 직전이나 직후에 항상 이루어져야 하는 작업이 있다면 willSetdidSet을 사용하세요. 이니셜라이저 밖에서 프로퍼티의 값이 변경될 때마다 불리게 되는 코드 블록입니다. 예를 들어 아래 예제에서는 willSet을 통해 삼각형의 변 길이와 사각형의 변 길이를 항상 동일하게 만들어줍니다.

  • class TriangleAndSquare {
  • var triangle: EquilateralTriangle {
  • willSet {
  • square.sideLength = newValue.sideLength
  • }
  • }
  • var square: Square {
  • willSet {
  • triangle.sideLength = newValue.sideLength
  • }
  • }
  • init(size: Double, name: String) {
  • square = Square(sideLength: size, name: name)
  • triangle = EquilateralTriangle(sideLength: size, name: name)
  • }
  • }
  • var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
  • print(triangleAndSquare.square.sideLength)
  • print(triangleAndSquare.triangle.sideLength)
  • triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
  • print(triangleAndSquare.triangle.sideLength)

  • optional value를 다룰 때는 operation(메서드, 프로퍼티, 서브스크립팅 등) 앞에 ? 를 써주시면 됩니다. ? 앞에 있는 것이 nil이라면 ? 뒤에 있는 모든 것들은 무시되고 expression 자체가 nil을 반환합니다. ? 앞의 있는 것이 nil이 아니라면 optional value는 unwrapped되어 실제 값을 사용하여 expression을 수행하게 됩니다. 두 경우 모두 전체 expression의 값은 optional value입니다(nil이 되는 경우가 있으니까).

  • let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
  • let sideLength = optionalSquare?.sideLength



  • Enumerations and Structures

    enum을 사용하여 Enumeration을 생성할 수 있습니다. 클래스를 비롯한 다른 타입들처럼 enumeration은 자신의 메서드를 가질 수 있습니다.

  • enum Rank: Int {
  • case ace = 1
  • case two, three, four, five, six, seven, eight, nine, ten
  • case jack, queen, king
  • func simpleDescription() -> String {
  • switch self {
  • case .ace:
  • return "ace"
  • case .jack:
  • return "jack"
  • case .queen:
  • return "queen"
  • case .king:
  • return "king"
  • default:
  • return String(self.rawValue)
  • }
  • }
  • }
  • let ace = Rank.ace
  • let aceRawValue = ace.rawValue

  • <Experiment>

    Rank 두 개의 raw value 값을 비교해보는 함수를 작성해보세요.


    스위프트에서는 기본적으로 Enumeration의 raw value를 0부터 할당하고 하나씩 올려갑니다. 그러나 명시적인 값을 할당할 수도 있습니다. 위 예제에서 Ace는 명시적으로 1을 raw value로 정했습니다. 나머지 케이스들은 명시적으로 raw value를 정하지 않았으므로 순차적으로 raw value가 매겨집니다. Raw value를 문자열이나 부동 소수점으로 정하는 것도 가능합니다. Raw value는 rawValue 라는 프로퍼티를 통해 접근할 수 있습니다.

    Raw value를 통해 enumeration의 인스턴스를 만드려면 init?(rawValue:) 이니셜라이저를 이용하세요. 이 이니셜라이저는 raw value와 매칭되는 케이스를 반환해줍니다. 매칭되는 케이스가 없다면 nil을 반환합니다.

  • if let convertedRank = Rank(rawValue: 3) {
  • let threeDescription = convertedRank.simpleDescription()
  • }

  • enumeration의 case value들은 실제 value들입니다(raw value를 다른 방식으로 표현한 것이 아닙니다). 따라서 의미있는 raw value가 없는 케이스에는 굳이 raw value를 지정하지 않아도 됩니다.

  • enum Suit {
  • case spades, hearts, diamonds, clubs
  • func simpleDescription() -> String {
  • switch self {
  • case .spades:
  • return "spades"
  • case .hearts:
  • return "hearts"
  • case .diamonds:
  • return "diamonds"
  • case .clubs:
  • return "clubs"
  • }
  • }
  • }
  • let hearts = Suit.hearts
  • let heartsDescription = hearts.simpleDescription()

  • <Experiment>

    Suitcolor() 메서드를 추가해보세요. spades와 clubs에 대해서는 "black"을, hearts와 diamonds에 대해서는 "red"를 반환하는 메서드입니다.


    위 예제에서 hearts 케이스가 참조되는 두 가지 방식을 살펴봅시다. 먼저 hearts 상수에 값을 할당할 때는 Suit.hearts 라는 풀네임으로 참조되었습니다. hearts 상수에 따로 타입이 명시되지 않았기 때문입니다. 두 번째로, Switch문 안에서는 .heart로 참조되었습니다. 이미 Suit라는 타입임을 알고 있기 때문에 굳이 Suit.hearts 를 쓰지 않고 .hearts라는 단축된 형태를 써도 됩니다. 타입을 알고 있을 때는 언제라도 이렇게 단축된 형태를 사용할 수 있습니다.

    Enumeration은 정의되는 시점에서 각 케이스의 raw value가 결정됩니다. 즉 하나의 Enumeration에서 만들어지는 모든 인스턴스들은 같은 케이스일 경우 언제나 같은 raw value를 가진다는 뜻입니다. Enumeration의 인스턴스를 생성하는 시점에서 결정되는 값(=각 인스턴스마다 달라지는 값)이 필요하다면, raw value 대신 associated value를 사용하세요. Associated value는 Enumeration 케이스 인스턴스의 stored property처럼 동작하는 값입니다. 예를 들어 일출 & 일몰 시간을 서버로부터 받아오는 상황을 생각해봅시다. 서버는 리퀘스트에 대한 응답으로 일출 & 일몰 시간을 제공하거나, 리퀘스트가 실패했다는 에러 메시지를 주어야 할 것입니다.

  • enum ServerResponse {
  • case result(String, String)
  • case failure(String)
  • }
  • let success = ServerResponse.result("6:00 am", "8:09 pm")
  • let failure = ServerResponse.failure("Out of cheese.")
  • switch success {
  • case let .result(sunrise, sunset):
  • print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
  • case let .failure(message):
  • print("Failure... \(message)")
  • }

  • <Experiment>

    ServerResponse에 대한 세 번째 케이스를 더해보고 switch 문에도 더해보세요.


    위 예제에서 ServerResponse의 일출 & 일몰 시간을 Switch문의 케이스 안에서 어떻게 가져오고 있는지 살펴볼 수 있습니다.

    struct 키워드를 이용하여 Structure를 생성하세요. Structure는 클래스로 할 수 있는 많은 것들을 똑같이 지원해줍니다. 메서드와 이니셜라이저도 포함해서요. Structure와 클래스의 가장 큰 차이점은 Structure는 다른 곳으로 값을 넘길 때 Copy가 되고 클래스는 참조가 넘어간다는 것입니다.

  • struct Card {
  • var rank: Rank
  • var suit: Suit
  • func simpleDescription() -> String {
  • return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
  • }
  • }
  • let threeOfSpades = Card(rank: .three, suit: .spades)
  • let threeOfSpadesDescription = threeOfSpades.simpleDescription()

  • <Experiment>

    Card에 다음과 같은 메서드를 추가해보세요: 카드의 풀 덱(rank와 suit의 모든 조합)을 생성해주는 메서드.




    Protocols and Extensions

    protocol을 이용하여 프로토콜을 선언해봅시다.

  • protocol ExampleProtocol {
  • var simpleDescription: String { get }
  • mutating func adjust()
  • }

  • Class, enumeration, struct 모두 프로토콜을 따를 수 있습니다.

  • class SimpleClass: ExampleProtocol {
  • var simpleDescription: String = "A very simple class."
  • var anotherProperty: Int = 69105
  • func adjust() {
  • simpleDescription += " Now 100% adjusted."
  • }
  • }
  • var a = SimpleClass()
  • a.adjust()
  • let aDescription = a.simpleDescription
  • struct SimpleStructure: ExampleProtocol {
  • var simpleDescription: String = "A simple structure"
  • mutating func adjust() {
  • simpleDescription += " (adjusted)"
  • }
  • }
  • var b = SimpleStructure()
  • b.adjust()
  • let bDescription = b.simpleDescription

  • <Experiment>

    위 프로토콜을 따르는 enumeration을 하나 작성해보세요.


    SimpleStructure에 사용된 mutating 키워드는 무엇을 위함일까요? 이 메서드가 structure를 변경(modify)할 수 있음을 명시해주는 키워드입니다. SimpleClass에서는 mutating 키워드를 사용하지 않고도 adjust() 메서드를 정의하고 있습니다. 클래스의 메서드는 항상 그 클래스를 변경(modify)할 수 있기 때문입니다.

    기존 타입에 기능을 더할 때는 extention 키워드를 사용해서 기존 타입을 확장해보세요. 새로운 메서드, computed property 같은 것들을 더할 수 있습니다. 확장을 통해 프로토콜을 따르게 할 수도 있습니다. 이때 따르고자 하는 프로토콜은 다른 곳(다른 라이브러리나 프레임워크도 포함해서)에 정의되어 있어도 상관없습니다.

  • extension Int: ExampleProtocol {
  • var simpleDescription: String {
  • return "The number \(self)"
  • }
  • mutating func adjust() {
  • self += 42
  • }
  • }
  • print(7.simpleDescription)

  • <Experiment>

    Double 타입을 확장해서 absoluteValue 라는 프로퍼티를 더해보세요.


    정의한 프로토콜을 마치 다른 타입들처럼 사용할 수 있습니다. 예를 들어 서로 타입은 다르더라도 동일하게 하나의 프로토콜을 따르고 있는 객체들이 있다고 해봅시다. 이 객체들을 collection으로 저장할 때 collection의 저장값 타입으로 프로토콜을 사용할 수 있습니다.

  • let protocolValue: ExampleProtocol = a
  • print(protocolValue.simpleDescription)
  • // print(protocolValue.anotherProperty) // Uncomment to see the error

  • 변수 protocolValue는 런타임에 SimpleClass 타입이 됩니다. 그러나 컴파일러는 이 사실을 미리 알지 못하기 때문에 protocolValue를 단순히 ExampleProtocol 타입으로 취급합니다. 즉 예제의 세 번째 라인이 가리키는 것처럼, SimpleClass에만 정의되어 있는 메서드나 프로퍼티에 접근하려고 시도하면 컴파일 오류가 납니다.




    Error Handling

    Error 프로토콜을 따르는 타입을 통해 에러를 표현할 수 있습니다.

  • enum PrinterError: Error {
  • case outOfPaper
  • case noToner
  • case onFire
  • }

  • 에러를 던지려면(throw) throw 키워드를 사용하세요. 에러를 던지는 함수에는 throws 라고 마크를 해주세요. 만약 함수 안에서 에러를 던지게 되면 함수는 그 즉시 return되고 함수를 호출한 코드에서 에러를 처리하게 됩니다.

  • func send(job: Int, toPrinter printerName: String) throws -> String {
  • if printerName == "Never Has Toner" {
  • throw PrinterError.noToner
  • }
  • return "Job sent"
  • }

  • 에러를 처리하기 위한 방법은 몇 가지 정도 있습니다. 그 중 하나는 do-catch를 이용하는 것입니다. do 블럭 안에서, 에러가 던져질 수 있는 코드 앞에 try 라는 키워드를 쓰세요. do 블럭 안에서 발생한 에러는 catch 블럭 안에서 error 라는 이름으로 접근할 수 있습니다. 물론 error 대신 다른 이름을 지정할 수도 있습니다.

  • do {
  • let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
  • print(printerResponse)
  • } catch {
  • print(error)
  • }

  • <Experiment>

    send(job:toPrinter:) 함수가 에러를 던지도록 프린터 이름을 "Never Has Toner"로 바꿔보세요.


    특정한 에러들을 처리하기 위해 catch 블럭을 여러 개 만들 수 있습니다. 마치 Switch문의 case를 만드는 것처럼 catch문을 작성하세요.

  • do {
  • let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
  • print(printerResponse)
  • } catch PrinterError.onFire {
  • print("I'll just put this over here, with the rest of the fire.")
  • } catch let printerError as PrinterError {
  • print("Printer error: \(printerError).")
  • } catch {
  • print(error)
  • }

  • <Experiment>

    do 블럭 안에서 에러가 던져질 수 있도록 코드를 추가해보세요. 첫 번째 catch 블럭에서 에러가 처리되게 하려면 어떤 종류의 에러가 필요할까요? 두 번째와 세 번째 블럭은요?


    에러를 처리하는 또 다른 방법은 try?를 이용하여 결과값을 optional로 바꾸는 것입니다. 만약 함수가 에러를 던진다면 결과값은 nil이 됩니다(발생된 에러는 무시됩니다). 함수가 에러를 던지지 않는다면 결과값은 함수가 리턴한 값을 담은 optional value가 됩니다.

  • let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
  • let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")
  • // 추가설명 print(printerSuccess) // Optional("Job sent") print(printerSuccess!) // "Job sent"


    함수가 리턴되기 직전에, 즉 함수의 코드 실행이 끝난 다음 맨 마지막에 실행시키고 싶은 코드가 있다면 defer 블럭을 이용하세요. defer 블럭 안의 코드는 함수가 에러를 던지든 말든 무조건 실행됩니다. setup과 cleanup 작업 등을 위해 사용해보세요.

  • var fridgeIsOpen = false
  • let fridgeContent = ["milk", "eggs", "leftovers"]
  • func fridgeContains(_ food: String) -> Bool {
  • fridgeIsOpen = true
  • defer {
  • fridgeIsOpen = false
  • }
  • let result = fridgeContent.contains(food)
  • return result
  • }
  • fridgeContains("banana")
  • print(fridgeIsOpen)



  • Generics

    꺾쇠 괄호 안에 이름을 넣어 제네릭 함수나 타입을 만들 수 있습니다.

  • func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
  • var result = [Item]()
  • for _ in 0..<numberOfTimes {
  • result.append(item)