🔗 Array Swift 공식문서

Array

순서가 있고, 임의 접근이 가능한 컬렉션

Overview

배열은 앱에서 가장 흔히 사용되는 데이터 타입 중 하나이다. Array 타입은 단일 타입의 요소들을 담기 위해 사용되며, 배열의 Element 타입이라고 한다. 배열은 정수, 문자열, 클래스 등 어떤 종류의 요소든 저장할 수 있다.

Swift에서 배열은 아주 간단하게 만들어 사용할 수 있다. 대괄호([]) 안에 값을 넣고, 컴마(,)로 그 값들을 나열한다. Swift에서는 추가적인 정보 없이 특정한 타입의 값들을 배열에 넣으면 자동으로 그 배열 요소의 타입을 추론한다. 타입 추론(inferring the array’s Element type)

// 정수(Int) 배열
let oddNumbers = [1, 3, 5, 7, 9, 11, 13, 15]
 
// 문자열(String) 배열
let streets = ["Albemarle", "Brandywine", "Chesapeake"]

또한, 배열의 요소 타입을 명시하여 빈 배열을선언할 수 있다.

var emptyDoubles: [Double] = []
 
var emptyFloats: Array<Float> = Array()

만약, 고정된 값을 기본 값으로 미리 초기화 해놓고 싶으면 Array(repeating: count: ) 으로 초기화 할 수 있다.

var digitCounts = Array(repeating: 0, count: 10)
print(digitCounts)
// "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]"

배열 요소에 접근하기

배열의 모든 요소에 접근하고자 한다면 for-in 문을 사용해 배열을 순회할 수 있다.

for street in streets {
	print("I don't live on \(street)")
}
// Prints "I don't live on Albemarle." 
// Prints "I don't live on Brandywine." 
// Prints "I don't live on Chesapeake."

isEmpty 프로퍼티를 사용하여 배열에 요소가 있는지 여부를 빠르게 확인할 수 있다.
또, count 프로퍼티를 통해 배열에 요소가 몇 개 있는지 확인할 수 있다.

if oddNumbers.isEmtpy {
	print("I don't know any odd numbers.")
} else {
	print("I know \(oddNumbers.count) odd numbers.")
}
// Prints "I know 8 odd numbers."

first , last 프로퍼티를 사용해 배열의 가장 첫번째, 마지막 요소에 안전하게 접근할 수 있다. 만약 배열이 비어있다면 두 프로퍼티는 nil을 반환한다.

if let firstElement = oddNumbers.first, let lastElement = oddNumbers.last {
	print(firstElement, lastElement, separator: ", ")
}
// Prints "1, 15" 
 
print(emptyDoubles.first, emptyDoubles.last, separator: ", ") 
// Prints "nil, nil"

배열 인덱스를 통해 배열의 개별 값에 접근할 수 있다.
비어있지 않은 배열 첫 요소 인덱스는 항상 0이다. 0 이상의 정수를 인덱스로 사용하여 접근할 수 있다. 하지만 음수나 배열의 길이와 동일하거나 그 이상의 정수로는 접근할 수 없다.
잘못된 인덱스 접근 시 런타임 에러(Index out of range)가 발생한다.

print(oddNumbers[0], oddNumbers[3], separator: ", ") 
// Prints "1, 7" 
 
print(emptyDoubles[0]) 
// Triggers runtime error: Index out of range

배열에 요소를 추가/삭제하기

예를 들어, 내가 수업하는 클래스에 등록한 학생들의 이름을 리스트에 저장해야 한다고 해보자.
등록 기간동안 학생들의 이름을 추가하거나 삭제해야한다.

var students = ["Ben", "Ivy", "Jordell"]

단일 요소의 값을 배열의 끝에 추가하려면 append(_: ) 메서드를 사용한다.
여러개의 요소를 한 번에 추가하려면 append(contentsOf: ) 메서드를 통해 매개변수로 배열이나 어떠한 종류의 시퀀스를 전달한다.

students.append("Maxime")
students.append(contentsOf: ["Shakia", "William"])
// ["Ben", "Ivy", "Jordell", "Maxime", "Shakia", "William"]

insert(_: at: ) 메서드로 단일 요소, insert(contentsOf: at:) 메서드로 여러 요소를 배열의 중간에 삽입할 수 있다.
삽입하고자 하는 인덱스의 다음 인덱스들은 공간을 만들기 위해 인덱스를 이동하게 된다.

students.insert("Liam", at: 3) 
// ["Ben", "Ivy", "Jordell", "Liam", "Maxime", "Shakia", "William"]

배열에서 요소를 제거하기 위해 remove(at:), removeSubrange(_:) , removeLast() 메서드를 사용할 수 있다.

// Ben's family is moving to another state 
students.remove(at: 0) 
// ["Ivy", "Jordell", "Liam", "Maxime", "Shakia", "William"] 
 
// William is signing up for a different class 
students.removeLast() 
// ["Ivy", "Jordell", "Liam", "Maxime", "Shakia"]
arr.removeSubrange(..<2) // 처음부터 1까지

이미 배열에 저장된 값을 변경하려면 인덱스를 통해 접근하여 변경할 수 있다.

if let i = students.firstIndex(of: "Maxime") {
	students[i] = "Max"
}
// ["Ivy", "Jordell", "Liam", "Max", "Shakia"]

배열 크기 확장하기

모든 배열은 값들을 저장하기 위해 일정 양의 메모리 공간을 미리 확보한다.

만약 배열에 요소를 추가했을 때 확보해 둔 용량을 초과하게 되면, 배열은 더 큰 메모리 영역을 새로 할당하고 기존 요소들을 그 공간으로 복사한다.
새로운 저장 공간은 이전 공간의 크기의 몇 배로 확장된다.
이러한 지수적 증가 전략여러 번의 append 연산에 대한 성능을 평균적으로 O(1)로 만들어준다.
재할당을 발생시키는 append 연산은 비용이 크지만, 배열이 커질수록 재할당 빈도수가 줄어들게 된다.

대략적으로 얼마만큼의 요소가 저장될 것인지에 대해 알고있다면, reserveCapacity(_:) 메서드를 사용하여 append시 재할당 되는 과정을 피할 수 있다.
그러면 capacity를 통해 할당된 용량을 확인할 수 있고, count 프로퍼티를 활용해 얼마나 더 저장할 수 있는지 확인하여 재할당을 하지 않도록 구현할 수 있다.

대부분의 배열은 연속적인 메모리 공간에 요소들을 저장한다.
배열의 요소 타입이 class거나 @objc 프로토콜 타입이라면 내부 저장 방식으로 연속 메모리 공간을 사용하거나 NSArray 인스턴스를 사용할 수도 있다. NSArray의 서브 클래스는 Swift의 Array타입으로 사용될 수 있기 때문에 내부 저장 구조가 어떻게 되어있는지, 성능이 효율적인지에 대해 보장하지 못한다.
클래스나 @objc 타입을 담은 swift 배열은 내부 구조와 성능을 항상 보장할 수 없다.

복사한 배열 수정하기

배열 내에 있는 각각의 값들은 모두 독립적인 값을 가진다.
정수나 구조체와 같은 단순한 타입에서 하나의 배열에서 값을 변경하더라도 복사본에는 영향이 미치지 않는다는 의미이다.

예를들어:

var numbers = [1, 2, 3, 4, 5]
var numbersCopy = numbers
numbers[0] = 100
print(numbers)
// "[100, 2, 3, 4, 5]'
print(numbersCopy)
// "[1, 2, 3, 4, 5]"
배열의 요소가 클래스 인스턴스인 경우

클래스 인스턴스일 경우에도 동작 원리는 동일하지만, 다른 점이 있다.
배열에 클래스 인스턴스가 저장된 경우에는 배열 외부에 존재하는 객체에 대한 참조 값이 배열에 저장되어 있는 것이다.
만약 한 배열에서 참조 값 자체를 변경하게 되면 변경한 배열에서만 변경사항이 반영된다.

즉, 배열에 저장되는 것은 값타입 객체에 대한 참조 값이 저장되있어서 값 타입인 참조 값 자체를 변경하면 기존과 동일한 원리로 해당 배열에서만 변경된다는 뜻

만약 두 배열에서 동일한 객체를 참조하고 있다면, 한 배열에서의 객체의 프로퍼티를 변경했을 때 그 변경사항은 두 배열 모두에서 관찰된다.

예를들어:

class IntergerReference {
	var value = 10
}
var firstIntegers = [IntegerReference(), IntegerReference()]
var secondIntegers = firstIntegers
 
// 참조 대상을 변경
firstIntegers[0].value = 100
print(secondIntegers[0].value) // "100"
 
// 참조를 변경
firstIntegers[0] = IntegerReference()
print(firstIntegers[0].value) // "10"
print(secondIntegers[0].value) // "100"
copy-on-write 최적화

배열은 표준 라이브러리에 있는 가변 크기 컬렉션들과 마찬가지로 copy-on-write 최적화를 사 용한다.
하나의 배열에 대한 여러 복사본은 그 중 하나를 수정하기 전까지 동일한 저장 공간을 공유한다.
수정 작업이 발생하면, 수정된 배열은 그제서야 별도의 독립된 저장 공간을 만들어 값을 실제로 복사하고, 그 후 수정을 하게된다.
또, 경우에 따라서 복사 횟수를 줄이기 위한 추가적인 최적화가 적용되기도 한다.

배열이 다른 복사본들과 공유되고 있는 상태라면, 첫 번째 수정 작업이 발생하는 배열에서는 별도의 저장 공간을 만들기 위한 실제 복사 비용이 발생하게 된다.
반대로 저장 공간을 공유하고 있지 않은 배열은 추가적인 복사 없이 바로 그 저장 공간에서 수정할 수 있다.

아래의 예시에서 numbers 배열은 같은 저장 공간을 공유하는 두개의 복사본과 함께 만들어진다.
원본 numbers 배열이 수정될 때, 원본 배열은 수정 하기 전에 자신만의 저장 공간을 만들어 복사본을 만들어 둔다. numbers에서의 추가적인 수정 작업들은 옮겨진 저장 공간에서 이루어지고, 두 복사본은 계속해서 기존 저장 공간을 공유하게 된다.

var numbers = [1, 2, 3, 4, 5]
var firstCopy = numbers
var secondCopy = numbers
 
numbers[0] = 100
numbers[1] = 200
numbers[2] = 300
// numbers -> [100, 200, 300, 4, 5]
// firstCopy, secondCopy -> [1, 2, 3, 4, 5]

Array와 NSArray간의 브리징

Array대신 NSArray 인스턴스가 요구되는 API들을 사용하게 될 때, type-case 연산자인 as 를 사용하여 인스턴스를 연결한다.
두 타입 간의 연결을 가능하게 하기 위해서는 배열의 타입은 class@objc 프로토콜 (Objective-C에서 가져온 프로토콜이거나 @objc 속성이 지정된 프로토콜)이어야 하거나 Foundation 타입으로 호환되는 타입이어야 한다.

Array NSArray

아래 예시는 Objective-C 메서드인 write(to: atomically:) 를 사용하기 위해 Array 인스턴스를 NSArray 타입으로 변환하는 방법을 보여준다.
이 예시에서 colors 배열은 배열의 요소 타입인 String이 NSString으로 브리징이 가능하기 때문에, 이 배열 또한 NSArray로 브리징 될 수 있다.
반면에, moreColors 배열의 요소는 Foundation 타입으로 브리징되지 않는 Optional<String> 타입이라서 as NSArray 수행 시 컴파일 에러가 발생한다.

let colors = ["periwinkle", "rose", "moss"]
let moreColors: [String?] = ["ochre", "pine"]
 
let url = URL(fileURLWithPath: "names.plist")
(colors as NSArray).write(to: url, automically: true)
// true
 
(moreColors as NSArray).write(to: url, atomically: true)
// error: cannot convert value of type '[String?]' to type 'NSArray'

따라서 배열 요소가 이미 class 또는 @objc 프로토콜 타입이라면 Array에서 NSArray로 브리징하는 것은 O(1)시간과 공간이 든다. 그렇지 않은 경우에는 변환해야하기 때문에 O(n)의 시간과 공간이 소요된다.

NSArray Array

배열의 Element가 class 혹은 @objc 프로토콜
NSArrayArray로 브리징 시도할 때 우선 내부적으로 copy(with:) 메서드를 호출해 배열을 불변 복사본을 만든다. 그 후 O(1)시간이 소요되는 추가적인 Swift 내부 작업을 수행하게 된다.

불변/가변 상태의 NSArray
이미 불변 상태인 NSArray 인스턴스의 경우 copy(with:) 호출은 보통 O(1)시간 안에 동일한 배열을 그대로 반환한다. 반면에 가변 인스턴스의 경우에는 복사 비용이 어떤지 명시되어있지 않다.
만약 copy(with:) 호출 결과로 동일한 배열 인스턴스가 반환된다면, NSArray와 Array는 동일한 저장 공간을 공유하게 되고, Array 간에 적용되는 것과 동일한 copy-on-write 최적화가 적용된다.

배열 요소의 타입이 String이나 Int와 같이 클래스가 아니면서 Foundation 타입으로 브리지될 수 있는 경우, NSArray에서 Array로의 브리징은 O(n)시간 동안 연속된 저장 공간으로 브리징을 복사하게 된다.
한 번 Array 인스턴스로 변환되고 나면, 그 요소들에 접근할 때 추가적인 브리징은 필요하지 않다.