Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Strings and Characters 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.
들어가며
String은 문자열을, Character는 문자를 저장할 수 있는 타입이다.
스위프트의 String은 단순한 문법으로 다룰 수 있다. 특히 String 두 개를 연결할 때 + 연산자만 사용하면 되는데, 이 부분은 Objective-C에 비하면 편해졌다.
String에 특정 상수/변수 등을 포함해야 할 때는 Objective-C와 다르게 String 리터럴 안에 \(변수이름)을 넣어야 한다. String Interpolation 부분에서 자세히 살펴보겠다.
유니코드 호환이 잘 된다. 모든 String은 encoding-independent Unicode character 로 구성되어 있다.
<note> Swift's String type is bridged with Foundation's NSString class. 현재는 Foundation을 import하면 캐스팅 없이 String에서 NSString의 메서드에 접근할 수 있다. (Foundation이 extend를 해놓았다)
String Literals
String Literal : double quote(")로 둘러싸인 String을 말한다. String 상수/변수에 이 값을 할당할 수 있다.
let someString = "Some string literal value" // String Literal로 초기화된 변수는 String 타입이 된다.
let lenghtOneString = "a" // 이 경우도 변수는 String 타입. Character 타입이라고 명시하면 Character 타입이 된다.
Initializing an Empty String
빈 문자열이 들어있는 String 타입 변수를 만드는 방법과 Empty String 체크방법
var emptyString = "" // empty string literal을 할당하거나
var anotherEmptyString = String() // 초기화 문법을 사용하거나
// 두 String 모두 똑같이 비어있다.
if emptyString.isEmpty && anotherEmptyString.isEmpty { // isEmpty : String의 property
print("Nothing to see here")
}
String Mutability
let으로 만들면 변경 불가, var로 만들면 변경 가능.
var variableString = "Horse"
variableString += " and carriage"
// variableString는 "Horse and carriage"
let constantString = "Highlander"
constantString += " and another Highlander"
// compile error. constant string cannot be modified
스위프트에서는 이처럼 String이라는 타입이 하나 있고, 그 타입을 상수로 만들지 변수로 만들지에 따라 Mutability가 결정된다. NSString / NSMutableString 라는 변수의 타입 자체로 Mutability를 결정했던 Objective-C와 Cocoa와는 다른 접근이다.
Strings Are Value Types
String 은 Value 타입이다.
따라서 함수에 전달되거나 새로운 String 변수에 할당되거나 할 때 값이 복사된다. String 의 참조가 아닌 그 안에 들어있는 값만 새로 복사되어 전달된다. 따라서 복사된 값을 변경해도 원본은 영향받지 않게 된다. 이 Value Type이라는 건 뒤에서 또 자세히 다루게 될 텐데, 값 복사본이 전달된다는 특성을 이해하고 있어야 원하는 코드를 작성할 수 있을 것이다. (NSString의 경우에는 참조가 전달되기 때문에, 기존에 Objective-C 개발을 했던 경우 착각을 하기 쉽다)
<참고1 - Value Type의 복사에 대해> Value Type의 할당을 할 때는 값 복사가 일어난다고 했다. 그러나 엄밀히 말하면 할당을 하는 시점마다 항상 복사가 일어나는 것은 아니다. 일단 복사는 하지않고 같은 곳만 바라보고 있다가, 값이 변경될 때에 복사를 하게 되는 것이다. 예를들어 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가 var로 선언이 되었다면 Name 프로퍼티는 변경 가능하지만, let으로 선언이 되었다면 Name 프로퍼티는 변경 불가능하게 된다. (Reference Type의 경우에는 var/let 어느 것으로 선언되더라도 내부값 변경 가능하다. 참고로 NSObject를 상속받은 객체, 기타 Class로 선언된 객체들이 Reference Type에 속한다.)
Working with Characters
String의 characters 프로퍼티를 사용하면 String 문자열 값의 각각의 문자(Character)에 접근할 수 있다. for-in loop 와 함께 사용해보자.
for character in "Dog!🐶".characters
{
print(character)
}
// D
// o
// g
// !
// 🐶
상수/변수를 생성할 때 Character 타입을 명시하고 single-character string literal 을 할당하면 Character 타입을 만들 수 있다.
let exclamationMark: Character = "!" // Character 명시 안하면 String이 됨
Character 값으로 구성된 배열을 통해 String 타입 변수를 만들 수 있다.
let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]
let catString = String(catCharacters)
print(catString) // Cat!🐱
Concatenating Strings and Characters
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
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
String Interpolation
상수, 변수, String literal을 조합하여 String 을 만들 수 있다. 넣고 싶은 아이템을 괄호+백슬러시로 묶는다. 예를 들어 변수는 \(변수이름) 형태로 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"
Unicode
String 은 사실 유니코드 스칼라(Unicode scalar) 값들로 이루어져 있다. 유니코드 스칼라는 Character 하나하나를 위한 유니크한 21-bit Number. 예를 들어 U+0061 for LATIN SMALL LETTER A ("a"), or U+1F425 for FRONT-FACING BABY CHICK ("🐥").
문자열 리터럴은 다음과 같은 Special Character를 가진다.
- \0 (null character), \\ (backslash), \t (horizontal tab), \n (line feed), \r (carriage return), \" (double quote) and \' (single quote)
- \u{n}, n is a 1–8 digit hexadecimal number -> 해당하는 유니코드 스칼라를 출력
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
스위프트의 모든 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 한
Counting Characters
String 을 구성하는 Character 개수를 세기 위해 String의 characters 프로퍼티의 count 프로퍼티를 사용한다. 참고로 Character 하나에 포함된 유니코드 스칼라가 2개 이상이라도 그와 관계없이 Character 하나 당 한 개로 계산된다.
let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenagerie has \(unusualMenagerie.characters.count) characters")
// prints "unusualMenagerie has 40 characters
var word = "cafe"
print("the number of characters in \(word) is \(word.characters.count)")
// prints "the number of characters in cafe is 4"
word += "\u{301}" // COMBINING ACUTE ACCENT, U+0301 -> café
print("the number of characters in \(word) is \(word.characters.count)")
// prints "the number of characters in café is 4”
<note> 같은 Character 라도 포함된 유니코드 스칼라의 수가 다를 수 있다(위에서 다룬 extended grapheme cluster 참조). 따라서 같은 Character 라도 차지하는 메모리가 다를 수 있다.
Accessing and Modifying a String
<String Indices>
String의 index
타입은 Index 이고 String 안의 특정 Character의 위치를 가리길 때 사용된다. 왜 Int 값으로 Character의 위치에 접근하지 못하고 굳이 index type을 써야할까? 그 이유는 위에서 설명한, Character마다 차지하는 메모리가 다를 수 있다는 점에 있다. 유니코드 스칼라를 몇 개씩 건너뛰어야 다음 Character가 나올지 미리 알 수 없기 때문에 그 역할을 해주는 index type을 사용하는 것이다.
String에는 index를 위한 다음 프로퍼티가 있다.
- startIndex : 첫 번째 Character 를 가리키는 index
- endIndex : 마지막 Character의 한 칸 뒤 index. 따라서 endIndex 에서 한 칸 앞으로 와야 마지막 Character를 가리키게 된다.
String에는 Index를 위한 다음 메서드가 있다.
- index(before:) : 바로 전
- index(after:) : 바로 뒤
- index(_:offsetBy:) : 오른쪽 방향으로 N만큼 떨어져있는 곳(음수면 왼쪽으로 N만큼)
let greeting = "Guten Tag!"
greeting[greeting.startIndex]
// G
greeting[greeting.endIndex]
// Error
greeting[greeting.index(after: greeting.endIndex)]
// Error
greeting[greeting.index(before: greeting.endIndex)]
// !
greeting[greeting.index(after: greeting.startIndex)]
// u
let index = greeting.index(greeting.startIndex, offsetBy: 7)
greeting[index]
// a
String 의 characters 프로퍼티의 indices 프로퍼티는 String 안의 Character들에 대한 index의 Range를 만들어준다. 이 프로퍼티를 for 문과 사용하면 Character를 순회하는 코드를 간결하게 작성할 수 있다.
for index in greeting.characters.indices
{
print("\(greeting[index]) ", terminator: "")
}
// prints "G u t e n T a g !”
(다시 한 번 주의) 위 for in loop에서의 greeting[index]에 들어가는 index는
0, 1 과 같은 number가 아니라
Index 타입의 값이다!
<Inserting and Removing>
// String 특정 위치에 Character 삽입하기
var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
// welcome now equals "hello!"
// String 특정 위치에 다른 String 삽입하기
welcome.insert(contentsOf: " there".characters, at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there!"
// 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"
참고로 RangeReplaceableCollection 프로토콜을 따르는 모든 타입에 대해 위 메서드를 적용할 수 있다. Array, Dictionary, Set 등.
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.")
}
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의 characters 프로퍼티와 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‼🐶"
위 String은 다음과 같은 Character 로 구성되어 있다.
- D
- o
- g
- !! (DOUBLE EXCLAMATION MARK or 유니코드 스칼라 U+203C)
- 🐶 (DOG FACE or 유니코드 스칼라 U+1F436)
이제 이 String의 값에 유니코드 표현법으로 접근해보자.
(1) UTF-8 Representation
String의 utf8 프로퍼티를 사용
- type : String.UTF8View
- collection of unsigned 8-bit (UInt8) values
for codeUnit in dogString.utf8 {
print("\(codeUnit) ", terminator: "")
}
print("")
// 68 111 103 226 128 188 240 159 144 182
처음 3개 codeUnit 68, 111, 103 은 각각 D, o, g 를 표현한다. 1byte 만으로 표현할 수 있는 이런 문자들은 ASCII 로 표기하였을 때도 동일한 코드를 가진다.
그 다음 3개 codeUnit 226, 128, 188은 3byte UTF-8이고 DOUBLE EXCLAMATION MARK 문자를 표현한다.
마지막 4개 codeUnit 240, 159, 144, 182는 4byte UTF-8이고 DOG FACE 문자를 표현한다.
(2) 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 ”
처음 3개 codeUnit 68, 111, 103은 각각 D, o, g 를 표현하며 이는 UTF-8 표현과 같은 값이다. D, o, g는 ASCII characters 로 표기할 수 있는 문자들이기 때문이다.
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란? 2byte인 UTF-16 으로 모든 문자를 표현하기에는 부족하기 때문에, 2byte만으로는 표현할 수 없는 예외 문자들을 Supplementary Characters 라고 정했다. 이 문자를 만들기 위한 인코딩 방식을 Surrogate Pair 라고 한다.
(3) 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"
처음 세 CodeUnit 68, 111, 103은 변함없이 D, o, g.
4번째 codeUnit 값 8252 도 변함없이 16진수 값 203C를 10진수로 나타낸 것. 유니코드 스칼라 U+203C는 DOUBLE EXCLAMATION MARK 문자를 표현함.
그 다음 codeUnit 값 128054는 DOG FACE인 유니코드 스칼라 U+1F436을 10진수로 나타낸 것이다.
UnicodeScalar를 통해 새로운 String 을 구성할 수도 있다.
for scalar in dogString.unicodeScalars {
print("\(scalar) ")
}
// D
// o
// g
// ‼
// 🐶
'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글
Control Flow (0) | 2017.02.15 |
---|---|
Collection Types - Dictionary (0) | 2017.02.11 |
Collection Types - Set (0) | 2017.02.11 |
Collection Types - Array (0) | 2017.02.11 |
Basic Operators (0) | 2017.02.05 |