swift 기본문법 - 제네릭(Generics)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


제네릭(Generics)

제네릭은 자료형에 구애받지 않는 코드를 의미한다. 스위프트는 태생적으로 강한 타입의 언어이기 때문에 변수와 인자, 리턴값 등 모두 자료형의 구속을 받게 된다. 이것때문에 때로는 비생산적이고 반복적인 코딩을 해야할 때가 발생하는데 제네릭은 이 부분을 해결해주는 유용한 코드로서 스위프트의 가장 강력한 기능 중 하나이다.

사실 스위프트의 array이나 dictionary도 제네릭이다. 예로 array에는 Int들을 담을 수도 있고 String도 담을 수 있기 때문이다.

따라서 이 제네릭을 사용하게 되면 유연하고 재사용성 높은 코드를 작성할 수 있다는 장점이 있다.

자. 아래 두 정수의 값을 바꿔주는 함수가 있다고 하자.

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

그런데 아래 사진에서와 같이 파라미터에 Double형을 넣으면 에러가 뜨게 된다.
swapTwoInts의 파라미터는 Int형이기 때문이다.

그런데 우리가 만약에 같은 기능을 하는 함수임에도 불구하고 String, Double 등 각각 다양한 타입의 파라미터를 받아주는 함수를 매번 만들어야 한다고 생각해보자. 매우…매우!!!! 비효율적이라는 생각이 들것이다.

바로 이때 generic을 사용하면 된다.

func swapTwoInts<T>(_ a: inout: T, _ b: inout: T) {
  let temporaryA = a
  a = b
  b = temporaryA
}

이렇게만 해주면 이 함수 하나로 Int, String, Double 변수들의 값을 바꿔줄 수 있다.
단 a, b가 모두 같은 타입일 때만 말이다!

그렇다면 이때 T 는 무엇을 의미할까?

T 타입 파라미터

1. 파라미터: 타입이 들어갈 부분에 T를 적었다.

이때 T는 Placeholder 타입 “이름”을 의미한다. String, Int타입도 이름으로 이 대신 T라는 타입이름 이 들어간것!
이 T는 swapTwoInts라는 함수가 호출될 때마다 결정된다. 그리고 이때 a와 b는 이 T타입과 반드시 일치해야 한다.

2. <T>

generic함수와 일반함수에서의 가장 큰 차이일 것이다.

제네릭함수는 함수 이름 옆에 위에서 말한 Placeholder 타입 이름이 온다. 대괄호(<>)로 묶어준 이유는 Swift에게 함수 정의 내 Placeholder 타입이름인 것을 알리기 위해서이다. 그냥 “T”라는 것은 Placeholder이므로, swiftsms “T”라는 실제 타입을 찾지 않는다.

따라서 아래와 같이 작성해도 문제 없다.

func swapTwoInts<zehye>(_ a: inout: zehye, _ b: inout: zehye) {
  let temporaryA = a
  a = b
  b = temporaryA
}

뿐만 아니라 <T>자리에는 여러개가 들어갈 수 있다(,로 구분)

그러나 swift는 매~우 안전한 언어이며 타입에 굉~장히 민감하기 때문에 a와 b의 타입은 반드시 같아야 한다.
즉, Int와 String을 서로 바꿀 수 없게 하며, 만약 이렇게 하면 컴파일 에러가 뜰 것이다!

뿐만 아니라 swift에는 이미 swap 이라는 함수가 있는데 이또한 제네릭 함수이다.

public func swap<T>(_ a: inout T, _ b : inout T)

타입 제약

위에서 구현한 swapTwoInts 함수는 이제 제네릭으로 구현되어 있기 때무에 모든 타입에서 잘 동작할 것이다.

그러나 가끔 특정한 타입에서만 제네릭 함수를 사용하고 싶을때도 있을 것이다. 이를 위해 타입제약(Type Constraint)이 있다.
타입 제약을 통해 타입 파라미터(Type Parameter, swapTwoInts에서는 T)가 특정 클래스로부터 상속되거나, 특정 프로토콜을 준수해야만 제네릭 함수를 쓸 수 있도록 제약을 걸 수 있다.

우리는 위에서 딕셔너리도 제네릭의 콜렉션이라는 것을 인지했다.

그런데 딕셔너리에서의 key에는 아무 타입이나 들어갈 것처럼 보이지만 사실 Hashable 프로토콜 을 준수해야만 Key로 들어올 수 있다.

public struct Dictionary<Key, Value> : Collection, ExpressibleByDictionaryLiteral where Key: Hashable {

}

즉, 우리가 이때까지 넣었던 Int, String, Double, Bool 등은 Hashable 프로토콜을 준수하고 있다는 것을 의미한다.

따라서 Dictionary는 타입 제약을 통해 특정한 클래스를 상속받거나, 특정한 프로토콜을 준수한 타입만이 들어올 수 있도록 구현되어 있다. 물론 제네릭을 통해서 말이다! 그럼 우리도 한번 만들어보도록 하자!

func somwFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
  .. function body goes here
}

위 코드는 타입 제약이 들어간 제네릭 함수의 syntax이다. 위 함수에서의 T,U모두 타입 파라미터이다.
T 옆에는 특정 클래스, U 옆에는 특정 프로토콜이 존재하며 두개의 구분은 , 으로 하였다.

  • T: SomeClass의 하위클래스여야 한다는 제약
  • U: SomeProtocol을 준수해야한다는 제약
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
  for (index, valiue) in array.enumerated() {
    if value == valueToFind {
      return index
    }
  }
  return nil
}

위 findIndex함수는 String 배열과 찾고싶은 String 하나를 파라미터로 주면 해당 String이 있으면 그 인덱스를 반화해주고 없으면 nil을 반환해주는 함수이다. 즉 [1,2,3,4,5,6]이라는 배열을 가지고 있는데 내가 찾고 싶은 Int가 몇번째 인덱스에 있는지 궁금할때 사용하는 함수를 의미한다.

위 함수를 이제 제네릭으로 바꿔보자!

func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
  for (index, value) in array.enumerated() {
    if value == valueToFind {
      return index
    }
  }
  return nil
}

그러면 위와 같은 오류가 발생할 것이다.

value == valueToFind 구문으로부터 발생하는 오류로 swift의 모든 타입이 저렇게 == 라는 여산자로 비교될 수 있는 것이 아니기 때문에 발생하는 에러이다. 즉, swift가 가능한 모든 타입 T에 대해 이 코드가 작동한다는 것을 보장할수 없기 때문에 컴파일 에러가 발생하는 것이다!

이때 사용할 개념이 Equatable 이다.

func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
  for (index, value) in array.enumerated() {
    if value == valueToFind {
      return index
    }
  }
  return nil
}

즉, Equatable을 준수하는 모든 타입은 위 findIndex 함수를 안전하게 사용할 수 있게 된다.

swift 기본문법 - 고차 함수(higher order function)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
인프런, 야곰의 스위프트 기본문법 강좌를 듣고 정리하였습니다.


고차 함수(higher order function)

전달인자로 함수를 전달받거나 함수 실행의 결과를 함수로 반환하는 함수 » map, filter, reduce

map

컨테이너 내부의 기존 데이터를 변형(transform)하여 새로운 컨테이너를 생성한다.
파라미터로 받은 클로저를 모든 요소에 실행한 그 결과를 반환

let numbers: [Int] = [0,1,2,3,4]
var doubleNumbers: [Int]
var strings: [String]

doubleNumbers = [Int]()
strings = [String]()

for number in numbers {
  doubleNumbers.append(number * 2)
  strings.append("\(number)")
}

print(doubleNumbers)  // [0,2,4,6,8]
print(strings)  // ["0", "1", "2", "3", "4"]

이를 map 메서드를 사용하여 변환해보자

doubleNumbers = numbers.map({ (number: Int) -> Int in return "(\number)"})

map의 전달인자로 클로저가 들어와 각각의 요소를 어떻게 변형해 무엇으로 돌려줄 것인지 지정이 가능하다.
Int요소를 하나하나 받아와 Int로 반환해줄 것이다. 그래서 새로운 컨테이너에 넣어달라고 할당함

매개변수, 반환타입, 반환 키워드(return) 생략, 후행 클로저 사용

doubleNumbers = numbers.map { $0 * 2 }
print(doubleNumbers)  // [0,2,4,6,8]

filter

컨테이너 내부의 값을 걸러서 새로운 컨테이너로 추출 » for 구문 사용! 변수 사용에 주목!

하나하나 요소를 필터링해서 조건에 부합하는 애들만 새로운 컨테이너로 만들어주는 것! > 필터된 값을 반환

var filtered: [Int] = [Int]()

for number in numbers {
  if number % 2 == 0 {
    filtered.append(number)
  }
}

print(filtered)  //[0,2,4]

위 예시는 for문을 만들기 위해 filtered라는 변수를 만들어줘야 했지만 filter를 사용할때는 그럴 필요가 없다.

그냥 상수로도 바로 받아올 수 있다. 변수로도 받아도 상관은 없다.

let evenNumbers: [Int] = numbers.filter {
  (number: Int) -> Bool in return number % 2 == 0
}
print(evenNumbers)  // [0,2,4]

매개변수, 반환타입, 반환 키워드(return) 생략, 후행 클로저 사용

let oddNumbers: [Int] = numbers.filter { $0 % 2 ! = 0 }
print(oddNumbers)  // [1,3]

reduce

컨테이너 내부의 콘텐츠를 하나로 통합한다.
초기값이 있고 초기값과 첫번째 요소 ~ 끝번째 요소까지의 실행된 최종 결과값만을 반환

let someNumbers: [Int] = [2,8,15]
var result: Int = 0

for number in someNumbers {
  result += number
}

print(result)  // 25

그런데 reduce메서드를 이용하면 아래와 같다.

let sum: Int = someNumbers.reduce(0, {
  (first: Int, second: Int) -> Int in return first + second
})

print(sum)  // 25
  • 초기값이 0이고
  • 모두 더해줄 것이다 라고 할때 reduce를 한다.
// 초기값이 3이고 someNumbers의 내부 모든 값을 더합니다.
let sumFromThree = someNumbers.reduce(3) { $0 + $1 }
print(sumFromThree)  // 28

sort & sorted

  • sort: 정렬하여 반환 > 원본에 영향을 줌
  • sorted: 기존 리스트를 건들지 않고 새로운 리스트에 정렬된 값을 반환

Monad

값이 있을 수도 있고 없을 수도 있는 컨텍스트를 가지는 함수객체타입이며, 함수객체와 모나드는 특정 기능이 아닌 디자인 패턴 혹은 자료구조라고 할 수 있다.

  • 컨텍스트(Context): 옵셔널처럼 값이 옵셔널 타입으로 감싸져있듯이 값을 담는 컨테이너 역할을 하는것
  • 함수객체: 맵을 적용할 수 있는 컨테이너 타입 (array, dictionary, set 등 컬렉션 타입들)

FlayMap

맵과 사용법이 같으며, 컨테이너로 쌓여져있으면 컨테이너 안의 값으로 작업을 수행하여 다시 포장하여 반환해준다.
플랫맵은 체인 형식으로 사용 가능하나 맵은 불가능 (optionals.flatMap{ $0 }.flatMap{ $0 })

let optionals: [Int?] = [0, 1, 2, nil, 4]
let map = optionals.map { $0 }
let flatMap = optionals.flatMap { $0 }

print(map) // [Optional(0), Optional(1), Optional(2), nil, Optional(4)]
print(flatMap) //[0, 1, 2, 4]

swift 기본문법 - 오류처리(error handling)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
인프런, 야곰의 스위프트 기본문법 강좌를 듣고 정리하였습니다.


오류처리(error handling)

Error 프로토콜과 (주로) 열거형을 통해서 오류를 표현한다.

enum 오류종류이름: Error {
  case 종류1
  case 종류2
  case 종류3
  case 종류4
  ...
}

예시는 아래와 같다.

enum VendingMachineError: Error {
  case invalidInput
  case insufficientFunds(moneyNeeded: Int)
  case outOfStock
}

함수에서 발생한 오류 던지기

이러한 오류들은 특정 함수안에서 오류가 발생했다고 (자신을 호출한 곳에) 오류를 던져준다.

오류 발생의 여지가 있는 메서드는 throws를 사용해 오류를 내포하는 함수임을 표시한다.

class VendingMachine {
  let itemPrice: Int = 100
  var itemCount: Int = 5
  var deposited: Int = 0

  // 돈 받기 메서드
  func receiveMoney(_ money: Int) throws {

    // 입력한 돈이 0이하면 오류를 던진다
    guard money > 0 else {
      throw VendingMachineError.invalidInput
    }

    // 오류가 ㅇ벗으면 정상처리
    self.deposited += money
    print("\(money)원 받음")
  }

  // 물건 팔기 메서드
  func vend(numberOfItems numberOfItemsToVend: Int) throws -> String {
    guard numberOfItemsToVend > 0 else {
      throw VendingMachineError.invalidInput
    }
    guard numberOfItemsToVend * itemPrice <= deposited else {
      let moneyNeeded: Int
      moneyNeeded = numberOfItemsToVend * itemPrice - deposited
      throw VendingMachineError.insufficientFunds(moneyNeeded: money)
    }
  }
}

오류 처리(try, do-catch)

  • 오류발생의 여지가 있는 throws 함수는 try를 사용해 호출해야한다. > try, try?, try!
  • 오류 발생의 여지가 있는 throws 함수는 do-catch 구문을 활용해 오류발생에 대비한다.
do {
  try machine.receiveMoney(0)
} catch VendingMachineError.invalidInput {
  print("입력이 잘못됐습니다")
} catch VendingMachine.insufficientFunds {
  print("\(moneyNeeded)원이 부족합니다")
} catch VendingMachineError.outOfStock {
  print("수량이 부족합니다")
}

반복된 catch가 별로라면 아래처럼 해도 된다.

do {
  try machine.receiveMoney(300)
} catch // let error 는 생략 가능하다
  switch error {
    case VendingMachineError.invalidInput:
      print("입력이 잘못되었습니다")
    case ....
    default:
    print("알수없는 오류 \(error)")
  }
}

그런데 이마저도 귀찮다.

do {
  result = tru machine.vend(numberOfItems: 4)
} catch {
  print(error)
}

혹은 아래로 한다

do {
  result = try machine.vend(numberOfItems: 4)
}

이 깔끔하지 않은 구문을 좀 더 명확히 해주기 위해 try? try!가 있다!

try?

  • 별도의 오류처리 결과를 통보받지 않는다.
  • 오류가 발생했으면 결과값을 nil로 돌려받는다.
  • 정상동작 후에는 옵셔널 타입으로 정상 반환값을 돌려받는다.
result = try? machine.vend(numberOfItems" 2)
result  // Optional("2 제공함")

result = try? machine.vend(numberOfItems" 2)
result  // nil

try!

  • 오류가 발생하지 않을 것이라는 강력한 확신을 가질 때
  • 정상 동작 후에 바로 결과값을 돌려받는다.
  • 오류가 발생하면 런타임 오류가 발생하여 애플리케이션 동작이 중지된다.
result = try! machine.vend(numberOfItems" 2)
result  // 1개 제공함

result = try! machine.vend(numberOfItems" 2) 

swift 기본문법 - 익스텐션(extension)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
인프런, 야곰의 스위프트 기본문법 강좌를 듣고 정리하였습니다.


익스텐션(extension)

구조체, 클래스, 열거형, 프로토콜 타입에 새로운 기능을 추가할 수 있는 기능이다.

기능을 추가하려는 타입의 구현된 소스코드를 알지 못하거나 볼수 없다해도 타입만 알고있다면 그 타입의 기능을 확장할 수 있다.

  • 연산 타입 프로퍼티 / 연산 인스턴스 프로퍼티
  • 타입 메서드 / 인스턴스 메서드
  • 이니셜라이저
  • 서브스크립트
  • 중첩 타입
  • 특정 프로토콜을 준수할 수 있도록 기능 추가

기존에 존재하는 기능을 재정의 할 수는 없다.

extension 확장할 타입 이름 {
  타입에 추가될 새로운 기능 구현
}

익스텐션은 기존에 존재하는 타입이 추가적으로 다른 프로토콜을 채택할 수 있도록 확장할 수도 있다.

extension 확장할 타입 이름: 프로토콜1, 프로토콜2, 프로토콜3... {
  프로토콜 요구사항 구현
}

익스텐션 구현 / 연산 프로퍼티 추가

extension Int {
  var isEven: Bool {
    return self % 2 == 0
  }
  var isOdd: Bool {
    return self % 2 == 1
  }
}
// 그냥 숫자를 써주게 되더라도 정수 타입(리터럴 문법) >> 일반 숫자로도 표현 가능
print(1.isEven)  // false

var number: Int = 3

익스텐션 구현 / 메서드 추가

extension Int {
  func multiply(by n: Int) -> Int {
    return self * n
  }
}

// 3이라는 Int타입의 리터럴 문법, Int 타입의 인스턴스로 취급
print(3.multiply(by: 3))  // 9

익스텐션 구현 / 이니셜라이저 추가

extension String {
  init(intTypeNumber: Int) {
    self = "\(intTypeNumber)"
  }
}

// 기존에 없던 이니셜라이저를 추가할 수도 있다.

swift 기본문법 - 프로토콜(protocol)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
인프런, 야곰의 스위프트 기본문법 강좌를 듣고 정리하였습니다.


프로토콜(protocol)

특정 역할을 수행하기 위한 메서드, 프로퍼티, 이니셜라이저 등의 요구사항을 정의한다.

어떤 타입에 이 기능이 필요해! 그러니 꼭 그 기능을 구현해놨어야해! 라고 강요하는 것과 같다.

구조체, 클래스, 열거형은 프로토코를 채택(adopted)하여 프로토콜의 요구사항을 실제로 구현 가능하다.
어떤 프로토콜의 요구사항을 모두 따르는 타입은 그 프로토콜을 준수한다(Conform)고 표현한다.
프로토콜의 요구사항을 충족시키려면 프로토콜이 제시하는 기능을 모두 구현해야 한다.

protocol 프로토콜 이름 {
  정의부
}

예시는 아래와 같다

protocol Talkable {
  // 프로퍼티 요구 >> 항상 var 키워드를 사용한다.
  var topic: String { get set }  // 읽기 쓰기 모두 가능한 프로퍼티
  var language: String { get }  // 읽기만 가능한 프로퍼티

  // 메서드 요구
  func talk()

 // 이니셜라이저 요구
  init(topic: String, language: String)
}

직접 구현을 하는것은 아니고 요구만 한다.

프로토콜의 채택 및 준수

// Person 구조체는 Talkable 프로토콜을 채택했다
struct Person: Talkable {

  // 저장 프로퍼티
  var topic: String
  var language: String

  // 위 저장 프로퍼티는 연산프로퍼티로 대체가 가능하다.
  var language: String { return "한국어" }

  var subject: String = ""
  var topic: String {
    set {
      self.subject = newValue
    }
    get {
      return self.subject
    }
  }
}

프로토콜 상속

클래스와 다르게 다중 상속이 가능하다.

protocol 프로토콜 이름: 부모 프로토콜 이름 목록 {
  정의부
}

프로토콜이 상속한 모든 메서드들을 구현하지 않으면 오류가 발생한다.

클래스 상속과 프로토콜

클래스에서 상속과 프로토콜 채택을 동시에 하려면 상속 받으려는 클래스를 먼저 명시하고 그 뒤에 채택할 프로토콜 목록을 작성한다.

순서가 바뀌면 안된다!

프로토콜 준수 확인

인스턴스가 특정 프로토콜을 준수하는 지 확인 가능하다 » is, as 연산자 사용