Swift

Subscript에 대해 알아보기 (feat.String)

GREEN.1229 2022. 10. 3. 11:25

안녕하세요. 그린입니다🍏
오늘 포스팅은 Subscript에 대해 알아보며 Swift 언어에서만 왜 String을 다룰때 Subscript가 어떻게 다른지 알아보겠습니다🙌

우선 Subscript라는것 많이 들어보셨을것 같아요.
그런데 아마도 제대로 설명하기에는 막막한 분들이 많을거라 생각합니다.
보면 아~ 이거! 하겠지만 용어만으로는 뭐였더라..? 하게 되죠.

그래서 우선 Subscript가 무엇인지 정의해보고 가겠습니다!

 

Subscript의 정의

서브스크립트는 클래스, 구조체, 열거형에서 정의해서 사용하는 스크립트입니다.
즉 컬렉션, 리스트, 시퀀스 등 집합 내 속한 요소에 쉽게 접근할 수 있게 해주는 문법인 셈이죠.
이를 이용해 메서드 구현없이도 단순히 값의 추가 및 획득 등이 가능합니다.
대표적인 예시로써 배열의 특정 요소를 접근하는 Array[index] 문법을 들 수 있습니다.
이 Subscript는 오버로드를 통해 다양하게 입맛대로 정의할 수 있습니다.

그럼 이 서브스크립트가 어떤 문법으로 구성되어 있을까요?

 

Subscript 문법

서브스크립트가 구현된 문법은 get set 메서드와 동일합니다.

subscript(index: Int) -> Int {
  get {
    // Return an appropriate subscript value here.
  }
  set(newValue) {
    // Perform a suitable setting action here.
  }
}

만약 set을 없애고 읽기 전용 즉, read only로 구현하고 싶다면 아래와 같이 선언해줄 수 있습니다.
set을 따로 설정하지 않고 지정한다면 기본값으로 newValue를 사용하게 되죠.

subscript(index: Int) -> Int {
  // Return an appropriate subscript value here.
}

아래와 같이 서브스크립트를 오버라이드해 읽기 전용으로 입맛대로 사용할 수 있습니다.

struct Age {
  let multiplier: Int
  subscript(index: Int) -> Int {
      return multiplier * index
  }
}

let greenAge = Age(multiplier: 12)
print("greenAge's converted age is \(greenAge[5])")

// Prints "greenAge's converted age is 60"

 

자 그럼 기본 문법 선언에 대해 알아봤으니 서브스크립트의 사용에 대해 알아보겠습니다!

 

Subscript 사용

사용하는 방법은 아주아주 쉽습니다.
아래 딕셔너리를 예시로 사용해보죠.

var ageOfDeveloper = ["green": 10, "taetae": 20, "tony": 30, "zeke": 40]
ageOfDeveloper["taetae"] = 50

[String: Int] 형을 갖는 딕셔너리에서 이렇게 키 값으로 값을 획득 및 추가할 수 있습니다.
키 값으로 50의 데이터로 변경하라는것이 서브스크립트 문법이라 볼 수 있습니다.
하나 참고해보고 넘어가면 좋은것은 딕셔너리의 리턴값은 옵셔널입니다.
딕셔너리에 특정 키 값이 없을 수도 있고 또한 키 값에 매칭되는 데이터가 nil일 수 있기 때문입니다!

아까 위에서 Subscript는 충분한 오버로딩이 가능하다고 했죠? 그거 보시죠!

 

Subscript 옵션

Subscript는 오버로딩과 input, output의 갯수 제한이 없으며 형태도 다양하게 정의할 수 있습니다.
아래 예시를 보시죠!

struct Matrix {
  let rows: Int, columns: Int
  var grid: [Double]
  
  init(rows: Int, columns: Int) {
    self.rows = rows
    self.columns = columns
    grid = Array(repeating: 0.0, count: rows * columns)
  }
  
  func indexIsValid(row: Int, column: Int) -> Bool {
    return row >= 0 && row < rows && column >= 0 && column < columns
  }
  
  subscript(row: Int, column: Int) -> Double {
    get {
      assert(indexIsValid(row: row, column: column), "Index out of range")
      return grid[(row * columns) + column]
    }
    set {
      assert(indexIsValid(row: row, column: column), "Index out of range")
      grid[(row * columns) + column] = newValue
    }
  }
}

매트릭스를 구성하는 즉 2차 행렬을 구성하는거로 볼 수 있어요.
subscript를 보면 row, column 두개의 input을 받고 하나의 Double 타입의 output을 배출해줍니다.
get set 내부 구현에서 유효 인덱스인지 체크하는 메서드를 호출하고 다르다면 종료시켜버리죠.
이 Matrix 타입을 이용해 아래와 같이 하나 인스턴스를 만들어볼께요.

var matrix = Matrix(rows: 3, colums: 3)

이렇다면 이 인스턴스는 3X3 2차원 행렬이 되는것이죠.
grid의 값을 보면 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]의 데이터를 가지고 있어요.
즉 이런 모양이겠죠?

[ 0.0, 0.0, 0.0
  0.0, 0.0, 0.0
  0.0, 0.0, 0.0 ]

여기서 데이터의 값을 서브스크립트로 접근해 할당할 수 있습니다.

matrix[1,1] = 7.7
matrix[2,2] = 8.8

요렇게 할당해주면 아래와 같이 데이터가 변경되게 됩니다.

[ 0.0, 0.0, 0.0
  0.0, 7.7, 0.0
  0.0, 0.0, 8.8 ]


자 아주 쉽죠 서브스크립트!?
다들 알고 계셨던걸 한번 리마인드 차원에서 정리되었다고 보면 되요ㅎㅎ
이제 그럼 다들 Subscript는 잘 리마인드 되었고 알고 계신다는 가정하에 두번째 주제로 넘어가보겠습니다.

다들 Swift언어로 코딩 테스트 혹은 알고리즘을 풀거나 할때 String, 문자열을 많이 다루시죠?
그때마다 다른 언어는 String[index] 요런 서브스크립트를 지원해서 Character, 문자를 뽑아오는데 Swift는 안되서 짜증나는 경우 많으시지 않았나요?
저는 그랬어요...
그래서 매번 firstIndex등의 형태로 접근하고 불러오는 번거로운 과정들을 거쳤죠.
아니면 프로젝트에서는 이걸 따로 별도 구현을 해놓고 사용하던가...
무튼 왜 다른 언어들은 되는데 Swift는 안돼? 이에 대한 이유를 알아보려합니다.

Swift의 String에서는 Int 인덱스 참조가 불가능한 이유

이것의 기반은 우선 아래와 같은 String에서 Int로의 인덱스 참조가 불가능하다는 사실입니다.

let name = "GREEN"
print(name[0])

이 코드가 안된다는 소리인데 이걸 돌리면 어떤 에러가 나올까요?

서브스크립트를 사용할 수 없다!
"String에서는 Int로 서브스크립트를 사용할 수 없고 String.index로 사용해야한다."
라고 합니다.
그럼 우선 안되는 사실은 확인했으니 왜 안되는지 논리적으로 짚고 넘어가보시죠!

Swift에서의 String은 문자들의 집합입니다.
그렇기에 문자들에 무언가 답이 있습니다.
Swift에서의 Character는 1개 이상의 유니코드 스칼라가 조합되어 만들어집니다.
여기서 중요한건 조합되어 만들어진다는것 자체가 럼 한개의 유니코드 스칼라로 만들어질 수도 있고 여러개의 유니코드 스칼라로 만들어질 수도 있다는것이죠.
그렇다는건 하나의 Character에 들어오는 유니코드 스칼라의 갯수가 다를 수 있다는것이고 그 말인즉슨 크기가 매번 일정치 않은 가변적이라는 소리죠!
그렇다는것은 우리는 다양하게 이 String의 조합을 볼 수 있다는거에요.
우린 하나의 문자열이라도 그대로 볼수도, 유니코드 스칼라의 조합들을 볼수도, utf8, utf16 구성으로 볼 수도 있습니다.
혹시 utf8과 유니코드들에 대해 조금 간결하고 명확한 설명이 필요하다면 아래 영상을 추천드립니다!

그렇기에 String이라고 해서 특정하게 Int 인덱스로 접근할 수 없는 이유는 코드보고 설명드릴께요!

let apple = "🍏"

print("Charater's count: \(apple.count)")
for i in apple {
    print(type(of: i),i)
    if i == apple.last {
        print("***************\n")
    }
}

print("Unicode Scalar's count: \(apple.unicodeScalars.count)")
for i in apple.unicodeScalars {
    print(type(of: i),i)
    if i == apple.unicodeScalars.last {
        print("***************\n")
    }
}

print("utf8 Scalar's count: \(apple.utf8.count)")
for i in apple.utf8 {
    print(type(of: i),i)
    if i == apple.utf8.last {
        print("***************\n")
    }
}

print("utf16's count: \(apple.utf16.count)")
for i in apple.utf16 {
    print(type(of: i),i)
    if i == apple.utf16.last {
        print("***************\n")
    }
}

이렇게 하나의 String을 가지고 다양한 방법으로 표현해보고 몇개의 요소가 들어가있는지 확인할 수 있는 코드를 작성했습니다.
이 결과는 어떻게 나올까요?

Charater's count: 1
Character 🍏
***************

Unicode Scalar's count: 1
Scalar 🍏
***************

utf8 Scalar's count: 4
UInt8 240
UInt8 159
UInt8 141
UInt8 143
***************

utf16's count: 2
UInt16 55356
UInt16 57167
***************

apple 이모지는 하나의 유니코드 스칼라로 이뤄져있고 utf8에서는 4개, utf16으로는 2개의 조합으로 이뤄져있네요.

자 그럼 거의 답이 나왔죠? 정리해보시죠!

String은 보시다시피 다양한 뷰로 나타날 수 있습니다.
그렇기에 Int 인덱스로 바로 접근한다는것은 어떤 형식으로 나타내야할지 모르는 크기가 가변적이기에 사용할 수 없습니다.
즉 다른 데이터 타입에서의 서브스크립트 형태로는 사용할 수 없는 이유죠.
그렇기에 초반에 설명드렸듯 String.Index 형태의 서브스크립트로 사용해줘야합니다.

그럼 String.Index가 어떤건지 어떤 형태인지 보시죠!

 

String의 Instance Subscript

공식문서의 String안에는 Instance Subscript가 있어요.
지정된 위치의 문자에 엑세스할 수 있는 서브스크립트로 선언은 아래와 같습니다.

subscript(i: String.Index) -> Character { get }

자, 그럼 우리는 아까도 나온 이 String.Index로 결국 원하는 문자열에서의 서브스크립트 인덱스 접근을 해줄 수 있습니다.

String.Index의 문서를 보면 문자열에서 문자 또는 코드 단위의 위치를 나타낸다고 해요.
선언은 이렇구요.

@frozen struct Index

자 그럼 이 형태로 만들기 위해서 어떤 이니셜라이저를 사용해야 될까요?

자 이니셜라이저들 보세요.
보시면 아까 위에서 저희가 했던 유니코드 스칼라, utf8, utf16 등 다양하죠?
즉 지정해줘야 된다는 소리입니다.

let str = "Greetings, friend! How are you?"
let firstSpace = str.firstIndex(of: " ") ?? str.endIndex
let substr = str[firstSpace...]

if let nextCapital = substr.firstIndex(where: { $0 >= "A" && $0 <= "Z" }) {
  print("Capital after a space: \(str[nextCapital])")
}

// Prints Capital after a space: H


즉 요러한 사용을 거쳐야됩니다.
우리가 알고리즘을 하거나 문자열에서 문자 추출을 위해 기초적으로는 많이 하던 방식이죠?
이것의 단점은 하나하나 앞에서부터 순차적으로 원소를 찾아야하는 방식이죠.
Swift에서 String은 BidirectionalCollection 프로토콜을 채택하고 있습니다.
만약 String이 RandomAccessCollection 프로토콜을 채택하고 있다면 다른 데이터 타입 및 언어처럼 String에서도 바로 인덱스를 넣어 원소를 바로 찾을 수 있었겠죠!?
무튼 그렇기에 String에서 어떤 문자를 찾을 경우에는 O(n)의 시간복잡도를 당연히 소비할 수 밖에 없습니다.
그렇기에 알고리즘에서 가끔 다른 언어들보다 같은 풀이인데도 시간 효율이 나지 않는 이유가 이때문입니다.
알고리즘을 몇번 하다보면 그래서 이러한 시간 효율에서 잡아먹히지 않고자 여러번 문자열 순회가 일어난다면 아래와 같이 아예 Array로 담아버려 사용하기도 합니다.

let greeting = "Hi My name is Green"
let greetingArr = Array(greeting)
print(greetingArr)
print(greetingArr[3])

// Prints ["H", "i", " ", "M", "y", " ", "n", "a", "m", "e", " ", "i", "s", " ", "G", "r", "e", "e", "n"]
// Prints M

 

그럼 String의 서브스크립트를 다른 데이터 타입처럼 바로 Int 인덱스 접근 가능한것처럼 구성해볼 수 있습니다.


서브스크립트를 String에서 extension해서 사용하기
아래와 같이 String 기본 타입을 extension해서 조금 편리하게 사용할 수 있습니다.

extension String {
  subscript(idx: Int) -> String? {
    guard(0..<count).contains(idx) else {
      return nil
    }
    let target = index(startIndex, offsetBy: idx)
    return String(self[target])
  }
}

let name = "GREEN"
print(name[1])

// Prints Optional("R")

자 조금 프로젝트할 때 편리하겠죠?

마지막으로 하나 더 타입 서브스크립트에 대해 아주 간단히 알아보고 끝내겠습니다!

 

타입 서브스크립트

똑같아요.
계속 주구장창 우리가 싸웠던건 인스턴스 서브스크립트에요.
근데 여러분도 알다시피 인스턴스 메서드와 타입 메서드가 있듯이 타입 서브스크립트도 있습니다.
즉, 타입 자체에서 바로 사용하는 서브스크립트죠.

struct Person {
  static var age: [Int] = [10, 20, 30]
 
  static subscript(index: Int) -> Int {
    return age[index]
  }
}

print(Person[1])
// Prints 20

끝!
타입 메서드와 동일해요 오버라이딩 가능하다? class로 불가능하다? static으로..!

마무리

자 이렇게 서브스크립트 나름 그래도 다시 정리해보며 박살내봤는데 도움이 되었으면 좋겠습니다!
모두 Swift의 문자열 다루기에서 조금 편리해지셨으면 좋겠습니다.
편리할 수 있을지는 모르겠지만.. 왜 불편했는지는 알아봤으니 만족합니다🙌

[참고자료]

https://developer.apple.com/documentation/foundation/data/3017410-subscript

 

Apple Developer Documentation

developer.apple.com

https://docs.swift.org/swift-book/LanguageGuide/Subscripts.html

 

Subscripts — The Swift Programming Language (Swift 5.7)

Subscripts Classes, structures, and enumerations can define subscripts, which are shortcuts for accessing the member elements of a collection, list, or sequence. You use subscripts to set and retrieve values by index without needing separate methods for se

docs.swift.org

https://developer.apple.com/documentation/swift/string/subscript(_:)-lc0v

 

Apple Developer Documentation

developer.apple.com

https://developer.apple.com/documentation/swift/string/index

 

Apple Developer Documentation

developer.apple.com

https://developer.apple.com/documentation/swift/bidirectionalcollection

 

Apple Developer Documentation

developer.apple.com

https://jcsoohwancho.github.io/2019-11-19-Swift-String-효율적으로-쓰기/

 

Swift String 효율적으로 쓰기

Swift의 문자열 타입인 String은 깊은 설계적 고민이 녹아있는 타입입니다. 내부는 굉장히 복잡하지만, 사용하는 입장에서는 내부 구현을 알지 못해도 잘 쓸 수 있습니다. 하지만 C, C++ 등의 문자열

jcsoohwancho.github.io