String은 Character의 연속이다. String의 내용은 Character의 컬렉션을 비롯해 다양하게 접근이 가능하다. Swift의 String과 Character는 빠르고, Unicode에 순응하는 방식을 제공한다.

 

 💡 String 타입은 Foundation 프레임워크의 NSString과 브릿지 되어있다. Foundation이 NSString에 정의된 메소드를 String타입에도 확장해 놓았기 때문에, Foundation을 import한다면 NSString으로 캐스팅 하지 않아도 해당 메소드들을 사용할 수 있다. / 참고: Bridging Between String and NSString

 

String Literals

따옴표 사용

let someString = "Some string literal value"

Multiline String Literals

3개의 따옴표를 사용

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."
"""
// \\를 사용하면 라인브레이킹이 무시된다.
  • 닫는 따옴표를 기준으로 기본 indent가 설정된다. (닫는 따옴표 보다 문자가 앞에 위치하면 컴파일 에러)

Special Characters in String Literals

백슬래시를 사용하여 특수문자나 유니코드를 사용할 수 있다.(따옴표나 백슬래시 등)

  • The escaped special characters \\0 (null character), \\\\ (backslash), \\t (horizontal tab), \\n (line feed), \\r (carriage return), \\" (double quotation mark) and \\' (single quotation mark)
  • An arbitrary Unicode scalar value, written as \\u{n}, where n is a 1–8 digit hexadecimal number (Unicode is discussed in Unicode below)
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

Extended String Delimiters

따옴표 앞뒤로 #을 붙이게 되면 모든 문자가 Literal하게 표현된다.

let line = #"Line 1\\nLine 2"#
// 개행을 하지 않고 \\n이 그대로 출력
let threeMoreDoubleQuotationMarks = #"""
Here are three more double quotes: """
"""#

Initializing an Empty String

var emptyString = ""               
var anotherEmptyString = String()  

String Mutability

variableString += " and carriage"

변수로 String을 선언한 경우 += 연산자로 덧붙일 수 있다.

💡 Obj-C와 Cocoa에서는 NSString이 아닌 NSMutableString을 명시적으로 사용해야 이와같은 수정이 가능하다.

 

String은 Value 타입이다

스트링은 값 타입이기 때문에, 메서드를 통과하거나 변수로 할당될 때 값이 복사된다. (단, Swift 컴파일러는 필요할때 String이 복사되도록 최적화를 한다.)

Working with Characters

  • for-in 루프를 사용하여 Character에 접근할 수 있다.
  • 명시적으로 Character 변수를 선언할 수 있다.
  • Character의 Array로 String을 초기화 할 수 있다.
for character in "Dog!🐶" {
    print(character)
}
// D
// o
// g
// !
// 🐶

let exclamationMark: Character = "!"

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

Concatenating Strings and Characters

  • + 연산자를 사용하여 두 문자열을 연결할 수 있다.
  • += 연산자를 사용해도 가능
  • String의 append 메서드를 사용하여 Character를 추가할 수 있다.
let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2
welcome += string2

let exclamationMark: Character = "!"
welcome.append(exclamationMark)

💡 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

String Interpolation

  • \\(some) 을 사용하여 변수나 상수를 표현할 수 있음
let multiplier = 3
let message = "\\(multiplier) times 2.5 is \\(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
  • delimiter(# )으로 감싼경우 내부에서 앞에 #을 명시함으로써 interpolation을 사용할 수 있다.
print(#"6 times 7 is \\#(6 * 7)."#)
// Prints "6 times 7 is 42."

💡 Interpolation의 소괄호 내에는 백슬래시, CR(carriage return), LF(line feed)가 들어갈 수 없다.

 

Unicode

  • String과 Character는 유니코드에 순응하는 타입임

Unicode Scalar Values

  • 스위프트의 네이티브 String 타입은 Unicode scalar로 구현되어 있다. 유니코드 스칼라는 21bit의 유니크한 문자를 위한 값이다. U+0061for LATIN SMALL LETTER A ("a"), or U+1F425for FRONT-FACING BABY CHICK("🐥")
  • 모든 21bit의 유니코드 스칼라 값이 문자를 위해 할당되는 것은 아니고, some 스칼라는 언제있을지 모를 UTF-16 Encoding을 위해 보존된다. 문자로 할당된 Scalar 값은 LATIN SMALL LETTER A 과 같은 이름을 가진다.

Extended Grapheme(자소) Clusters

Swift Character 인스턴스는 extended grapheme cluster(펼쳐진 자소의 군집) 를 표현한다. extended grapheme cluster는 하나나 하나 이상의 Unicode scalar value로 이루어져 있으며, 사람의 입장에서 하나의 문자를 표현하게 된다.

아래의 예시를 보면 single Unicode scalar é (LATIN SMALL LETTER E WITH ACUTE, or U+00E9)가 2개의 Unicode Scalr의 조합으로 표현될 수 있다. 유니코드 텍스트 렌더링 시스템을 통해 렌더링 되어 하나의 문자로 표현된다. 하나의 Character를 표현하기 위한 유연한 방법인 것이다.

let eAcute: Character = "\\u{E9}"                         // é
let combinedEAcute: Character = "\\u{65}\\u{301}"          // e followed by ́
// eAcute is é, combinedEAcute is é
let precomposed: Character = "\\u{D55C}"                  // 한
let decomposed: Character = "\\u{1112}\\u{1161}\\u{11AB}"   // ᄒ, ᅡ, ᆫ
let enclosedEAcute: Character = "\\u{E9}\\u{20DD}"
// enclosedEAcute is é⃝

국가 심볼을 표현하기 위해서 사용될 수도 있다.

let regionalIndicatorForUS: Character = "\\u{1F1FA}\\u{1F1F8}"
// regionalIndicatorForUS is 🇺🇸

Counting Characters

String이 가지고 있는 Character의 Count를 셈하기 위해서는 count 프로퍼티를 사용한다. 위에서 보았던 extended grapheme cluster는 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"

💡 extended grapheme cluster는 여러개의 Unicode scalar로 구성될 수 있다. 이것이 의미하는 바는 서로 다른 캐릭터 혹은 같은 캐릭터에 대한 다른 표현법은 할당을 위해 서로 다른 메모리 공간을 가질 수 있다는 점이다. 이러한 점 떄문에, 스위프트의 Character는 같은 메모리 공간을 갖지 않는다. 그래서, String의 Character의 갯수를 셈하는 것은 iterating을 통하지 않고는 불가능하다.(어디가 Character의 바운더리인지 알 수 없으므로) 따라서 String의 count는 O(N)의 시간복잡도를 가진다. 같은 이유로 파이썬과 같은 언어와는 달리 String의 캐릭터 요소에 서브스크립트[Int] 문법으로 접근할 수 없다.

같은 문자를 가지고 있지만 NSString으로 선언되면 count 프로퍼티의 값이 달라질 수도 있다. NSString의 길이는 16-bit 기준(UTF-16 representation)으로 표현하고 extended grapheme clusters는 고려하지 않기 때문이다.

 

Accessing and Modifying a String

String Indices

각각의 String은 String.Index라는 인덱스 타입을 가지고 있다. 위에서 언급했듯 서로다른 문자는 서로 다른 메모리 공간을 차지하기 때문에 특정 문자가 어디에 위치하는지 찾기 위해서는 String내부의 각각의 Unicode Scalar를 처음이나 끝에서 부터 순회해야만 한다.

startIndex 프로퍼티를 사용해서 String의 첫번째 캐릭터에 접근할 수 있다. endIndex는 마지막 문자의 다음 위치를 나타내기 때문에 String의 유효한 index로서 사용될 수 없다. 빈 문자열이라면, startIndex와 endIndex는 동일하다.

  • index(before:) - 해당 인덱스의 이전 인덱스
  • index(after:) - 해당 인덱스의 이후 인덱스
  • index(_:offsetBy:) - 얼마만큼 이후로 떨어진 인덱스
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]

greeting[greeting.endIndex] // Error
greeting.index(after: greeting.endIndex) // Error
  • indices 프로퍼티를 사용하면 각각의 문자열의 인덱스들을 순회할 수 있다.
for index in greeting.indices {
    print("\\(greeting[index]) ", terminator: "")
}
// Prints "G u t e n   T a g ! "

💡 위에서 사용한 index관련 메소드들은 Collection 프로토콜을 준수하는 어느 타입에서든지 사용할 수 있다.(Array, Dictionary, Set 등)

 

Inserting and Removing

  • 문자를 삽입하는 메서드
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!"
  • 문자를 제거하는 메서드
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"

💡 insert나 remove 메서드는 RangeReplaceableCollection 프로토콜을 채택한 어느 타입에서든지 사용할 수 있다. (Array, Dictionary, Set 등)

 

Substrings

prefix(_:) 과 같은 메서드를 사용한 결과물은 String이 아니고 Substring 타입이다. Substring은 string에서 사용할 수 있는 대부분의 메서드를 가지고 있어 마치 String처럼 다룰 수 있다. 다만, String과는 다르게 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)

스트링과 동일하게, Substring은 각각의 문자가 저장되는 공간을 차지하게 된다. 하지만 성능 최적화 측면에서 두 타입의 동작이 다른데, Subsstring은 원본 String을 저장하기 위해 사용되는 메모리 공간의 일부를 재사용 하거나, 다른 Substring을 저장했던 메모리 공간의 일부를 재사용 할 수 있다는 점이다. (String도 비슷한 방식으로 최적화를 하나, 다만 2개의 String이 같은 메모리 공간을 공유한다면, 그 String은 서로 같다는 점이 다르다.)

이러한 성능 최적화는 String이나 Substring을 수정하기 전까지는 성능 비용을 지불하지 않아도 된다는 것을 의미한다. 결론적으로 Substring은 원본 String의 공간을 재사용하기 때문에 Substring이 사용되는 한 원본 String이 해제되지 않고 메모리에서 계속 유지되기 때문에, Substring은 장기 저장에는 적합하지 않다.

위의 예제에서, beginning은 원본 string인 greeting의 메모리 공간을 공유하게 되며 newString으로 새로 선언을 했을때 비로소 자신만의 메모리 공간을 가지게 된다.

💡 String과 Substring 모두 StringProtocol을 준수한다.

 

Comparing Strings

스트링과 문자열의 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"
  • 서로 다른 Unicode-Scalar 값을 가지더라도, Extended grapheme cluster에 의거해 같은 외형과 Linguistic meaning(?)을 가진다면, 같은 문자로 취급된다.
// "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"
  • 아래의 경우는 외형은 같으나 하나는 키릴문자고 하나는 라틴문자의 Linguistic meaning을 가지기 때문에 다른것으로 취급된다.
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."

💡 스위프트에서 String과 character의 비교는 locale-sensitive하지 않는다.(지역설정에 영향을 받지 않는다.)

Prefix and Suffix Equality

  • hasPrefix(_:) hasSuffix(_:) 메서드를 사용하여, 접두사/ 접미어에 대한 비교를 수행할 수 있다.
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"
]

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"

💡 hasPrefix(_:) hasSuffix(_:) 메서드도 위와 동일하게 extended grapheme clusters를 통한 비교를 수행한다

 

Unicode Representations of Strings

Unicode 문자열이 텍스트 파일이나 다른 방식으로 쓰여진다면, String의 Unicode scalar들은 Unicode-defined encoding 형태로 인코딩된다. 각각의 인코딩은 string을 code unit의 형태로 변환한다. 3가지 인코딩 형태가 있다.

  • UTF-8 encoding form (which encodes a string as 8-bit code units)
  • UTF-16 encoding form (which encodes a string as 16-bit code units)
  • UTF-32 encoding form (which encodes a string as 32-bit code units)

Swift는 String의 서로다른 Unicode 표현방법에 접근하는 방식을 제공한다. 위에서 살펴봤듯, for-in으로 순회를 하는 방법이 그 예이다.

그외에 아래의 3가지 다른 접근방법을 제공한다.

  • A collection of UTF-8 code units (accessed with the string’s utf8 property)
  • A collection of UTF-16 code units (accessed with the string’s utf16 property)
  • A collection of 21-bit Unicode scalar values, equivalent to the string’s UTF-32 encoding form (accessed with the string’s unicodeScalars property)

아래의 스트링을 통해 하나씩 살펴보자

let dogString = "Dog‼🐶"
//D, o, g, ‼ (DOUBLE EXCLAMATION MARK, or Unicode scalar U+203C), and the 🐶 character (DOG FACE, or Unicode scalar U+1F436)

UTF-8 Representation

프로퍼티를 사용하여 UTF-8 표현방식을 사용할 수 있다. UInt8(unsinged 8-bit) 값의 컬렉션이다.

💡 아래 표의 Character에 위치한 표현법인 21-bit Unicode-Scalar 값이다.

 

for codeUnit in dogString.utf8 {
    print("\\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 226 128 188 240 159 144 182 "
  • 첫 3개 (68, 111, 103) decimal codeUnit 값은 각각 D, o, g Charater의 UTF-8 표현이고 ASCII 표현과 같다.
  • 다음 3개의 (226, 128, 188) decimal codeUnit 값은 3바이트의 UTF-8 표현이며 DOUBLE EXCLAMATION MARK를 표현한다.
  • 마지막 4개의 (240, 159, 144, 182) decimal codeUnit 값은 4바이트의 UTF-8 표현이며 DOG FACE 문자를 표현한다.

UTF-16 Representation

UInt16의 컬렉션이다.

for codeUnit in dogString.utf16 {
    print("\\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 55357 56374 "
  • 첫 3개 (68, 111, 103) decimal codeUnit 값은 각각 D, o, g Charater의 UTF-16 표현이고 UTF-8 표현과 같다. (Unicode Scalar가 ASCII를 표현하기 때문에)
  • 다음 1개의 (8252) decimal codeUnit 값이 16진수로 표현했을 때 203C와 같기 때문에 쌍느낌표의 Unicode Scalar Value와 같다. UTF8과는 다르게 하나의 codeUnit으로 표현가능한 것을 확인할 수 있다.
  • 마지막 2개의 (55357and 56374)는 Surrogate(서러게이트) pair를 나타낸다. 각각 high-surrogate인 U+D83D low-surrogate U+DC36 두 값을 의미한다.

💡 UTF-16은 16비트 이상의 문자를 표현할 떄를 대비해서 서러게이트를 만들어 놓았다. https://ko.wikipedia.org/wiki/UTF-16

Unicode Scalar Representation

UnicodeScalar의 컬렉션이다. 각각의 UnicodeScalar 21-bit 값이며 value라는 프로퍼티를 가지는데, 이 프로퍼티는 UInt32 타입이다.

for scalar in dogString.unicodeScalars {
    print("\\(scalar.value) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 128054 "
  • 첫 3개 (68, 111, 103) decimal codeUnit 값은 각각 D, o, g Charater의 UTF-32 표현이다.
  • 다음 1개의 (8252) decimal codeUnit 값도 UTF-16과 동일하다.
  • 마지막 1개의 (128054)값은 16진수로 Unicode Scalar값인 1F436 와 동일하다.

그냥 UnicodeScalar값을 출력하면, 각각의 문자가 그대로 출력된다.

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

'iOS > Swift' 카테고리의 다른 글

[Swift] COW(Copy-on-Write)  (0) 2022.08.24
[Swift] High-Order Function (고차함수)  (0) 2021.11.14