스위프트는 인덱스로 문자열(String)의 요소에 접근하려고 하면 'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead.
에러 메시지를 보여준다. 스위프트의 문자열은 왜 인덱스로 요소에 접근하지 못할까?
let array: [Int] = [0, 1, 2, 3, 4, 5]
print(array[3])
let string: String = "Hello world" // error
print(string[3])
스위프트의 문자열은 서브스크립트(subscript)를 지원하지 않기 때문에 인덱스를 이용해 요소에 접근할 수 없다. 서브스크립트를 지원하지 않은 이유를 알기 위해선 스위프트가 어떻게 문자열을 표현하는지를 알아야 한다.
C++, Java는 아스키코드를 사용해 1byte로 문자를 표현하고 이러한 고정된 크기의 문자들의 집합으로 문자열을 표현한다. 하지만 스위프트는 extended grapheme clusters를 이용해 문자 한 개를 표현한다. 즉 스위프트의 문자열은 유니코드 문자열로 가변 크기의 유니코드 스칼라 값들로 이루어져 있다. 따라서 하나의 문자를 저장하기 위해서 1byte 이상의 메모리를 사용하기 때문에 정수 인덱스 한 개로 접근할 수 없는 것이다.
아래와 같이 똑같은 문자열을 나타내는 변수가 있을 때 두 변수를 출력하면 값은 같지만 문자열을 이루고 있는 유니코드 스칼라 값을 보면 서로 다르다. 그 이유는 프랑스어의 é 는 한 개의 유니코드의 값으로 나타낼 수 있을 뿐만 아니라, 알파벳 e 뒤에 ́ 를 붙여서 만들 수도 있기 때문이다.
let cafe1 = "Café"
let cafe2 = "Cafe\u{301}"
print(cafe1) // Café
print(cafe2) // Café
print(Array(cafe1.unicodeScalars)) // ["C", "a", "f", "\u{00E9}"]
print(Array(cafe2.unicodeScalars)) // ["C", "a", "f", "e", "\u{0301}"]
스위프트의 String은 구조체이기 때문에 서브스크립트를 정의해서 사용할 수 있다. 그렇다면 String을 확장해 서브스크립트를 정의하면 인덱스로 접근할 수 있지 않을까? 스위프트의 func index(_ i: String.Index, offsetBy n: String.IndexDistance) -> String.Index
를 사용하여 서브스크립트를 정의해보자.
extension String {
subscript(index: Int) -> String? {
guard index >= 0 && index < self.count else {
return nil
}
let target = self.index(self.startIndex, offsetBy: index)
return String(self[target])
}
}
String을 확장하여 잘못된 인덱싱을 막기 위해 인덱스가 올바른 범위 내에 있는지 확인한 다음 index(_:, offsetBy:)
메서드를 사용하여 값을 얻은 후 리턴하는 서브스크립트를 정의하고 아래와 같이 인덱싱하여 값에 접근할 수 있다.
let cafe = "Cafe\u{301}" // Café
print(cafe[10]) // nil
print(cafe[3]) // Optional("é")