Swift Deep Dive: String은 왜 Subscripts로 접근할 수 없을까?
Subscipts가 뭔지부터 보자!
배열을 다룰때 빠지지 않는 개념이 있다. 바로 서브스크립트(Subscripts).
공식 문서에 나와있는 설명을 보니... 서브스크립트란 연속된 형태(sequence), 리스트 등의 집합 원소에 간단하게 접근할 수 있는, 클래스 / 구조체 / 열거형에서 정의할 수 있는 문법이다. 문장이 다소 길지만 결론은 Array나 Dictionary 등과 같은 집합 형태에 있는 원소들을 [](대괄호)를 이용하여 쉽게 불러올(접근할) 수 있다는 것이다.
공식 docs에 대한 정확한 해석은 아래 Github blog를 참고하면 좋습니다
(https://jusung.gitbook.io/the-swift-language-guide/language-guide/12-subscripts)
클래스, 구조체 그리고 열거형에서 스크립트를 정의해 사용할 수 있습니다. 서브스크립트란 콜렉션, 리스트, 시퀀스 등 집합의 특정 멤버 엘리먼트에 간단하게 접근할 수 있는 문법입니다. 서브스크립트를 이용하면 추가적인 메소드 없이 특정 값을 할당(assign)하거나 가져올 수(retrieve) 있습니다. 예를들면, 배열(Array) 인스턴스의 특정 엘리먼트는 someArray[index]문법으로, 사전(Dictionary) 인스턴스의 특정 엘리먼트는 someDictionary[key]로 접근할 수 있습니다. 하나의 타입에 여러 서브스크립트를 정의할 수 있고 오버로드(Overload)도 가능합니다. 뿐만아니라 단일 인자 값을 넘어, 필요 따라 복수 인자 값을 사용할 수 있습니다.
코드로 보자!
subscript(index: Int) -> Element {
get {
// return the value corresponding to the given index
}
set(newValue) {
// set the value corresponding to the given index
}
}
🙋🏻♂️ 공식 Docs에 나와있는 예시를 바탕으로 보면, 여기서 Element는 우리가 찾거나 설정하고자하는 객체의 타입을 나타낸다. 그리고 Index는 객체에 접근하려는 인덱스 값을 나타낸다. 여기서 나오는 get과 set은 연산프로퍼티에 나오는 개념이다. 연산프로퍼티란 저장 프로퍼티와 달리 저장 공간을 갖지 않고 다른 저장 프로퍼티의 값을 읽어 연산을 실행하거나, 프로퍼티로 전달 받은 값을 다른 프로퍼티에 저장하는 것을 말하는데... 이를 전부 설명하긴 너무 길어지니 여기선 패스하겠다!
(참고로 연산프로퍼티는 get만 사용하는 get only 형태는 가능하지만, set만 사용하는 형태는 불가능하다. 따라서 set 없이 subscript문을 쓴다면, 자동으로 get only 형태가 적용된다.)
이를 조금 응용해보면 아래와 같다.
class MyClass {
var numbers = [1, 2, 3, 4, 5]
subscript(index: Int) -> Int {
get {
return numbers[index]
}
set(newValue) {
numbers[index] = newValue
}
}
}
🙋🏻♂️ MyClass 클래스에 정수 배열이 저장되어 있다. 그리고 원소에 접근하기 위해 subscript 메소드를 정의해주었다.
let obj = MyClass()
let value = obj[2] // returns 3
obj[3] = 10 // sets the value at index 3 to 10
🙋🏻♂️ 이제 우리는 MyClass의 객체 obj에 index 파라미터를 사용하여 numbers 배열의 인덱스에 접근할 수 있다. 이 부분이 값을 가져오는 get이다. 그리고 세번째 줄에서 obj index 3의 값(기존의 값: 4)을 10으로 변경해주는 것을 볼 수 있다. 이 부분이 새로운 값을 받아오는 set의 활용이다.
오버로드도 가능!
class MyStringClass {
var string = "Hello, World!"
subscript(index: Int) -> Character {
return string[string.index(string.startIndex, offsetBy: index)]
}
subscript(index: String.Index) -> Character {
return string[index]
}
}
let obj = MyStringClass()
let value = obj[7] // returns "W"
let value2 = obj[obj.string.index(of: ",")!] // returns ","
🙋🏻♂️ 오버로드란 파라미터의 타입이나 파라미터의 수가 다른 여러 subscript를 정의하는 기능이다. 이를 통해 특정 요구 사항에 따라 집합 원소에 접근하고 수정하는 방법을 정의할 수 있다. 위의 코드처럼 index를 Int뿐만 아니라 String.Index를 활용하는 subscript 메서드도 정의 할 수 있다.
그래서... 왜 String은 Subscript로 접근이 불가능할까??
그것은 Swift의 문자열이 연속적인 문자 배열로 메모리에 저장되지 않기 때문이다...!
스위프트 문자열은 인코딩과 문자열의 길이와 같은 추가적인 정보와 함께 유니코드 문자의 buffer를 포함한 구조로 구현되어있다.
즉, 유니코드 문자의 가변성 성질로 인해 문자열의 length와 incoding이 다른 문제가 생기고 이를 막기 위해 subscript로의 접근을 제한해 놓은 것이다.
Buffer?
컴퓨터 과학에서 버퍼는 데이터가 한 위치에서 다른 위치로 이동하는 동안 데이터를 임시로 보관하는 데 사용되는 메모리 영역입니다. 버퍼는 일반적으로 I/O 작업, 네트워크 통신 및 이미지 처리와 같은 광범위한 컴퓨팅 작업에 사용됩니다.
버퍼는 저장되는 데이터 유형과 사용되는 컨텍스트에 따라 다양한 형태를 취할 수 있습니다. 예를 들어, 버퍼는 단순한 바이트 배열, 끝에 도달하면 순환하는 순환 버퍼 또는 메모리의 두 영역을 번갈아 가며 사용하는 이중 버퍼로 구현될 수 있습니다.
버퍼의 주요 목적은 데이터를 처리하거나 전송할 준비가 될 때까지 데이터를 보관할 수 있는 임시 저장 공간을 제공하는 것입니다. 버퍼는 또한 데이터 흐름 속도의 변동을 완화하거나 시스템의 다른 부분 간에 동기화 지점을 제공하는 데 사용할 수 있습니다.
🙋🏻♂️ 솔직히 문서에 적힌 글들을 보자면... 내 언어 인지력에 문제가 있나라는 생각이 들 정도로 말이 복잡한 경우가 많다 ㅎ
역시 빠르게 이해하기 위해서는 코드 예시!
let str1 = "café"
let str2 = "café"
print(str1.count) // 4
print(str2.count) // 5
let str1FirstCharacter = str1[str1.startIndex] // "c"
let str2FirstCharacter = str2[str2.startIndex] // "c"
let str1LastCharacter = str1[str1.index(before: str1.endIndex)] // "é"
let str2LastCharacter = str2[str2.index(before: str2.endIndex)] // "e"
🙋🏻♂️ 위의 코드는 cafe라는 문자열을 받은 상수 str1, str2의 예시이다. 코드에서 보기에는 두 문자열이 동일해보인다. 하지만 막상 문자열의 길이를 카운트 해주면 str1의 경우 4, str2의 경우 5가 나온다. 유니코드는 우리가 눈으로 보기에 같은 문자열도 내부적으로 코드를 달리하고 있는 경우가 종종 있다는 것이다. 이는 우리나라 한글에도 적용되는데 간단하게 예를들면 유니코드는 "ㅎ" + "ㅏ" + "ㄴ" -> "한" 이렇게 자음과 모음을 모아서 하나의 유니코드 값으로 저장하기도 한다. 이는 곧 index로 접근했을때 오류를 일으킬 소지가 다분하다.
즉, 유니코드의 가변성 때문에 위의 코드처럼 String.Index 형태로 접근해야 우리가 원하는 위치의 문자열 값을 정확히 가져올 수 있는 것이다.
그럼 Swift 개발자는 영원히 Subscripts를 이용해서 String에 접근할 수 없나요.. ㅠ
라고 한다면 또 그렇지는 않겠다.
🙋🏻♂️ 굳이 Int의 index형태로 접근하고 싶다면, String 구조체에 대해 extension을 만들고 이에 대해 직접 메서드를 구현해주면 된다.
extension String {
subscript(index: Int) -> String? {
guard (0..<count).contains(index) else {
return nil
}
let target = index(startIndex, offsetBy: index)
return String(self[target])
}
}
let greeting = "Hello World!"
greeting[0] // Optional("H")
🙋🏻♂️ 위와 같이 String 구조체에 subscript 메서드를 추가하여 사용할 수 있다. 하지만 결국 target 상수에서 보면 알 수 있듯이 이러한 메서드의 형태도 Swift의 index 특징을 그대로 따르는 코드로 정의되어 있다. 결국 겉으로 보기에는 Int index가 잘 작동하는 것처럼 보이지만 뒤로는 String.Index 형태가 작동되고 있으니... 그냥 Swift가 하라는 대로 안전하게 .index 형태로 구현하는 법을 익히자!