Swift/개념 & 응용

[Swift] String.Index 활용법

유정주 2022. 10. 18. 16:17
반응형

서론

저는 String을 다룰 때 편의를 위해 [Character] 타입으로 변환하면서 사용해 왔습니다.

이러면 subscript를 이용해 원소에 접근할 수 있지만 배열로 변환하는 시간이 추가로 필요합니다.

그래서 String.Index를 잘 다뤄보고자 포스팅을 작성하기로 했습니다.

 

이번 포스팅은 String.Index가 무엇인지보다 여러 상황에서 어떻게 사용하는지를 다룹니다.

아래에서 꾸준히 나오는 string 변수는 "abcdefg" 입니다.

 

String.Index

이 주제는 이미 지난 포스팅에서 다룬 적이 있습니다.

 

[Swift] Unicode Scalar와 String의 Random Access

서론 오늘 iOS Developers KR 카톡방에서 재밌는 사실을 알았습니다. (사실 공식 문서에 나와 있는 내용이라 이전에도 알고 있긴 했음 ㅎ;;;) 👨‍👩‍👧‍👧는 여러 개의 유니코드가 합체하여 이

jeong9216.tistory.com

String은 Character의 집합입니다.

Character는 유니코드 문자로 이루어지는데 유니코드의 크기가 다양하기 때문에 정수로 index를 구분하기 애매합니다.

따라서 String은 Random Access를 채택하는 대신 BidirectionalCollection 프로토콜을 채택하여 순차 탐색하도록 구현되었습니다.

이런 이유로 String은 Subscript 대신 String.Index라는 구조체로 String의 인덱스를 관리하게 되었죠.

 

더 자세한 내용은 위 포스팅을 참고해 주세요.

 

String.Index -> Int

여러 String.Index 관련 메서드를 살펴보기 전에 String.Index를 Int로 변환하는 방법을 알아보겠습니다.

String.Index를 출력해보면 

print(string.startIndex) //Index(_rawBits: 15)

String.Index의 구조체 내용이 나오기 때문입니다 ;;

 

String의 distance(from:to:)를 이용하면 Int 인덱스를 구할 수 있습니다.

distance(from:to:)는 from과 to의 거리를 리턴하는 메서드입니다.

let distance = string.distance(from: string.startIndex, to: 변환할 String.Index)

그래서 이렇게 startIndex와 원하는 String.Index를 비교하면 Int 값을 얻을 수 있습니다.

 

문자열의 첫 글자 구하기

startIndex

Swift는 String.Index 타입의 startIndex라는 구조체를 지원합니다.

startIndex를 이용하면 문자열의 시작 인덱스를 알 수 있습니다.

startIndex를 String의 Subscript로 전달하면 해당 인덱스의 문자를 구할 수 있습니다.

let string: String = "abcdefg"

let first = string.startIndex
print(string[first]) //a

 

prefix(_:)

prefix 메서드를 이용하면 0~n번째까지의 Substring을 구할 수 있습니다.

let first = string.prefix(2)
print(first) //ab

String에서 prefix 메서드는 O(n)의 시간 복잡도를 가집니다.

이것도 BidirectionalCollection을 채택하고 있기 때문입니다.

 

문자열의 n 번째 글자 구하기

index(after:)

문자열의 n 번째 글자는 index(after:)을 이용해 쉽게 구할 수 있습니다.

index(after:)은 매개변수로 String.Index를 받고 전달받은 String.Index의 다음 String.Index를 구할 수 있습니다.

예를 들어, startIndex의 다음 인덱스는 두 번째 인덱스입니다.

let first = string.startIndex
let second = string.index(after: first)
print(string[second]) //b

string에 second를 전달하여 출력하면 두 번째 인덱스 문자인 b가 출력하는 것을 볼 수 있습니다.

 

String.Index(encodedOffset:)

String.Index를 생성할 때 encodedOffset 프로퍼티를 설정하면 n 번째 String.Index를 생성할 수 있습니다.

print(string[String.Index(encodedOffset: 3)]) //d

3을 encodedOffset로 설정해서 생성하면 네 번째 문자인 d가 출력됩니다.

인덱스는 0부터 시작이니 3은 네 번째 문자가 됩니다.

 

index(_:offsetBy:)

offsetBy에 정수 n을 입력하면 Index에서 n만큼 이동한 String.Index를 구할 수 있습니다.

let start = string.startIndex
print(string[string.index(start, offsetBy: 0)]) //a
print(string[string.index(start, offsetBy: 2)]) //c

첫 번째 코드는 start에서 0만큼 움직였으니 start 그대로이기 때문에 a가 출력됩니다.

두 번째 코드는 start에서 2만큼 증가한 String.Index이기 때문에 c가 출력됩니다.

 

offsetBy에는 음수도 전달할 수 있습니다.

let index = String.Index(encodedOffset: 3)
print(string[string.index(index, offsetBy: -2)]) //b

3에서 -2를 한 1 번째 글자인 b가 출력됩니다.

 

주의할 점은 string 범위를 벗어날 경우 런타임 에러가 발생합니다.

startIndex에서 -1을 하면 string의 범위를 벗어납니다.

실행해보면 런타임 에러가 나는 것을 볼 수 있습니다.

 

문자열의 마지막 문자 구하기

endIndex

String.Index 타입인 endIndex는 String의 마지막 인덱스를 가리킵니다.

이때 주의할 점은 마지막 빈 공간(C언어라고 치면 \0)을 가리킨다는 것입니다.

그래서 endIndex에 접근하면 런타임 에러가 발생하게 됩니다.

이럴 땐 index(before:)을 사용하면 됩니다.

print(string[string.index(before: string.endIndex)]) //g

endIndex의 바로 이전 인덱스를 출력하니 마지막 글자인 g가 출력되었습니다.

 

suffix(_:)

suffix(_:)를 이용하면 뒤에서부터 n개의 Substring을 구할 수 있습니다.

print(string.suffix(3)) //efg

뒤에서부터 3개의 글자를 Substring으로 반환해서 efg가 출력됩니다.

 

특정 문자의 인덱스 구하기

특정 문자가 몇 번째 인덱스에 있는지 궁금할 때가 있습니다.

그럴 땐 firstIndex(of:), lastIndex(of:), index(of:)를 이용하면 됩니다.

 

firstIndex(of:)

firstIndex(of:)는 매개변수로 전달한 문자가 처음 나오는 인덱스를 리턴합니다.

만약 입력한 문자가 없다면 nil이 출력됩니다.

값을 비교할 수 있어야 하기 때문에 element는 Equatable 해야 합니다.

(Equatable : https://jeong9216.tistory.com/466)

 

print(string.firstIndex(of: "d")) //d의 String.Index
print(string.firstIndex(of: "h")) //nil

 

여기서 초반에 알아본 String.Index -> Int 방법으로 d의 String.Index를 변환해보면

let distance = string.distance(from: string.startIndex, to: string.firstIndex(of: "d")!)
print(distance) //3

정상적으로 3이 출력됩니다.

 

firstIndex는 문자가 가장 먼저 나오는 String.Index를 반환하기 때문에 일치하는 문자가 여러 개면 가장 먼저 나오는 글자의 인덱스를 리턴합니다.

let string = "aaaaa"
let distance = string.distance(from: string.startIndex, to: string.firstIndex(of: "a")!)
print(distance) //0

a가 가장 먼저 나오는 위치는 0이기 때문에 0이 출력됩니다.

 

lastIndex(of:)

lastIndex는 마지막으로 나오는 String.Index를 반환합니다.

일치하는 문자가 없다면 nil을 반환합니다.

lastIndex 메서드는 firstIndex 메서드와 비교했을 때 마지막에 오는 String.Index라는 점만 다르기 때문에 짧게만 보고 넘어가겠습니다.

let string = "aaaaa"
let distance = string.distance(from: string.startIndex, to: string.lastIndex(of: "a")!)
print(distance) //4

a가 마지막으로 오는 위치는 4 이기 때문에 4가 출력됩니다.

 

index(of:)

first, last 같은 수식어가 붙지 않은 index(of:)는 어떤 메서드일까요?

index(of:)는 firstIndex(of:)와 동일한 기능을 수행합니다.

공식문서 설명도 같고... 완전히 동일한 함수 같아요..? (왜 나눈 거지 ㅎ;;)

 

Extension 해서 배열처럼 사용하기

String.Index에 대해 열심히 알아봤지만, 그럼에도 배열의 Subscript가 간편한 건 사실입니다.

String을 Extension해서 Subscript를 추가해 봅시다.

 

extension String {
    subscript(index: Int) -> Character {
        return self[String.Index(encodedOffset: index)]
    }
}

print(string[0]) //a
print(string[2]) //c
print(string[string.count-1]) //g

Int형 index를 Subscript로 전달하면 String.Index를 생성해서 해당 Index의 Character를 반환하도록 했습니다.

이제 배열로 변환하지 않아도 간편하게 Subscript를 이용할 수 있어요.

 

마무리

오늘은 열심히 String.Index의 활용 방법에 대해 알아보았습니다.

String의 순회 속도는 배열의 순회 속도보다 느립니다.

따라서 String.Index는 순회 용도가 아니라 원소를 구할 때 사용하는 것이 적절합니다.

상황에 따라 String.Index와 배열 변환을 선택해서 구현하시면 좋겠죠??

 

감사합니다!


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형