[이전 포스트]프로그래밍 언어 Swift 기초 다지기 1
클로져
실행 가능한 코드 블록입니다. 일급 객체로 전달인자나 변수 또는 상수에 저장할 수 있습니다.
함수를 이름이 있는 클로져라고도 부릅니다.
{ (매개변수 목록) -> 타입 in
/* 내부 코드 */
} // 클로져
let add: (Int, Int) -> Int = { (a: Int, b: Int) in
return a + b
} // 클로져로 생성한 add
let result: Int = add(1, 2) // result = 3
let sub: (Int, Int) -> Int = { (a: Int, b: Int) in
return a - b
} // 클로져로 생성한 sub
let caculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
return method(a, b)
} // 클로져로 생성한 caculate
var caculated1 = caculate(a: 3, b: 2, method: sub) // caculated1 = 1
var caculated2 = caculate(a: 3, b: 2, method: {(left: Int, right: Int) -> Int in
return left * right
}) // 클로져로 구현한 multiply, caculated2 = 6
후행 클로져
클로져가 함수의 마지막 전달인자라면 마지막 매개변수를 생략한 후 외부에 구현할 수 있습니다.
result = caculate(a: Int, b: Int) { (left: Int, right: Int) -> Int in
return left + right
} // 후행 클로져
caculate 의 반환타입이 Int 인 점이 명확하므로 반환타입을 생략해도 문제가 없습니다.
그러나 in 키워드는 절대 생략할 수 없습니다.
result = caculate(a: Int, b: Int) { (left: Int, right: Int) in
return left + right
} // 반환타입이 생략된 클로져
단축 인자 클로져
클로져의 매개변수 이름이 굳이 불필요하다면 단축 인자 이름을 사용할 수 있습니다.
result = caculate(a: Int, b: Int, method: {
return $0 + $1
} // $0 은 첫번째 인자, $1 은 두번째 인자를 의미합니다.
후행 클로져와 함께 사용할 수도 있습니다.
result = caculate(a: Int, b: Int) {
return $0 + $1
} // 단축인자 + 후행 클로져
클로져가 반환한 값이 있다면 클로져의 마지막 줄의 결과값은 암시적 반환 값으로 취급됩니다.
result = caculate(a: Int, b: Int) {
$0 + $1
} // 암시적으로 $0 + $1 값이 반환값이 됩니다.
한 줄로 쓸 수도 있습니다.
result = caculate(a: Int, b: Int) { $0 + $1 }
프로퍼티
프로퍼티는 구조체, 열거형, 클래스 내부에 선언합니다. 그 역할에 따라 저장 프로퍼티, 연산 프로퍼티, 인스턴스 프로퍼티, 타입 프로퍼티가 있습니다.
열거형 내부에는 연산 프로퍼티만 구현할 수 있으며 이 연산 프로퍼티는 var 로만 선언할 수 있습니다.
get 블록을 통해 읽기, set 블록을 통해 쓰기를 구현할 수 있습니다.
struct Student {
// 인스턴스 저장 프로퍼티
var name: String = ""
var `class`: String = "Swift"
var koreanAge: Int = 0
// 인스턴스 연산 프로퍼티
var westernAge: Int {
get {
return koreanAge - 1
}
set(inputValue) {
koreanAge = inputValue + 1
}
}
// 타입 저장 프로퍼티
static var typeDescription: String = "학생"
// 연산 저장 프로퍼티 (읽기전용)
var selfIntroduce: String {
get {
return "저는 \(self.class)반 \(self.name)입니다."
}
} // 인스턴스 메서드를 대체합니다.
// 타입 연산 프로퍼티 (읽기전용)
static var selfIntroduce: String {
return "학생타입입니다."
} // 타입 메서드를 대체합니다.
} // 구조체 생성
print(Student.selfIntroduce) // 학생타입입니다.
var steve: Student = Student()
steve.koreanAge = 10
steve.name = "Steve"
print(steve.name) // Steve
print(steve.selfIntroduce) // 저는 Swift반 Steve입니다.
print("제 한국나이는 \(steve.koreanAge)살이고 미국 나이는 \(steve.westernAge)살입니다.")
프로퍼티 감시자
프로퍼티 감시자란 프로퍼티 값이 변경될 때 특정한 동작을 수행하도록 하는 기능입니다. 값이 변경되기 직전에 willset 블럭이 수행되고, 값이 변경된 직후 didset 블록이 수행됩니다. 다만 연산프로퍼티에서는 사용될 수 없습니다.
struct Money {
var currencyRate: Double = 1100 {
willset (newRate){
print("환율이 \(currencyRate)에서 \(newRate)으로 변경될 예정입니다")
}
didset (oldRate) {
print("환율이 \(oldRate)에서 \(currencyRate)으로 변경되었습니다")
}
}
}
상속
클래스, 또는 프로토콜만 상속할 수 있습니다. 다만 다중 상속은 불가능합니다.
class 클래스이름 : 상속받을클래스이름 {
/* 내부 코드 */
}
상속을 원하지 않는 프로퍼티의 경우 static 이나 final 키워드를 앞에 붙이면 됩니다. 이 프로퍼티들은 재정의할 수 없습니다.
자식 클래스에서 부모 클래스의 프로퍼티를 호출하고 싶다면 super 와 프로퍼티이름을 . 으로 연결하면 됩니다.
인스턴스 생성자 (Initializer)
프로퍼티는 모두 기본값을 가져야 합니다. 만약 기본값을 할당하기 어려운 경우에는 이니셜라이져(Initializer) 라는 것을 사용하면 됩니다.
class Person {
var name: String
var age: Int! // 기본값이 필수이지만 기본값을 할당하지는 않을 때, 암시적 추출 옵셔널을 사용합니다.
var nickname: String? // 기본값이 필수는 아닐 때, 옵셔널을 사용합니다.
init(name: String) {
self.name = name
} // 이니셜라이져
convenience init(name: String, nickname: String) {
self.init(name)
self.nickname = nickname
} // convenience 키워드는 init 이 자기 자신의 클래스 내부에 있다는 의미입니다.
} // 클래스 생성
var steve: Person = person(name: "steve", age: 20, nickname: "jobs")
var ive: Person = person(name: "ive") // age 가 없으므로 에러입니다.
ive.age = 22
var ive: Person = person(name: "ive")
이니셜라이져가 옵셔널을 반환하도록 하여 nil 을 반환하도록 합니다.
class Person {
var name: String
var age: Int
var nickname: String? // 기본값이 필수는 아닐 때, 옵셔널을 사용합니다.
init?(name: String, age: Int) {
if (0...120)contains(age) == false {
return nil
} // 만약 나이가 0 에서 120 중에 없다면 nil 을 반환합니다.
self.name = name
self.age = age
} // nil 을 반환할 수 있는 이니셜라이져
} // 클래스 생성
var steve: Person = person(name: "steve", age: 20) // 옵셔널 타입이 아니므로 에러입니다.
var steve: Person? = person(name: "steve", age: 150) // 나이가 너무 많으므로 nil 입니다.
print(steve) // nil 이므로 에러입니다.
var ive: Person? = person(name: "ive", age: 20)
print(ive)
인스턴스 소멸자 (Deinitializer)
인스턴스가 메모리에서 해제되는 시점에 호출됩니다. 클래스에만 구현할 수 있습니다.
class Person {
var name: String
var pet: String?
var child: String
init(name: String, child: String) {
self.name: String = name
self.child: String = child
}
deinit {
if let petname = pet?.name {
print("\(name)가 \(pet)을 \(child)에게 양도합니다.")
}
} // 인스턴스가 소멸할 때 pet 이 있으면 child 에게 pet 을 양도하는 소멸자입니다.
}
옵셔널 체이닝
옵셔널의 내부의 내부의 … 내부로 옵셔널이 연결되어 있을 때 간단하게 사용할 수 있는 방법입니다. 조건문 여러개를 복잡하게 사용해야 하는 낭비를 없애줍니다.
func person1(owner: Person) {
if let myjob = owner?.home?.name?.job {
print("나의 직업은 \(myjob)입니다.")
}
} // 옵셔널 체이닝
만약 이 코드를 옵셔널 체이닝을 사용하지 않으려면 아래처럼 해야 합니다.
func person2(owner: Person) {
if let owner = owner {
if let home = owner.home {
if let `name` = home.name {
if let myjob = `name`.job {
print("나의 직업은 \(myjob)입니다.")
}
}
}
}
} // 옵셔널 체이닝을 사용하지 않은 코드
nil 병합 연산자 (??)
var job: String = nil
steve?.home?.name?.job // nil
steve?.home?.name?.job ?? 무직 // 무직
옵셔널 값이 nil 일 경우 nil 을 반환합니다.
타입 캐스팅
var number1: Int = 10
var number2: Double = (Double)number1
위 코드는 타입 캐스팅이 아닙니다. number1 에 맞는 Double 형 인스턴스를 새로 생성하는 형변환입니다.
is 키워드는 클래스의 인스턴스가 부모 혹은 자식 타입으로 사용할 수 있는지 확인하기 위해 사용됩니다.
class Person {
/* 내부코드 */
}
class Student: Person {
/* 내부코드 */
}
class UniversityStudent: Student {
/* 내부코드 */
}
var steve: Student = Student()
var ive: UniversityStudent = UniversityStudent()
steve is Person // true
steve is Student // true
steve is UniversityStudent // false
업 캐스팅
as 키워드를 사용하면 인스턴스를 부모 클래스의 인스턴스로 사용할 수 있도록 합니다.
var tim: Person = UniversityStudent as Person()
var gates: Any = Person() // as Any 는 암시적으로 생략할 수 있습니다.
다운 캐스팅
as? 은 옵셔널 타입 캐스팅입니다. 조건에 부합하지 않는다면 nil 을 반환합니다.
var lisa: Person?
lisa = ive as? UniversityStudent
lisa = ive as? Student // ive 가 UniversityStudent 이므로 nil
lisa = steve as? UniversityStudent // steve 가 Person 이므로 nil
as! 를 사용하면 강제로 타입 캐스팅을 할 수 있습니다. 조건에 부합하지 않는다면 런타임 에러입니다.
lisa = ive as! UniversityStudent
lisa = ive as! Student // ive 가 UniversityStudent 이므로 런타임 에러
lisa = steve as! UniversityStudent // steve 가 Person 이므로 런타임 에러
Assert
assert 는 배포하는 어플리케이션에 제외되는, 오직 디버깅을 위해서만 사용되는 키워드입니다. 조건 검증을 위해서 사용되며 만약 조건이 일치하지 않으면 어플리케이션이 종료됩니다.
var number: Int = 1
assert(number == 1) // 조건이 일치하므로 지나갑니다.
assert(number == 0, "number 가 0 이 아닙니다.") // 조건이 다르므로 메세지를 출력하고 중지합니다.
Guard
guard 는 조건문처럼 특정 조건이 발생했을 때 동작하는 빠른 종료 키워드입니다.
var number: Int = 1
guard let mynumber = number,
mynumber < 100 else {
print("100 보다 큰 숫자입니다.")
return
} // else 구문 내에 반드시 return 이나 break 가 있어야 합니다.
print("나이는 \(mynumber)입니다.") // guard 에 쓴 mynumber 변수는 밖에서도 사용 가능합니다.
변수를 밖에서도 사용할 수 있다는 점과, else 문 내에 return 이나 break 가 반드시 있어야 한다는 점이 if 문 과의 차이점입니다.
프로토콜
프로토콜(protocol) 이란 특정 기능을 수행하기 위해 메서드나 프로퍼티의 요구사항을 정의한 것을 말합니다.
구조체, 클래스, 열거형은 프로토콜을 채택하여(adopted) 프로토콜의 요구사항을 실제로 구현할 수 있습니다. 프로토콜의 요구사항을 모두 따르는 타입을 프로토콜을 준수한다(confirmed) 고 말합니다.
protocol Talkable {
// 프로퍼티 요구
var topic: String { get set } // 읽기 쓰기 모두 되어야 함
var language: String { get } // 읽기 전용이어야 함
// 메서드 요구
func talk()
// 이니셜라이져 요구
init(topic: String, language: String)
} // 프로토콜 선언
struct Person: Talkable {
/* 내부 코드 */
} // 프로토콜을 채택한 구조체
프로퍼티 요구는 항상 var 을 사용해야 합니다.
프로토콜은 상속할 수 있습니다. 그리고 클래스와는 다르게 다중 상속도 가능합니다.
protocol Readable {
func read()
} // 프로토콜 선언
protocol Writeable {
func write()
} // 프로토콜 선언
protocol ReadSpeakable : Readable {
func speak()
} // 프로토콜 상속
protocol ReadWriteSpeakable : Readable, Writeable {
func speak()
} // 프로토콜 다중 상속
클래스와 프로토콜 모두 상속받는 경우, 클래스를 먼저 써주고 그 다음에 프로토콜을 써주면 됩니다.
익스텐션 (Extension)
익스텐션(extension) 은 타입에 새로운 기능을 추가할 수 있는 Swift 의 놀랍고도 강력한 기능입니다. 즉 소스 코드를 알지 못하더라도 타입만 알고 있으면 새로운 기능을 추가할 수 있습니다. 외부 라이브러리나 프레임워크에 나만의 기능을 추가할 때 사용됩니다.
extension 확장할타입이름: 프로토콜 {
/* 추가할 기능 */
}
extension Int {
var isEven: Bool {
return self % 2 == 0
}
var isOdd: Bool {
return self % 2 == 1
}
} // 익스텐션 선언
1.isEven // false
2.isEven // true
오류 처리
Swift 는 Error 프로토콜과 열거형을 통해 에러를 처리합니다. 오류를 처리하기 위해서는 throw 키워드를 사용하면 됩니다.
enum VendingMachineError: Error {
case invalidInput
case insufficientFunds(moneyNeeded: Int)
case outOfStock
} // 오류 표현
func vendingMachine {
/* 내부 코드 */
guard money > 0 else {
throw VendingMachineError.invalidInput
} // 오류 처리
}
do-catch 구문을 활용해서 에러를 처리하는 방법도 있습니다.
do {
try machine.reviewMoney(0)
} catch VendingMachineError.invalidInput {
print("입력이 잘못되었습니다.")
} // do-catch 구문
do {
try machine.reviewMoney(0)
} catch {
switch error {
case VendingMachineError.invalidInput:
print("입력이 잘못되었습니다.")
case VendingMachineError.outOfStock:
print("재고가 부족합니다.")
}
} // switch-case 를 활용한 do-catch 구문
do {
try machine.reviewMoney(0)
} catch {
print(error)
} // 케이스별로 에러처리할 필요가 없는 경우
do {
try machine.reviewMoney(0)
} // 케이스별로 에러처리할 필요가 없는 경우 2
try 키워드를 사용하는 경우도 있습니다. try 를 사용하게 되면 만약 오류가 발생할 경우 nil 값을 반환받을 수 있습니다.
var result = try? machine.vend(numberOfItems: 2)
// 만약 에러일 경우 nil
result = try! machine.vend(numberOfItems: 2)
// 만약 에러일 경우 런타임 에러
고차함수
고차함수란 전달인자로 함수를 전달받거나 함수 실행의 결과를 함수로 반환하는 함수입니다. 고차함수로는 크게 map, filter, reduce 가 있습니다.
map 함수는 기존 데이터를 변형하여 새로운 컨테이너를 생성합니다. for 문 대신 클로져로 간단하게 선언할 수 있습니다.
let numbers: [Int] = [0, 1, 2, 3, 4]
let doubleNumbers: [Int]
doubleNumbers = numbers.map({ (number: Int) -> Int in
return number * 2
}) // [0, 2, 4, 6, 8]
doubleNumbers = numbers.map { $0 * 2 } // 후행 클로져, [0, 4, 8, 12, 16]
filter 함수는 값을 걸러서 새로운 컨테이너를 생성합니다.
let numbers: [Int] = [0, 1, 2, 3, 4]
let evenNumbers: [Int] = numbers.filter { (number: Int) -> Bool in
return number % 2 == 0
} // [0, 2, 4]
let oddNumbers: [Int] = numbers.filter { $0 % 2 == 1 } // [1, 3]
reduce 함수는 컨테이너 내부의 요소들을 모두 합칠 때 사용하는 키워드입니다. 첫 매개변수로 초깃값을 주고 그 뒤에 클로져를 넣을 수 있습니다.
let numbers: [Int] = [0, 1, 2, 3, 4]
let sum1: Int = numbers.reduce(0, { (first: Int, second: Int) -> Int in
return first + second
}) // 10
let sum2: Int = numbers.reduce(0) { $0 + $1 } // 10