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


들어가며

String 은 문자열을, Character 는 문자를 저장할 수 있는 타입입니다. Swift String 은 단순한 문법으로 다룰 수 있으며, 그럼에도 불구하고 빠릅니다. 특히 String 두 개를 연결할 때 + 연산자만 사용하면 되는데, 이 부분은 Objective-C 에 비하면 참 편해졌습니다.

String 에 특정 상수/변수 등을 포함해야 할 때는 Objective-C 와 다르게 String Literal 안에 \(변수이름)을 넣어야 합니다. 이게 무슨 말인지는 String Interpolation 부분에서 자세히 알려드리겠습니다.

Swift 에서의 모든 String 은 encoding-independent Unicode character 로 구성되어 있으며, 유니코드 호환도 잘 됩니다.

NOTE

Swift’s String type is bridged with Foundation’s NSString class. Foundation 모듈이 String 의 extension 을 구현해놓았기 때문에, 현재는 Foundation 만 import 하면 String 으로도 NSString 의 메서드들을 사용할 수 있습니다.

Foundation 과 Cocoa 사이에서의 String 사용 방법에 대한 더 자세한 정보는 Bridging Between String and NSString 을 참고하세요.




String Literals

String Literal 이란 double quotes( )로 둘러싸인 문자열을 말합니다. 이것을 통해 미리 String 값을 define 해놓고, String 상수/변수에 그 값을 할당할 수 있습니다.

let someString = "Some string literal value" // String Literal로 초기화된 변수는 String 타입 (타입유추됨)
let lenghtOneString = "a" // 이 경우도 변수는 String 타입. Character 타입이라고 명시하면 Character 타입이 된다.
cs


Multiline String Literals

만약 여러 라인으로 나눠서 입력할 String Literal 이 필요하다면 그 시작과 끝을 three double quotation marks( """ )로 감싸세요.

let quotation = """
The White Rabbit put on his spectacles.  "Where shall I begin,
please your Majesty?" he asked.
 
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
 
 
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""
 
 
// backslash (\) 를 써주면 String 값에서는 그 라인에서 자동 줄바꿈되지 않습니다
let softWrappedQuotation = """
The White Rabbit put on his spectacles.  "Where shall I begin, \
please your Majesty?" he asked.
 
"Begin at the beginning," the King said gravely, "and go on \
till you come to the end; then stop."
"""
 
 
// 이 경우 앞뒤로 \n(라인 피드)가 들어갑니다
let lineBreaks = """
 
This string starts with a line break.
It also ends with a line break.
 
"""
cs


멀티라인 String 은 들여쓰를 할 수 있습니다. 들여쓰기를 하려면 마침큰따옴표(closing quotation marks, """ )전에 공백(whitespace)을 넣으세요. 그러면 다른 라인들도 그만큼의 공백은 String 값에 포함되지 않고 그냥 무시됩니다.


Special Characters in String Literals

String literal 은 다음의 특별한 문자를 포함할 수 있습니다:

    • escaped special characters: \0 (null character), \\ (backslash), \t (horizontal tab), \n (line feed), \r (carriage return), \" (double quotation mark) and \' (single quotation mark)

    • arbitrary Unicode scalar: \u{n}, where n is a 1–8 digit hexadecimal number with a value equal to a valid Unicode code point

예제를 봅시다. 

let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
// "Imagination is more important than knowledge" - Einstein
let dollarSign = "\u{24}"        // $,  Unicode scalar U+0024
let blackHeart = "\u{2665}"      // ♥,  Unicode scalar U+2665
let sparklingHeart = "\u{1F496}" // 💖, Unicode scalar U+1F496
 
let threeDoubleQuotationMarks = """
Escaping the first quotation mark \"""
Escaping all three quotation marks \"\"\"
"""
cs




Initializing an Empty String

빈 문자열이 들어있는 String 타입 변수를 만드는 방법을 알아봅시다:

var emptyString = "" // empty string literal을 할당하거나
var anotherEmptyString = String()  // 초기화 문법을 사용하거나
// 두 String 모두 똑같이 비어있다.
cs


String 값이 비어있는지 확인하려면 Boolean 타입인 isEmpty 프로퍼티를 체크해보세요:

if emptyString.isEmpty {
    print("Nothing to see here")
}
// Prints "Nothing to see here"
cs




String Mutability

변수는 값 변경 가능, 상수는 값 변경 불가의 룰이 똑같이 적용됩니다:

var variableString = "Horse"
variableString += " and carriage"
// variableString is now "Horse and carriage"
 
let constantString = "Highlander"
constantString += " and another Highlander"
// this reports a compile-time error - a constant string cannot be modified
cs


Swift 에서는 이처럼 String 이라는 타입이 하나 있고, 그 타입을 상수로 만들지 변수로 만들지에 따라 Mutability가 결정됩니다. 이것은 NSString / NSMutableString 등 변수의 타입 자체로 Mutability를 결정했던 Objective-C & Cocoa 와는 다른 방식입니다.



Strings Are Value Types

Swift 의 StringValue type 입니다. 따라서 함수나 메서드로 전달되거나 새로운 상수나 변수로 할당될 때에 그 String 값은 복사 됩니다. 이 경우 String 의 참조가 아닌 그 안에 들어있는 값만 새로 복사되어 전달되는 것이기 때문에, 복사된 값을 변경해도 원본은 영향받지 않습니다. 이 Value type 이라는 것은 Structures and Enumerations Are Value Types 에서 자세히 다루게 될 텐데, 값 복사본이 전달된다는 특성을 이해하고 있어야 앞으로 원하는 코드를 작성할 수 있을 것입니다. (NSString 의 경우에는 참조가 전달되기 때문에, 기존에 Objective-C 개발을 했던 경우 참조 전달과 착각하여 실수를 하기 쉽습니다.)

<추가설명1 - Value type 의 복사에 대해> Value type 의 할당을 할 때는 값 복사가 일어난다고 했습니다. 그러나 엄밀히 말하면 할당을 하는 시점마다 항상 복사가 일어나는 것은 아닙니다. 일단 복사는 하지않고 같은 곳만 바라보고 있다가, 값이 변경될 때에 복사를 하게 됩니다. 이것은 Swift 의 컴파일러가 최적화를 해주기 때문인데요. 왜 이런 식으로 동작하는 것일까요? 예를들어 Value type 중 하나인 Array 를 생각해보겠습니다. 만약 용량이 상당히 큰 Array 가 있는데, 이것을 함수 파라미터로 넘기거나 할 때마다 항상 Array 전체 복사가 일어난다면 성능이 많이 떨어질 것입니다.

<추가설명2 - Value type 과 Reference type 의 차이점 한 가지> Value type 은 var 로 선언될 경우에만 내부값 변경이 가능하고, Reference type 은 var 와 let 어느 것으로 선언되든지 내부값 변경이 가능합니다. 예를 들어 다음과 같은 Struct 하나를 떠올려봅시다: 이 Struct 에는 Name 이라는 내부 프로퍼티가 하나 있고 Name 은 var 로 선언되어 있습니다. 단순히 생각해보면 Name 을 언제든 바꿀 수 있을 것 같은 생각이 듭니다. 그러나 Struct 는 Value type 입니다. 따라서 이 Struct 가 만약 let 으로 선언되었다면 Name 프로퍼티는 변경이 불가능합니다. (반면 Reference type 의 경우에는 var 와 let 어느 것으로 선언되더라도 내부값을 변경할 수 있습니다. 참고로 NSObject 를 상속받은 객체, 기타 Class로 선언된 객체들이 Reference type 에 속합니다.)



Working with Characters

Stringcharacters 프로퍼티를 사용하면 String 값의 각각의 문자(Character)에 접근할 수 있습니다다. for-in loop 를 이용하여 iterating 해봅시다:

for character in "Dog!🐶" {
    print(character)
}
// D
// o
// g
// !
// 🐶
cs


for-in loop 에 대해서는 For-In Loops 에 자세히 기술되어 있습니다.

상수/변수를 생성할 때 Character 타입임을 명시하고 single-character string literal 을 할당하면 Character 타입을 만들 수 있습니다:

let exclamationMark: Character = "!"
cs


Character 값들로 구성된 array 를 이니셜라이저에 넘겨서 String 타입을 만들 수 있습니다:

let catCharacters: [Character] = ["C""a""t""!""🐱"]
let catString = String(catCharacters)
print(catString)
// Prints "Cat!🐱"
cs




Concatenating Strings and Characters

Swift 의 String 값들은 +, += 연산자를 통해 서로 더할 수 있습니다. Character 값을 String 값에 더하려면 append() 메서드를 사용해야 합니다. 예제로 알아보겠습니다:

let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2
// welcome now equals "hello there"
 
var instruction = "look over"
instruction += string2
// instruction now equals "look over there"
 
let exclamationMark: Character = "!"
welcome.append(exclamationMark)
// welcome now equals "hello there!"
 
// <주의> welcome += exclamationMark 는 서로 타입이 달라서 Compile error
cs


Character 타입은 오직 하나의 문자만 그 안에 담을 수 있습니다. 따라서 Character 타입의 변수에 다른 String 이나 Character 값을 더하는 것은 불가능합니다.

var str: String = "str"
var char: Character = "c"
 
let str_str = str + str
let char_char = char + char // compile error
let str_char = str + char // compile error
let str_append_char = str.append(char)
 
let str_let: String = "str_let"
let str_let_append_char = str_let.append(char) //compile error
cs


여러 줄의 string literal 을 더하는 경우에는 마지막 라인에 line break (\n) 가 포함되어 있느냐 없느냐에 따라 결과가 달라지는 경우가 있습니다. 예제로 알아봅시다:

let badStart = """
one
two
"""
let end = """
three
"""
print(badStart + end)
// Prints two lines:
// one
// twothree
 
let goodStart = """
one
two
 
"""
print(goodStart + end)
// Prints three lines:
// one
// two
// three
cs




String Interpolation

상수, 변수, String literal 을 조합하여 String 을 만들 수 있습니다. 넣고 싶은 아이템을 괄호 + 백슬러시( )로 묶습니다. 예를 들어 변수를 String literal 에 포함시키고 싶다면 \(변수이름) 형태로 String literal 내의 적절한 위치에 넣습니다.

let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
cs


Note

String literal 괄호 안의 수식에는 unescaped backslash( ), carriage return, line feed 등을 포함시킬 수 없습니다. 그러나 다른 String literal 을 포함시킬 수는 있습니다.




Unicode

String 은 사실 유니코드 스칼라(Unicode scalar) 값들로 이루어져 있습니다. 유니코드 스칼라는 Character 하나하나를 위한 유니크한 21-bit Number 입니다. 예를 들어 U+0061 for LATIN SMALL LETTER A ("a"), or U+1F425 for FRONT-FACING BABY CHICK ("🐥").

Swift 의 모든 Character 인스턴스는 하나의 extended grapheme cluster 를 가집니다. Extended grapheme cluster 란, 유니코드 스칼라의 sequence 인데, combine 되면 사람이 읽을 수 있는 하나의 문자가 됩니다. 다음 예제를 봅시다:

let eAcute: Character = "\u{E9}" // é 
let combinedEAcute: Character = "\u{65}\u{301}" // e followed by ́
// eAcute is é (유니코드 스칼라 1개), combinedEAcute is é (유니코드 스칼라 2개)
// 두 개는 같은 문자!
 
let precomposed: Character = "\u{D55C}"                  // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}"   // ᄒ, ᅡ, ᆫ
// precomposed is 한, decomposed is 한
 
let enclosedEAcute: Character = "\u{E9}\u{20DD}"
// enclosedEAcute is é⃝
 
let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
// regionalIndicatorForUS is 🇺🇸
 
cs




Counting Characters

String 을 구성하는 Character 개수를 알고 싶으면 String 의 count 프로퍼티를 사용합니다:

let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenagerie has \(unusualMenagerie.count) characters")
// Prints "unusualMenagerie has 40 characters"
cs


참고로 Character 하나에 포함된 유니코드 스칼라가 2개 이상이라도 그와 관계없이 Character 하나 당 count 는 하나로 간주됩니다. count 프로퍼티는 Character 의 개수를 세는 것이지 유니코드 스칼라의 개수를 세는 것이 아닙니다.

var word = "cafe"
print("the number of characters in \(word) is \(word.count)")
// Prints "the number of characters in cafe is 4"
 
word += "\u{301}"    // COMBINING ACUTE ACCENT, U+0301
 
print("the number of characters in \(word) is \(word.count)")
// Prints "the number of characters in café is 4"
cs


위 예제에서 볼 수 있는 것처럼 두 String 이 눈에는 같은 String 처럼 보일지 몰라도 그것을 구성하는 유니코드 스칼라의 개수와 종류는 다를 수도 있습니다. 따라서 똑같은 것처럼 보이는(same string's representation) 두 String 이 서로 다른 양의 메모리를 차지하고 있는 경우도 있습니다.




Accessing and Modifying a String

String 의 값에 접근하거나 변경하는 방법을 알아봅시다. String 의 메서드, 프로퍼티, 서브스크립트 문법 등을 통해 가능합니다.


String Indices

String 값을 구성하는 Character 들 각각이 String 의 어느 위치에 있는지 가리키기 위하여 index typeString.Index 가 사용됩니다.

왜 Int 값으로 Character 의 위치에 접근하지 못하고 굳이 index type 을 써야할까요? 그 이유는 위에서 설명한 유니코스 스칼라에 있습니다. Character 마다 차지하는 메모리가 다를 수 있으며, 유니코드 스칼라를 몇 개씩 건너뛰어야 다음 Character 가 나올지 미리 알 수 없기 때문에 그 역할을 해주는 index type 을 사용하는 것입니다.


String 에는 index 를 위한 다음 프로퍼티가 있습니다.

  • startIndex : 첫 번째 Character 를 가리키는 index
  • endIndex : 마지막 Character의 한 칸 뒤 index. 따라서 endIndex 에서 한 칸 앞으로 와야 마지막 Character 를 가리키게 됩니다.
String 이 비어있을 경우, startIndex 와 endIndex 는 같습니다.

String 에는 Index 를 위한 다음 메서드가 있습니다.

  • index(before:) : 바로 전
  • index(after:) : 바로 뒤
  • index(_:offsetBy:) : 오른쪽 방향으로 N만큼 떨어져있는 곳(음수면 왼쪽으로 N만큼)


String 의 특정 인덱스에 있는 Character 에 접근하기 위하여 서브스크립트 문법을 사용할 수 있습니다.

let greeting = "Guten Tag!"
greeting[greeting.startIndex]
// G
greeting[greeting.index(before: greeting.endIndex)]
// !
greeting[greeting.index(after: greeting.startIndex)]
// u
let index = greeting.index(greeting.startIndex, offsetBy: 7)
greeting[index]
// a
cs


String 의 range 를 벗어난 index 에 접근하려고 하면 런타임 에러가 발생됩니다.
greeting[greeting.endIndex] // Error
greeting.index(after: greeting.endIndex) // Error
cs


String 의 indices 프로퍼티는 String 안의 Character 들에 대한 index 의 Range 를 만들어줍니다. 이 프로퍼티를 for 문과 사용하면 String 내의 각 문자를 순회하는 코드를 간결하게 작성할 수 있습니다.

for index in greeting.indices {
    print("\(greeting[index]) ", terminator: "")
}
// Prints "G u t e n   T a g ! " // 다시 한 번 강조하지만, 위 코드에서의 index 는 0, 1 등의 number type 이 아니라 index type!
cs

NOTE
프로퍼티 startIndexendIndex, 메서드 index(before:), index(after:), 그리고 index(_:offsetBy:)Collection 프로토콜을 따르기만 한다면 어떤 타입이든 사용가능합니다. String, Array, Dictionary, Set 등의 타입이 해당됩니다.


Inserting and Removing

String 의 특정 인덱스에 Character 하나를 삽입하려면 insert(_:at:) 메서드를 사용하세요. 그리고 특정 인덱스에 다른 String 을 삽입하려면 insert(contentsOf:at:) 메서드를 사용하세요.

// String 특정 위치에 Character 삽입하기
var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
// welcome now equals "hello!"
 
// String 특정 위치에 다른 String 삽입하기
welcome.insert(contentsOf: " there", at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there!"
cs


반대로 특정 Character, String 을 제거하는 경우에는 remove(at:) 와 removeSubrange(_:) 메서드를 사용하세요.

// String에서 특정 위치의 Character 지우기
welcome.remove(at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there"
 
// String에서 sub string 지우기
let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range)
// welcome now equals "hello"
cs


NOTE

RangeReplaceableCollection 프로토콜을 따르는 어떤 타입이건 insert(_:at:), insert(contentsOf:at:), remove(at:)removeSubrange(_:) 메서드를 사용할 수 있습니다. String, Array, Dictionary, Set 등 모두 포함됩니다.





Substrings

String 의 서브 스트링을 가져올 때—예를 들어, 서브스크립트를 사용하거나 prefix(_:) 같은 메서드를 사용해서—그 결과는 Substring 인스턴스입니다. 또 다른 String 이 아니라요. Swift 의 Substring 은 String 의 메서드와 똑같은 메서드들을 많이 가지고 있습니다. 즉 String 을 다룰 때와 같은 방식으로 Substring 을 다룰 수 있습니다. 그러나 Substring 은 String 을 가지고 특정 액션을 수행하는 짧은 시간에서만 사용하도록 하세요. 만약 긴 시간동안 Substring 을 저장하고 사용해야 한다면 Substring 을 String 의 인스턴스로 변환한 다음 그렇게 하세요. 예제를 보겠습니다:


let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
// beginning is "Hello"
 
// Convert the result to a String for long-term storage.
let newString = String(beginning)
cs


Substring 도 String 과 마찬가지로 자신을 구성하는 문자열들이 저장되는 메모리를 참조하고 있습니다. 그러나 String 과 Substring 은 성능 최적화 측면에서 달라지는데요, Substring 은 Original String 을 저장하기 위해 사용되는 메모리의 일부를 재사용할 수 있습니다. 또한 다른 Substring 을 저장하기 위해 사용되는 메모리의 일부를 재사용할 수도 있습니다. (String 역시 비슷한 최적화가 이루어지지만, String 의 경우에는 두 개의 String 이 같은 메모리를 공유한다면 이 두 개의 String 은 서로 동일합니다(equal 관계에 놓여 있습니다).) 이런 성능 최적화의 효과로, String 이나 Substring 의 값을 수정하기 전까지는 메모리 복사가 이루어지지 않습니다. 즉 메모리 복사에 대한 자원을 절약할 수 있습니다. 앞서 언급했던 것처럼 Substring 은 긴 시간 사용하기에는 적절하지 않습니다(not suitable for long-term storage). 왜냐하면 Substring 은 Original String 의 메모리를 재사용하기 때문에 Substring 이 사용되는 동안에는 이 Original String 이 통째로 메모리에 남아 있어야 하기 때문입니다.


위 예제에서 greeting 은 String 입니다. 따라서 greeting 은 자신을 구성하는 문자열들이 저장되는 메모리를 참조하고 있습니다. beginninggreeting 의 Substring 이기 때문에, beginninggreeting 이 사용하는 메모리를 재사용합니다. 그와 다르게 newString 은 String 입니다. 따라서 newString 은 자신만의 메모리를 할당받습니다. 아래 그림은 이런 관계들을 보여주고 있습니다:


NOTE

StringSubstring 둘 다 StringProtocol 을 따르고 있습니다. 그러니 String 값을 조작하는 함수에 이 타입들을 편리하게 사용할 수 있습니다.




Comparing Strings


String 비교에는 세 가지 종류가 있습니다.

  • String and Character Equality : 문자열 전체 비교. 참고로 두 Character의 유니코드 스칼라의 조합에 차이가 나더라도 결과적으로 외관과 의미가 동일한 Character라면 두 Character는 같다고 취급.
  • prefix equality : 앞이 일치하는지
  • suffix equality : 뒤가 일치하는지

let quotation = "We're a lot alike, you and I."
let sameQuotation = "We're a lot alike, you and I."
 
if quotation == sameQuotation {
    print("These two strings are considered equal")
}
 
if quotation.hasPrefix("We") {
    print("quotation start with We")
}
 
if quotation.hasSuffix("I.") {
    print("quotation end with I.")
}
cs




Unicode Representations of Strings

앞서 살펴보았듯이 String은 Character(문자열)로 구성되고, Character는 Unicode Scalar로 구성됩니다.


Unicode String 을 텍스트 파일이나 다른 저장소에 쓰게될 때, String 을 구성하는 유니코드 스칼라들은 다음 encoding forms 중 하나로 인코딩됩니다.

  • UTF-8 encoding form (인코딩을 위한 codeUnit이 8-bit)
  • UTF-16 encoding form (인코딩을 위한 codeUnit이 16-bit)
  • UTF-32 encoding form (인코딩을 위한 codeUnit이 32-bit)


앞에서는 String을 for-in loop 로 돌면서, String 을 구성하는 각각의 Character 값에 접근하는 법을 알아보았습니다. (엄밀히 말하면 Unicode extended grapheme cluster를 하나씩 for문으로 도는 것입니다.)

지금부터는 String의 값을 유니코드 표현법으로 접근하는 법을 살펴봅시다. 종류는 다음 세 가지가 있습니다.

  • UTF-8 code units의 collection (String의 utf8 프로퍼티를 통해 접근)
  • UTF-16 code units의 collection (String의 utf16 프로퍼티를 통해 접근)
  • UTF-32 code units의 collection (String의 unicodeScalars 프로퍼티를 통해 접근)


예제를 통해 하나씩 살펴봅시다.

let dogString = "Dog‼🐶"
cs

위 String 은 다음과 같은 Character 들로 구성되어 있습니다.

  • D
  • o
  • g
  • !! (DOUBLE EXCLAMATION MARK or 유니코드 스칼라 U+203C)
  • 🐶  (DOG FACE or 유니코드 스칼라 U+1F436)


이제 이 String의 값에 유니코드 표현법으로 접근해봅시다.



UTF-8 Representation


Stringutf8 프로퍼티를 사용하세요.


  • Type: String.UTF8View
  • collection of unsigned 8-bit (UInt8) values



for codeUnit in dogString.utf8 {
    print("\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 226 128 188 240 159 144 182 "
cs


처음 3개 codeUnit 값 (68, 111, 103) 은 각각 D, o, g 를 표현합니다. 1 byte 만으로 표현할 수 있는 이런 문자들은 ASCII 로 표기했을 때도 동일한 코드를 가집니다. 그 다음 3개 codeUnit 값 (226, 128, 188) 은 3 byte UTF-8 이고 DOUBLE EXCLAMATION MARK 문자를 표현합니다. 마지막 4개 codeUnit 값 (240, 159, 144, 182) 는 4 byte UTF-8 이고 DOG FACE 문자를 표현합니다.



UTF-16 Representation


String 의 utf16 프로퍼티를 사용하세요.


  • Type: String.UTF16View
  • collection of unsigned 16-bit (UInt16) values



for codeUnit in dogString.utf16 {
    print("\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 55357 56374 "
cs


처음 3개 codeUnit 값 (68, 111, 103) 은 각각 D, o, g 를 표현하며 이는 UTF-8 표현과 같은 값입니다. D, o, 는 ASCII character 로 표기할 수 있는 문자들이기 때문입니다.

4번째 codeUnit 값 (8252) 는 16진수 값 203C 를 10진수로 나타낸 것입니다. 유니코드 스칼라 U+203C DOUBLE EXCLAMATION MARK 문자를 표현합니다. 이 문자는 UTF-16 에서 1개의 code unit 으로 나타낼 수 있습니다.

그러나 그 다음 나오는 DOG FACE 문자는 UTF-16 에서 2개의 codeUnit 이 필요합니다. 55357(U+D83D)과 56374(U+DC36)은 DOG FACE를 나타내기 위한 UTF-16 surrogate pair 입니다.

<보충설명> surrogate pair 란? 2 byte 인 UTF-16 으로 모든 문자를 표현하기에는 부족하기 때문에, 2 byte 만으로는 표현할 수 없는 예외 문자들을 Supplementary Characters 라고 정했습니다. 이 문자를 만들기 위한 인코딩 방식을 Surrogate Pair 라고 합니다.




Unicode Scalar Representation


String 의 unicodeScalars 프로퍼티를 사용하세요.


  • Type: UnicodeScalarView
  • collection of values of type UnicodeScalar
  • 각 UnicodeScalar value 라는 프로퍼티를 가진다. (scalar의 21-bit value, 즉 Uint32 값)



for scalar in dogString.unicodeScalars {
    print("\(scalar.value) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 128054 "
cs


처음 세 개의 UnicodeScalar 값 (68, 111, 103) 은 변함없이 D, o, 입니다.

4번째 codeUnit 값 (8252) 도 변함없이 16진수 값 203C 를 10진수로 나타낸 것입니다. 유니코드 스칼라 U+203C DOUBLE EXCLAMATION MARK 문자를 표현합니다.

그 다음 UnicodeScalar 값 128054 DOG FACE 인 유니코드 스칼라 U+1F436 을 10진수로 나타낸 것입니다.

다음과 같이 UnicodeScalar 를 통해 새로운 String 값을 구성할 수도 있습니다:


for scalar in dogString.unicodeScalars {
    print("\(scalar) ")
}
// D
// o
// g
// ‼
// 🐶
cs


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

Strings and Characters  (0) 2018.06.20
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.2)의 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.2' 카테고리의 다른 글

Strings and Characters  (0) 2018.06.20
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.2)의 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.2' 카테고리의 다른 글

Strings and Characters  (0) 2018.06.20
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

+ Recent posts

티스토리 툴바