Swift 정리 - 3. 데이터 타입 기초


Swift의 데이터 타입을 알아보자.


Swift의 기본 데이터 타입은 구조체를 기반으로 하여 Swift의 다양한 기능(extension, generic) 등을 사용해서 구현되어 있다. Swift의 모든 데이터 타입 이름은 첫 글자가 대문자로 시작하는 대문자 카멜케이스를 사용한다.

Integers

Swift.org

정수 타입. 정수들은 signed(양, 음, 0) 혹은 unsigned(양, 0)이다.

Swift는 signed와 unsigned 정수를 8, 16, 32, 64 bit 형태로 제공한다. 즉 저장할 수 있는 데이터의 크기에 따라 타입이 분리되어 있다.

Integer Bounds

각 정수 타입의 최솟값과 최댓값을 min, max 프로퍼티를 사용해서 접근할 수 있다.

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

Int

대부분의 경우 코드에서 정수의 특정 크기를 지정할 필요가 없다. Swift는 현재 플랫폼의 기본 단어 사이즈와 같은 크기를 갖는 Int라는 정수 타입을 추가로 제공한다.

  • 32-bit 플랫폼에서 IntInt32와 크기가 같다.
  • 64-bit 플랫폼에서 IntInt64와 크기가 같다.

특정 사이즈의 정수를 가지고 작업해야 하는 게 아니라면 항상 Int를 사용해라. 이는 코드 일관성과 상호 운영성을 위해서다.

상호 운영성이라는 게 처음 들으면 무슨 말인가 싶지만 아래의 코드를 통해 보면 단번에 이해할 수 있다.

image

64-bit 운영체제 컴퓨터에서 위 코드는 정상적으로 실행될까? 그렇다면 아래의 코드는 어떨까?

image

정답은 둘 다 컴파일 에러가 발생한다는 것이다.

image

Swift는 type safe한 언어이고, Int와 Int64, Int32 타입을 명확하게 구분하고 있다. 따라서 나는 64 비트 운영체제에서 작업하고 있으니까 Int64 타입을 써야지! 하고 Int64로 모든 정수를 선언했다면 32비트 운영체제에서 프로그램이 돌아가지 않을 수 있을 뿐더러 굳이 Int64를 쓸 필요가 없다. Int로 선언하기만 하면 컴퓨터의 플랫폼에 따라 Int의 크기가 정해질 것이다.

UInt

Swift는 부호가 없는 UInt 타입을 제공하고, 이 타입도 현재 플랫폼의 기본 단어 크기와 같은 크기를 갖는다.

Int의 최댓값 이상, UInt의 최댓값 미만을 사용하는 특정한 경우 등이 아닌 경우에는 Int를 사용하는 것이 좋다. 이는 위에서 언급한 정보를 교환하고 사용할 수 있게 하기 위함인데, 같은 정수라 하더라도 IntUInt는 완전히 다른 타입으로 인식된다. 따라서 IntUInt를 혼용해서 사용하면 정수 타입의 변수끼리라도 값을 교환할 때 자원이 많이 소모될 수 있다.

Floating-Point Numbers

Floating-point 숫자는 부동소수점을 사용하는 실수다. 부동 소수 타입은 정수형보다 더 넓은 범위의 수를 저장할 수 있다. Swift는 두 가지의 부호가 있는 부동 소수 타입을 지원한다.

  • Double은 64-bit 부동 소수를 표현한다.
  • Float는 32-bit 부동 소수를 표현한다.

Double은 최소 15자리의 십진수를 표현할 수 있지만, Float는 6 자리의 숫자까지만 표현할 수 있다. 따라서 사용하고자 하는 값의 범위에 따라 코드에서 다르게 사용하면 된다. 둘 다 사용할 수 있는 경우라면 Double을 사용하는 것이 좋다.

어떤 부동 소수 타입에 저장했느냐에 따라 이를 사용할 때 사용하는 값의 정확도는 달라질 수 있다.

image

Type Safety and Type Inference

Swift는 type-safe한 언어다. Type safe한 언어는 값들의 타입을 명확하게 만들어야 한다. 만약 코드 상에서 String이라고 선언했다면 여기에 실수로라도Int 값을 전달 할 수 없다.

Swift는 type safe하기 때문에, 코드를 컴파일할 때 type check를 하고 일치하지 않는 타입을 에러라고 표시한다. 이를 통해 개발 단계에서 에러들을 탐지하고 해결할 수 있다.

Type-checking은 다른 타입의 값들을 가지고 작업할 때 에러를 피할 수 있게 해준다. 하지만 모든 상수와 변수를 선언할 때마다 타입을 명시할 필요는 없다. 만약 타입을 명시하지 않으면 Swift는 type inference를 통해 적절한 타입을 찾는다. 타입 추론은 내가 제공한 값을 관찰해서 컴파일 할 때 특정 형태의 표현에서 타입을 추론할 수 있게 해준다.

추론은 상수와 변수의 초깃값을 지정할 때 특히 유용하다. 이는 내가 선언하는 시점에 상수와 변수에 literal 값(혹은 literal)을 할당해서 이뤄진다.(literal value는 소스코드에서 내가 작성하는 값을 의미하고, 밑에 예제에서의 42, 3.14159와 같은 값이 된다.) 부동 소수 리터럴의 타입을 명시하지 않으면 Swift는 Double 타입으로 추론한다. Swift는 항상 부동 소수 타입을 추론할 때 Float보다는 Double을 택한다.

let meaningOfLife = 42
// meaningOfLife is inferred to be of type Int
let pi = 3.14159
// pi is inferred to be of type Double

만약 정수와 부동 소수 리터럴을 쓴다면, Double로 타입이 추론될 것이다.

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

Numeric Literals

정수 리터럴은 아래와 같다.

  • 10진수(decimal)
  • 2진수(binary) : 앞에 0b가 붙는다.
  • 8진수(octal) : 앞에 0o가 붙는다.
  • 16진수(hexadecimal) : 앞에 0x가 붙는다.

아래의 코드에서 정수 리터럴은 모두 17의 값을 갖는다.

let decimalInteger = 17
let binaryInteger = 0b10001       // 17 in binary notation
let octalInteger = 0o21           // 17 in octal notation
let hexadecimalInteger = 0x11     // 17 in hexadecimal notation

10진수 실수는 exponent를 가질 수 있는데, e의 대문자 혹은 소문자로 표시한다.

10진수의 경우 지수로 exp를 갖고 있다면 수는 10^exp에 의해 곱해칠 것이다. 16진수의 경우는 2^exp에 의해 곱해진다. 16진수는 p를 사용해서 지수를 표현할 수 있다.

  • 1.25e2 means 1.25 x 10^2, or 125.0.
  • 1.25e-2 means 1.25 x 10^-2, or 0.0125.
  • 0xFp2 means 15 x 2^2, or 60.0.
  • 0xFp-2 means 15 x 2^-2, or 3.75.

정수 리터럴은 읽기 더 쉽게 다른 형태로 존재할 수 있다. _와 0을 추가로 붙여서 정수와 실수를 더 가독성 있게 만들 수 있다.

let paddedDouble = 000123.456
let oneMillion = 1_000_000
let justOverOneMillion = 1_000_000.000_000_1

Numeric Type Conversion

음수가 아닌 값이라 해도 일반적으로 사용되는 상수와 정수에는 Int 값을 사용해라. 모든 상황에서 기본 정수 타입을 사용하는 것은 코드 상에서 즉각적으로 상호 운영할 수 있고 정수 리터럴 타입에서 추론된 타입과도 일치한다.

외부 소스에 데이터의 크기가 특별히 정해졌거나, 성능, 메모리 사용, 혹은 다른 필수적인 최적화가 필요한 경우에서만 다른 정수 타입을 사용해라. 이런 상황에서 크기가 명시적으로 정해진 타입을 사용하는 것은 실수로 오버플로우 되는 값들을 찾는 걸 도와주고 사용되는 데이터들의 타입을 문서화 하는데 도움이 된다.

Integer Conversion

저장될 수 있는 정수의 범위는 각 정수 타입에 따라 다르다. Int8의 경우 -128 ~ 127까지 저장할 수 있지만, UInt8의 경우 0 ~ 255까지 저장할 수 있다. 이런 범위 내에 있지 않는 값은 컴파일 할 때 에러로 표시된다.

let cannotBeNegative: UInt8 = -1
// UInt8 can't store negative numbers, and so this will report an error
let tooBig: Int8 = Int8.max + 1
// Int8 can't store a number larger than its maximum value,
// and so this will also report an error

숫자 타입들이 저장할 수 있는 값의 범위가 다르기 때문에, 어떤 타입을 선택할지 골라야 한다. 아래의 예제를 보면, 하나는 타입이 UInt16이고, 다른 하는 UInt8이다. 얘네는 같은 타입이 아니기 때문에 바로 더해질 수 없다. 대신, 이 예제에서는 one의 값으로 새로운 UIt16 타입의 값을 생성하고, 이 값을 원래 값 대신 사용한다.

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

이제 덧셈에 사용되는 두 타입이 모두 UInt16이기 떄문에 덧셈을 할 수 있다. 두 개의 UInt16 값이 더해졌기 때문에 twoThosandAndOneUInt16으로 추론된다.

타입(초깃값)은 Swift 타입을 초깃값으로 초기화하는 기본적인 방법이다. 위 예제에서 UInt16UInt8 값을 수용하는 initializer가 있기 때문에 존재하는 UInt8값으로 UInt16의 값을 만들 수 있었던 것이다. 따라서 아무 값이나 생성자에 전달 할 수 없고, UInt16이 제공하는 생성자에서 받을 수 있는 타입이 되어야 한다. 존재하는 타입이 더 많은 타입을 받을 수 있도록 생성자들을 제공할 수 있는데, 이는 extension을 통해 할 수 있다.

Integer and Floating-Point Conversion

정수와 부동 소수 타입간의 변환은 꼭 명시적으로 이뤄져야 한다.

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

부동 소수를 정수로 변환하는 것도 명시적으로 작성되어야 한다. 정수 타입은 Double이나 Float 값으로 초기화될 수 있다.

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

소수점 아래의 부분들은 위 방법으로 정수를 만들 때 항상 잘려진다. 즉 4.75는 4가 되고, -3.9는 -3이 된다.

수 리터럴을 조합하는 규칙은 상수와 변수들을 조합하는 것과는 다르다. 수 리터럴 3은 바로 리터럴 0.14159 값과 바로 더해질 수 있는데, 그 이유는 수 리터럴들은 명시적인 타입을 가지고 있지 않기 때문이다. 그들의 타입은 컴파일러에 의해 평가될 때에 추론된다.

image

Type Aliases

Type aliases는 존재하는 타입의 대체할 수 있는 이름을 정의한다. typealias 키워드로 타입 별칭을 정의할 수 있다.

Type aliases는 문맥상 더 적절한 이름으로 존재하는 타입을 언급하고 싶을 때 유용하다.

typealias AudioSample = UInt16

var maxAmplitudeFound = AudioSample.min
// maxAmplitudeFound is now 0

Booleans

Swift는 Bool이라고 불리는 boolean 타입이 있다.

Swift의 type 안전성은 bool이 아닌 값이 Bool로 치환되는 것을 막는다. 아래의 예제는 컴파일 타입 에러가 난다.

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

Strings and Characters

Swift.org:Strings and Characters

Swift의 String은 빠르고 현대적으로 구현되어 있다. 모든 문자열은 별도로 인코딩되는 유니코드 문자들로 구성되어 있고, 다양한 유니코드 문자들에 접근할 수 있게 해준다.

Swift의 String 타입은 Foundation의 NSString 클래스와 연결되어 있다. Foundation은 String을 확장해서 NSString에 정의된 메서드를 사용할 수 있게 했다. 즉 Foundation을 임포트했다면 캐스팅없이 String에서도 NSString 메서드를 사용할 수 있다는 뜻이다.

String Literals

String literal은 나열된 문자들을 큰 따옴표로 감싼 것이다.

let someString = "Some string literal value"

Multiline String Literals

만약 여러 줄의 문자열이 필요하다면 여러줄 문자열 literal을 사용한다. 세 개의 큰 따옴표로 문자들을 감싸면 된다. 큰 따옴표 세 개를 쓰고 한 줄을 내리고, 마지막 줄도 한 줄을 내려서 써야 한다.

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 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."
"""

여러 줄에서 첫 줄과 마지막 줄에 빈 줄을 넣고 싶으면 아래와 같이 첫 줄과 마지막 줄을 비워둔다.

let lineBreaks = """

This string starts with a line break.
It also ends with a line break.

"""

여러 줄 문자열은 감싸고 있는 코드에 맞추기 위해 앞 부분이 들여써질 수 있다. 닫는 """ 기호 이전에 있는 공백은 무시하게 끔 되어 있다. 즉 아래와 같이 표현할 수 있다.

image

Special Characters in String Literals

문자열 리터럴은 아래의 특별한 문자를 포함할 수 있다.

특수문자설명
\0문자열이 끝났음을 알리는 null 문자
\\백슬래시
\t탭 키를 눌렀을 때와 같은 효과
\n줄바꿈 문자
\r문자열 맨 앞으로
\"큰 따옴표
\'작은 따옴표
  • \u{n}으로 표기하는 유니코드 스칼라 값은 n이 16진수의 1-8자릿수 값을 가진다.

아래 코드는 특별한 문자들의 예시를 보여준다.

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

여러 줄 문자열에서는 하나가 아니라 세 개의 큰따옴표를 붙여 사용하기 때문에, 문자열 안에서 escaping하지 않고 큰 따옴표를 포함시킬 수 있다. 대신 """를 문자열 내에서 사용하고 싶다면 하나 이상의 큰따옴표를 escape해야 한다.

let threeDoubleQuotationMarks = """
Escaping the first quotation mark \"""
Escaping all three quotation marks \"\"\"
"""

Extended String Delimiters

문자열 내에서 특수 문자를 사용하는데 그 역할을 사용하지 않고 문자만을 포함시키고 싶다면 extended delimiter를 사용할 수 있다. 큰따옴표로 감싸진 문자열을 샵(#)으로 감싸면 된다. 예를 들어 문자열 리터럴 #"Line 1\nLine 2"#\n 문자를 출력한다.

Initializing and Empty String

빈 문자열 리터럴을 할당하거나 String 생성자를 사용해서 빈 문자열을 만들 수 있다.

var emptyString = ""               // empty string literal
var anotherEmptyString = String()  // initializer syntax
// these two strings are both empty, and are equivalent to each other

String 값이 비었는지 확인하려면 isEmpty Boolean 프로퍼티를 확인하면 된다.

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

String Mutability

String을 수정할 수 있는지를 표시하려면 변수로 만들고, 아니라면 상수로 만든다.

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

Strings Are Value Types

Swift의 String 타입은 값 타입이다. 만약 새로운 String 값을 생성하면, 이 String 값은 함수나 메서드에 전달되거나 상수나 변수에 할당되었을 때 복사된다. 이때 존재하는 String 값의 새로운 복제본이 생성되고, 원래 버전이 아니라 새로운 복사본이 전달되거나 할당된다.

Swift의 String의 이런 copy-by-default 동작은 함수나 메서드가 String 값을 전달할 때, 그게 어디서 왔는지는 상관 없이 정확한 String 값을 가지고 있다는 것을 확실하게 해준다. 따라서 내가 그걸 수정하기 전까지 전달한 문자열이 수정되지 않을 것임을 확신해도 된다.

이 과정에서 Swift의 컴파일러는 string의 사용을 최적화해서 꼭 필요할 때만 복제본을 만들도록 한다. 이는 문자열을 값 타입으로 다뤄도 좋은 성능을 갖게 된다는 걸 의미한다.

Working with Characters

Character는 말 그대로 ‘문자’를 의미하고, 문자의 집합이 아닌 단 하나의 문자를 의미한다. Swift는 유니코드 문자를 사용하므로 영어 뿐만 아니라 유니코드에서 지원하는 모든 언어, 특수기호등을 사용할 수 있다.

for-in 반복문을 사용해서 문자열의 각 문자에 접근할 수 있다.

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

대신, 아래와 같이 Character 타입 annotation을 사용해서 한 문자의 문자열 리터럴로 상수나 변수를 생성할 수 있다.

let exclamationMark: Character = "!"

String은 생성자에 Character 값들의 배열을 전달해서도 구성할 수 있다.

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

Concatenating Strings and Characters

String 값은 새로운 String 값을 생성하기 위해 + 연산자를 사용해서 합쳐질 수 있다.

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"

또한 String타입의 append() 메서드를 사용해서 문자열에 문자 값을 더할 수 있다.

let exclamationMark: Character = "!"
welcome.append(exclamationMark)
// welcome now equals "hello there!"

Character는 무조건 한 글자만 가지고 있어야 하기 때문에 Character 변수에 String이나 Character를 더할 수 없다.

만약 여러줄의 문자열 리터럴을 사용하고, 각 문자열 사이에 개행을 하고 싶다면 마지막 줄에 빈 줄을 아래와 같이 포함시켜야 한다.

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

Unicode

Unicode는 다른 쓰기 시스템의 텍스트를 인코딩하고, 표현하고, 처리하기 위한 국제적인 표준이다. 유니코드는 어떤 언어의 문자라도 표준화된 형태로 표현할 수 있게 해주고, 텍스트 파일이나 웹 페이지와 같은 외부 소스에서 이들을 읽거나 쓸 수 있게 해준다. Swift의 StringCharacter타입은 완전히 유니코드를 준수한다.

Unicode Scalar Values

Swift의 String 타입은 Unicode scalar value를 기반으로 생성되었다. 유니코드 스칼라 값은 특정 문자나 기호를 나타내는 고유한 21-bit 자다. 예를 들어 U+0061는 라틴 소문자 a 이고, U+1F425는 앞을 보고 있는 병아리 🐥 이다.

모든 21-bit 유니코드 스칼라 값이 문자에 할당된 것은 아니다. 어떤 값들은 미래에 할당될 용도나 UTF-16 인코딩을 위해 예약되어 있다. 문자에 할당된 스칼라 값들은 일반적으로 LATIN SMALL LETTER AFRONT-FACING BABY CHICK와 같은 이름을 가지고 있다.

Extended Grapheme Clusters

모든 Swift의 Character 타입 인스턴스는 하나의 extended grapheme cluster를 나타낸다. 확장된 자소 집합은 하나의 사람이 읽을 수 있는 문자를 생성하는 하나 이상의 유니코드 스칼라들의 나열이다.

예를 들어 é는 유니코드 스칼라 é(LATIN SMALL LETTER E WITH ACUTE, or U+00E9)로 표현할 수 있다. 하지만, 같은 문자는 스칼라들의 조합으로도 나타낼 수 있는데, e (LATIN SMALL LETTER E, or U+0065) 뒤에 COMBINING ACUTE ACCENT scalar (U+0301)를 붙이는 것이다.

두 경우 모두에서 문자 é는 하나의 Swift Character 값으로 표현되는데, 이는 extended grapheme cluster를 나타낸다. 첫 번째 경우에 cluster는 하나의 스칼라 값을 포함하는데, 두 번째 경우에서는 두 개의 스칼라를 포함한다.

let eAcute: Character = "\u{E9}"                         // é
let combinedEAcute: Character = "\u{65}\u{301}"          // e followed by ́
// eAcute is é, combinedEAcute is é

Extended graphme cluster는 많은 복잡한 문자를 하나의 Character 값으로 표현하는 유연한 방법이다. 예를 들어, 한글은 한 글자를 조합된 형태이거나 분리된 형태로 표현할 수 있다. 두 표현 방법모두 Swift에서는 하나의 Character 값으로 여겨진다.

let precomposed: Character = "\u{D55C}"                  // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}"   // ᄒ, ᅡ, ᆫ
// precomposed is 한, decomposed is 한

Counting Characters

문자열의 글자수를 세려면 count 프로퍼티를 사용한다.

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

Swift가 extended grapheme cluster를 사용한다고 해서 문자열을 합치는거나 수정하는 것이 항상 문자열의 글자수에 영향을 주지는 않는 다는 것을 주목하자.

예를 들어 문자열을 네 글자 단어 cafe로 초기화하고, 끝에 COMBINING ACUTE ACCENT (U+0301)를 더해도 문자열의 글자수는 4가 되는데, 이는 네 번째 글자가 e가 아닌 é가 되기 때문이다.

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"

Extended grapheme cluster는 여러 개의 유니코드 스칼라로 구성될 수 있다. 이건 문자마다, 혹은 같은 문자라도 다른 표현을 가진 문자들이 저장을 위해 메모리에 차지하는 공간이 제각각일 수 있다는 걸 의미한다. 이 때문에, Swift의 문자열 내의 문자들은 다른 양의 메모리를 차지하게 된다. 결국 extended grapheme cluster 경계를 결정하기 위해 문자열을 직접 훑어보지 않고서는 문자열 내의 문자의 개수를 알 수는 없다. 만약 아주 긴 문자열을 가지고 작업한다면, count 프로퍼티는 전체 문자열을 훑어서 문자의 개수를 알아낼 수 있다는 것에 주의해라. count 프로퍼티에 의해 리턴되는 문자 개수는 같은 문자를 포함한 NSStringlength 프로퍼티와 항상 같지는 않다. NSString의 길이는 UTF-16 표기법에 따른 16 비트 코드 유닛에 기반하고 있다.

Accessing and Modifying a String

메서드, 프로퍼티를 사용하거나 subscript 문법을 사용해서 문자열을 접근하고 수정할 수 있다.

String Indices

String 값은 연관된 index type, String.Index를 가지고 있는데, 문자열의 각각의 위치의 Character와 일치한다.

문자마다 메모리에 저장할 용량이 다를 수 있기 때문에, 어떤 Character가 어떤 위치에 있는지 알기 위해 String의 처음이나 끝부터 각 유니코드 스칼라를 반복해야 한다. 이 이유때문에 Swift 문자열은 정수 값으로 인덱스가 붙여질 수 없다.

startIndex 프로퍼티를 사용해서 String의 첫 번째 Character에 접근할 수 있다. endIndex 프로퍼티는 String의 마지막 문자 뒤의 위치다. 즉 endIndex 프로퍼티는 문자열의 subscript로 올바른 인자가 되지 못한다. 만약 String 이 빈 문자열이라면 startIndex, endIndex는 같다.

Stringindex(before:)index(after:)를 사용해서 주어진 인덱스의 이전, 이후 인덱스들에 접근할 수 있다. 주어진 인덱스에서 더 떨어진 인덱스에 접근하려면 index(_:offsetBy:)을 사용한다.

subscript 문법을 사용해서 문자열의 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

문자열내의 모든 각각의 문자에 접근하려면 indicies 프로퍼티를 사용한다.

for index in greeting.indices {
  print("\(greeting[index]) ", terminator: "")
}

Collection 프로토콜을 따르는 타입이라면 startIndex, endIndex 프로퍼티와 index(before:), index(after:), index(_:offsetBy:) 메서드를 사용할 수 있다. 위에 나온 String, 그리고 컬렉션 타입인 Array, Dictionary, Set에서도 쓸 수 있다.

Inserting and Removing

특정 인덱스에 하나의 문자를 삽입하려면 insert(_:at:) 메서드를 사용하고, 특정 인덱스에 다른 문자열을 삽입하려면 insert(contentsOf:at:) 메서드를 사용한다.

var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
// welcome now equals "hello!"

welcome.insert(contentsOf: " there", at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there!"

특정 인덱스에 있는 하나의 문자를 지우려면 remove(at:) 메서드를 사용하고, 범위 내의 문자열을 지우려면 removeSubrange(_:) 메서드를 사용한다.

welcome.remove(at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there"

let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range)
// welcome now equals "hello"

RangeReplaceableCollection 프로토콜을 따르는 타입이라면 insert(_:at:), insert(contentsOf:at), remove(at:), removeSubrange(_:) 메서드를 쓸 수 있다. Array, Dictionary, Set에서도 쓸 수 있다.

Substrings

문자열의 부분 문자열을 가져올때(예를 들어 subscript를 사용하거나 prefix(_:)같은 메서드를 사용할 때) 결과는 다른 문자열이 아닌 Substring의 인스턴스가 된다. Swift의 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)

문자열처럼, 각 부분 문자열은 해당 부분 문자열을 구성하고 있는 문자들이 저장된 메모리의 한 영역을 갖고 있다. 문자열과 부분 문자열의 차이는 성능 최적화 관점에서 보면 부분 문자열은 기존 문자열을 저장했던 메모리의 일부나 다른 부분 문자열을 저장했던 메모리의 일부를 재사용할 수 있다. (문자열도 비슷한 최적화를 하고 있지만 만약 두 문자열이 메모리를 공유하고 있다면 두 문자열은 같은 것이다.) 이런 성능 최적화는 문자열이나 부분 문자열을 수정하기 전까지 메모리를 복사하는 것의 성능을 걱정할 필요가 없다는 것이다. 위에서 언급했듯이, 부분 문자열은 long-term storage에 적합하지 않다. 왜냐하면 부분 문자열은 원래 문자열의 공간을 재사용하고, 원래의 문자열은 부분 문자열이 사용되는 한 메모리에 가능한 오래 있어야 하기 때문이다.

위의 예제에서는 greeting은 문자열이고, 이는 문자열을 구성하는 문자들이 저장되어 있는 메모리가 있다는 것이다. beginninggreeting의 부분 문자열이기 때문에, gretting이 사용하는 메모리를 재사용한다. 반대로, newString은 문자열이고, 부분 문자열에서 생성되었을 때 자기만의 저장공간을 가지게 된다. 아래 그림은 이런 관계를 보여준다.

image

Comparing Strings

Swift는 문자열을 비교할 수 있는 세 가지 방법을 제공한다. 문자열과 문자 동일성, 접두사, 접미사 동일성 비교다.

String and Character 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")
}
// Prints "These two strings are considered equal"

String값은 만약 그들의 extended grapheme cluster들이 canonically하게 같을 때 같다고 여겨진다. Extended grapheme cluster는 그들이 다른 유니코드 스칼라로 구성돼도 같은 언어적 의미와 외형을 갖는다면 canonically하게 같다.

예를 들어, LATIN SMALL LETTER E WITH ACUTE (U+00E9)LATIN SMALL LETTER E (U+0065)뒤에 COMBINING ACUTE ACCENT (U+0301)가 붙은 것과 canonically하게 같다.

// "Voulez-vous un café?" using LATIN SMALL LETTER E WITH ACUTE
let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"

// "Voulez-vous un café?" using LATIN SMALL LETTER E and COMBINING ACUTE ACCENT
let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?"

if eAcuteQuestion == combinedEAcuteQuestion {
    print("These two strings are considered equal")
}
// Prints "These two strings are considered equal"

반대로, 영어에서 사용되는 LATIN CAPITAL LETTER A (U+0041, or "A")와 러시아에서 사용되는 CYRILLIC CAPITAL LETTER A (U+0410, or "А")는 같지 않다. 이들은 외형상으로는 매우 비슷해보이지만, 언어적 의미에서는 그렇지 않다.

let latinCapitalLetterA: Character = "\u{41}"

let cyrillicCapitalLetterA: Character = "\u{0410}"

if latinCapitalLetterA != cyrillicCapitalLetterA {
    print("These two characters aren't equivalent.")
}
// Prints "These two characters aren't equivalent."

Prefix and Suffix Equality

문자열이 특정 접두사나 접미사를 가지고 있는지 확인하려면 hasPrefix(_:)hasSuffix(_:) 메서드를 호출하면 되고, 둘 다 하나의 String 타입의 인자를 받고 불리언 값을 리턴한다.

let romeoAndJuliet = [
    "Act 1 Scene 1: Verona, A public place",
    "Act 1 Scene 2: Capulet's mansion",
    "Act 1 Scene 3: A room in Capulet's mansion",
    "Act 1 Scene 4: A street outside Capulet's mansion",
    "Act 1 Scene 5: The Great Hall in Capulet's mansion",
    "Act 2 Scene 1: Outside Capulet's mansion",
    "Act 2 Scene 2: Capulet's orchard",
    "Act 2 Scene 3: Outside Friar Lawrence's cell",
    "Act 2 Scene 4: A street in Verona",
    "Act 2 Scene 5: Capulet's mansion",
    "Act 2 Scene 6: Friar Lawrence's cell"
]

hasPrefix(_:) 메서드를 사용해서 remeoAndJuliet 배열 내에 Act 1 연극이 얼마나 있는지 셀 수 있다.

var act1SceneCount = 0
for scene in romeoAndJuliet {
    if scene.hasPrefix("Act 1 ") {
        act1SceneCount += 1
    }
}
print("There are \(act1SceneCount) scenes in Act 1")
// Prints "There are 5 scenes in Act 1"

비슷하게 hasSuffix(_:) 메서드를 사용해서 특정 주소에서 열리는 연극을 찾을 수도 있다.

var mansionCount = 0
var cellCount = 0
for scene in romeoAndJuliet {
    if scene.hasSuffix("Capulet's mansion") {
        mansionCount += 1
    } else if scene.hasSuffix("Friar Lawrence's cell") {
        cellCount += 1
    }
}
print("\(mansionCount) mansion scenes; \(cellCount) cell scenes")
// Prints "6 mansion scenes; 2 cell scenes"

Unicode Representations of Strings

유니코드 문자열이 텍스트 파일에 쓰이거나 다른 저장소에 쓰일 때, 해당 문자열에 있는 유니코드 스칼라는 여러 Unicode-defined encoding 형태 중 하나로 인코팅 된다. 즉 여러 유니코드 인코딩 방법이 있고 그 중 하나로 인코딩 되는 거다. 각 형태는 문자열을 code unit이라 불리는 작은 단위로 문자열을 인코딩한다. 이들은 UTF-8 인코딩 형태(문자열을 8비트 code unit으로 인코딩하는 형태), UTF-16 인코딩 형태(문자열을 16비트 code unit으로 인코딩하는 형태), 그리고 UTF-32 인코딩 형태(문자열을 32 비트 code unit으로 인코딩하는 형태)를 포함한다.

Swift는 문자열의 유니코드 표현에 접근할 수 있는 여러 다른 방법을 제공한다. for-in문으로 문자열 내를 훑어 Unicode extended grapheme cluster인 각각의 Character 값들에 접근할 수 있다.

대신, 다음의 세 개의 Unicode-준수 표현법 중 하나를 선택해서 String 값에 접근할 수도 있다.

  • UTF-8 code unit의 컬렉션 (문자열의 utf8프로퍼티로 접근할 수 있다.)
  • UTF-16 code unit의 컬렉션 (문자열의 utf16 프로퍼티로 접근할 수 있다.)
  • UTF-32 인코딩 형태와 같은 21 비트 유니코드 스칼라 값의 컬렉션 (문자열의 unicodeScalars 프로퍼티로 접근할 수 있다.)

아래에서는 D, o, g, ‼ (DOUBLE EXCLAMATION MARK, or Unicode scalar U+203C), 🐶 (DOG FACE, or Unicode scalar U+1F436) 문자로 구성된 문자열의 다른 표현법들을 볼 것이다.

let dogString = "Dog‼🐶"

UTF-8 표현법

문자열의 utf8 프로퍼티를 반복해서 UTF-8 표현에 접근할 수 있다. 이 프로퍼티는 String.UTF8View의 타입으로, UInt8 값들의 컬렉션이며 각 요소는 문자열의 UTF-8 표현법의 한 바이트를 차지한다.

image

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

위 예제에서, 처음 세 10진수 codeUnit 값들 (68, 111, 103)은 각각 D, o, g를 표현하고, UTF-8 표현이 그들의 ASCII 표현과 같다. 다음 세 codeUnit 값 (226, 128, 188)은 3 bypte UTF-8 표현이고, 마지막 네 개의 codeUnit 값들 (240, 159, 144, 182)은 강아지 문자의 4 bypte UTF-8 표현법이다.

UTF-16 표현법

문자열의 utf-16 프로퍼티를 반복해서 UTF-16 표현에 접근할 수 있다. 이 프로퍼티는 String.UTF16View의 타입으로, UInt16 값들의 컬렉션이다.

image

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

로직은 위와 똑같다.

Unicode Scalar Representation

문자열의 unicodeScalars 프로퍼티 내에서 반복하며 유니코드 스칼라 표현에 접근할 수 있다. 이 프로퍼티는 UnicodeScalarView 타입이고, UnicodeScalar 타입 값들의 컬렉션이다.

UnicodeScalr는 스칼라의 21비트 값을 리턴하는 value 프로퍼티가 있고, UInt32 값으로 표현된다.

image

Any, AnyObject와 nil

Swift.org:Any

Any 타입은 다른 모든 타입의 값들을 가질 수 있다. 변수 또는 상수의 데이터 타입이 Any로 지정되어 있으면 그 변수, 상수에는 어떤 종류의 데이터 타입이든지 할당할 수 있다. Any는 다음의 타입의 인스턴스의 구상 타입(concrete type)으로 쓰일 수 있다.

  • 클래스, 구조체, 열거형
  • metatype(e.g. Int.self)
  • any 타입 요소들의 튜플
  • 클로저나 함수 타입
let mixed: [Any] = ["one", 2, true, (4, 5.3), { () -> Int in return 6 }]

만약 인스턴스의 구상 타입으로 Any를 사용한다면 객체의 프로퍼티나 메서드에 접근하기 전에 해당 인스턴스를 이미 알고 있는 타입으로 캐스팅해야 한다. Any 타입의 인스턴스들은 그들의 원래 다이나믹 타입을 유지하고 있고, 타입 캐스팅 연산자(as, as?, as!) 중 하나를 사용해서 캐스팅 될 수 있다. 예를 들어, String들을 담고 있는 배열의 첫 번째 객체를 as?를 사용해서 다운캐스팅 할 수 있다.

if let first = mixed.first as? String {
    print("The first item, '\(first)', is a string.")
}
// Prints "The first item, 'one', is a string."

AnyObject 프로토콜은 Any 타입과 비슷하다. Any보다는 좀 더 한정된 의미로 클래스의 인스턴스만 할당할 수 있다. 모든 클래스들은 내부적으로 AnyObject를 따르고 있다. 언어에 의해 정의된 Any와는 다르게, AnyObject는 Swift 표준 라이브러리에 의해 정의되었다.

참고로 AnyAnyObject는 가능한 사용하지 않는 것이 좋다. Swift는 타입에 엄격하기 때문에 Any, AnyObject로 선언된 변수의 값을 가져다 쓰려면 매번 타입 확인 및 변환을 해야하고 예상치 못한 오류가 발생할 수 있다.

nil은 특정 타입이 아니라 ‘없음’ 을 나타내는 Swift의 키워드다. 즉 변수나 상수에 값이 없는 상태를 표현하기 위해 특별한 값 nil이 있는 것이다. 변수나 상수에 값이 없는 상태일 때, 즉 nil일 때 해당 변수나 상수에 접근하면 잘못된 메모리 접근으로 런타임 오류가 발생한다.

nil을 옵셔널이 아닌 변수와 상수에 쓸 수 없다. 만약 상수나 변수가 특정 조건에서 값이 없는 상태가 된다면 옵셔널 타입으로 선언해야 한다. 옵셔널에 대해서는 이후 자세히 볼 것이다.

Swift의 nilObjective-Cnil은 같지 않다. Objective-C에서, nil은 존재하지 않는 객체를 가리키는 포인터다. Swift에서 nil은 포인터가 아니라, 특정 타입의 값이 존재하지 않음을 나타낸다.