- 근본으로돌아가자(7)-String,Array으로 시작해서 Sequence까지2025년 04월 04일
- 2료일
- 작성자
- 2025.04.04.오후03:49
자 우리가 일반적으로 사용하는 String은 Character로 이루어져있고 Character는 가변적인 하나이상의 유니코드스칼라로 이루어져 배열처럼 정수 인덱스로 접근할수 없다. 그래서 String.index를 사용하고 startIndex, endIndex, index(after:)같은 메서드로 안전하게 탐색한다. 특히 이모지의 경우 여러 유니코드 스칼라로 구성된다.
퀴즈: 👍? => U+1F44D인 단일유니코드스칼라.
let emoji = "👩🚀" for scalar in emoji.unicodeScalars { print(scalar) } // 출력: // U+1F469(👩) // U+200D // U+1F680(🚀)
여기까지가 면접의 단골질문이자 당연히 알아야하는 것입니다. 물론 아닐수도..? 모르면 지금 배우면 됏죠!!ㅎㅎ
자 오늘은 더 딥하게 기본적인 String을 다뤄볼 계획입니다.
String기본개념
Swift에서 결국 String은 유니코드 문자의 시퀀스를 표현하는 타입이다.
Sequence?
이미지를 보면 Sequence를 루트로 밑에 많은 콜렉션들이 sequence프로토콜을 기반으로 퍼져나가고 있다.
일련의 요소를 순회할 수 있는 타입을 정의합니다.그러나 이 프로토콜은 list의 value들에 대해 다시 접근을 할 수 있게 해준다는 보장을 해주지는 않는다고합니다. <- 이게 몬소린가 했더니 Array가 아닌 데이터 스트림이나 난수 시퀀스 일수도 있기에!
protocol Sequence { associatedtype Element associatedtype Iterator: IteratorProtocol where Iterator.Element == Element func makeIterator() -> Iterator }
Element: Sequence가 제공하는 요소의 타입
Iterator: IteratorProtocol을 준수하는 반복자 타입으로. 요소를 하나씩 꺼내는 역할을 한다.
makeIterator(): Sequence요소를 순회할 수 있는 반복자를 반환한다.
결국 Sequence프로토콜은 iterator를 만드는 기능을 하고 있고 순회(iterator)할수 있는 이유도 이 때문이다.
IteratorProtocol
protocol IteratorProtocol { associatedtype Element mutating func next() -> Element? }
next Element를 가지고 오는 방법을 알고 있는 타입을 말합니다. 더이상 요소가 없을때 nil을 리턴해서 끝을 알 수 있습니다.
iterator타입을 바로 사용할수도있지만 일반적으로 컴파일러는 for문을 사용할때 iterator를 만든다고 합니다.
struct Countdown: Sequence { let start: Int func makeIterator() -> CountdownIterator { return CountdownIterator(start: start) } } struct CountdownIterator: IteratorProtocol { var current: Int init(start: Int) { self.current = start } mutating func next() -> Int? { guard current > 0 else { return nil } defer { current -= 1 } return current } } // 사용 예제 let countdown = Countdown(start: 3) for num in countdown { print(num) // 3, 2, 1 }
커스텀 Sequnece를 만들어 보았다. iterator를 만들고 element 의 타입은 Int로 하여 카운트다운의 예제이다.
이왕 하는김에 이미지에 있는 다른 타입들도 공부해보자;
Collection
이미지와 같이 Sequence 프로토콜을 채택하고 있고 item에 접근할때 Index라는 것을 사용하기에 sequnce의 확장버전이라고 생각하면 될듯합니다. index를 통해 O(1)의 시간으로 접근을 합니다. index가 보통 int지만 Opaque Value(dictionary, string)도 가능하다.Comparable 만족하면 된다.
Sequence와 차이는 항상 start와 end가 존재한다. 그리고 collection의 부분을 나타내는 subsequence도 존재한다.
MutableCollection
index를 통해 element에 접근하여 값을 변경할수 있는 collection타입을 의미한다.
subscript(position: Self.Index) -> Self.Element { get set }
extension MutableCollection { mutating func swapAt(_ i: Self.Index, _ j: Self.Index) mutating func partition(by: (Self.Element) throws -> Bool) rethrows -> Self.Index // ... }
또한 알고리즘을 풀면서 힙정렬때 swapAt을 사용햇는데 가능했던 이유가 mutableColection내에 이미 메서드가 정의된 덕!!
extension MutableCollection where Self : BidirectionalCollection { @inlinable public mutating func partition(by: ...) rethrows -> Self.Index @inlinable public mutating func reverse() }
보다 보면 BidirectionalCollection을 준수하는 MutableCollection에 대해서는 두가지 메서드를 더 효율적으로 제공한다.
- partition: 컬렉션의 요소들을 특정 조건에 따라 두부분으로 재배열한다. 조건을 충족하는 요소들은 모두 조건을 충족하지 않는 요소들 뒤에 위치하게 된다. 메서드는 그리고 피벗 인덱스르 ㄹ반환한다. 이 인덱스는 두 파티션의 경계를 나타낸다.
즉, 이 인덱스 이전은 조건을 충족하지 않는 요소들이고 이후의 모든 요소는 조건을 충족한다.
파티션 작업은 안정적이지 않다. 같은 요소의 상대적 순서가 바뀔수있다.
기존의 MutableCollection의 경우 단방향 순회만 가능했다. 하지만 BidrectionalCollection의 경우 양방향 순회가 가능하므로 더 효율적으로 교환횟수를 줄이는 방법을 사용한다.
var numbers = [30, 40, 20, 30, 30, 60, 10] let p = numbers.partition(by: { $0 > 30 }) // p == 5 // numbers == [30, 10, 20, 30, 30, 60, 40]
numbers는 그대로 개수는 같지만 순서가 바뀌었다. 30을 초과하지않는것들이 앞에있고 p는 5를 가리킨다. 즉 6부터가 만족한다는뜻
양방향인 경우 앞에서는 predicate가 true인 요소를 찾고 뒤에서는 predicate가 false인 요소를 찾아 교환한다고 한다. 교차할때까지/
언제 사용할까? 완전한 정렬없이 특정조건에따라 요소를 그룹화할때 -> 시간복잡도는 O(N)
RangeReplaceableCollection
컬렉션의 요소를 특정 범위에서 교체하거나 삽입, 삭제할 수 있는 기능을 제공하는 프로토콜!
위의 MutableCollection과 같이 Collection프로토콜을 채택하지만 완전 다른 목적으로 설계되었다.
MutableCollection: 기존 요소의 값을 변경할 수 있지만 컬렉션의 크기는 변경 불가!
RangeReplaceableCollection: 요소 추가/제거/교체를 통해 컬렉션의 크기를 변경 ㄱㄴ
protocol RangeReplaceableCollection: Collection { associatedtype SubSequence init() mutating func replaceSubrange<C>(_ subrange: Range<Self.Index>, with newElements: C) where C: Collection, Self.Element == C.Element }
1. 빈 컬렉션을 생성하는 이니셜라이저
2. 범위를 대체하는 메서드(특정 범위의 요소를 제거하고 새로운 요소로 교체/ 여기서 개수 달라도 상관없다.)
var nums = [10, 20, 30, 40, 50] nums.replaceSubrange(1...3, with: repeatElement(1, count: 5)) print(nums) // [10, 1, 1, 1, 1, 1, 50]
이걸 보니 떠오르는게 append, insert, remove같은 메서드들이 이래서 가능하겟구나.
StringProtocol
protocol StringProtocol: BidirectionalCollection, RangeReplaceableCollection, Comparable, Hashable, ExpressibleByStringLiteral where Element == Character { // 문자열 관련 메서드와 속성 }
이전에 공부했던 프로토콜을 채택한것을 볼 수 있다. 이를 통해 비교, 해싱 등 다양한 기능을 지원한다.
Element가 Character로 고정되어 있어 이 프로토콜 준수하는 타입은 Character 시퀀스로 동작한다. 이 제약 덕분에 항상 우리는 여러 유니코드로 구성되어있는 Character를 시퀀스 할 수 잇는것!!
문자열 추상화: Swift에는 String과 substring이라는 두 가지 주요 문자열 타입이 있다. 이 두타입은 구현방식과 메모리 관리 방식에서 차이가 있지만 크게 보면 유사하다. StringProtocol은 이 두 타입 간의 공통 인터페이스를 정의함으로써 코드 중복을 방지한다.
protocol StringProtocol { var utf8: UTF8View { get } var utf16: UTF16View { get } var unicodeScalars: UnicodeScalarView { get } }
여러 수준의 문자열 표현에 접근할수있는 뷰를 제공한다. 이를 통해
let text = "Café 🇰🇷" // 다양한 표현에 접근 text.count // 7 (문자 수) text.unicodeScalars.count // 8 (유니코드 스칼라 수) text.utf16.count // 10 (UTF-16 코드 유닛 수) text.utf8.count // 13 (UTF-8 바이트 수)
유니코드스칼라, UTF-8, UTF-16에 모두 조작가능하다
메모리 효율성: StringProtocol의 중요한 기여 중 하나가 SubString은 원본 String의 저장소를 공유하므로 대용량 문자열의 일부를 처리할때 복사비용이 발생하지 않는다.
결국 우리는 stringProtocol을 통해 string, substring 두곳에서 동시에 사용가능한 메서드를 정의할수있다.
ex) hasPrefix
String 성능
+ 연산
public static func + (lhs: String, rhs: String) -> String { var result = lhs result.append(rhs) return result }
String은 COW로 수정할때 복사하기에 append할때 result에 새 객체를 만들고 메모리에 할당한다.
=> + 많이 하면 메모리 할당 및 해제에 대한 비용이 들겟지?
+= 연산
public static func += (lhs: inout String, rhs: String) { lhs.append(rhs) }
새 인스턴스가 아닌 참조로 문자열을 추가하기에 메모리를 할당해주지 않아 더 효율적이다.
문자열 버퍼
내부적으로 동적 버퍼를 사용하며, 버퍼가 가득차면 더 큰 버퍼를 할당한다.
1. 일반적으로 버퍼 크기는 약 2배씩 증가.
2. 새 버퍼 할당 시 기존 내용이 새 위치로 복사
3. 기존 버퍼는 해제.
var efficientResult = "" efficientResult.reserveCapacity(5000) // 충분한 용량 미리 확보 for i in 1...1000 { efficientResult += String(i) }
그러기에 미리 충분한 용량을 만들고 거기서 문자열을 추가해주는 것 이 훨씬 효율적이다.
이상적인 경우 재할당 횟수가 1회고 복사 작업 감소도 되고 메모리 단편화 방지까지 가능하다.
SSO
Swift는 작은 문자열(15바이트 이하)에 대해서는 힙 할당 없이 문자열 객체 자체에 직접 저장한다.
이렇게 Swift 문자열 시스템의 구성과 해당 안에 있는 프로토콜들을 살펴보았다. 이를 이해하면추후에 개발할때 더 메모리 효율적이고 성능 뛰어난 앱을 개발할 수 있을거같다.!! 역시 근본 시리즈가 알면서도 새롭고 재밌다. 다음주제는..?
'면접준비' 카테고리의 다른 글
Metal(2)-셰이더 코드 작성까지 (0) 2025.04.02 Metal(1)- 메탈을 알기전에 필요한 것들 (1) 2025.03.29 Autolayout 모든 것: 사이클부터 제약조건까지 (0) 2025.03.26 Apple의 보안 (0) 2025.03.15 근본으로 돌아가자(6) Image (2) 2025.03.05 다음글이전글이전 글이 없습니다.댓글