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

Swift : 고급 문법 [where 절 - 특정 패턴과 결합]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

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

 

where 절

Swift에서 where 절은 특정 패턴과 결합하여 조건을 추가하는 역할을 한다. 조건을 더 추가하고 싶을 때, 특정 타입에 제한을 두고 싶을 때 등 다양한 용도로 사용된다.

 

언제 어디서든 필요할 때마다 나타나 도와주는 역할을 가지고 있다.

 

타입에 제약을 주는 것은 개발자가 제네릭 메서드, 첨자, 타입과 관련한 타입 매개변수의 요구사항을 정의할 수 있게 해 준다. 관련 타입들의 요구사항 정의를 위해 개발자는 where 절을 사용할 수 있다.

 

where 절은 where 키워드를 사용하며, 그 뒤에 관련 타입에 대한 제약사항이나 충족해야 하는 관계 조건을 정의하면 된다. 블록의 타입, 메서드가 시작하는 중괄호({}) 전에 활용할 수 있다.

where 절의 활용

where 절은 크게 두 가지 용도로 사용된다.

  • 패턴과 결합하여 조건 추가
  • 타입에 대한 제약 추가

즉, 특정 패턴에 Bool 타입 조건을 지정하거나 어떤 타입의 특정 프로토콜 준수 조건을 추가하는 등의 기능이 있다.

 

아래 예시는 배열 컬렉션을 for 문으로 순회하는 데 있어서 순회하는 요소의 조건을 짝수로 한정해 출력한다. where 절은 프로토콜 타입의 제약 외로 특정 타입 값을 충족해야 하는 관계 조건을 제약사항으로 둘 수 있다.

값 바인딩, 와일드카드 패턴

let arr: [Int] = [1, 2, 3, 4, 5, 6, 7, 8]
// 값 바인딩, 와일드 카드 패턴
// i 요소가 2로 나웠을때 0일 경우(짝수)일 때만 블록 실행
for i in arr where i % 2 == 0 {
    print(i)
}

/*
2
4
6
8
*/

옵셔널 패턴과 결합

where 절은 옵셔널 패턴과도 결합 가능하다.

let arr: [Int?] = [1, 2, 3, 4, 5, 6, 7, 8]

// 옵셔널 패턴과 결합
for case let i? in arr where i % 2 == 0 {
    print(i)
}

/*
2
4
6
8
*/
let arrOfOptionalString: [String?] = ["서근", nil, "개발노트", "히", nil, nil]

for case let value? in arrOfOptionalString where value.count > 1 {
    print("String 타입을 찾는 중.... '\(value)'")
}
/*
 String 타입을 찾는 중.... '서근'
 String 타입을 찾는 중.... '개발노트'
*/

타입 캐스팅 패턴과 결합

let anyValue: Any = "ABC"

switch anyValue {
case let value where value is String:
    print("value는 String 타입 '\(anyValue)' 이다.")
case let value where value is Double:
    print("value는 Double 타입 '\(anyValue)' 이다.")
default:
    print("anyValue")
}

//value는 String 타입 'ABC' 이다.

표현 패턴과 결합

var point: (Int, Int) = (1, 2)

switch point {
case (0, 0):
    print("원점")
 //point 첫번째 값이 1과 다른 값인지 비교하여 값이 같지 않다면 출력
case (-2...2, -2...2) where point.0 != 1:
    print("(\(point.0)), (\(point.1))은 원점과 가깝다.")
default:
    print("포인트 : (\(point.0), \(point.1))")
}

//포인트 : (1, 2)

제네릭 타입의 연관 타입 제약 추가

protocol Talkable {}
protocol CallToAll {
    func callToAll()
}

struct Person: Talkable {}
struct Animal {}
struct WhoCare: Talkable {}

extension Array: CallToAll where Element: Talkable {
    func callToAll() {
        print("call to all")
    }
}

let people: [Person] = []
let cats: [Animal] = []
let whoCare: [WhoCare] = []

people.callToAll()
//cats.callToAll{} Animal 은 Talkable 프로토콜을 준수하지 않았기 때문에 런타임 오류
whoCare.callToAll()

프로토콜, 익스텐션과 결합

프로토콜 익스텐션에 where 절을 사용하게 되면 이 익스텐션이 특정 프로토콜을 준수하는 타입에만 적용될 수 있도록 제약을 줄 수 있다.

 

즉, 익스텐션이 적용된 프로토콜을 준수하는 타입 중 where 절 뒤에 제시되는 프로토콜도 준수하는 타입만 익스텐션이 적용되도록 제약을 준다는 의미이다.

 

 

아래 예제에선 각 extension Selfprintable 위에 where 절을 사용했고, 그 뒤에 이 프로토콜을 준수하는 타입(Self: FixedWidthInteger, Self: SignedInteger)을 정의했다. 이것이 바로 제약을 준 것이다.

 

눈치챘겠지만, 여러 프로토콜을 제시하고 싶다면 쉼표(,)로 구분해주면 된다.

 

그러니까 FixedWidthInteger[각주:1]SignedInteger[각주:2]을 준수하면서 SelfPrintable을 준수하는 타입이면 이 메시지를 출력한다는 의미인 것이다.

protocol SelfPrintable {
   func printSelf()
 }

 struct Person: SelfPrintable {}

 extension Int: SelfPrintable {}
 extension UInt: SelfPrintable {}
 extension String: SelfPrintable {}
 extension Double: SelfPrintable {}
 
 extension SelfPrintable where Self: FixedWidthInteger, Self: SignedInteger {
   func printSelf() {
     print("FixedWidthInteger와 SignedInteger을 준수하면서 SelfPrintable을 준수하는 타입 \(type(of:self))")
   }
 }

 extension SelfPrintable where Self: CustomStringConvertible {
   func printSelf() {
     print("CustomStringConvertible을 준수하면서 SelfPrintable을 준수하는 타입 \(type(of:self))")
   }
 }

 extension SelfPrintable {
   func printSelf() {
     print("그 외 SelfPrintable을 준수하는 타입 \(type(of:self))")
   }
 }

TIP
 
 

소문자 self와 대문자 Self의 차이
self는 인스턴스 프로퍼티와 인스턴스 메서드를 가리킬 때 사용 되는데, 모든 인스턴스들이 암시적으로 생성하는 프로퍼티이다. 즉, self는 자기 자신을 가리키는 프로퍼티인데, 클래스, 구조체, 열거형 등 인스턴스 내에서 사용된다. 

Self는 타입 프로퍼티와 타입 메서드를 가리킬 때 사용된다. 즉, Self 타입은 특정한 타입이 아닌, 타입의 이름을 반복해서 쓰는 대신 현재 타입을 쓸 수 있도록 도와주는 역할을 한다. 만약 클래스, 구조체, 열거형 등에서 Self를 사용하면 그 타입을 가리킨다는 의미이다. 

이제 위 where 절을 활용한 프로토콜 익스텐션을 호출하려면 아래와 같이 코드를 작성하면 된다.

 Int(-8).printSelf()
 // FixedWidthInteger와 SignedInteger을 준수하면서 SelfPrintable을 준수하는 타입 Int
 
 UInt(8).printSelf()
 // CustomStringConvertible을 준수하면서 SelfPrintable을 준수하는 타입 UInt
 
 String("서근").printSelf()
 // CustomStringConvertible을 준수하면서 SelfPrintable을 준수하는 타입 String
 
 Double(8.0).printSelf()
 // CustomStringConvertible을 준수하면서 SelfPrintable을 준수하는 타입 Double
 
 Person().printSelf()
 // 그 외 SelfPrintable을 준수하는 타입 Person

또 다른 예제를 보자면..

아래 예제는 Numeric[각주:3]프로토콜을 준수하면서 Printable 프로토콜을 준수하는 타입만 적용되도록 제약해준 익스텐션이다.

protocol Printable {
    func printSelf()
}

extension Int: Printable {}
extension String: Printable {}

// 익스텐션과 프로토콜의 조합
// Numeric프로토콜을 준수하면서 Printable 프로토콜을 준수하는 타입만 적용되도록 제약/
extension Printable where Self: Numeric {
    func printSelf() {
        print("Numeric을 준수하면서 Printable을 준수하는 타입 \(type(of:self))")
    }
}

extension Printable {
    func printSelf() {
        print("Prinatable만 준수하는 타입 \(type(of:self))")
    }
}

마찬가지로 아래와 같이 호출 가능한데, 익스텐션 타입으로 바로 호출도 가능하고 아니면 인스턴스를 만들어서 호출해 줄 수 있다.

Int(5).printSelf()
// Numeric을 준수하면서 Printable을 준수하는 타입 Int
String("서근").printSelf()
// Prinatable만 준수하는 타입 String

let someInt: Int = 10
let someString: String = "서근개발노트"
someInt.printSelf()
// Numeric을 준수하면서 Printable을 준수하는 타입 Int
someString.printSelf()
// Prinatable만 준수하는 타입 String

 

 

읽어주셔서 감사합니다 🤟

 

 

 

  1. FixeWidthInteger 프로토콜은 정수 오버플로 연산을 지원하기 위한 요구사항이다. [본문으로]
  2. SignedInteger 프로토콜은 스위프트의 부호가 있는 정수 타입을 위한 요구사항이다. [본문으로]
  3. Numeric 프로토콜은 곱셈을 지원하는 값이 있는 유형 타입이다. Numeric정수 및 부동 소수점 숫자와 같은 스칼라 값에 대한 산술에 적합한 기반을 제공한다. Numeric프로토콜을 일반 제약 조건으로 사용하여 스위프트 표준 라이브러리의 모든 숫자 형식에서 작동하는 일반 메서드를 작성할 수 있다. [본문으로]

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


서근


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