DADAHAE's Log
비싼 장난감 가지고 노는 중 (❁´▽`❁)*✲゚*
[TIL] 22-10-04: Swift(Error Handling, Type, Property Observer, enum, Generic, Protocol Oriented)
22-10-04 화요일
4주차

 

 

Swift

error handling
  • PLAN B
  • 대안, 비상계획

에러 핸들링 이해하기

  • 앞서 배운 것
    • 프로퍼티 래퍼
    • 배열, 딕셔너리 컬렉션 (+ Set)
  • 이번에 배울 것
    • Swift를 이용한 에러 핸들링
    • error type
    • throwing method와 함수(function)
    • guarddefer 구문
    • do-catch 구문

 

1.  Complie 불가

2.  작동 중 오류, runtime

  • Any

3.  논리적 오류

  • 예상치 못한 에러를 발생함.

 

 


 

 

✔️ Error Handling

 

Intro

 Error = 오류
: 제대로 된, 기대한 상황이 아닐 때

Swift 코드를 아무리 신중하게 설계하고 구현했다 해도 앱은 통제할 수 없는 상황은 언제든지 발생할 것이다.

  • 활성화된 인터넷 연결을 기반으로 동작하는 앱은 아이폰이 네트워크 신호를 잃는 것을 제어할 수 없다.
  • 사용자가 비행기 모드(airplane mode)를 바활성화하는 것도 막을 수 없다.

앱이 해낼 수 있는 것은 그러한 에러를 확실하게 처리하도록 구현하는 것이다.

  • 앱을 계속 사용하려면 활성화된 인터넷 연결이 필요하다는 것을 사용자가 알 수 있도록 메시지를 표시할 수 있다.

📌 Swift에서 에러를 처리하는 2가지 단계

1.  iOS 앱의 메서드 안에서 원하는 결과가 나오지 않을 경우 에러 발생(throwing)

2.  메서드가 던진(throwing) 에러를 잡아서 처리하기

 

  • 에러를 던질 경우, 해당 에러의 특성을 식별하여 취할 수 있는 가장 적절한 동작을 결정하는 데 사용되는 특정 에러 타입 중 하나가 될 것이다.
  • 에러 타입 값은 Error 프로토콜을 따르는 모든 값이 될 수 있다.

 

  • 앱 내의 메서드에서 에러를 던지도록(throw) 구현하는 것도 중요하지만, iOS SDK의 많은 API 메서드(특히, 파일 처리와 관련된 메서드)도 앱 코드 내에서 처리되어야 할 에러를 던진다는 것도 알아두어야 한다.

 

 

에러 타입 선언하기

Representing and Throwing Errors

예) 원격 서버에 파일을 전송하는 메서드가 있다고 하자.

이 메서드는 여러 원인으로 파일 전송에 실패할 가능성이 있다.

  • 네트워크 연결이 없거나 느림
  • 전송할 파일을 차지 못함

이러한 모든 에러는 다음과 같이 Error 프로토콜을 따르는 열거형 내에서 표현되도록 할 수 있다.

아래와 같이 에러 타입(error type)을 선언하면 에러가 발생했을 때 사용할 수 있다.

enum FileTransferError: Error {
	case noConnection
	case lowBandwidth
	case fileNotFound
}

위 value 들 중에서 하나만 값으로 가진다.

 

 

 

에러 던지기

Throwing
오류 상황을 발생시킨다.
  • throws 키워드
    • 메서드나 함수가 에러를 던질 수 있다는 것을 선언할 때 사용
func transferFile() throws {  // throws는 protocol 아니다. keyword 이다!
}

위 함수는 오류가 발생할 수 있음을 표기한다.

 

  • 결과를 반환하는 메서드나 함수의 경우 throws 키워드는 반환타입 앞에 위치한다.
func transferFile() throws -> Bool {
}

 

  • 오류가 발생할 수 있도록 메서드를 선언했으니 오류가 발생할 때 에러를 던지는 코드를 추가할 수 있다.
  • throw 구문 + guard 구문을 결합하여 사용할 수 있다.

아래의 코드는 상태 값으로 제공되는 상수들을 선언한 다음, 메서드에 대한 guard 동작과 throw 동작을 구현하였다.

let connectionOK = true
let connectionSpeed = 30.00
let fileFound = false

enum FileTransferError: Error {  // 오류 이름
	case noConnection    // 구체적 상황
	case lowBandwidth
	case fileNotFound
}

func trnasferFile() throws {
	guard connectionOK else {
		throw FileTransferError.noConnection
	}

	guard connectionOK else {
		throw FileTransferError.lowBandwidth
	}

	guard connectionOK else {
		throw FileTransferError.fileNotFound
	}
}
  • guard 구문은 각 조건이 참인지 검사
    • 만약 거짓이라면 else 구문에 포함된 코드가 실행

 

 

오류 상황을 알려주는 메서드와 함수 호출하기

메서드 또는 함수가 오류를 발생할 수 있도록 선언했다면(throw) 일반적인 방법으로는 호출할 수 없다.

  • try 구문
try trnasferFile()

이 함수는 실패할 확룔(오류)이 있다는 뜻이다.

그러니까 도전! 이라는 키워드를 붙이는 거다. 실패할 수도 있으니까.

 

 

  • do-catch 구문

error만을 위한 제어문이다.

func sendFile() -> String {
	do {
		try fileTransfer()
	} catch FileTransferError.noConnection {
		return "No Network Connection"
	} catch FileTransferError.lowBandwitdh {
		return "File Transfer Speed too low"
	} catch FileTrnasferError.fileNotFound {
		return "File not Found"
	} catch {
		return "Unknown error"
	}
	
	return "Successful transfer"
}

return 이후는 실행이 안된다.

  • sendFile 메서드는 여러 에러에 대하여 해당 에러에 대한 설명을 담고 있는 문자열 값을 반환한다.
  • 네번재 catch 절은 에러에 대한 패턴 매칭이 이뤄지지 않은 ‘catch-all’ 구문이다.

 

 

에러 객체에 접근하기

메서드 호출이 실패하면 반드시 실패한 원인을 구별할 수 있는 NSError 객체가 반환될 것이다.

  • catch 구문에서 가장 필요한 것
    • 이 객체에 대해 접근하여 앱 코드 내에서 취할 수 있는 가장 적절한 동작을 실시하는 것
do {
	try filemgr.createDirectory(atPath:newDir, withIntermediateDiretories: true, attributes: nil)
} catch let error {
	print("Error: \\(error.localizedDescription)")
}
  • let error 에서 error는 NSError 타입이다.
  • error.localizedDescription 은 property로 에러 내용을 확인할 수 있다.

위 코드는 새로운 파일 시스템 디렉터리를 생성하고자 할 때 catch 구문 내에서 에러 객체에 접근하는 방법을 보여준다.

근데 이런 코드보다는 앞의 do-catch 구문으로 각각의 상황에 대처하는 코드를 더 많이 쓴다. 오히려 개발 단계에서 더 많이 쓴다.

 

 

에러 캐칭 비활성화하기

DANGER
웬만하면 쓰지말 것.

try! 구문을 사용하면 do-catch 구문 내에서 메서드가 호출되도록 감싸지 않아도 throwing message 가 강제로 실행된다.

 

do-catch 메서드로 일일이 해주기 귀찮아잉.

강제 언래핑 할래~ → 하지마

 

  • 컴파일러에게 이 메서드 호출은 어떠한 에러도 발생하지 않을 것이라고 알려주는 것과 동일함
  • 이렇게 사용했는데도 에러가 발생한다면 런타임 에러가 될 것이다.

 

그러므로 가급적 사용하지 않도록 하자.

try! trnasferFile()

처음부터 느낌표를 안쓰고 계속 해내는 것이 중요하다.

냅두고 좀있다 한다? 십중팔구 안함. 정석대로 가자.

 

 

defer 구문

앞에서 구현한 sendFile 메서드는 에러를 처리하는 일반적인 시나리오를 보여준다.

 

do-catch 구문에 있는 각각의 catch 절은 호출하는 메서드에게 제어권을 반환하기 위해 return 구문을 포함하였다

But, 에러의 종류와는 상관없이 제어권을 반환하기 전에 어떠한 별도의 작업을 수행하는 것이 더 효과적인 경우가 있을 수 있다.

  • 예를 들어 sendFile 메서드에서 제어권을 반환하기 전에 임시 파일들을 지워야 할 경우(사태 수습)가 발생할 수 있다.
  • instagram에 원본 사진에 필터를 씌워서 올릴 때, 남겨진 원본 파일은 그대로 두나?
  • nono. 용량때매 삭제해줘야함

이것을 defer 구문을 이용하면 가능하다.

 

defer 구문은 메서드가 결과를 반환하기 직전에 실행되어야 하는 일련의 코드를 지정할 수 있게 해준다.

func sendFile() -> String {
	defer {     // return 직전에 수행된다.
		removeTmpFiles()
		closeConnection()
	}

	do {
		try fileTransfer()
	} catch FileTransferError.noConnection {
		return "No Network Connection"
	} catch FileTransferError.lowBandwitdh {
		return "File Transfer Speed too low"
	} catch FileTrnasferError.fileNotFound {
		return "File not Found"
	} catch {
		return "Unknown error"
	}
	
	return "Successful transfer"
}

이 메서드가 어떤 반환을 하든지 제어권을 반환하기 전에 removeTmpFiles 메서드와 closeConnection 메서드가 항상 호출될 것이다.

  • return 구문이 있어야만 수행되는 것이 아니다. ‘제어권을 반환할 때’ 이다.
  • 즉, 함수가 종료될 때 무조건 실행된다.

 

 

요약

에러 핸들링 이해하기
  • 에러 핸들링
    • 강력하고 안정적인 iOS 앱을 만드는 가장 기본적인 파트이다.
    • Swift 2에서 등장
    • 에러를 처리하는 작업이 훨씬 쉬워졌다.
  • 에러 타입
    • Error 프로토콜을 따르는 값들(enum)을 이용하여 생성
    • 열거형처럼 구현되는 것이 일반적임
  • throws 키워드
    • 에러를 던지는 메서드와 함수 선언
  • gurad, throws 구문
    • 에러 타입을 기반으로 한 에러들을 던지기 위하여 메서드나 함수 코드 내에서 사용된다.
  • try 구문, do-catch 구문
    • throw가 붙은 메서드는 try로 호출되며, 반드시 do-catch 구문으로 감싸여야 한다.
    • do-catch 구문은 철저하게 나열된 catch 패턴으로 구성
    • 각각의 catch 구문은 특정 에러에 실행될 코드를 담는다.
  • defer 구문
    • 메서드가 반환될 때 실행될 정리(cleanup) 작업을 정의할 수 있다

 

 

 

 


 

 

 

앞에서 했던 내용들 중에서 설명이 더 필요한 부분을 더 알아보겠다.

 

 

✔️ Type

Swift의 타입은 3가지 기본 그룹으로 구분된다.

  • struct : value type
  • enum : value type
  • class : reference type

 

Swift의 구조체나 열거형은 대다수 언어보다 더 현저하게 강력하다.

  • property, initilizer, method 지원 뿐만 아니라 확장하거나 protocol을 따를 수 있다.

 

 

각 진수에 따라 정수를 표현하는 방법

let decimalInteger: Int = 28
let binaryInteger: Int = 0b11100
let octalInterger: Int = 0o34
let hexadecimalInteger: Int = 0x1c
  • 10진수: 우리가 평소에 쓰던 숫자와 동일하게 작성
  • 2진수: 접두어 0b를 사용
  • 8진수: 접두어 0o를 사용
  • 16진수: 접두어 0x를 사용

 

 

정수와 부동소수점 변환

Integer and Floating-Point Conversion

정수와 부동 소수점 숫자 타입의 변환은 명시적으로 변환해야 한다.

이 코드에서 상수 three는 타입 Double의 새로운 값으로 생성하는데 사용되어 덧셈의 양쪽이 동일한 타입이다.

이 변환이 없으면 덧셈이 허용되지 않는다.

let three = 3
let pointOneFourOneFiveNine = 0.14159
let pi = Double(three) + pointOneFourOneFiveNine
// pi equals 3.14159, and is inferred to be of type Double

let myInt: Int = three + Int(pointOneFourOneFiveNine) + Int(myDouble)

 

부동 소수점을 정수로 변환하는 것 또한 명시적으로 변환해야 한다.

부동 소수점 값은 새로운 정수 값으로 초기화할 때 소수점 아래를 버림한다. (반올림x)

이것은 4.75는 4, 그리고 -3.9는 -3이 된다는 의미이다.

let integerPi = Int(pi)    // 3
// integerPi equals 3, and is inferred to be of type Int

 

 

 

문자열

연산자를 통한 문자열 결합

  • + 연산자를 이용해 결합
// 연산자를 통한 문자열 결합
let hello: String = "Hello"
let dadahae: String = "ned"
var greeting: String = hello + " " + dadahae + "!"
print(greeting)  // Hello dadahae!

gretting1 = hello
greeting += " "
greeting += dadahae
greeting += "!"
print(greeting) // Hello dadahae!

 

연산자를 통한 문자열 비교

  • == 연산자를 이용해 비교
// 연산자를 통한 문자열 비교
var isSameString: Bool = false

isSameString = (hello == "Hello")
print(isSameString) // true

isSameString = (hello == "hello")
print(isSameString) // true

isSameString = (dadahae == "dadahae")
print(isSameString) // true

isSameString = (hello == dadahae)
print(isSameString) // false

 

메서드를 통한 접두어 확인

  • hasPrefix 메서드를 이용해 확인

맨 앞부터 이런 문자열로 시작되는가!

// 메서드를 통한 접두어 확인
var hasPrefix: Bool = false
hasPrefix = hello.hasPrefix("He")
print(hasPrefix)    // true

hasPrefix = hello.hasPrefix("HE")
print(hasPrefix)    // false

hasPrefix = greeting.hasPrefix("Hello")
print(hasPrefix)    // true

hasPrefix = dadahae.hasPrefix("ae")
print(hasPrefix)    // false

hasPrefix = hello.hasPrefix("Hello")
print(hasPrefix)    // true

 

메서드를 통한 접미어 확인

  • hasSuffix 메서드를 이용해 확인
// 메서드를 통한 접미어 확인
var hasSuffix: Bool = false
hasPrefix = hello.hasSuffix("He")
print(hasPrefix)    // false

hasPrefix = hello.hasSuffix("llo")
print(hasPrefix)    // true

hasPrefix = greeting.hasSuffix("dadahae")
print(hasPrefix)    // false

hasPrefix = greeting.hasSuffix("dadahae!")
print(hasPrefix)    // true

hasPrefix = dadahae.hasSuffix("ae")
print(hasPrefix)    // true

 

빈 문자열 확인

  • isEmpty 프로퍼티를 이용해 확인
// 프로퍼티를 통한 빈 문자열 확인
var isEmptyString: Bool = false
isEmptyString = greeting.isEmpty
print(isEmptyString) // false

greeting = "안녕"
isEmptyString = greeting.isEmpty
print(isEmptyString) // true

greeting = ""
isEmptyString = greeting.isEmpty
print(isEmptyString) // true

nil, space는 빈문자열이 아니다.

 

문자열 길이 확인

  • count 프로퍼티를 이용해 확인
// 프로퍼티를 이용해 문자열 길이 확인

print(greeting.count) // 0

greeting = "안녕하세요"
print(greeting.count) // 5

greeting = "안녕!"
print(greeting.count) // 3

 

 


아래의 내용(set, enum, property observer)도 Type 안에서 이야기를 했지만,

각각의 주제들이 모두 큰 주제로 다룰 수 있다고 봐서 블로그에는 큰 단락(Heading 1)으로 정리했습니당.

 

✔️ Set

Set
Collection = array, dictionary, set
  • 순서 없음(인덱스 없다)
  • 중복 불가
var letters = Set<Character>()
print("letters is of type Set<Character> with \\(letters.count) items.")
// Prints "letters is of type Set<Character> with 0 items."

letters.insert("a")
letters = []

var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]

 

집합 접근과 수정

Accessing and Modifying a Set
  • count 프로퍼티
    • 읽기 전용
    • 집합의 아이템 갯수를 알 수 있다.
  • 부울 isEmpty 프로퍼티
    • count 프로퍼티가 0과 같은지 확인 가능
  • insert(_:) 메서드
    • 집합에 새로운 아이템 추가
print("I have \\(favoriteGenres.count) favorite music genres.")
// Prints "I have 3 favorite music genres."

if favoriteGenres.isEmpty {
	print("As far as music goes, I'm not picky.")
} else {
	print("I have particular music preferences.")
}
// Prints "I have particular music preferences."

favoriteGenres.insert("Jazz")
// favoriteGenres now contains 4 items

 

  • remove(_:) 메서드
    • 집합의 아이템을 삭제할 수 있다.
    • 집합에 아이템이 있을 경우 → 삭제
    • 없을 경우 → nil 반환

 

  • removeAll() 메서드를 사용하여 전체 아이템 삭제

 

  • contain(_:) 메서드
    • 집합에 특정 아이템이 포함되어 있는지 알 수 있다.
if let removedGenre = favoriteGenres.remove("Rock") {
	print("\\(removeGenre)? I'm over it.")
} else {
	print("I never much cared for that.")
}
// Prints "Rock? I'm over it."

if favoriteGenres.contains("Funk") {
	print("I get up on the good foot.")
} else {
	print("It's too funky in here.")
}
// Prints "It's too funky in here."

 

집합 반복

Iterating Over a Set
  • for-in 루프
    • 집합에 값을 반복할 수 있다.
  • sorted() 메서드
    • Swift의 Set 타입은 정의된 순서를 가지고 있지 않다.
    • 특정 순서로 집합의 값을 반복하려면… 집합의 요소를 < 연산자를 사용하여 정렬하여 반환하는 sorted() 메서드를 사용해야 한다.
for genre in favoriteGenres {
	print("\\(genre)")
}
// Classical
// Jazz
// Hip hop

for genre in favoriteGenres.sorted() {
	print("\\(genre)")
}
// Classical
// Jazz
// Hip hop

 

집합 연산 수행

Performing Set Operations

콜렉션 타입 (Collection Types)

  • a.union(b)
  • a.intersection(b)
  • a.symmetricDifference(b)
  • a.subtracting(b)

와 같은 기본적인 집합 연산을 효율적으로 수행할 수 있다.

 

  • 초집합 (superset)
    • a는 b의 모든 요소를 포함하므로, 집합 a는 b의 초집합
  • 부분집합 (subset)
    • b의 모든 요소가 a에 포함되어 있으므로 집합 b는 집합 a의 부분집합
  • 분리집합(disjoint)
    • 집합 b와 집합 c는 공통 요소가 없다.

 

  • isSubset(of:)
  • isSuperset(of:)
  • isStrictSubset(of:)
  • isStrictSuperset(of:)
  • isDisjoint(with:)

 

 


 

 

✔️ enum

열거형, 값들의 집합으로 이루어진 타입

파이를 묘사하는 enum을 정의해보자.

// 파이를 묘사하는 열거형
enum PieType {
	case Apple   // case들은 중복이 되면 안된다.
	case Cherry
	case Pecan
}

let favoritePie = PieType.Apple

 

Swift는 enum 값들을 매칭하기에 좋은 강력한 switch문을 가지고 있다.

switch문은 모든 case를 포함해야 한다.

switch 표현식의 경우 각각의 값은 명시적이든 default: case 든 반드시 처리되어야 한다.

 

Swift는 C언어와 달리 switch 문의 case들이 다른 case로 빠지지 않는다. 일치되는 case 코드만 실행된다.

// 파이를 묘사하는 열거형
enum PieType {
	case Apple   // case들은 중복이 되며 ㄴ안된다.
	case Cherry
	case Pecan
}

let favoritePie = PieType.Apple

let name: String
switch favoritePie {
case .Apple:
	name = "Apple"
case . Cherry:
	name = "Cherry"
case .Pecan:
	name = "Pecan"
}
  • 타입을 아니까 .(dot)값만 써도 된다.

 

Swift의 enumcase에 연관된 원시 값을 가질 수 있다.

타입이 명시된 enum을 가지고 rawValue를 사용하여 PieType의 인스턴스를 요청할 수 있다.

그리고 그 값을 enum 타입을 초기화할 수 있다.

이것은 enum의 실제 case에 상응하는 원시 값이 없을 수 있기 때문에 옵셔널을 반환한다.

따라서 이 경우가 옵셔널 바인딩의 좋은 예시다.

// 파이를 묘사하는 열거형에 원시 값 적용
enum PieType {
	case Apple = 0
	case Cherry
	case Pecan
} // 아래쪽 case에는 자동으로 1, 2가 부여된다.

let pieRawValue = PieType.Pecan.rawValue
// pieRawValue는 2를 값으로 가진 Int 타입이다

if let pieType = PieType(rawValue: pieRawValue) {
	// 'pieType'!이 유효한 값을 가지면
}

 

 

 

 


 

 

 

 

📌 Property

  • class
  • struct
  • enum

모두 연산 프로퍼티 사용가능~ (get, set)

 

 

✔️ Property observers

property observers는 프로퍼티의 값이 변경되는지 관찰하고 응답한다.
이 값이 확인될 예정, 수정될 예정인데 어떡하지?!
  • 종류
    • 이후 - did
    • 이전 - will
프로퍼티의 현재 값이 새로운 값과 같더라도 프로퍼티의 값이 설정될 때 호출된다

 

  • 상황
    • 정의한 저장된 프로퍼티
    • 상속한 저장된 프로퍼티
    • 상속한 계산된 프로퍼티 (get, set)
상속된 프로퍼티의 경우 하위 클래스의 프로퍼티를 재정의하여 프로퍼티 관찰자를 추가한다.
정의한 계산된 프로퍼티의 경우 관찰자를 생성하는 대신에 프로퍼티의 setter를 이용하여 값 변경을 관찰하고 응답한다.

 

📌 프로퍼티에 관찰자를 정의하는 방법: 2가지 (선택사항, 둘다 정의 가능)

  • willSet
    • 값이 저장되기 직전에 호출
    • 상수 파라미터로 새로운 프로퍼티 값이 전달
    • willSet 구현의 일부로 이 파라미터에 특정 이름을 가질 수 있따.
    • 파라미터 명과 구현 내에 소괄호를 작성하지 않으면 파라미터는 newValue의 기본 파라미터 명으로 만들어질 수 있다.
  • didSet
    • 새로운 값이 저장되자마자 호출
    • 예전 프로퍼티 값을 포함한 상수 파라미터가 전달
    • 파라미터 명을 사용하거나 oldValue인 기본 파라미터 명을 사용할 수 있다.
    • didSet 관찰자 내의 프로퍼티에 값을 할당한다면 새로운 값으로 방금 설정한 값을 대체한다.
class StepCounter {
	var totalSteps: Int = 0 {
		willSet(newTotalSteps) {
			print("About to set totalSteps to \\(newTotalSteps)")
		}
		didSet {
			if totalSteps > oldValue {
				print("Added \\(totalSteps - oldValue) steps")
			}
		}
	}
}

let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps

stepCounter.totalSteps = 260
// About to set totalSteps to 360
// Added 160 steps

stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps

 

 

 


 

 

 

✔️ Generic

📌 Generic code

  • 정의한 요구사항에 따라 모든 타입에서 동작할 수 있는 유연하고 재사용 가능한 함수와 타입을 작성할 수 있다.
  • 중복 피함
  • 명확하고 추상적인 방식으로 의도를 표현하는 코드 작성 가능
🦁 sum 함수가 있다고 하자. 이 함수 타입은 (Int, Int) → Int 이다.
근데 Float는 안되나? 그럼 타입을 바꿔서 새로 만들어야 한다.
Double은 안되나? 또 하나 만들어야 한다.
아 귀찮아. 한번에 해결하면 안돼? 로직은 같은데 타입만 다른거잖아! → Generic

C#에서 처음 적용되었다. 개념은 존재했지만 아무도 못하고 있다가.. MS의 C#에 처음 적용된거다.

 

  • 제너릭은 Swift의 강력한 특징 중 하나
  • Swift 표준 라이브러리 대부분은 제너릭 코드로 되어 있다.

 

예) Swift의 Array와 Dictionary 타입은 둘 다 제너릭 콜렉션이다.

Int값을 가진 배열, 또는 String 값을 가진 배열 또는 실제로 Swift에서 생성될 수 있는 다른 모든 타입에 대한 배열을 생성할 수 있다.

모든 지정된 타입의 값을 저장하기 위한 딕셔너리를 생성할 수 있고 해당 타입에 대한 제한은 없다.

 

 

The Problem That Generic Solve

다음의 두 Int 값을 바꾸는 swapTwoInts(::) 라는 제너릭이 아닌 함수를 보자.

이 함수는 In-Out 파라미터 (In-Out Parameters)에서 설명한 대로 a와 b의 값을 바꾸기 위해 in-out 파라미터를 사용하여 만든다.

swapTwoInts(::) 함수는 b의 값을 a로, a의 값을 b로 바꾼다.

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
	let temporaryA = a
	a = b
	b = temporaryA
}

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \\(someInt), and anotherInt is now \\(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"

 

swapTwoInts(::) 함수는 유용하지만 Int 값만 사용이 가능하다. Double, String 등의 값으로 사용하길 원하면 타입만 다르고 내용이 동일한 함수를 만들어야 한다. 즉, 함수 바디는 동일하다. 차이는 값의 타입이다.

단일 함수로 작성하면 더 유용하고 유연하다. 제너릭 코드는 이러한 함수를 작성할 수 있다.

 

Generic Functions

  • 모든 타입과 함께 동작 가능

아래는 swapTwoValues(::) 라는 위의 swapTwoInts(::) 함수의 제너릭 버전이다.

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
	let temporarayA = a
	a = b
	b = temporaryA
}
  • <T> : 나는 제너릭 함수다.
    • type은 T로 부를게.
    • 실제 타입 이름 대신에 T라는 임의의 타입 이름을 사용한다.
    • 실제 함수가 호출될 때마다 T의 실제 타입이 결정된다.

 

  • swapTwoValues(::)가 호출될 때마다 T로 사용한 타입은 함수에 전달된 값의 타입으로부터 유추된다.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
	let temporarayA = a
	a = b
	b = temporaryA
}

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and antoherInt is not 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"
  • 각각의 경우 타입 파라미터는 함수가 호출될 때마다 실제 타입으로 대체된다.
  • 콤마로 구분된 꺽쇠 괄호 안에 여러 개 타입 파라미터를 작성 → 하나 이상의 타입 파라미터를 제공 가능

 

Naming Type Parameters

대부분의 경우 타입 파라미터는 타입 파라미터와 제너릭 타입 간의 관계나 함수간의 관계를 나타내기 위해

  • Dictionary<Key:Value> 에서 Key, Value
  • Array<Element>에서 Element

그러나 의미있는 관계가 없을 때는 T, U, V와 같은 단일 문자를 사용하여 이름을 지정하는 것이 일반적이다.

 

 

 

 


 

 

✔️ 프로토콜 지향 프로그래밍

2015년 WWDC에서 처음이자 마지막으로 다루었다.

Protocol-Oriented Programming in Swift - WWDC15 - Videos - Apple Developer

 

Protocol-Oriented Programming in Swift - WWDC15 - Videos - Apple Developer

At the heart of Swift's design are two incredibly powerful ideas: protocol-oriented programming and first class value semantics. Each of...

developer.apple.com

크러스티 개발자를 설득해보자! OOP가 좋은 이유 나열...

크러스티 : 아니? 나는 sturct와 enum으로 충분히 class 구현함 ㅋ

그러면서 POP와 OOP의 차이점 설명하고, POP가 강력한 이유 어필함!

 

프로토콜 지향 프로그래밍 예제

Talkable 프로토콜은 Person 이라는 구조체 타입에만 채택이 되었으므로, 여러 프로퍼티와 메서드를 구현하더라도 Person에만 구현하면 되므로 큰 문제가 없다.

protocol Talkable {
	var topic: String { get set }
	func talk(to: Self)
}

struct Person: Talkable {
	var topic: String
	var name: String

	func talk(to: Person) {
		print("\\(topic)에 대해 \\(to.name)에게 이야기합니다")
	}
}

 

그런데 Talkable 이라는 프로토콜을 다른 타입에도 채택하고 싶다면 그 타입에서도 Talkable 프로토콜이 요구하는 사항을 모두 구현해야 한다.

struct Monkey: Talkable {
	var topic: String

	func talk(to: Monkey) {
		print("우끼끼 꺄꺄 \\(topic)")
	}
}
  • 객체 지향
    • 나란? 내가 누구의 속성을 물려 받았는가
  • 프로토콜 지향
    • 나란? 내가 무엇을 하는 존재인가
    • 나라는 존재는 내가 하는 행동으로 결정된다.

 

나를 결정짓는 것은 기억이 아니라 앞으로의 행동이다.

 

프로토콜이 요구하는 사항을 미리 모두 한꺼번에 구현해둘 수 있다면 중복된 코드를 피할 수 있다.

이 코드에서는 Person과 Monky에 Talkable의 요구사항인 talk(to:)메서드를 구현하지 않았음에도 전혀 오류가 발생하지 않는다.

이렇게 하나의 프로토콜을 만들어두고, 초기 구현을 해둔다면 여러 타입에서 해당 기능을 사용하고 싶을 때 프로토콜을 채택하기만 하면 된다.

protocol Talkable {
	var topic: String { get set }
	func talk(to: Self)
}

// 익스텐션을 사용한 프로토콜 초기 구현
extension Talkable {
	func talk(to: Self) {
		print("\\(to)! \\(topic)")
	}
}

struct Person: Talkable {
	var topic: String
	var name: String
}

struct Monkey: Talkable {
	var topic: String
}

let dada = Person(topic: "Swift", name: "dadahae")
let kang = Person(topic: "Internet", name: "vic")

dada.talk(to: kang)
kang.talk(to: dada)

 

  • Q. Talkable의 talk 메서드의 파라미터로 Self가 들어가는데, 이 Self의 첫글자가 대문자여도 되나요? 보통 self는 소문자를 쓰길래 궁금했습니다.
  • A. 대문자 Self는 타입 자체를 정의할 때 사용하고, 소문자 self는 타입의 객체를 의미한다고 합니다 프로토콜에서의 Self는 자기 자신을 채택하는 "타입 자체"를 의미한다고 합니다 아, 그래서 소문자 self로 접근해야 그 객체 내부의 속성이나 메소드에 접근할 수 있는 것 같네요..!

 

  • 만약 프로토콜 초기 구현과 다른 동작을 해야 한다면, 그저 그 타입에 프로토콜의 요구사항을 재정의해주면 된다.
protocol Talkable {
	var topic: String { get set }
	func talk(to: Self)
}

// 익스텐션을 사용한 프로토콜 초기 구현
extension Talkable {
	func talk(to: Self) {
		print("\\(to)! \\(topic)")
	}
}

struct Person: Talkable {
	var topic: String
	var name: String
}

struct Monkey: Talkable {   
	var topic: String
	func talk(to: Monkey) {    // 재정의
		print("\\(to) 우끼끼")
	}
}

let sunny = Monkey(topic: "바나나")
let jack = Monkey(topic: "나무")

sunny.talk(to: jack)

 

  • 프로토콜 초기 구현을 잘 해둔다면 여러 프로토콜을 그저 채택하기만 하면 그 타입에 기능이 추가된다.
protocol Flyable { func fly() }

extension Flyable {
	func fly() {
		print("푸드득")
	}
}

protocol Runnable { func run() }

extension Runnable {
	func run() {
		print("후다닥")
	}
}

protocol Swimable { func swim() }

extension Swimable {
	func swim() {
		print("어푸어푸")
	}
}

protocol Talkable { func talk() }

extension Talkable {
	func talk() {
		print("재잘재잘")
	}
}

struct Bird: Flyable, Talkable { }

let bird = Bird()
bird.fly()
bird.talk()

struct Person: Runnable, Swimable, Talkable { }

let person = Person()
person.run()
person.talk()
person.swim()

 

객체 지향 vs. 프로토콜 지향

📌 객체 지향

  • class
  • 상속으로 계층화
    • override로 메소드 추가 및 대체
    • 하나의 객체는 단일 상속만 가능 (C++ 예외)
  • Reference Type
    • 메모리 공유로 적은 메모리 사용
    • 의도치 않은 갱신을 막기 위한 Lock 기능 필요
  • 멀티 스레드 / 프로세스에 약점

 

📌 프로토콜 지향

  • struct, enum
  • protocol로 동실 속성 부여
    • extension으로 동일 메소드 구현
    • 하나의 객체에 여러 protocol 부여 가능
  • Value Type
    • 매번 복제로 많은 메모리 사용
    • 지연 복제로 극복: lazy
      • 처음엔 복사 no, 복사되었다고 말한 내용이 진짜 변경이 되면 그제서야 천천히 복사해온다.

 

 


주의할 점

  • Swift는 Duck 타입 언어가 아닌 강한(Strong) 타입 언어다.
  • 객체 지향, 프로토콜 지향 어느 것도 만능은 아니다.
  • 프로토콜 지향의 값 타입 기반은 유사성이 있지만, 함수 언어의 개념은 별개다.
    • 클로저에서 함수 언어의 특성이 나타난다.
  • 우리가 활용할 객체가 Class 기반인지, Struct 기반인지 반드시 참고하고 사용할 필요가 있다.

그 밖의 주제들

  • 옵셔널 체이닝(Optional Chaining)
  • 동시성(Concurrency: async/await)
  • 중첩된 타입(Nested Types)
  • 불투명한 타입(Opaque Types: some)
  • 자동 참조 카운팅(Automatic Reference Counting)
  • 일급함수
    • map, reduce, filter, mutating, actor
  Comments,     Trackbacks