swift

String은 왜 subscript로 접근이 안될까

kimyounggyun 2022. 9. 19. 18:57

스위프트는 인덱스로 문자열(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)를 지원하지 않기 때문에 인덱스를 이용해 요소에 접근할 수 없다. 서브스크립트를 지원하지 않은 이유를 알기 위해선 스위프트가 어떻게 문자열을 표현하는지를 알아야 한다.

 

개발자 문서의 String 정의

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("é")