xho95 (소중한꿈)'s Swift Life

Apple 에서 공개한 The Swift Programming Language (Swift 5.7) 책의 Types 부분1을 번역하고, 설명이 필요한 부분은 주석을 달아서 정리한 글입니다. 전체 번역은 Swift 5.7: Swift Programming Language (스위프트 프로그래밍 언어) 에서 확인할 수 있습니다.

Types (타입)

스위프트에는, 두 가지 종류의 타입이 있는데: 이름 붙인 타입과 복합 타입이 그것입니다. 이름 붙인 타입 (named type) 은 정의할 때 한 특별한 이름을 줄 수 있는 타입입니다. 이름 붙인 타입은 클래스와, 구조체, 열거체, 및 프로토콜을 포함합니다. 예를 들어, MyClass 라는 이름으로 사용자가-정의한 클래스의 인스턴스는 MyClass 라는 타입을 가집니다. 사용자가-정의하는 이름 붙인 타입에 더하여, 스위프트 표준 라이브러리는 흔히 사용하는 수많은 이름 붙인 타입을 정의하는데, 배열과, 딕셔너리, 및 옵셔널 값을 나타내는 것도 포함합니다.

다른 언어에선 보통 기초 또는 근원으로 고려하는-수치 값, 문자, 및 문자열 같은-자료 (data) 타입은 실제론 이름 붙인 타입으로, 스위프트 표준 라이브러리 안에서 구조체로 정의 및 구현되어 있습니다. 이름 붙인 타입이기 때문에, Extensions (익스텐션; 확장)Extension Declaration (익스텐션 선언) 에서 논한 것처럼, 프로그램의 필요에 적합하도록, 익스텐션 선언으로, 이들의 동작을 확장할 수 있습니다.

복합 타입 (compound types) 은 이름이 없는 타입으로, 스위프트 언어 그 자체에 정의되어 있습니다. 두 가지의 복합 타입이 있는데: 함수 타입과 튜플 타입이 그것입니다. 복합 타입 안에 이름 붙인 타입과 다른 복합 타입이 담겨 있을 수도 있습니다. 예를 들어, 튜플 타입인 (Int, (Int, Int)) 는 두 원소를 담고 있는데: 첫 번째는 이름 붙인 타입인 Int 이고, 두 번째는 또 다른 복합 타입인 (Int, Int) 입니다.

이름 붙인 타입이나 복합 타입 주위에 괄호를 둘 수 있습니다. 하지만, 타입 주위에 괄호를 추가한다고 어떤 효과가 있는 건 아닙니다. 예를 들어, (Int)Int 와 같은 겁니다.

이 장은 스위프트 언어 자체가 정의하는 타입을 논의하고 스위프트의 타입 추론 동작도 설명합니다.

GRAMMAR OF A TYPE 부분 생략 - 링크

Type Annotation (타입 보조 설명)

타입 보조 설명 (type annotation) 은 변수 또는 표현식의 타입을 명시적으로 지정합니다. 타입 보조 설명은, 다음 예제에서 보듯, 콜론 (:) 으로 시작해서 타입으로 끝납니다:

let someTuple: (Double, Double) = (3.14159, 2.71828)
func someFunction(a : Int) { /* ... */ }

첫 번째 예제에선, someTuple 표현식이 (Double, Double) 이라는 튜플 타입을 가진다고 지정합니다. 두 번째 예제에선, someFunction 함수의 매개 변수 aInt 타입을 가진다고 지정합니다.

타입 보조 설명은 타입 앞에 타입 특성 목록을 옵션으로 담을 수 있습니다.2

GRAMMAR OF A TYPE ANNOTATION 부분 생략 - 링크

Type Identifier (타입 식별자)

타입 식별자 (type identifier) 는 이름 붙인 타입 또는 이름 붙거나 복합인 타입의 타입 별명을 가리킵니다.

대부분의 시간 동안, 타입 식별자는 식별자와 동일한 이름을 가진 이름 붙인 타입을 직접적으로 가리킵니다. 예를 들어, Int 라는 타입 식별자는 Int 라고 이름 붙인 타입을 직접적으로 가리키며, Dictionary <String, Int> 라는 타입 식별자는 Dictionary <String, Int> 라고 이름 붙인 타입을 직접적으로 가리킵니다.

타입 식별자가 동일한 이름의 타입을 가리키지 않는 경우가 두 가지 있습니다. 첫 번째 경우는, 타입 식별자가 이름 붙거나 복합인 타입의 타입 별명을 가리키는 겁니다. 구체적인 사례로, 아래 예제의, 타입 보조 설명에서 사용한 Point(Int, Int) 라는 튜플 타입을 가리킵니다.

typealias Point = (Int, Int)
let origin: Point = (0, 0)

두 번째 경우는, 타입 식별자가 점 (.) 구문을 사용하여 다른 모듈에서 선언하거나 다른 타입 안에 중첩한 이름 붙인 타입을 가리키는 겁니다. 예를 들어, 다음 코드에 있는 타입 식별자는 ExampleModule 모듈 안에서 선언한 MyType 이라는 이름 붙인 타입을 참조합니다.

var someValue: ExampleModule.MyType

GRAMMAR OF A TYPE IDENTIFIER 부분 생략 - 링크

Tuple Type (튜플 타입)

튜플 타입 (tuple type) 은 쉼표로-구분한 타입 목록을, 괄호로 테두리 친 겁니다.

함수 반환 타입으로 튜플 타입을 사용하면 여러 값을 담은 단일 튜플로 함수 반환을 할 수 있습니다. 튜플 타입의 원소에 이름을 붙일 수도 있고 이 이름을 사용하여 개별 원소 값을 참조할 수도 있습니다. 원소 이름은 식별자와 그 바로 뒤의 콜론 (:) 으로 구성됩니다. 이러한 특징 둘 다를 실제로 보여주는 예제는, Functions with Multiple Return Values (반환 값이 여러 개인 함수) 부분을 보도록 합니다.

튜플 타입의 원소에 이름이 있을 땐, 그 이름도 타입의 일부입니다.

var someTuple = (top: 10, bottom: 12)   // someTuple 의 타입은 (top: Int, bottom: Int) 임
someTuple = (top: 4, bottom: 42)        // 괜찮음: 이름이 일치함
someTuple = (9, 99)                     // 괜찮음: 이름이 추론됨
someTuple = (left: 5, right: 5)         // 에러: 이름이 일치하지 않음

모든 튜플 타입엔 두 개 이상의 타입이 담겨 있지만, () 라는, 빈 튜플 타입의 타입 별명인 Void 는 예외입니다.

GRAMMAR OF A TUPLE TYPE 부분 생략 - 링크

Function Type (함수 타입)

함수 타입 (funtion type) 은 함수나, 메소드, 또는 클로저의 타입을 나타내며 화살표 (->) 로 구분된 매개 변수와 반환 타입으로 구성됩니다:

    (parameter type-매개 변수 타입) -> return type-반환 타입

매개 변수 타입 (parameter type) 은 쉼표로-구분한 타입 목록입니다. 반환 타입 (return type) 이 튜플 타입일 수 있기 때문에, 여러 개의 값을 반환하는 함수와 메소드도 지원합니다.

(어떤 타입 T 에 대한) 함수 타입 () -> T 의 매개 변수에 autoclosure 특성을 적용하면 자신을 호출한 쪽에서 암시적으로 클로저를 생성할 수 있습니다. 이는 함수를 호출할 때 클로저를 명시하지 않아도 표현식의 평가를 미루게 하는 구문상의 편의를 제공합니다. 자동 클로저 함수 타입 매개 변수의 예제는, Autoclosures (자동 클로저) 부분을 보도록 합니다.

함수 타입의 매개 변수 타입 (parameter type) 는 가변 (variadic) 매개 변수를 가질 수 있습니다. 구문상, 가변 매개 변수는 기초 타입 이름과 바로 뒤의 세 점 (...) 으로, Int... 처럼, 구성됩니다. 가변 매개 변수는 기초 타입 이름의 원소를 담은 배열로 취급합니다. 구체적인 사례로, 가변 매개 변수 Int...[Int] 로 취급합니다. 가변 매개 변수의 사용 예제는, Variadic Parameters (가변 매개 변수) 부분을 보도록 합니다.

입-출력 (in-out) 매개 변수를 지정하려면, 매개 변수 타입의 접두사로 inout 키워드를 앞에 붙입니다. 가변 매개 변수나 반환 타입은 inout 키워드로 표시할 수 없습니다. 입-출력 매개 변수는 In-Out Parameters (입-출력 매개 변수) 에서 논의합니다.

함수 타입에 매개 변수가 하나만 있는데 그 매개 변수 타입이 튜플 타입이면, 함수 타입을 작성할 때 튜플 타입에 반드시 괄호가 있어야 합니다. 예를 들어, ((Int, Int)) -> Void 는 단일한 튜플 타입 매개 변수인 (Int, Int) 를 취하고 어떤 값도 반환하지 않는 함수 타입입니다. 이와 대조하여, 괄호가 없는, (Int, Int) -> Void 는 두 개의 Int 매개 변수를 취하고 어떤 값도 반환하지 않는 함수 타입입니다. 마찬가지로, Void() 의 타입 별명이기 때문에, 함수 타입 (Void) -> Void 는-단일한 빈 튜플 인자를 취하는 함수인-(()) -> () 와 똑같습니다. 이 타입은-인자를 취하지 않는 함수인-() -> () 와는 똑같지 않습니다.3

함수와 메소드 안의 인자 이름은 해당 함수 타입의 일부분이 아닙니다.4 예를 들면 다음과 같습니다:

func someFunction(left: Int, right: Int) {}
func anotherFunction(left: Int, right: Int) {}
func functionWithDifferentLabels(top: Int, bottom: Int) {}

var f = someFunction // f 의 타입은 (Int, Int) -> Void 이지, (left: Int, right: Int) -> Void 가 아님.
f = anotherFunction              // 괜찮음
f = functionWithDifferentLabels  // 괜찮음

func functionWithDifferentArgumentTypes(left: Int, right: String) {}
f = functionWithDifferentArgumentTypes     // 에러

func functionWithDifferentNumberOfArguments(left: Int, right: Int, top: Int) {}
f = functionWithDifferentNumberOfArguments // 에러

인자 이름표는 함수 타입의 일부분이 아니기 때문에, 함수 타입을 작성할 때 생략합니다.

var operation: (lhs: Int, rhs: Int) -> Int     // 에러
var operation: (_ lhs: Int, _ rhs: Int) -> Int // 괜찮음
var operation: (Int, Int) -> Int               // 괜찮음

함수 타입이 하나 이상의 화살표 (->) 를 포함하고 있으면, 함수 타입을 오른쪽에서 왼쪽으로 그룹짓습니다. 예를 들어, (Int) -> (Int) -> Int 함수 타입은 (Int) -> ((Int) -> Int) 라고 이해합니다-즉, 함수가 취하는 건 Int 고 반환하는 건 Int 를 취하고 반환하는 또 다른 함수입니다.

에러를 던지거나 (throw) 다시 던질 수 있는 (rethrow) 함수 타입엔 반드시 throws 키워드를 표시해야 합니다. throws 키워드는 함수 타입의 일부분이며, 던지지 않는 함수는 던지는 함수의 하위 타입입니다. 그 결과, 던지는 함수를 쓸 수 있는 곳이면 던지지 않는 함수를 사용할 수 있습니다. 던지는 및 다시 던지는 함수는 Throwing Functions and Methods (던지는 함수와 메소드) 부분과 Rethrowing Functions and Methods (다시 던지는 함수와 메소드) 부분에서 설명합니다.

Restrictions for Nonescaping Closures (벗어나지 않는 클로저의 제약 사항)

매개 변수가 벗어나지 않는 함수면 Any 타입의 속성이나, 변수, 또는 상수에 저장할 수 없는데, 이들이 값이 벗어나게할 지도 모르기 때문입니다.

매개 변수가 벗어나지 않는 함수면 또 다른 벗어나지 않는 함수 매개 변수에 인자로 전달할 수 없습니다. 이런 제약 사항은 스위프트가 메모리 충돌 검사를 실행 시간 대신 컴파일 시간에 더 많이 하도록 돕습니다. 예를 들면 다음과 같습니다:

let external: (() -> Void) -> Void = { _ in () }
func takesTwoFunctions(first: (() -> Void) -> Void, second: (() -> Void) -> Void) {
  first { first {} }       // 에러
  second { second {} }     // 에러

  first { second {} }      // 에러
  second { first {} }      // 에러

  first { external {} }    // 괜찮음
  external { first {} }    // 괜찮음
}

위 코드에서, takesTwoFunctions(first:second:) 의 매개 변수는 둘 다 함수입니다. 어느 매개 변수도 @escaping 으로 표시하지 않아서, 그 결과 둘 다 벗어나지 않습니다.

위 예제에서 “에러 (Error)” 로 표시한 네 함수 호출은 컴파일러 에러를 일으킵니다. firstsecond 매개 변수는 벗어나지 않는 함수기 때문에, 이들을 또 다른 벗어나지 않는 함수 매개 변수의 인자로 전달할 순 없습니다. 이와 대조적으로, “괜찮음 (OK)” 으로 표시한 두 함수 호출은 컴파일러 에러를 일으키지 않습니다. 이러한 함수 호출은 제약 사항을 위반하지 않는데 이는 externaltakesTwoFunctions(first:second:) 의 매개 변수가 아니기 때문입니다.

이 제약 사항을 피할 필요가 있으면, 매개 변수 하나를 벗어난다고 표시하거나, 아니면 withoutActuallyEscaping(_:do:) 함수를 써서 벗어나지 않는 함수 매개 변수 중 하나를 임시로 벗어나는 함수로 변환합니다. 메모리 접근 충돌을 피하는 것에 대한 정보는, Memory Safety (메모리 안전성) 을 보도록 합니다.

GRAMMAR OF A FUNCTION TYPE 부분 생략 - 링크

Array Type (배열 타입)

스위프트 언어는 스위프트 표준 라이브러리의 Array<Element> 타입에 대해 다음 같은 수월한 구문을 제공합니다:

    [type-타입]

다른 말로 해서, 다음의 두 선언은 서로 같은 겁니다:

let someArray: Array<String> = ["Alex", "Brian", "Dave"]
let someArray: [String] = ["Alex", "Brian", "Dave"]

두 경우 모두, someArray 상수가 문자열 배열이라고 선언합니다. 배열 원소는 대괄호 안에 유효 색인 값을 지정하는 첨자를 통하여 접근할 수 있는데: someArray[0] 는 0번 색인의 원소인, "Alex" 를, 참조합니다.

다차원 배열을 생성하려면 대괄호 쌍을 중첩하면 되는데, 여기서 가장 안쪽 대괄호 쌍 안에 원소의 기초 타입 이름을 담습니다. 예를 들어, 세 쌍의 대괄로로 3-차원 정수 배열을 생성할 수 있습니다:

var array3D: [[[Int]]] = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]

다차원 배열 안의 원소에 접근할 땐, 가장-왼쪽 첨자의 색인이 가장 바깥쪽 배열 색인에 있는 원소를 참조합니다. 오른쪽으로 그 다음 첨자의 색인은 한 수준 안에 중첩된 배열 색인에 있는 원소를 참조합니다. 그렇게 쭉 계속됩니다. 이는 위 예제에서, array3D[0][[1, 2], [3, 4]] 를 참조하고, array3D[0][1][3, 4] 를 참조하며, array3D[0][1][1] 은 4 라는 값을 참조한다는 걸 의미합니다.

스위프트 표준 라이브러리의 Array 타입에 대한 자세한 논의는, Arrays (배열) 부분을 보도록 합니다.

GRAMMAR OF A ARRAY TYPE 부분 생략 - 링크

Dictionary Type (딕셔너리 타입)

스위프트 언어는 스위프트 표준 라이브러리의 Dictionary<Key, Value> 타입에 대해 다음 같은 수월한 구문을 제공합니다:

    [key type-키 타입: value type-값 타입]

다른 말로 해서, 다음의 두 선언은 서로 같은 겁니다:

let someDictionary: [String: Int] = ["Alex": 31, "Paul": 39]
let someDictionary: Dictionary<String, Int> = ["Alex": 31, "Paul": 39]

두 경우 모두, someDictionary 상수가 키는 문자열이고 값은 정수인 딕셔너리라고 선언합니다.

딕셔너리의 값은 대괄호 안에 해당 키를 지정하는 첨자를 통하여 접근할 수 있는데: someDictionary["Alex"]"Alex" 키와 결합된 값을 참조합니다. 첨자는 딕셔너리의 값 타입에 대한 옵셔널 값을 반환합니다. 딕셔너리 안에 지정한 키가 담겨 있지 않으면, 첨자가 nil 을 반환합니다.

딕셔너리의 키 타입은 스위프트 표준 라이브러리의 Hashable 프로토콜을 반드시 준수해야 합니다.

스위프트 표준 라이브러리의 Dictionary 타입에 대한 자세한 논의는, Dictionaries (딕셔너리) 부분을 보도록 합니다.

GRAMMAR OF A DICTIONARY TYPE 부분 생략 - 링크

Optional Type (옵셔널 타입)

스위프트 언어는, 스위프트 표준 라이브러리 안에서, Optional<Wrapped> 라는 이름 붙인 타입의 수월한 구문으로 접미사 ? 를 정의합니다. 다른 말로 해서, 다음의 두 선언은 서로 같은 겁니다:

var optionalInteger: Int?
var optionalInteger: Optional<Int>

두 경우 모두, optionalInteger 변수가 옵셔널 정수 타입을 가진다고 선언합니다. 타입과 ? 사이엔 공백이 없다는 걸 기억하기 바랍니다.

Optional<Wrapped> 타입은, nonesome(Wrapped) 라는, 두 case 값을 가진 열거체로써, 이를 사용하여 있을 수도 있고 없을 수도 있는 값을 나타냅니다. 어떤 타입이든 옵셔널 타입이라고 명시적으로 선언 (하거나 암시적으로 변환) 할 수 있습니다. 옵셔널 변수나 속성을 선언할 때 초기 값을 제공하지 않으면, 그 기본 값은 자동으로 nil 이 됩니다.

옵셔널 타입의 인스턴스예 값이 담겨 있으면, 밑에서 보는 것처럼, 접미사 연산자 ! 로 그 값에 접근할 수 있습니다:

optionalInteger = 42
optionalInteger! // 42

! 연산자를 사용하여 nil 값을 가진 옵셔널의 포장을 풀면 실행 시간 에러가 됩니다.

옵셔널 사슬과 옵셔널 연결을 사용하여 옵셔널 표현식에 대한 연산을 조건부로 수행할 수도 있습니다. 값이 nil 이면, 연산 수행을 안하며 따라서 실행 시간 에러도 만들지 않습니다.

옵셔널 타입의 더 많은 정보와 사용 방법 예ㄴ제를 보려면, Optionals (옵셔널) 부분을 보도록 합니다.

GRAMMAR OF A OPTIONAL TYPE 부분 생략 - 링크

Implicitly Unwrapped Optional Type (암시적으로 포장 푸는 옵셔널 타입)

스위프트 언어는, 스위프트 표준 라이브러리 안에서, Optional<Wrapped> 라는 이름 붙인 타입의 수월한 구문으로 접미사 ! 를 정의하는데, 접근할 때 자동으로 포장을 푸는 추가적인 동작을 가집니다. 사용하려는 암시적으로 포장 푸는 옵셔널이 nil 값이면, 실행 시간 에러를 가질 겁니다. 암시적으로 포장 푸는 동작을 예외로 하면, 다음의 두 선언은 서로 같은 겁니다:

var implicitlyUnwrappedString: String!
var explicitlyUnwrappedString: Optional<String>

타입과 ! 사이에는 공백이 없음을 기억하기 바랍니다.

암시적으로 포장을 풀면 그 타입을 담은 선언의 의미를 바꾸기 때문에, 딕셔너리나 배열 원소 타입 같이-튜플 타입이나 일반화 타입 안에 중첩된 옵셔널 타입은 암시적으로 포장을 푼다고 표시할 수 없습니다. 예를 들면 다음과 같습니다:

let tupleOfImplicitlyUnwrappedElements: (Int!, Int!)  // 에러
let implicitlyUnwrappedTuple: (Int, Int)!             // 괜찮음

let arrayOfImplicitlyUnwrappedElements: [Int!]        // 에러
let implicitlyUnwrappedArray: [Int]!                  // 괜찮음

암시적으로 포장 푸는 옵셔널은 옵셔널 값과 똑같은 Optional<Wrapped> 타입이기 때문에, 옵셔널을 사용할 수 있는 코드와 동일한 모든 곳에서 암시적으로 포장 푸는 옵셔널을 사용할 수 있습니다. 예를 들어, 암시적으로 포장 푸는 옵셔널 값을 옵셔널 변수와, 상수, 및 속성에 할당할 수도 있고, 그 반대도 마찬가지입니다.

옵셔널 처럼, 암시적으로 포장 푸는 옵셔널 변수나 속성을 선언할 때 초기 값을 제공하지 않으면, 그 기본 값은 자동으로 nil 이 됩니다.

옵셔널 사슬을 사용하여 암시적으로 포장 푸는 옵셔널 표현식에 대한 연산을 조건부로 수행합니다. 값이 nil 이면, 연산 수행을 안하며 따라서 실행 시간 에러도 만들지 않습니다.

암시적으로 포장 푸는 옵셔널 타입에 대한 더 많은 정보는, implicitly Unwrapped Optionals (암시적으로 포장 푸는 옵셔널) 부분을 보도록 합니다.

GRAMMAR OF AN IMPLICITLY UNWRAPPED OPTIONAL TYPE 부분 생략 - 링크

Protocol Composition Type (프로토콜 합성 타입)

프로토콜 합성 타입 (protocol composition type) 은 지정한 프로토콜 목록 안의 각 프로토콜을 준수한 타입, 또는 주어진 클래스의 하위 클래스면서 지정한 프로토콜 목록 안의 각 프로토콜을 준수한 타입을, 정의합니다. 타입 보조 설명과, 일반화 매개 변수 절, 및 일반화 where 절 안의 타입을 지정할 때만 프로토콜 합성 타입을 사용할 수도 있습니다.

프로토콜 합성 타입의 형식은 다음과 같습니다:

    Protocol 1-프로토콜 1 & Protocol 2-프로토콜 2

프로토콜 합성 타입은 여러 프로토콜의 필수 조건을 준수한 타입의 값을, 타입이 준수하고자 하는 각각의 프로토콜을 상속한 새로운, 이름 붙인 프로토콜을 명시하여 정의하지 않고도, 지정하는 걸 허용합니다. 예를 들어, ProtocolA 와, ProtocolB, 및 ProtocolC 를 상속한 새로운 프로토콜을 선언하는 대신 ProtocolA & ProtocolB & ProtocolC 라는 프로토콜 합성 타입을 사용할 수 있습니다. 마찬가지로, SuperClass 의 하위 클래스면서 ProtocolA 도 준수하는 새로운 프로토콜을 선언하는 대신 SuperClass & ProtocolA 를 사용할 수 있습니다.

프로토콜 합성 목록 안의 각 항목은 다음 중 하나이며; 최대 한 개의 클래스를 목록에 담을 수 있습니다:

프로토콜 합성 타입에 타입 별명을 담을 땐, 정의 안에 동일한 프로토콜이 한 번 이상 나타내는 게 가능합니다-중복한 건 무시합니다. 예를 들어, 아래 코드의 PQR 정의는 P & Q & R 과 같은 겁니다.

typealias PQ = P & Q
typealias PQR = PQ & Q & R

GRAMMAR OF A PROTOCOL COMPOSITION TYPE 부분 생략 - 링크

Opaque Type (불투명 타입)

불투명 타입 (opaque type) 은, 실제 고정 타입의 지정 없이, 프로토콜이나 프로토콜 합성을 준수한 타입을 정의합니다.

불투명 타입은 함수나 첨자 연산의 반환 타입, 또는 속성의 타입으로 나타납니다. 불투명 타입은, 배열의 원소 타입이나 옵셔널로 포장한 타입 같은, 튜플 타입 또는 일반화 타입의 일부분으로 나타날 수 없습니다.

불투명 타입의 형식은 다음과 같습니다:

    some constraint-구속 조건

구속 조건 (constraint) 은 클래스 타입이나, 프로토콜 타입, 프로토콜 합성 타입, 또는 Any 입니다. 나열한 프로토콜이나 프로토콜 합성을 준수한, 또는 나열한 클래스를 상속한 타입의 인스턴스인 값만 불투명 타입의 인스턴스로 사용할 수 있습니다. 불투명 값과 상호 작용하는 코드는 구속 조건 (constraint) 에서 정의한 방식의 인터페이스로만 값을 사용할 수 있습니다.

프로토콜 선언은 불투명 타입을 포함할 수 없습니다. 클래스는 최종이 아닌 메소드의 반환 타입으로 불투명 타입을 사용할 수 없습니다.

자신의 반환 타입으로 불투명 타입을 사용한 함수는 반드시 단일한 실제 타입을 공유하는 값을 반환해야 합니다.5 반환 타입은 함수의 일반화 타입 매개 변수 부분인 타입을 포함할 수 있습니다. 예를 들어, 함수 someFunction<T>()TDictionary<String, T> 타입의 값을 반환할 수 있습니다.

GRAMMAR OF AN OPAQUE TYPE 부분 생략 - 링크

Metatype Type (메타타입 타입)

메타타입 타입 (metatype type) 은, 클래스 타입과, 구조체 타입, 열거체 타입, 및 프로토콜 타입을 포함한, 어떤 타입에 대한 타입을 가리킵니다.

클래스나, 구조체, 또는 열거체 타입의 메타타입은 그 타입 이름 뒤에 .Type 이 붙은 겁니다. 실행 시간에 프로토콜을 준수한 고정 타입이 아닌-프로토콜 (자체) 타입의 메타타입은 그 프로토콜 이름 뒤에 .Protocol 이 붙은 겁니다. 예를 들어, 클래스 타입인 SomeClass 의 메타타입은 SomeClass.Type 이고 프로토콜인 SomeProtocol 의 메타타입은 SomeProtocol.Protocol 입니다.

self 접미사 표현식을 사용하면 타입을 값으로 접근할 수 있습니다. 예를 들어, SomeClass.self 는, SomeClass 의 인스턴스가 아닌, SomeClass 그 자체를 반환합니다. 그리고 SomeProtocol.self 는, 실행 시간에 SomeProtocol 을 준수한 타입의 인스턴스가 아닌, SomeProtocol 그 자체를 반환합니다. 다음 예제에서 보는 것처럼, type(of:) 함수를 타입의 인스턴스로 호출하면 그 인스턴스의 동적인, 실행 시간 타입을 값으로 접근할 수 있습니다:

class SomeBaseClass {
  class func printClassName() {
    print("SomeBaseClass")
  }
}
class SomeSubClass: SomeBaseClass {
  override class func printClassName() {
    print("SomeSubClass")
  }
}
let someInstance: SomeBaseClass = SomeSubClass()
// someInstance 의 컴파일-시간 타입은 SomeBaseClass 이고,
// someInstance 의 실행-시간 타입은 SomeSubClass 임
type(of: someInstance).printClassName()
// "SomeSubClass" 를 인쇄함

더 많은 정보는, 스위프트 표준 라이브러리 안의 type(of:) 항목을 보도록 합니다.

그 타입의 메타타입 값으로 타입의 인스턴스를 생성하려면 초기자 표현식6 을 사용합니다. 클래스 인스턴스라면, 호출한 초기자를 반드시 required 키워드로 표시하거나 전체 클래스를 final 키워드로 표시해야 합니다.

class AnotherSubClass: SomeBaseClass {
  let string: String
  required init(string: String) {
    self.string = string
  }
  override class func printClassName() {
    print("AnotherSubClass")
  }
}
let metatype: AnotherSubClass.Type = AnotherSubClass.self
let anotherInstance = metatype.init(string: "some string")

GRAMMAR OF A METATYPE TYPE 부분 생략 - 링크

Any Type (Any 타입)

Any 타입은 다른 모든 타입 값을 담을 수 있습니다. Any 는 다음에 있는 어떤 타입의 인스턴스에 대한 고정 타입으로도 사용할 수 있습니다.

let mixed: [Any] = ["one", 2, true, (a, 5.3), { () -> Int in return 6 }]

Any 를 인스턴스의 고정 타입으로 쓸 땐, 그것의 속성이나 메소드에 접근하기 전에 알려진 타입으로 인스턴스를 변환할 필요가 있습니다. 고정 타입이 Any 인 인스턴스는 자신의 원본 동적 타입을 유지하며-as 나, as?, 및 as!-타입-변환 연산자로 그 타입으로 변환할 수 있습니다. 예를 들어, 혼성 배열7 의 첫 번째 객체를 String 으로 조건부 내림 변환하려면 다음 처럼 as? 를 사용합니다:

if let first = mixed.first as? String {
  print("The first item, '\(first)', is a string.")
}
// "The first item, 'one', is a string." 을 인쇄함

변환 (casting) 에 대한 더 많은 정보는, Type Casting (타입 변환) 을 보도록 합니다.

AnyObject 프로토콜은 Any 타입과 비슷합니다. 모든 클래스는 암시적으로 AnyObject 를 준수합니다. 언어에서 정의하는, Any 와 달리, AnyObject 는 스위프트 표준 라이브러리에서 정의합니다. 더 많은 정보는, Class-Only Protocols (클래스-전용 프로토콜) 부분과 AnyObject 항목을 보도록 합니다.

GRAMMAR OF AN ANY TYPE 부분 생략 - 링크

Self Type (Self 타입)

Self 타입은 정해진 타입이라기 보단, 현재 타입을 그 타입의 이름을 반복하거나 알지 않고도 편리하게 가리키게 해주는 겁니다.

프로토콜 선언이나 프로토콜 멤버 선언 안의, Self 타입은 프로토콜을 준수하는 최종 결과 타입을 가리킵니다.

구조체나, 클래스, 및 열거체 선언 안의, Self 타입은 선언이 도입한 타입을 가리킵니다. 타입 멤버 선언 안의, Self 타입은 그 타입을 가리킵니다. 클래스 선언의 멤버 안에선, Self 가 다음으로만 있을 수 있습니다:

예를 들어, 아래 코드는 반환 타입이 Self 인 인스턴스 메소드 f 를 보여줍니다:

class Superclass {
  func f() -> Self { return self }
}
let x = Superclass()
print(type(of: x.f()))
// "Superclass" 를 인쇄함

class Subclass: Superclass { }
let y = Subclass()
print(type(of: y.f()))
// "Subclass" 를 인쇄함

let z: Superclass = Subclass()
print(type(of: z.f()))
// "Subclass" 를 인쇄함

위 예제의 마지막 부분은 Self 가, 변수 자체의 컴파일-시간 타입인 Superclass 가 아닌, z 값의 실행 시간 타입인 Subclass 를 가리킨다는 걸 보여줍니다.

중첩 타입 선언 안에선, Self 타입이 가장 안쪽의 타입 선언이 도입한 타입을 가리킵니다.

Self 타입은 스위프트 표준 라이브러리의 type(of:) 함수와 동일한 타입을 가리킵니다.8 Self.someStaticMember 라고 써서 현재 타입의 멤버에 접근하는 건 type(of: self).someStaticMember 라고 쓰는 것과 똑같습니다.

GRAMMAR OF A SELF TYPE 부분 생략 - 링크

Type Inheritance Clause (타입 상속 절)

타입 상속 절 (type inheritance clause) 은 이름 붙인 타입이 상속하는 클래스와 이름 붙인 타입이 준수하는 프로토콜이 어느 것인지 지정하는데 사용합니다. 타입 상속 절은 콜론 (:) 으로 시작하고, 그 뒤에 타입 식별자 목록을 둡니다.

클래스 타입은 단일한 상위 클래스를 상속하며 프로토콜은 어떤 개수든 준수할 수 있습니다. 클래스를 정의할 땐, 상위 클래스 이름이 반드시 타입 식별자 목록 첫 번째에 있어야 하며, 그 뒤로 클래스가 준수해야 할 프로토콜은 어떤 개수든 둘 수 있습니다. 클래스가 또 다른 클래스를 상속하지 않으면, 목록의 시작을 프로토콜로 대신할 수 있습니다. 클래스 상속에 대한 논의 확장 및 여러 예제들은, Inheritance (상속) 장을 보도록 합니다.

다른 이름 붙인 타입9 들은 프로토콜 목록만 상속 또는 준수할 수 있습니다. 프로토콜 타입은 다른 프로토콜을 어떤 개수든 상속할 수 있습니다. 프로토콜 타입이 다른 프로토콜을 상속할 땐, 다른 프로토콜에 있던 필수 조건 집합을 한군데로 모으며, 현재 프로토콜을 상속한 어떤 타입이든 반드시 그 모든 필수 조건을 다 준수해야 합니다.

열거체 정의의 타입 상속 절은 프로토콜 목록이거나, 자신의 case 값에 원시 값을 할당하는 열거체인 경우, 그 원시 값의 타입을 지정하는 단일한, 이름 붙인 타입일 수 있습니다.10 타입 상속 절을 사용하여 자신의 원시 값 타입을 지정하는 열거체 정의 예제는, Raw Values (원시 값) 부분을 보도록 합니다.

GRAMMAR OF A TYPE INHERITANCE CLAUSE 부분 생략 - 링크

Type Inference (타입 추론)

스위프트는 타입 추론 (type inference) 을 광범위하게 사용하여, 코드의 수많은 변수와 표현식에서 타입 또는 타입 일부분을 생략하는 걸 허용합니다. 예를 들어, var x: Int = 0 라고 쓰는 대신, 타입을 완전히 생략하여, var x = 0 라고 쓸 수 있습니다-컴파일러는 xInt 타입 값의 이름이라는 걸 올바로 추론합니다. 이와 비슷하게, 상황으로 전체 타입을 추론할 수 있을 땐 타입 일부분을 생략할 수 있습니다. 예를 들어, let dict: Dictionary = ["A": 1] 이라고 쓰면, 컴파일러가 dict 의 타입이 Dictionary<String, Int> 라고 추론합니다.

위의 두 예제 모두, 표현식 트리 (tree) 의 말단 (leaves) 부터 근원 (root) 까지 타입 정보를 전달합니다. 즉, var x: Int = 0 에 있는 x 의 타입은 0 의 타입을 첫 번째로 검사한 다음 이 타입 정보를 (변수 x 라는) 근원까지 전달함으로써 추론합니다.

스위프트의, 타입 정보는-근원에서 말단까지-반대 방향으로 흐를 수도 있습니다. 다음 예제에서, 구체적 사례로, eFloat 상수 절의 명시적 타입 보조 설명 (인 : Float) 은 수치 글자 값 2.71828 의 추론 타입이 Double 대신 Float 타입이도록 합니다.

let e = 2.71828 // e 의 타입을 Double 로 추론합니다.
let eFloat: Float = 2.71828 // eFloat 의 타입은 Float 입니다.

스위프트의 타입 추론은 단일한 표현식 또는 구문 수준 연산입니다. 이는 표현식에서 생략한 타입이나 타입 일부분의 추론에 필요한 모든 정보는 반드시 표현식이나 자신의 하위 표현식의 타입-검사에서 접근 가능해야 한다는 의미입니다.

다음 장

Expressions (표현식) >

참고 자료

  1. 전체 원문은 Types에서 확인할 수 있습니다. 

  2. : @escaping (Int) -> Int 같이 : (Int) -> Int 앞에 @escaping 등을 둘 수 있다는 의미입니다. 

  3. (Void) -> Void(()) -> () 와 똑같은 타입이고, () -> Void() -> () 와 똑같은 타입입니다. 참고로 스위프트엔 Void -> Void 라는 함수 타입이 없으며, 이러면 에러가 발생합니다. 

  4. 앞서 본 것처럼, 튜플 타입은 이렇지 않습니다. 

  5. 함수 안의 모든 return 문으로 반환하는 타입이 모두 똑같아야 한다는 의미입니다. 

  6. ‘초기자 표현식 (initializer expression)’ 이란 타입에 있는 초기자를 .init 를 이용하여 명시적으로 호출하는 표현식을 말합니다. ‘초기자 표현식’ 에 대한 더 자세한 정보는, Initializer Expression (초기자 표현식) 부분을 보도록 합니다. 

  7. ‘혼성 배열 (heterogeneous array)’ 은 배열 안의 객체들이 모두 다른 클래스지만, 공통의 상위 클래스를 가진 배열을 의미합니다. (이 정의는 OOP 를 기준으로 한 것이라, 클래스보다는 구조체를 많이 사용하는 스위프트에서는 의미가 조금 다를 것 같습니다. 스위프트라면 공통의 상위 클래스보단 공통으로 준수한 프로토콜이 될 것 입니다.) 혼성 배열에 대한 더 자세한 정보는, ‘MathWorks’ 의 Designing Heterogeneous Class Hierarchies 항목을 보도록 합니다. 

  8. 이 설명만 보면, type(of:) 함수를 호출하면 되므로, Self 가 필요 없을 것 같습니다. 하지만 type(of:) 함수는 컴파일 시간에 호출하는 게 아닙니다. f() 함수의 반환 값은 컴파일 시간에 정해야 하는데, 이 때는 type(of:) 함수를 호출할 수 없습니다. 즉, Self 는 컴파일 시간에 현재 타입의 동적 타입을 지정하기 위해 사용하는 겁니다. type(of:) 함수에 대한 더 자세한 내용은 Apple 개발자 문서의 type(of:) 항목을 보도록 합니다. 

  9. 여기서 말하는 ‘다른 이름 붙인 타입 (other named types)’ 은, 방금 전에 설명한 클래스를 제외한, 구조체와, 열거체, 및 프로토콜을 말합니다. 즉, 클래스 상속은 클래스만 할 수 있고, 클래스를 제외한 다른 타입들은 프로토콜만 상속할 수 있다는 의미입니다. 

  10. enum MyEnumeration: A { ... } 라고 했을 때, A 는 프로토콜이거나, 원시 값 (raw value) 타입이라는 의미입니다.