서브스크립트
서브스크립트 (Subscripts)
콜렉션의 요소에 접근하는 방법을 살펴봐요. 배열에서는 someArray[index], 딕셔너리에서는 someDictionary[key]처럼 대괄호 안에 값 하나를 넣어 요소를 읽거나 쓰는 문법. 바로 그 문법을 우리가 원하는 타입에도 직접 만들어 줄 수 있는데, 그걸 서브스크립트라고 불러요. 값을 넣거나 꺼내는 전용 메서드를 따로 만들 필요 없이, [] 하나로 둘 다 해결할 수 있어요.
클래스, 구조체, 열거형은 *서브스크립트(subscript)*를 정의할 수 있어요. 서브스크립트는 콜렉션, 리스트, 시퀀스의 멤버 요소에 접근하기 위한 단축 문법이에요. 설정과 조회를 위한 별도의 메서드를 만들지 않고도, 인덱스로 값을 설정하고 읽을 수 있게 해주죠. 예를 들어 배열 인스턴스의 요소는 someArray[index]로, 딕셔너리 인스턴스의 요소는 someDictionary[key]로 접근해요.
한 타입에 서브스크립트를 여러 개 정의할 수도 있어요. 그리고 호출할 때 대괄호 안에 넘기는 인덱스 값의 타입에 따라 적절한 서브스크립트 오버로드가 골라져요. 서브스크립트가 꼭 한 차원일 필요는 없어서, 입력 매개변수를 여러 개 받는 서브스크립트를 정의해 커스텀 타입의 필요에 맞출 수도 있어요.
본문
서브스크립트 문법 (Subscript Syntax)
서브스크립트를 쓰면 인스턴스 이름 뒤의 대괄호 안에 값 하나 이상을 적어서 타입의 인스턴스를 조회할 수 있어요. 그 문법은 인스턴스 메서드 문법과 계산 프로퍼티 문법 둘 다를 닮았어요. 서브스크립트 정의는 subscript 키워드로 시작하고, 인스턴스 메서드와 같은 방식으로 입력 매개변수 하나 이상과 반환 타입을 지정해요.
인스턴스 메서드와 달리 서브스크립트는 읽기-쓰기 또는 읽기 전용이 될 수 있어요. 이 동작은 계산 프로퍼티에서와 같은 방식으로 getter와 setter로 전달돼요:
subscript(index: Int) -> Int {
get {
// Return an appropriate subscript value here.
}
set(newValue) {
// Perform a suitable setting action here.
}
}
newValue의 타입은 서브스크립트의 반환 값 타입과 같아요. 계산 프로퍼티에서처럼 setter의 (newValue) 매개변수를 지정하지 않을 수도 있어요. 직접 지정하지 않으면 setter에 newValue라는 이름의 기본 매개변수가 제공돼요.
읽기 전용 계산 프로퍼티에서처럼, get 키워드와 중괄호를 없애면 읽기 전용 서브스크립트의 선언을 간단히 줄일 수 있어요:
subscript(index: Int) -> Int {
// Return an appropriate subscript value here.
}
정수의 n단 곱셈표를 나타내는 TimesTable 구조체를 정의해서 읽기 전용 서브스크립트의 구현 예시를 볼게요:
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// Prints "six times three is 18".
이 예시에서는 3단 곱셈표를 나타내기 위해 TimesTable의 새 인스턴스를 만드는데, 구조체의 initializer에 값 3을 넘겨 인스턴스의 multiplier 매개변수 값으로 쓰게 해요. 그리고 threeTimesTable[6]처럼 호출해서 threeTimesTable 인스턴스를 서브스크립트로 조회해요. 이 호출은 3단 곱셈표의 여섯 번째 항목을 요청하는 거라 값 18, 그러니까 3 곱하기 6을 반환해요.
Note: n단 곱셈표는 고정된 수학 규칙에 기반해요.
threeTimesTable[someIndex]에 새 값을 설정하는 건 적절하지 않으므로,TimesTable의 서브스크립트는 읽기 전용으로 정의돼요.
서브스크립트 사용 (Subscript Usage)
"서브스크립트"의 정확한 의미는 사용되는 맥락에 따라 달라져요. 서브스크립트는 보통 콜렉션, 리스트, 시퀀스의 멤버 요소에 접근하기 위한 단축 문법으로 쓰여요. 그리고 특정 클래스나 구조체의 기능에 가장 적절한 방식으로 자유롭게 구현할 수 있어요.
예를 들어 Swift의 Dictionary 타입은 Dictionary 인스턴스에 저장된 값을 설정하고 조회하는 서브스크립트를 구현해요. 딕셔너리에 값을 설정하려면 서브스크립트 대괄호 안에 딕셔너리의 키 타입에 해당하는 키를 넣고, 딕셔너리의 값 타입에 해당하는 값을 그 서브스크립트에 할당하면 돼요:
var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs["bird"] = 2
위 예시는 numberOfLegs라는 변수를 정의하고, 세 개의 키-값 쌍을 담은 딕셔너리 리터럴로 초기화해요. numberOfLegs 딕셔너리의 타입은 [String: Int]로 추론돼요. 딕셔너리를 만든 뒤, 이 예시는 서브스크립트 할당을 사용해 "bird"라는 String 키와 2라는 Int 값을 딕셔너리에 추가해요.
Note: Swift의
Dictionary타입은 키-값 서브스크립팅을 옵셔널 타입을 받고 반환하는 서브스크립트로 구현해요. 위numberOfLegs딕셔너리의 경우 키-값 서브스크립트가Int?, 즉 "옵셔널 int" 타입의 값을 받고 반환해요.Dictionary는 모든 키가 값을 가질 것은 아니라는 사실을 모델링하기 위해 옵셔널 서브스크립트 타입을 쓰고, 어떤 키에nil값을 할당해 그 키의 값을 삭제하는 방법도 제공해요.
서브스크립트 옵션 (Subscript Options)
서브스크립트는 입력 매개변수를 몇 개든 받을 수 있고, 이 입력 매개변수는 어떤 타입이든 될 수 있어요. 또 어떤 타입의 값이든 반환할 수도 있어요.
함수처럼 서브스크립트도 가변 개수의 매개변수를 받거나 매개변수에 기본값을 제공할 수 있어요. 다만 함수와 달리 서브스크립트는 in-out 매개변수는 쓸 수 없어요.
클래스나 구조체는 필요한 만큼 많은 서브스크립트 구현을 제공할 수 있고, 서브스크립트를 사용하는 시점에 대괄호 안에 담긴 값의 타입에 따라 사용할 적절한 서브스크립트가 추론돼요. 여러 서브스크립트를 정의하는 이 방식을 *서브스크립트 오버로딩(subscript overloading)*이라고 불러요.
서브스크립트가 단일 매개변수를 받는 경우가 가장 흔하지만, 타입에 적절하다면 매개변수 여러 개를 받는 서브스크립트를 정의할 수도 있어요. 다음 예시는 Double 값들의 2차원 행렬을 나타내는 Matrix 구조체를 정의하는데, 이 구조체의 서브스크립트는 정수 매개변수 두 개를 받아요:
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
}
}
}
Matrix는 rows와 columns라는 두 매개변수를 받는 initializer를 제공하고, rows * columns 개의 Double 값을 저장할 만큼 큰 배열을 만들어요. 행렬의 각 위치는 초기값 0.0을 가지는데, 이렇게 하기 위해 배열의 크기와 초기 셀 값 0.0을 배열 initializer에 넘겨 올바른 크기의 새 배열을 만들고 초기화해요.
적절한 행과 열의 개수를 initializer에 넘겨 새 Matrix 인스턴스를 만들 수 있어요:
var matrix = Matrix(rows: 2, columns: 2)
위 예시는 두 개의 행과 두 개의 열을 가진 새 Matrix 인스턴스를 만들어요. 이 Matrix 인스턴스의 grid 배열은 사실상 행렬을 왼쪽 위에서 오른쪽 아래 방향으로 읽은 펼쳐진(flattened) 버전이에요.
행렬의 값은 행과 열 값을 서브스크립트에 콤마로 구분해 넘겨서 설정할 수 있어요:
matrix[0, 1] = 1.5
matrix[1, 0] = 3.2
이 두 문장은 서브스크립트의 setter를 호출해 행렬의 오른쪽 위 위치(row는 0, column은 1)에 값 1.5를, 왼쪽 아래 위치(row는 1, column은 0)에 값 3.2를 설정해요.
Matrix 서브스크립트의 getter와 setter 둘 다 서브스크립트의 row와 column 값이 유효한지 확인하는 assertion을 담고 있어요. 이 assertion을 돕기 위해 Matrix는 요청한 row와 column이 행렬의 경계 안에 있는지 확인하는 indexIsValid(row:column:)라는 편의 메서드를 포함해요:
func indexIsValid(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}
행렬 경계 밖에 있는 서브스크립트에 접근하려 하면 assertion이 발동돼요:
let someValue = matrix[2, 2]
// This triggers an assert, because [2, 2] is outside of the matrix bounds.
타입 서브스크립트 (Type Subscripts)
위에서 설명한 인스턴스 서브스크립트는 특정 타입의 인스턴스에 대해 호출하는 서브스크립트예요. 반면 타입 자체에 대해 호출하는 서브스크립트도 정의할 수 있는데, 이런 서브스크립트를 *타입 서브스크립트(type subscript)*라고 불러요. subscript 키워드 앞에 static 키워드를 적어서 타입 서브스크립트임을 나타내요. 클래스는 그 대신 class 키워드를 써서, 서브클래스가 슈퍼클래스의 해당 서브스크립트 구현을 오버라이드하도록 허용할 수도 있어요. 아래 예시는 타입 서브스크립트를 정의하고 호출하는 방법을 보여줘요:
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
static subscript(n: Int) -> Planet {
return Planet(rawValue: n)!
}
}
let mars = Planet[4]
print(mars)
더 알아보기
- 서브스크립트는
subscript키워드로 정의하고, 인스턴스 뒤 대괄호[]안에 값을 넣어 호출해요. 읽기-쓰기(getter·setter)와 읽기 전용(getter만) 둘 다 가능해요. - 한 타입에 서브스크립트를 여러 개 정의할 수 있고, 대괄호 안에 넘기는 값의 타입에 따라 적절한 오버로드가 골라져요(서브스크립트 오버로딩).
- 서브스크립트는 매개변수 기본값이나 가변 매개변수를 쓸 수 있지만, 함수와 달리 in-out 매개변수는 쓸 수 없어요.
static(클래스에서는class) 키워드를 붙이면 인스턴스가 아니라 타입 자체에 호출하는 타입 서브스크립트를 정의해요.- 관련 주제로는 배열·딕셔너리 문법을 다루는 "콜렉션 타입"과, 읽기 전용 getter를 간단히 쓰는 패턴을 보여주는 "속성" 장이 이어져요.