• Home
  • About
    • 황우진 블로그 photo

      황우진 블로그

      경희대학교 컴퓨터공학과 재학중

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

프로그래밍 언어 Swift 기초 다지기 2

29 Jan 2020

Reading time ~10 minutes

[이전 포스트]프로그래밍 언어 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


swift Share Tweet +1