Apple 제공 Swift 프로그래밍 가이드(4.2)의 The basics 부분을 공부하며 정리한 글입니다. 개인적인 생각, 이해를 돕기 위한 예제도 조금 들어가있습니다.
들어가며
Swift는 iOS, macOS, watchOS, tvOS 앱 개발을 위한 새로운 프로그래밍 언어입니다. 그렇지만 Swift의 많은 부분들은 당신이 C와 Objective-C 개발 경험이 있다면 친숙하게 느껴질 것입니다.
C와 Objective-C에서 사용하는 모든 근본적인 타입들에 대해 Swift는 자신만의 고유한 버전을 제공합니다. 예를들어 Integer를 위한 Int, floating-point value를 위한 Double과 Float, 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의 "콘솔" 패널에 출력해줍니다. separator와 terminator 파라미터는 디폴트 값을 가집니다. 따라서 호출 시 생략해도 무방합니다. 생략 시 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 타입들의 최소값과 최대값에 min과 max라는 프로퍼티를 통해 접근할 수 있습니다.
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, or125.0
.1.25e-2
means 1.25 x 10-2, or0.0125
.
16진수에 대한 예제:
0xFp2
means 15 x 22, or60.0
.0xFp-2
means 15 x 2-2, or3.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)해야 합니다. 아래 예제를 보시면, 상수 twoThousand는 UInt16 타입이고 상수 one은 UInt8 타입입니다. 이 두개는 직접적으로 더해질 수 없습니다. 서로 같은 타입이 아니기 때문입니다. 아래 예제에서는 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.75 는 4 가 되고, -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 에서는 true 와 false 두 가지의 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") 는 Int 와 String 을 함께 묶습니다. 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" 는 어떤 숫자 값으로 변환하면 좋을지 알 수가 없죠.
아래 예제에서 이 이니셜라이저(String 을 Int 로 변환하는 이니셜라이저)를 사용하고 있습니다:
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 Int 는 Int 가 아니라 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 라는 새로운 상수에 그 값을 할당하라."
변환이 성공적이라면 상수 actualNumber 는 if 문의 첫 번째 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
Assertion 과 precondition 은 런타임에 발생하는 체크입니다. 이것들을 이용하여 특정 코드를 실행하기 위해 필수적인 조건을 충족하고 있는지를 체크할 수 있습니다. 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 공식 가이드 > Swift 4' 카테고리의 다른 글
Strings and Characters (0) | 2018.06.20 |
---|---|
Basic Operators (0) | 2018.03.23 |
A Swift Tour (0) | 2017.10.17 |
Version Compatibility (0) | 2017.10.04 |
About Swift (0) | 2017.10.04 |