본문 바로가기
Swift

패턴에 관하여

by iOS 개린이 2023. 3. 15.

-패턴이란 "단독 또는 복합 값의 구조" 라고 한다.

 

-Swift에는 문법에 응용할 수 있는 여러 가지 패턴이 존재한다.

이 패턴들을 잘 사용하면 코드의 양을 줄이거나, 문법의 활용이 수월해질 수 있다.

 

-대부분의 패턴은 switch, if, guard, for 등의 키워드와 아주 친하며 두 개 이상의 키워드가 합을 이뤄 동작한다.

(특히 switch 구문에서 유용하게 사용됨.)

 

-Swift의 패턴은 크게 두 가지로 나뉜다.

 

1. 값을 해제하거나 무시하는 패턴(어떤 종류의 값과도 일치하는 패턴)

 

-단순 상수, 변수 및 옵셔널 바인딩에서 값을 소멸시키는데 사용된다.

-이러한 패턴에 대한 Type Anotation을 지정하여 특정 유형의 값과만 일치하도록 제한을 걸 수 있다.

-패턴의 종류에는 와일드 카드 패턴, 식별자 패턴, 값 바인딩 패턴, 튜플 패턴이 있다.

 

 

2. 패턴 매칭을 위한 패턴(어떤 종류의 값과도 일치하지 않는 패턴)

 

-전체 패턴 매칭에 사용된다. 여기서 일치시키려는 값은 런타임에 없을 수 있다.

-패턴의 종류에는 열거형 케이스 패턴, 옵셔널 패턴, 표현 패턴, 타입 캐스팅 패턴이 있다.

 

 

하나씩 알아보자.

 

 

와일드카드 패턴

-와일드카드 식별자인 _ 를 사용한 패턴을 말한다.

-우리가 알고있던 것이라서 너무나도 간단함.

 

 

위의 코드에서 case 옆에 _ 보이는가?

_ 이게 바로 와일드카드 식별자다.

와일드카드 식별자를 사용하면 "이 자리에 올 것이 무엇이든지 상관하지 마라" 는 뜻이다. 즉, 와일드카드 식별자가 있는 곳의 값은 무시하면 됨.

 

 

식별자 패턴

-변수 또는 상수의 이름에 알맞는 값을 어떤 값과 매치시키는 패턴을 말한다.

 

이게 패턴이었구나 싶기도 할 정도로 우리에게 익숙한 것.

let someValue : Int = 10

 

someValue라는 상수에 Int 타입의 값 10을 할당한다.

 

이게 식별자 패턴인 이유는

someValue의 타입인 Int와 할당하려는 값 10의 타입이 매치되면 someValue는 10이라는 값의 식별자가 되는 것이다.

경우에 따라서 식별자 패턴은 값 바인딩 패턴의 일종라고 할 수도 있다.

 

 

값 바인딩 패턴

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

-switch 구문에서 많이 사용된다.

let human = ("Jiho", 27, "Male")

switch human {
    
case let (name, age, gender):
    print("Name : \(name), Age : \(age), Gender : \(gender)")
}

//print "Name : Jiho, Age : 27, Gender : Male"

 

case에 보면 상수 name, age, gender로 human의 각 요소들을 바인딩 한 것을 볼 수 있다.

매치되는 값 "Jiho", 27, "Male" 을 name, age, gender로 바인딩하고, 바인딩한 값을 case내에서 사용할 수 있다.

 

이게 바로 값 바인딩 패턴

 

 

튜플 패턴

-튜플 패턴은 ( ) 소괄호로 묶인 0개 이상의 패턴을 쉼표로 분리하는 리스트다.

-튜플패턴은 해당 튜플 타입의 값과 일치한다.

 

 

(x, y) : (Int, Int)를 쉼표로 구분했고, 두 요소 모두 Int 타입인 값 1, 2와 매칭된다.

 

이것이 튜플 패턴

 

 

열거형 케이스 패턴

-값을 열거형 타입의 case와 매치시키는 패턴이다.(기존 열거형의 대소문자와 일치시킨다.)

-열거형 케이스 패턴은 switch 구문 case 레이블과 if, while, guard 및 for 구문의 조건에서 볼 수 있다.

-만약 연관값이 있는 열거형 케이스와 매치하려고 한다면 열거형 케이스 패턴에는 반드시 튜플 패턴이 함께해야 한다.

 

//Int를 이용한 열거형 케이스 패턴

let someInt : Int = 30

if case 0...100 = someInt {
    print("0 <= \(someInt) <= 100") //0 <= 30 <= 100
} 

//string을 이용한 열거형 케이스 패턴

let someString : String = "Jiho"

if case "Jiho" = someString {
    print(someString)   //"Jiho"
}

 

case를 이용해서 0...100 범위 내에  someInt가 일치하는지 확인하고, 일치한다면 내부 코드를 실행한다.

 

 

옵셔널 패턴

-Swift의 옵셔널은 열거형으로 구현되어 있다.

그래서 Enum Optional 내부를 보면 두 개의 case가 존재한다.

1. 값이 연관값으로 들어가있는 .some 케이스

2. 값이 없는 .none 케이스 

 

따라서 열거형 케이스 패턴을 이용하는 것이 가능함.

let number : Int? = 10

if case .some(let value) = number {
    print(value) //10
}

 

옵셔널인 number에 값이 존재한다면 내부 코드가 실행된다. (이건 옵셔널 패턴이 아니라 옵셔널을 열거형 케이스 패턴으로 사용한 것)

 

-옵셔널 패턴은 옵셔널의 두 가지 케이스 중 .some 케이스의 연관값을 상수 또는 변수에 매칭하는 것을 말한다.

if case let value? = number {
    print(value) //10
}

 

위처럼 식별자 value 뒤에 ? 를 붙여서 나타내는 것이 옵셔널 패턴이다.

또한 number는 옵셔널 타입이지만 value는 옵셔널 바인딩 된 값으로 나오기 때문에 어떻게 보면 옵셔널 언래핑의 기능도 한다고 볼 수 있다.

 

옵셔널 패턴의 장점

-옵셔널 패턴의 장점은 옵셔널 배열을 for문을 통해 nil값을 제외하고 추출할 때 나타난다.

let optionalArray : [Int?] = [nil, 2, 3, nil, 5]
var count = 0

for i in optionalArray {
    count += 1
    guard let number = i else{continue}
    print(number) 
}

print("반복문 실행 횟수 : \(count)") 

//
2
3
5
반복문 실행 횟수 : 5

 

옵셔널 배열을 for문을 통해 돌려주는데, 안의 요소가 nil이라면 print가 실행되지 않고, 다시 for문이 돌아간다.

이 코드를 옵셔널 패턴으로 변경해주면?

 

let optionalArray : [Int?] = [nil, 2, 3, nil, 5]
var count = 0

for case let value? in optionalArray {
    count += 1
    print(value)
}

print("반복문 실행 횟수 : \(count)")

//
2
3
5
반복문 실행 횟수 : 3

 

 

 옵셔널 바인딩 했을 때의 코드보다 더 깔끔해졌고, 반복문의 실행횟수가 줄어든 것을 볼 수 있다.

반복문의 실행횟수가 줄어든 이유는 배열의 요소를 가져올 때,  값이 있는 옵셔널일 때만 for문이 돌아가기 때문이다. 

 

이것이 옵셔널 패턴

 

타입 캐스팅 패턴

-타입 캐스팅에도 is와 as가 있듯이 타입 캐스팅 패턴에도 is 패턴과 as 패턴이 있다.

 

is 패턴

-is (Type) 식으로 사용한다.

-switch의 case 레이블에서만 사용이 가능하다.

let someValue : Any = 100

switch someValue {
case is Int :
    print("Int")
    
default:
    print("Nothing")
}

 

someValue가 Int 타입인지 체크하고, 맞다면 해당 코드가 실행된다.

하지만 is 패턴은 타입 체크만 가능하고, 캐스팅된 해당값을 사용할 수는 없다.

 

as패턴

-패턴 as (Type) 식으로 사용한다.

let someValue : Any = 100

switch someValue {
case let value as Int :
    print(value)
    
default:
    print("Nothing")
}

 

someValue의 타입이 Int와 동일하면 타입 캐스팅이 완료되어 value에 할당해준다.

타입 캐스팅으로 인해 value의 타입은 Any가 아닌 Int가 된다.

 

 

표현 패턴

-표현식의 값을 평가한 결과를 이용하는 것이다.

-switch문 case 레이블에서만 사용이 가능하다.

-표현패턴은 ~= 연산자를 통해 결과가 true를 반환할 때 일치가 성공한다.

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

 

let someValue = (0, 0)

switch someValue {
    
case (1, 1):
    print("Check value")

case (-1...1, -1...1):
    print("Check range")
    
default:
    print("default")
}

//Check range

 

표현 패턴은 값을 평가한 결과를 이용한다고 했다.

case의 조건을 보면 x나 y같이 변수로 나타내는 것이 아닌 1, 1 처럼 값을 주고, 이 값이 평가된 결과가 true일 때 해당 코드가 실행된다.

또한 범위를 통해 평가한 결과를 이용할 수도 있다.

 

~= 의 사용은?

let point = (0, 0)

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

switch point {
    
case ("0", "0") :
    print("원점")
    
default :
    print("점 (\(point.0), \(point.1))")
}

//"원점"

 

위의 코드에서 switch문을 보면 결괏값이 "원점" 이 나오는 것을 볼 수 있다.

분명 point는 내부 요소값이 모두 Int일텐데, String 타입인 "0"의 조건이 어떻게 맞는 것인지?

위에 표현 패턴으로 정의해둔 함수를 지워보면 case에 타입이 달라 매치할 수가 없다고 에러가 발생한다.

즉, 표현 패턴의 함수가 Int와 String 타입을 비교할 수 있도록 만들어 준 것이다.

 

먼저 ~= 연산자의 정의를 보자.

func ~=<T>(a: T, b: T) -> Bool where T : Equatable

 

~= 연산자는 제네릭 함수고, a, b 파라미터가 Equatable 프로토콜을 준수한다면 a와 b의 값이 일치하는 지 확인할 수 있다.

 

그리고 위의 코드에서 함수 부분을 보자.

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

 

이 함수는 ~= 연산자를 오버로딩한 것으로 value를 String 타입으로 바꿔서 String 타입끼리 비교가 되도록 만든 것이다.

case문의 ("0", "0")안의 첫번째 요소 "0"이 patten 전달인자로, point의 첫번째 요소 0이 value 전달인자로 들어가고, 내부에서 비교한 결과가 모두 true가 반환되었을 때 해당 코드가 실행되는 것이다.

 

근데 switch문에는 이 함수를 적용하지 않았고, ~= 연산자도 보이지 않는데 어떻게 실행된 것일까?

이유는 ~= 연산자는 패턴매칭을 위해 case문 내부적으로 사용되기 때문이다. 

case문에서 Equatable값과 일치할 때, ~= 연산자가 뒤에서 호출된다고 한다.

 

 

 

 

 

 

 

Reference:

-Swift ) Patterns (tistory.com)

-야곰님의 Swift 문법 개정 3판

 

 

'Swift' 카테고리의 다른 글

Error Handling(에러 처리)에 관하여  (0) 2023.03.16
Where절에 관하여  (0) 2023.03.15
Nested Types(중첩 타입)에 관하여  (0) 2023.03.13
타입 캐스팅에 관하여  (0) 2023.03.13
모나드에 관하여  (0) 2023.03.10