ArraySlice 와 Range
배열은 단일 인덱스를 통해 하나의 원소를 액세스하는 것 외에도 범위(Range<Index>
)값을 통해 하나 이상의 원소로 이루어진 부분 배열을 액세스하는 것이 가능하다.
let a = Array<Int>(1...10)
let b = a[3..<6] // [4, 5, 6]
범위는 ...
연산자와 ..<
연산자를 통해서 만들 수 있는데, 전자는 닫힌 범위, 후자는 열린 범위이다. 열린 범위는 맨 끝 값을 포함하지 않는 범위이다. 즉, 1...10 ==> 1, 2, 3 .. 10
이고 1..<10 ==> 1, 2, 3, .. 9
이다.
범위에 대해서도 좀 더 살펴봐야하겠지만, 여기서는 이 부분열에 대해 생각해보자. 이제 배열에 대해 보다 깊숙히 들여다 볼 차례이다.
ArraySlice
Swift 의 배열은 내부적으로 연속된 공간을 할당받아서 값을 저장한다. 이러한 메모리 레이아웃은 C의 그것과 유사하며, 실제 Swift의 배열은 C의 배열과 유사한 성능을 낸다. 또한 배열은 참조가 아닌 값 시멘틱을 따르는 타입이다. 하지만 실제로 배열은 너무나 자주 쓰이는 타입이고, 이곳에서 저곳으로 전달되거나 이 변수, 저 변수에 대입되기도 하는데, 이 때마다 값의 복사가 일어나는 것은 지나친 리소스 낭비이기 때문에 내부적으로 copy-on-write전략에 의해서 단순히 참조만 전달될 때에는 마치 참조 시멘틱처럼 참조만 전달되다가 참조 중 어느 한 곳에서 변경이 일어나면 그 시점에 실제 배열의 사본이 만들어지는 구조를 택한다.
배열을 Range
로 액세스하는 경우에 배열의 일부인 부분배열이 만들어진다. 부분 배열은 Array
타입이 아닌 ArraySlice
타입이다. 왜냐하면 부분배열을 Array
타입으로 만들면, “자르기”가 일어나는 시점에 부분 열이 사본으로 만들어지는 것은 물론, 원본 배열의 경우에도 다른 참조가 있다면 원본에 대한 복사도 일어날 수 있기 때문이다.
하지만 이런 글에서 간단히 예로 드는 것처럼 우리가 사용하는 배열이 항상 고작 몇 개의 정수를 보관하는 용도로는 사용되지 않는다. 실제 사용에서는 제법 큰 메모리 용량을 차지하는 – 그것도 연속적인 메모리 공간을 요구하는 – 자료 구조이다. 거대한 연속 메모리 공간을 추가적으로 할당하고 복사하는 행위는 시스템 차원에서는 너무나 비용이 큰 작업이다.
따라서 배열의 부분열은 기본적으로 writing 하지 않을 것이라는 가정을 한다. 그리하어 ArraySlice
는 원래 배열의 스토리지를 참조한다. 그러면서 Collection
프로토콜을 비롯하여 Array
가 제공하는 API들을 제공한다. ArraySlice
에서 치환등의 작업이 일어나면 원본 배열에 영향을 준다.
이런 연유로 Swift는 배열과 부분열을 구분하고 있고, 이는 용도에 근거해서 구분된 것이다. 따라서 이 둘은 엄연히 다른 타입이며, Array<T>
를 요구하는 함수에 ArraySlice<T>
를 넣을 수는 없다. (이 때는 Array()
를 이용해서 별도의 사본 배열을 만들어야 한다.)
ArraySlice
를 쓸 때의 주의점은 다음과 같다.
- 원본 배열 전체의 스토리지를 참조만 한다.
- 스토리지자체는 원본 배열이므로, 내부적으로 이중의
startIndex
,endIndex
를 가져서 필요한 영역만큼을 액세스하게끔 한다. 따라서startIndex
의 값은 항상0
이라는 보장이 없다. 따라서 안전을 위해서는 특정한 정수값보다는startIndex
,endIndex
를 쓰도록 하자. - ArraySlice가 살아있는 동안에 Array에 대한 참조는 계속 유지되므로, 이는 오랜시간동안 사용하지 않는 것이 좋다. 특히 라이프 사이클이 긴 클로저 등에서 암묵적으로
ArraySlice
를 참조하지 않도록 한다. 이는 원본 배열 자체의 라이프 사이클을 강제로 연장하여 리소스 낭비의 원인이 될 수 있다. prefix
,suffix
와 같은 부분열(Sequence.Subsequcene
)을 리턴하는 메소드들도 모두ArraySlice
를 리턴하며, 독립적인 부분열 사본을 리턴하는게 아니다.
Range
Swift에서 ArraySlice를 만들 때 흔히 사용하는 타입으로 범위를 나타내는 Range
타입이 있다.1 이 는 대소 비교가 가능한 두 값을 기준으로 그 내부 범위를 표현하는 타입이다. 이 때 각각의 범위는 upperBound
, lowerBound
이다. 2
이는 만드는 방법에 따라서 닫힌 범위와 열린 범위의 두 종류가 되는데, 열린 범위는 상위 경계값을 포함하지 않으며, 닫힌 범위는 상위 경계값도 포함한다. Swift에서 아직은 하위 경계를 포함하지 않는 종류의 범위 타입은 존재하지 않는다.
Range
의 생성은 ...
이나 ..<
를 쓴다.3 전자의 경우에는 upperBound
값이 영역에 포함되며 이 경우에는 ClosedRange
로 따로 분류한다.
let underFive = 0.0..<5.0
print(underFive.contains(3.2)) // true
print(underFive.contains(5.0)) // false
print(underFive.contains(7.1)) // false
Range
는 contains(_:)
메소드를 갖고 있지만 Sequence
는 아니다. 그저 특정값이 해당 범위에 들어가는지만 검사한다.
메소드
contains(Bound)
: 특정 값이 범위내에 있는 지 검사clamped(to:Range<Bound>)
: 다른Range
와 겹치는 부분을 구한다.4overlaps(_:)
다른Range
,ClosedRange
와 비교하여 겹치는 부분이 있는지를 검사한다.~=
연산자를 지원한다.Range ~= Bound
의 형식으로 써서 우변의 값이 좌변의 영역 내에 있는지를 검사한다.
if case Range = Bound
는 if Range ~= Bound
와 같다.
주목할 부분은 Bound
는 대소비교가 가능하기만 하면 될 수 있는데, String
도 같은 타입 끼리는 (대소라기보다는 전후 관계이지만) 비교 가능하다. 따라서 다음과 같은 계산도 가능하다. 5
let lowerCased = "a"..."z"
print(lowerCased.contains("h")) // true
print(lowerCased.contains("2")) // false
CountableRange
Range
타입은 단순한 범위임에 반해 CountableRange
는 계단처럼 연속되는 값들의 범위이다. 예를 들어 Double
의 경우 1.0..<3.0
에는 무수히 많은 개수의 Double
타입 값이 들어갈 수 있다. 하지만 1..<3
의 경우에는 1, 2
단 두 개의 값만 있는 셈이다. 즉 Range
가 경사로라면 CountableRange
는 계단이라 할 수 있다. 이렇게 하나씩 세어 올라갈 수 있는 성질을 Stridable
이라 하는데 Swift에서는 Int
패밀리와 포인터타입이 이를 갖추고 있다.
따라서 일반적으로 정수를 이용해서 Range
를 만들면 Range<Int>
가 아닌 CountableRange<Int>
가 생성된다.
let oneToNine = 1..<9
debugPrint(Mirror(reflecting:oneToNine)) // Mirror for CountableRange<Int>
CountableRange
는 정해진 개수만큼 연속된 값을 가지며, 인덱스에 의해 특정 값을 지목할 수도 있다. 따라서 Collection
의 필요조건을 만족하며, 시퀀스가 제공하는 여러 메소드들을 사용할 수 있다.
정수 인덱스 사용시 주의점
CountableRange<Int>
를 생성했을 때의 유의점은 범위의 첫 인덱스가 항상 0이 아니라는 점이다.
print((-99..<100)[0]) // ERROR: ambiguous use of 'subscript'
CountableRange
의 각 원소는 인덱스와 동일한 값이다. 따라서 -99..<100
의 [0]
번 인덱스의 값은 0이다. 하지만 보통 이 경우는 -99
를 얻게 될 거라고 예측하게되는데 이 혼란을 피하기 위해서 CountableRange<Int>
에 대해 직접 서브스크립션을 하면 그냥 에러가 나게끔 구현되어 있다.
func bracket<T>(_ x: ClosedRange<T>, _ i: T) -> T {
return x[i]
}
이런식으로 x
같이 타입이 고정된 함수에 포함시켜서 서브스크립션을 하면 된다. 물론 이 경우에도
bracket(30..<50, 12) // Error!!!
이렇게 되는 부분은 알아두도록 하자.
목차
- Array – Array 타입, 생성과 조작
- Array – Sequence 프로토콜
- Array – Collection 프로토콜
- Array – ArraySlice
- Array – NSArray
NSRange
와는 다르다.NSRange
는시작위치; 길이
의 정보를 담고서 특정한 구역 내부의 커버리지를 표현하는데 반해,Range
는 그야말로 특정한 두 값 사이의 범위를 나타낸다. 따라서NSRange
의location
,length
는 모두 정수이지만,Range
의 양쪽 끝 값은 실수를 허용한다.- 당연한 이야기지만
Range
의 각 끝단을 가리키는 값은Comparable
을 만족해야 한다. - 각각
ClosedRange<T>
,Range<T>
를 생성하는 연산자이다. clamped(to:)
는Range
는Range
하고만 연산가능하며,ClosedRange
는ClosedRange
하고만 연산가능하다. 반면overlaps(_:)
는Range
,ClosedRange
상호간에 검사가능하다.- Swift 문자열은
<
,>
연산자로 비교할 때, 대소문자를 구분하지 않는다. 이것이 Linux용 Swift에서만 발생하는 부분인지는 확인이 필요하다.