Swift 언더스코어(_): wildcard pattern·인자 레이블·property wrapper

반응형

Swift 언더스코어 _는 하나의 기능 이름이 아니다. pattern에서는 값을 bind하지 않는 wildcard이고, function parameter 앞에서는 argument label을 생략하며, property wrapper가 붙은 property 앞에서는 compiler가 만든 storage 이름에 나타난다. 위치에 따라 의미를 따로 읽어야 한다.

pattern에서 값을 bind하지 않는다

Swift pattern 문서_를 어떤 값과도 match하지만 그 값을 무시하는 wildcard pattern으로 정의한다.

let person = (name: "Alice", age: 30)
let (_, age) = person

for _ in 0..<3 {
    print("retry")
}

tuple의 첫 값이나 loop의 현재 index를 쓰지 않겠다는 의도가 드러난다. switch에서도 tuple의 일부만 match할 수 있다.

let point = (3, 0)

switch point {
case (_, 0):
    print("x-axis")
case (0, _):
    print("y-axis")
default:
    print("other")
}

case _:도 모든 값을 match하지만 단순 catch-all에는 default:가 더 익숙하고 의도가 분명하다. tuple 일부나 associated value 일부를 버릴 때 wildcard의 장점이 커진다.

function의 argument label을 생략한다

function parameter에는 call site에서 쓰는 argument label과 function body에서 쓰는 parameter name이 있다. Swift function 문서처럼 label 자리에 _를 적으면 호출할 때 label을 쓰지 않는다.

func greet(_ name: String, from hometown: String) -> String {
    "Hello, \(name) from \(hometown)"
}

print(greet("John", from: "Seoul"))

name 값 자체를 무시한 것이 아니다. body에서는 여전히 name으로 접근한다. 바뀐 것은 call이 greet(name:from:)가 아니라 greet(_:from:) 형태가 된다는 점이다. greet()처럼 argument까지 사라지는 것도 아니다.

closure parameter를 명시적으로 버린다

callback signature가 값을 주지만 현재 closure에서는 필요 없을 때 _를 쓴다.

let values = [1, 2, 3]

values.forEach { _ in
    print("one item")
}

values.enumerated().forEach { index, _ in
    print("index: \(index)")
}

불필요한 임시 이름을 만들지 않으면서 callback shape는 그대로 유지한다. 다만 값이 곧 필요해질 가능성이 높거나 domain 의미가 중요하면 이름을 붙이는 편이 읽기 쉬울 수 있다.

expression result를 의도적으로 버린다

반환값이 있지만 이 위치에서는 사용하지 않겠다는 사실을 명시할 때 _ = expression을 쓸 수 있다.

_ = values.firstIndex(of: 2)

단순히 경고를 없애기 위한 습관으로 남용하면 계산 결과나 error signal을 놓칠 수 있다. function이 반환값을 꼭 확인해야 하는 API인지 먼저 본다.

property wrapper에서는 wrapped value와 storage를 구분한다

property wrapper를 적용하면 source code에서 보이는 property 외에 compiler가 wrapper storage와 projected value 접근을 만든다. Swift property wrapper 문서의 개념으로 보면 다음 세 이름의 역할이 다르다.

import SwiftUI

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}
  • count: wrapped value인 Int를 읽고 쓴다.
  • $count: projected value다. @State에서는 child control에 넘길 Binding<Int>가 된다.
  • _count: compiler가 합성한 wrapper storage 또는 toolchain이 생성한 backing peer에 해당한다.

그러므로 button action에서 _count += 1로 숫자를 증가시키는 code는 맞지 않는다. _countInt 자체가 아니다. Apple State 문서도 일반적인 read·write는 count, binding은 $count를 사용하도록 보여 준다. underscore storage는 custom initializer에서 wrapper 자체를 구성하는 제한된 상황에 등장할 수 있지만, 일상적인 state mutation API로 사용하지 않는다.

숫자 literal의 underscore는 가독성 separator다

숫자 사이의 _는 값을 버리는 wildcard가 아니라 자릿수를 읽기 쉽게 하는 separator다.

let timeoutNanoseconds = 1_000_000_000
let color = 0xFF_FF_00

runtime value는 underscore를 쓰지 않은 literal과 같다. 같은 글자라도 pattern과 lexical literal에서 역할이 다르다.

property wrapper initialization과 app lifecycle은 Swift 초기화와 @main, access level 속에서 underscore-backed property를 숨기는 기준은 Swift 접근 제어에서 이어서 볼 수 있다.

정리

위치 _의 의미
tuple·loop·switch pattern match하지만 bind하지 않음
function parameter label call site argument label 생략
closure parameter 전달된 값을 사용하지 않음
_ = expression result를 의도적으로 폐기
_property property wrapper의 synthesized storage 계열
numeric literal 가독성을 위한 separator

underscore를 “값 무시 기호” 하나로 외우기보다, 지금 보고 있는 위치가 pattern, declaration, generated storage, literal 중 어디인지 먼저 확인하면 오해가 줄어든다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글