3. Variables and Simple Type (5) - Built-In Simple Types (String)

Seoyoung Lee·2023년 1월 5일
0
post-thumbnail
post-custom-banner

String

String 오브젝트 타입은 텍스트를 나타낸다. String을 표현하는 가장 간단한 방법은 큰 따옴표로 리터럴을 감싸는 것이다.

let greeting = "Hello"

스위프트의 String은 매우 현대적이다. 이모지 같은 유니코드 문자를 문자열 리터럴에 바로 사용할 수 있다. 유니코드 스칼라 값을 알고 있다면 \u{n} 을 사용해도 된다. 중괄호 안의 n은 1~8자리의 16진수이다.

스위프트에서 \ 는 이스케이프 문자를 의미한다. 가장 대표적인 이스케이프 문자는 \n , \t , \" , \\ 이다.

이스케이프 문자를 여러 번 사용하면 문자열 리터럴의 가독성이 낮아진다. 특히 정규 표현식 패턴을 사용할 때 큰 문제가 된다.

예를 들어, \b\d\d\b 패턴은 "\\b\\d\\d\\b" 로 써야 한다. 그러나 문자열을 하나 이상의 # 으로 감싸면 이스케이프 문자와 \ 를 생략할 수 있다.

let pattold = "\\b\\d\\d\\b"
let pattnew = #"\b\d\d\b"# // same thing
let pattnew2 = ##"\b\d\d\b"## // same thing

이런 리터럴을 raw string literal이라고 한다. 단, raw string literal에서 \ 를 이스케이프 문자로 쓰고 싶다면 리터럴을 감싸는 # 의 개수와 같은 # 문자를 붙여주어야 한다. 예를 들어 #"hello\nthere"#\n 을 포함하지 않지만 #"hello\#nthere"#\n 을 포함한다.

Multiline String Literal

문자열 리터럴은 여러 줄로 표현할 수도 있다. 이를 multiline string literal이라고 한다.

multiline string literal을 만드는 규칙은 아래와 같다.

  • 문자열의 시작과 끝을 """ 으로 감싼다.
  • 문자열의 첫 줄에는 """ 만 있어야 한다.
  • 문자열의 마지막 줄에는 """ 와 공백만 있어야 한다.
  • 문자열의 마지막에 있는 암시적 줄바꿈 문자는 무시된다.
  • 문자열을 닫는 """ 의 들여쓰기는 텍스트의 들여쓰기에 영향을 준다. 텍스트들은 닫는 """ 까지는 들여 써야 한다.

인용 부호는 \ 를 붙이지 않아도 된다. \ 로 끝나는 줄은 줄바꿈이 되지 않는다.

func f() {
	let s = """
	Line "1"
		Line 2 \
	and this is still Line 2
	"""
	// ...
}

위의 s 는 두 줄 짜리 문장으로, 두 번째 줄은 Line 2 and this is still line 2 가 된다.

문자열 보간법

문자열 보간법에서 추가적인 파라미터를 사용할 수 있다. 이 파라미터는 첫 번째 파라미터가 변환되는 방법을 정의한다.

let s = "You have \(n, roman:true) widgets" // You have V widgets.

문자열 연결

두 문자열을 연결하는 가장 간단한 방법은 + 연산자를 이용하는 것이다.

let s = "hello"
let s2 = " world"
let greeting = s + s2 // hello world

+ 연산자가 오버로딩되기 때문에 이런 방법을 사용할 수 있다. + 연산자는 피연산자가 숫자일 때와 문자열일 때 각각 다른 작업을 한다. 모든 연산자는 오버로딩될 수 있으며, 사용자가 임의로 오버로딩해서 새로운 작업을 만들 수도 있다.

+ 연산자는 += 로 줄여서 사용할 수도 있다. += 의 왼쪽에 오는 변수는 반드시 var 로 선언되어야 한다.

+= 대신 append(_:) 인스턴스 메소드를 호출할 수도 있다.

var s = "hello"
let s2 = " world"
s.append(s2)

joined(separator:) 메소드를 사용해서도 문자열을 연결할 수 있다.

let s = "hello"
let s2 = "world"
let space = " "
let greeting = [s, s2].joined(separator:space)

문자열의 길이

문자열의 길이를 알기 위해서는 count 프로퍼티를 사용한다. length 가 아닌 count 라고 불리는 이유는 문자열은 실제로 길이를 갖고 있지 않기 때문이다.

문자열은 연속된 유니코드 코드포인트로 이루어져 있는데, 여러 개의 유니코드 코드포인트는 하나의 문자를 만들기 위해 합쳐질 수 있다.

for...in 문을 사용하면 문자열의 문자를 하나씩 확인할 수 있다.

더 나아가서는 문자열을 utf8 프로퍼티와 utf16 프로퍼티를 사용해서 UTF-8 코드포인트 또는 UTF-16 코드포인트로 분해할 수 있다.

let s = "\u{BF}Qui\u{E9}n?"
for i in s.utf8 {
	print(i) // 194, 191, 81, 117, 105, 195, 169, 110, 63
}

unicodeScalars 프로퍼티는 UnicodeScalar 구조체로 표현되는 문자열의 UTF-32 코드포인트들의 모음을 나타낸다.

다른 언어들과 달리 스위프트는 기본적인 문자열 조작에 관한 메소드들이 많지 않다. 부족한 기능들은 Foundation 프레임워크에서 제공되기 때문이다. 스위프트의 String은 Foundation의 NSString과 연결된다. 즉, Foundation의 NSString의 프로퍼티와 메소드들을 스위프트의 String에서도 쓸 수 있다.

let s = "hello world"
let s2 = s.capitalized // "Hello World"

capitalized 프로퍼티는 Foundation 프레임워크에 있는 프로퍼티로, 스위프트가 아니라 Cocoa가 제공한다.

let s = "hello"
let range = s.range(of:"ell") // Optional(Range(...))

위 코드에서 스위프트 String은 NSString이 되고, NSString 메소드가 호출되어서 Foundation NSRange struct이 리턴된다. 그리고 NSRange는 Swift Range로 변환되고 옵셔널로 감싸진다.

💡 String과 NSString 원소의 차이

스위프트의 String의 원소는 문자(Character)이지만 NSString의 원소는 UTF-16 코드포인트이다. NSString의 방법은 스위프트에 비해 빠르고 효율적이지만 스위프트의 방법은 더 직관적이다.

Character와 String Index

코드포인트는 숫자지만 우리는 문자(character)를 주로 하나의 글자로 생각한다. 코드포인트와 문자의 공통점은 유니코드로 제공된다는 것이다. 스위프트는 Character 오브젝트 타입을 제공함으로써 하나의 문자 클러스터를 표현한다.

String은 문자 시퀀스이다. 문자 시퀀스의 일부가 아닌 다른 Character 오브젝트를 보는 일은 거의 없다. 또한 리터럴 Character를 쓰는 방법조차 존재하지 않는다. Character를 만들기 위해서는 문자 하나를 가진 String를 이용해서 초기화를 해야 한다. Character에서 String으로 변환하는 것도 가능하다.

let c = Character("h")
let s = (String(c)).uppercased()

Character들은 서로 비교도 가능하다.

String은 Character의 Sequence인 동시에 Character의 Collection이다. Sequence와 Collection은 프로토콜이다. 중요한 점은 String은 Sequence와 Collection의 프로퍼티와 메소드를 상속받는다는 것이다.

스위프트의 인덱스들은 모두 0부터 시작한다. 그러나 인덱스 값은 Int가 아니다.

dropFirstdropLast 메소드는 첫 번째 또는 마지막 문자를 제거한 새 문자열을 반환하는 효과를 가진다.

사실 두 메소드가 리턴하는 값은 Substring 인스턴스이다. Substring struct는 새 String을 생성하지 않고 원본 String의 일부분을 가리킨다.

let s = "hello"
let s2 = s.dropFirst() // "ello"

위 코드에서 s.dropFirst()"hello" 의 일부분인 "ello" 를 가리킨다. 이때 새 문자열이 생성되지 않기 때문에 추가적인 메모리 역시 필요하지 않는다.

String을 사용한 작업은 대부분 Substring으로도 가능하다. 그럼에도 둘은 명백히 다른 클래스들임을 명심하자.

let s = "hello"
let c = s[1] // compile error

위와 같은 방법으로는 문자열의 특정 문자에 접근할 수 없다. String의 인덱스들은 Int 값이 아니라 중첩된 타입인 String.Index (String.CharacterView.Index의 type alias)이기 때문이다.

String의 startIndexendIndex 또는 firstIndex , lastIndex 의 리턴 값과 index(_:offsetBy:) 메소드를 이용해서 원하는 인덱스에 접근할 수 있다.

let s = "hello"
let ix = s.startIndex
let ix2 = s.index(ix, offsetBy:1)
let c = s[ix2] // "e"

스위프트는 실제로 시퀀스를 순회하기 전까지는 문자 시퀀스의 문자들이 어디에 있는지 모르기 때문에 위와 같은 방법으로 인덱스에 접근해야 한다.

참고 - Swift String 효율적으로 쓰기

문자열 인덱스를 startIndex 또는 endIndex 의 오프셋으로 생각해야 하는 이유는 이 값들이 우리가 생각하는 값이 아닐 수 있기 때문이다. 이는 특히 Substring을 다룰 때 중요하다.

let s = "hello"
let s2 = s.dropFirst() // "ello"

s2.startIndex 는 무엇일까? 정답은 1이다. s2는 “hello”를 가리키는 Substring이기 때문이다. s2.firstIndex(of:"o") 역시 3이 아니라 4인데, 원본 문자열인 “hello”를 기준으로 인덱스 값이 계산되기 때문이다.

character 시퀀스는 Character 오브젝트의 배열로 변환할 수 있다. Array("hello") 는 “h”, “e”, … 문자들의 배열을 생성한다. 배열 인덱스들은 Int형이기 때문에 사용하기 편리하다. 또한 문자들의 배열 역시 String으로 변환할 수 있다.

profile
나의 내일은 파래 🐳
post-custom-banner

0개의 댓글