궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 고급 문법 [패턴 - Patterns]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님의 Swift 프로그래밍 3판을 참고하여 작성되었습니다.

 

지금 까지 기초문법에 대해 포스팅해봤는데, 이제부터 고급(?) 문법에 대해 알아볼까 한다.

패턴 Patterns

Swift 에는 문법에 응용할 수 있는 다양한 종류의 패턴(Pattern)이 있다. 

 

패턴은 '단독 또는 복합 값의 구조를 나타내는 것'

패턴 매칭은 '코드에서 어떤 패턴의 형태를 찾아내는 행위' 라고 할 수 있다.

 

이게 도대체 무슨 뜻....? 

 

쉽게 말해 '이러이러한  것을 표현하고 싶다면, 이러이러한 패턴을 통해 표현하면 돼!'라고 이해하면 된다...!

 

대부분의 패턴은 switch, if, guard, for 등의 키워드와 아주 친하고, 두 개 이상의 키워드가 합을 이뤄 동작한다. 특히 switch 구문에서 강력한 힘을 발휘한다.

 

Swift의 패턴 대표 종류

  1. 어떤 종류의 값과도 일치하는 패턴 
    • 와일드카드 패턴, 식별자 패턴 이를 포함하는 값 바인딩 패턴, 튜플 패턴
  2. 어떤 종류의 값과도 일치하지 않는 패턴
    • 열거형 케이스 패턴, 옵셔널 패턴, 타입 캐스팅 패턴, 표현 패턴

 

첫 번째 패턴(어떠한 종류의 값과도 일치하는 패턴)은 단순 상수, 변수 및 옵셔널 바인딩에서 값을 소멸시키는 데 사용된다. 이러한 패턴에 대한 타입 어노테이션을 지정하여 특정 유형의 값과만 일치하도록 제한을 걸 수 있다.

 

두 번째 패턴(어떤 종류의 값과도 일치하지 않는 패턴)은 전체 패턴 매칭에 사용된다. 여기서 일치시키려는 값은 런타임에 없을 수 있다. 이 패턴에는 switch 구문, do-catch, if case, while, guard, for-in 구문이 있다.

어떤 종류의 값과도 일치하는 패턴

와일드카드 패턴(Wildcard Pattern)

와일드카드 패턴은 '이 자리에 올 것이 무엇이든 간에 상관하지 마라'라는 뜻이다. 즉, 와이드카드 식별자가 위치한 곳의 값은 무시한다.

 

와일드카드 패턴은 모든 값을 일치시키고 무시하며 밑줄(_)로 구성된다. 

 

보통 switch 구문에서 사용될 수도 있고, for-in 구문에서도 사용할 수 있다.

for _ in 1...5 {

}

이런 식으로 단지 1... 5를 반복하며 닫힌 구간에서 현재 값을 무시한다. 

 

예를 들어보자면..

let string: String = "서근"

switch string {
    case _: print(string)
} // 서근
let optionalString: String? = "옵셔널 서근"

switch optionalString {
//optionalString이 "옵셔널 서근"일 때만 실행됨.
case "옵셔널 서근"?: print(optionalString)
//optionalString이 Optional("옵셔널 서근")외의 값이 있을 때만 실행됨.
case _?: print("값이 있지만, '옵셔널 서근'은 아니다.")
// 값이 없을 때 실행됨.
case nil: print("nil")
}
//optionalString이 "옵셔널 서근"이기 때문에 print: Optional("옵셔널 서근")
let seogun = ("서근", 100, "블로그")

switch seogun {
// 첫 번째 요소가 "서근" 일 때만 실행됨.
case ("서근", _, _): print("안녕하세요 서근님!")
fallthrough //위 case가 실행되도 아래 case를 확인함.
// 두 번째 요소가 100 일 때만 실행됨.
case (_, 100, _): print("unknown님의 값은 100입니다.")
// 그 외 어떤 요소든지 상관 없이 실행됨.
case (_, _, _): print("안녕하세요 unknown!")
}
/*
안녕하세요 서근님!
unknown님의 값은 100입니다.
*/
for _ in 0..<2 {
    print("서근 개발노트")
}
/*
 서근 개발노트
 서근 개발노트
*/

현재 값을 무시하고 싶다면? 밑줄(_)을 사용하면 된다. 이것이 바로 와일드카드 패턴!

식별자 패턴(Identifier Pattern)

식별자 패턴은 모든 값과 일치하며, 일치된 값을 상수 또는 변수 이름에 매치시킨다.

let someValue: Int = 100
//let(상수) someValue(식별자): Int(타입) = 100(값)

위 코드에서 let someValue: Int = 100과 같이 someValue 상수를 선언하는 동시에 someValue100이라는 값을 할당하려고 한다.

 

이때 someValue의 타입인 Int와 할당하려는 100의 타입이 매치된다면! someValue100이라는 값의 식별자가 된다!!!! 그니까, 일치가 성공하면 값 100someValue라는 상수의 '이름'에 바인딩(할당) 되는 것.

 

변수 또는 상수 선언의 왼쪽에 있는 패턴이 식별자 패턴인 경우, 식별자 패턴은 암시적으로 값 바인딩 패턴의 하위 패턴이다.

 

이 식별자 패턴은 평소에 너무나 당연히 사용해왔던 모습이기 때문에 이해하기 쉽...

값 바인딩 패턴(Value-Binding Pattern)

값 바인딩 패턴은 일치하는 값을 상수 또는 변수 이름에 바인딩(할당) 한다.

 

일치하는 값을 상수 이름에 바인딩하는 '값 바인딩 패턴'은 let 키워드로 시작한다.

 

변수 이름에 바인딩하는 변수는 var 키워드로 시작한다.

 

값 바인딩 패턴의 일종인 '식별자 패턴'은 매칭 되는 값을 새로운 이름의 변수 또는 상수에 바인딩한다. 

let seogun = ("서근", 100, "서근 개발노트")

switch seogun {
//name, age, blog는 seogun의 각각의 요소와 바인딩 한다.
case let (name, age, blog): print("이름: \(name), 나이: \(age), 블로그명: \(blog)")
}
//이름: 서근, 나이: 100, 블로그명: 서근 개발노트

switch seogun {
case (let name, let age, let blog): print("이름: \(name), 나이: \(age), 블로그명: \(blog)")
}
//이름: 서근, 나이: 100, 블로그명: 서근 개발노트

switch seogun {
//와일드카드 패턴 활용 가능.
case (let name, _, let blog):  print("이름: \(name), 나이: Unknown, 블로그명: \(blog)")
} //이름: 서근, 나이: Unknown, 블로그명: 서근 개발노트
let point = (3, 2)

switch point {
case let (x, y): print("The point is at (\(x), \(y)).")
}
// The point is at (3, 2).

튜플의 요소를 분해해서 각 요소의 값을 해당 식별자 패턴에 바인딩할 수 있다.

 

튜플 패턴인 (name, age, blog)의 각 식별자 패턴에 분배하고, 이 동작으로 인해 switch의 각 case들은 case(name, age, blog) 그리고, case(let name, let age, let blog)는 같은 값을 찾는 것이다.

튜플 패턴(Tuple Pattern)

튜플 패턴은 소괄호(()) 내에 0개 이상의 패턴을 쉼표로 분리하는 리스트이다. 튜플 패턴은 해당 튜플 타입의 값과 일치한다.

 

예를 들어 let (x, y): (Int, Int) = (1, 2) 와 같이  상수를 선언하면 (x, y): (Int, Int)라고 사용된 튜플 패턴은 두 요소가 모두 Int 타입인 튜플하고만 매치된다는 의미이다.

 

튜플 패턴을 for-in 구문 또는 변수나 상수 선언에서 사용한다면 와일드카드 패턴, 식별자 패턴, 옵셔널 패턴, 또 다른 튜플 패턴 등을 함께 사용할 수 있다.

 

예를 들어 튜플 패턴 (x, 0)0이 표현 패턴이다.

 

표현 패턴은 '어떠한 종류의 값과도 일치하지 않는 패턴'이라는 의미이고, 튜플 패턴은 '어떤 종류의 값과도 일치하는 패턴'을 의미한다.

let (a): Int = 2
print(a) //2

let (x, y): (Int, Int) = (3, 5)
print(x) //3

let (myName, blogName): (String, String) = ("서근", "개발노트")
print("이름: \(myName), 블로그명: \(blogName)") //이름: 서근, 블로그명: 개발노트
let name: String = "서근"
let age: Int = 100
let blog: String = "서근 개발노트"

switch (name, age, blog) {
case ("서근", _, _): print("안녕하세요 서근님!")
case (_, _, "서근 개발노트"): print("서근 개발노트 입니다!")
case (_, _, _): print("Unknown")
fallthrough
default: print("정보를 입력해주세요.")
} //안녕하세요 서근님!
let points: [(Int, Int)] = [(0, 0), (1, 0), (1, 1), (2, 0), (2, 1)]

for (x, _) in points {
    print(x)
}
/*
 0
 1
 1
 2
 2
*/

어떤 종류의 값과도 일치하지 않는 패턴

열거형 케이스 패턴(Enumeration Case Pattern)

열거형 케이스 패턴은 값을 열거형 타입의 case와 배치시킨다. 즉, 기존 열거형의 대소문자와 일치시킨다.

 

열거형 케이스 패턴은 switch 구문 case 레이블과 if, while, guardfor-in 구문의 조건에서 볼 수 있다. 만약 연관 값이 있는 열거형 케이스와 매치하려고 한다면 열거형 케이스 패턴에는 반드시 튜플 패턴이 함께해야 한다.

 

연관 값 이란? 열거형 내의 case가 자신의 연관 값을 가질 수 있는데, 연관 값은 각 항목 옆에 소괄호로 묶어 표현하는 것이다.

let someValue: Int = 100
if case 0...200 = someValue {
    print("0 <= \(someValue) <= 200")
} //0 <= 100 <= 200


let anotherValue: String = "서근"
if case "서근" = anotherValue {
    print(anotherValue)
} //서근
enum MainDish {
    case pasta(taste: String)
    case pizza(dough: String, topping: String)
    case chicken(withSauce: Bool)
    case rice
}

var dishes: [MainDish] = []
var dinner: MainDish = .pasta(taste: "까르보나라")
dishes.append(dinner) //까르보나라

if case .pasta(let taste) = dinner {
    print("\(taste) 파스타") //까르보나라 파스타
}

dinner = .pizza(dough: "치즈크러스트", topping: "쉬림프")
dishes.append(dinner)

func whatIsThis(dish: MainDish) {
    guard case .pizza(let dough, let topping) = dinner else {
        print("이것은 피자가 아닙니다")
        return
    }
    print("\(dough) \(topping) 피자")
}

whatIsThis(dish: dinner) //치즈크러스트 쉬림프 피자

옵셔널 패턴(Optional Pattern)

옵셔널 패턴은 식별자와 그 뒤에 물음표(?)로 구성되며, 열거형 패턴과 동일한 위치에 나타난다. 또, 옵셔널 값을 저장하는 배열의 for-in구문을 통한 순환에서 nil 이 아닌 값을 찾는 데도 유용하게 사용한다.

var optionalValue: Int? = 100

/*========*/

if case .some(let value) = optionalValue {
    print(value)
} //100

if case let value? = optioanlValue {
   print(value)
} //100

/*========*/

func isItHasValue(_ optionalValue: Int?) {
     guard else .some(let value) = optionalValue else {
        print("none")
        return
     }
     print(value) 
} 
isItHasValue(optionalValue) //100

/*========*/

while case .some(let value) = optionalValue {
    print(value)
    //break 사용해도 됨
    optionalValue = nil
} //100 
print(optionalValue) //nil

옵셔널 패턴은 for-in구문에서 옵셔널 값의 배열을 반복할 수 있는 편리한 방법을 제공한다. 이 경우 nil 이 아닌 요소에 대해서만 루프 본문을 실행한다. for-in구문에도 case let을 사용할 수 있다.

let arrayOfOptionalInt? : [Int?] = [nil, 2, 3, nil, 5]

for case let number? in arrayOfOptional {
    print("Found a \(number)")
} 
/* 
 Found a 2
 Found a 3
 Found a 5
*/

타입 캐스팅 패턴(Type-Casting Pattern)

 

타입 캐스팅 패턴에는 isas 패턴이 있다.

 

is 패턴switchcase 레이블에서만 사용 가능하며, is (TYPE_NAME)과 같이 쓸 수 있다.

is type

as 패턴SomePattern as (TYPE_NAME)과 같이 쓸 수 있다.

pattern as type

is 패턴런타임 시 해당 값의 타입이 is 패턴의 오른쪽에 지정된 타입 또는 해당 타입의 자식 클래스와 동일한 경우 값과 일치하게 된다.

 

또, is 패턴은 as연산자와 비슷한 역할인 타입 캐스트를 수행하지만, 리턴된 타입은 버리는 점에서 is 연산자와 같이 동작한다. 

 

as 패턴프로그램 실행 중에 값을 타입이 as 오른쪽에 지정된 타입 또는 해당 타입의 자식 클래스와 동일한 경우 값과 일치하게 된다.

 

일치가 성공하면 일치된 값을 유형이 as 패턴의 오른쪽에 지정된 패턴으로 반환된다.

let someValue: Any = 100

switch someValue {
   //타입이 Int인지 확인하지만 캐스팅된 값을 사용할 수는 없다.
   case is String: print("It's String")
   //타입 확인과 동시에 캐스팅까지 완료해 value에 값이 저장된다.
   case let value as Int: print(value + 1)
   
   default: print("String타입도 Int 타입도 아닙니다.")
} 
//101

표현 패턴(Expression Patter)

 

표현 패턴은 매우 유용한 패턴 중 하나이다. 가히 모든 패턴 중 최고 이며 궁극의 패턴 매칭을 이루어 낼 수 있는 패턴이라고 할 수 있다.

 

그 이유는 ~= 연산자를 중복 정의(overload) 하거나 ~= 연산자를 새로 정의하거나 또는 자신이 만든 타입에 ~= 연산자를 구현해준다면 자신이 원하는 대로 패턴을 완성시킬 수 있기 때문이다. 더불어 제네릭까지 추가하게 된다면 활용도는 아주 높아진다.

TIP
 
 

~= 연산자
~= 연산자는 범위를 체크해주는 간편한 연산자이다. 어떤 숫자를 가지고 있고, 이 숫자가 10 이상인지 100 이하인지 확인하고 싶을 때

if 10...100 ~= n { print("10이상 100이하 입니다.") }

이런 식으로 깔끔하게 사용 가능하다. 

표현 패턴은 표현식의 값을 나타낸다. 표현 패턴은 switch 구문 case 레이블에서만 나타난다.

 

표현 패턴은 Swift 표준 라이브러리의 패턴 연산자인 ~= 연산자의 연산 결과가 true를 반환하면 일치가 성공한다.

 

~= 연산자는 같은 타입의 두 값을 비교할 때 == 연산자를 사용한다. 

 

표현 패턴은 정숫값과 정수의 범위를 나타내는 Range 객체와 매치시킬 수도 있다.

switch 10 {
case 0...10:
    print("10 is in 0...10")
default: 
    print("10 is not in 0...10")
}    // 10 is in 0...10


let value = 10
//0에서 10사이 범위 안의 값을 찾음
if 0...10 ~= value {
    print("value is in 0...10")
}    // value is in 0...10

이런식으로 범위 안의 값을 찾을 때, 사용할 수 있는 연산자 이고, 이 연산자는 switchcase 구문에서 내부적으로 호출해서 쓰인다.

let point = (1, 2)

switch point {
case (0, 0):
    print("(0, 0)은 원점입니다. ")
case (-2...2, -2...2):
    print("(\(point.0), \(point.1))은 원점과 가깝습니다.")
default:
    print("점은 (\(point.0), \(point.1)) 입니다.")
}

// Prints "(1, 2)는 원점과 가깝습니다."

위 예제를 보면 튜플 패턴에서 (x, y) 같이 변수로 나타내는 것이 아닌 '값'으로 case (0, 0) 준 것을 확인할 수 있다.

 

표현 패턴은 '값'을 나타낸다. 그리고 값뿐만 아니라 '범위'를 줄 수도 있다.

 

그럼 ~= 연산자를 오버로딩[각주:1]해보도록 하자!

let point = (1, 2) //Int 값이 있는 튜플

func ~= (pattern: String, value: Int) -> Bool {
    return pattern == "\(value)"
}

switch point {
case ("0", "0"):
    print("(0, 0)은 원점입니다.")
default:
    print("점은 (\(point.0), \(point.1)) 입니다.")
}
// Prints "점은 (1, 2) 입니다."

위 예제에서 상수 point는 분명 Int 타입 튜플로 되어있는데, switch 구문을 보면 case("0", "0")으로 String 타입으로 정의되어 있다. 이 의미는 point라는 튜플 패턴이 switch 구문으로 들어와서 저 case를 한 번 검사한다는 의미이다.

 

그래서 IntString을 비교하게 된다.

 

이를 가능하게 해 주는 것이 바로 오버로딩한 ~= 연산자이다.

TIP
 
 

~=(_:_:) 제네릭 연산자
두 파라미터가 동일한 값으로 일치하는지 여부를 나타내는 Bool값을 반환한다. 즉, 두 파라미터(a, b)Equatable을 준수하기만 한다면, 두 값이 일치하는지 확인할 수 있다. ~=는 패턴 매칭 연산자로서, 주로 case 문 패턴 매칭을 가능하게 해 준다.

자 계속해서 오버로딩한 func ~= 코드를 보자면..

func ~= (pattern: String, value: Int) -> Bool {
    return pattern == "\(value)"
}

왼쪽은 Equatable[각주:2]을 준수하는 String 타입, 오른쪽은 Equatable을 준수하는 Int 타입이고 truefalse를 반환해준다.

 

그리고 patternvalue를 비교하는데, valueString 타입으로 변환해서 비교하고 있다. 만약 이 두 개가 같다면 true를 반환하고, 다르다면 false를 반환한다.

 

그런데 이상한 점이 하나 있다.

switch point {
case ("0", "0"):
    print("(0, 0)은 원점입니다.")
default:
    print("점은 (\(point.0), \(point.1)) 입니다.")
}
// Prints "점은 (1, 2) 입니다."

switch 구문에서는 ~= 연산자를 사용하지 않았는데, 위에서는 StringInt를 비교한다고 했다...! (?) 그런데 그 어디에도 ~= 연산자가 보이지 않는다!! 

 

아까 언급했던 '이 연산자는 switch의 case 구문에서 내부적으로 호출해서 쓰인다.' 이 부분이 그 해답이다.

 

~= 연산자를 패턴 매칭을 위해 case 내부적으로 사용되는데, case문에서 Equatable값과 일치할 때 이 ~= 연산자는 뒤에서 호출된다.

 

이것이 바로 표현 패턴을 활용해서 더욱 강력하게 사용할 수 있는 점이다!!!

 

 

읽어주셔서 감사합니다 🤟

 

 

 

  1. 오버로딩이란 함수의 이름은 같으나 매개변수, 리턴타입 등을 다르게 하여 함수를 중복으로 선언할 수 있게 해주는 것이다. 보통 함수 이름이 중복되는 경우에는 사용 불가한데, 이럴때 오버로딩을 사용하면 함수를 여러개 만들 수 있다. [본문으로]
  2. Equatable 프로토콜은 == 및 != 연산자를 사용해 값이 동일한지 판단할 수 있는 타입이다. 스위프트 표준 라이브러리에 정의된 대부분의 타입은 Equatable 프로토콜을 준수한다. 사용자 정의 타입에서 Equatable 프로토콜을 채택하면 해당 타입을 위한 == 연산자를 구현해야 한다. 그러면 표준 라이브러리에서 != 연산자를 자동으로 구현해 준다. [본문으로]

잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.