Swift 문법 기초 2편
<타입 캐스팅>
인스턴스의 타입을 확인하거나 어떠한 클래스의 인스턴스를
해당 클래스 계층구조의 슈퍼 클래스나 서브 클래스로 취급하는 방법
class MediaItem {
var name: String
init(name: String){
self.name = name
}
}
class Movie: MediaItem {
var director: String
init(name: String, director: String) {
self.director = director
super.init(name: name)
}
}
class Song: MediaItem {
var artist: String
init(name: String, artist: String) {
self.artist = artist
super.init(name: name)
}
}
// library는 타입추론을 통해 super class인 MediaItem으로 형변환됨
let library = [
Movie(name: "기생충", director: "봉준호"),
Song(name: "Butter", artist: "BTS"),
Movie(name: "올드보이", director: "박찬욱"),
Song(name: "Wonderfull", artist: "Oasis"),
Song(name: "Rain", artist: "이적")
]
var movieCount = 0
var songCount = 0
// is 연산자를 통해 인스턴스의 클래스 파악 - moviCount는 2, songCount는 3
for item in library {
if item is Movie {
movieCount += 1
} else if item is Song {
songCount += 1
}
// as? - 다운캐스팅 (옵셔널 값 반환)
// library 내 인스턴스 들은 Movie class or Song class
// Movie or Song으로 다운캐스팅이 될수 있으면 해당 내용을 출력
for item in library {
if let movie = item as? Movie {
print("Movie: \(movie.name"), dir. \(movie.director)")
}
else if let song = item as? Song {
print("Song: \(song.name"), dir. \(song.artist)")
}
}
// as! - 강제 언래핑 (항상 성공할 것이라는 확신이 있을 때 사용)
< assert문 / guard문 >
- assert문
- 특정 조건을 체크, 조건이 성립되지 않으면 메세지와 함께 에러를 발생시킴
- 디버깅 모드에서만 동작. 디버깅 중 조건의 검증을 위하여 사용 - guard문
- 함수 내부에서 조건식으로 뭔가를 검사하여 그 다음에 오는 코드를 실행할지 말지 결정
- gaurd문에서 주어진 조건문이 거짓일 때 구문이 실행
- eary exit: 특정 조건문을 만족하지 않으면 이후 코드를 실행하지 않도록 방어코드로 활용
(잘못된 값이 함수에 들어오지 않기 위한 방어)
// assert함수
var value = 0
assert(value == 0)
value = 2
assert(value == 0, "값이 0이 아닙니다") // 런타임 에러. console창에 "Assertion faild: 값이 0이 아닙니다" 출력
guard문을 사용하면 옵셔널 바인딩 된 상수를 조건문 범위 밖에서도 사용 가능
func guardTest(value: Int) {
guard value == 0 else {return}
print("안녕하세요")
}
guardTest(value: 2) // guard문에 막혀서 아무것도 출력되지 않음 (함수 종료)
guardTest(value: 0) // guard문을 통과해서 그 다음 구문인 "안녕하세요"가 출력됨
// guard문을 통한 옵셔널 바인딩
func guardTest2(value: Int?) {
guard let value = value else {return} // value에 값이 들어가 있으면 옵셔널 바인딩 해서 value상수에 넣기
print(value)
}
guardTest2(value: 2) // 2가 출력됨
guardTest2(value: nil) // guard문에 막혀서 함수가 종료
<프로토콜>
기본 형식
protocol SomeProtocol {
<#requirements#>
}
protocol SomeProtocol2 {
<#requirements#>
}
// 구조체가 두개의 프로토콜을 따르도록 지정
struct SomeStruct: SomeProtocol, SomeProtocol2 {
}
// 1. 프로퍼티 요구
// 특정 프로토콜을 재택한 타입이 어떤 프로퍼티를 구현해야 하는지 요구할 수 있음
protocol FirstProtocol {
var name: Int {get set} // 읽기,쓰기 가능
var age: Int {get} // 읽기 전용
}
// 프로토콜에서 "타입 프로퍼티"를 요구하려면 항상 static 키워드를 붙여야 함
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
// 예시) struct가 프로토콜을 준수
protocol FullyNames {
var fullName: String { get set }
func printFullName() // 몸통부는 없어도 되지만 매개변수의 기본값 지정은 불가
}
// FullyNames 프로토콜이 요구하는 사항을 모두 충족하면 에러메시지 사라짐
struct Person: FullyNames {
var fullName: String
func printFullName() {
print(fullName)
}
}
// 2. initialize 요구
// 특정 프로토콜을 재택한 타입이 생성자도 준수하도록 요구 가능
protocol SomeProtocol {
init(someParameter: Int)
}
protocol SomeProtocol2 {
init()
}
// struct에서는 생성자 요구사항을 준수할때 required 식별자 필요X
// class에서는 생성자 요구사항을 준수할때 required 식별자가 필요
class SomeClass: SomeProtocol2 {
required init() {
}
}
< extension >
- swift의 강력한 기능 중 하나
- 기존의 클래스, 구조체, 열거형, 프로토콜에 새로운 기능을 추가하는 기능
1) 타입 프로퍼티 / 연산 인스턴스 프로퍼티
2) 타입 메서드 / 인스턴스 메서드
3) 이니셜라이저
4) 서브스크립트
5) 중첩 타입
6) 특정 프로토콜을 준수하도록 기능 추가
* 새로운 기능을 추가할수는 있으나 기존 기능을 오버라이딩 하는건 불가
(저장 프로퍼티나 감시자는 추가 X)
// 예시1) int형 타입에 연산 프로퍼티 추가
extension Int {
var isEven: Bool { // 짝수인지 판단
return self % 2 == 0
}
var isOdd: Bool {
return self % 2 == 1
}
}
var number = 3
number.isOdd // true
number.isEven // false
// 예시2) 메소드 기능 추가 : 문자열을 int형으로 바꿔주는 기능
extension String {
func convertToInt() -> Int? {
return Int(self)
}
}
var string = "0"
string.convertToInt() // "0"문자열이 int형 0 으로 변환됨
< 열거형 >
- 연관성이 있는 값들을 모아놓은 것
(ex. 봄/여름/가을/겨울 -> 계절)
enum CompassPoint {
case north
case south
case east
case west
// case north, south, east, west 이렇게도 사용 가능
}
var direction = CompassPoint.east
// 값 변경 시 .만 입력해도 관련 변수들 등장
// 컴파일러가 direction변수는 CompassPoint열거형 타입이라는 것을 추론하기 때문
// 열거형 이름을 생략하고 내부 항목 이름만으로도 사용 가능
direction = .west
// 활용예시) switch함수
switch direction {
case .north:
print("north")
case .south:
print("south")
case .east:
print("east")
case .west:
print("west")
}
// 각 항목이 특정 타입의 값을 원시값으로 갖도록 지정 가능
// CompassPoint2 열거형의 각 항목이 string형의 초기값을 갖도록 지정
enum CompassPoint2: String {
case north = "북"
case south = "남"
case east = "동"
case west = "서"
}
var direction2 = CompassPoint2.north
// 원시값을 가져올땐 rawValue사용
switch direction2 {
case .north:
print(direction2.rawValue)
case .south:
print(direction2.rawValue)
case .east:
print(direction2.rawValue)
case .west:
print(direction2.rawValue)
}
// 각 항목이 특정 타입의 값을 원시값으로 갖도록 지정 가능
// CompassPoint2 열거형의 각 항목이 string형의 초기값을 갖도록 지정
enum CompassPoint2: String {
case north = "북"
case south = "남"
case east = "동"
case west = "서"
}
// 1) 원시값을 가지고 열거형 반환하기
let direction3 = CompassPoint2(rawValue: "남")
// direction3 상수에 south가 들어감
// 2) 열거형 항목에 연관값 추가
enum PhoneError {
case unKnown
case batteryLow(String)
}
let error = PhoneError.batteryLow("배터리가 곧 방전됨")
// batteryLow("배터리가 곧 방전됨") 출력
// 3) 연관값만 추출하려면 switch문 활용
switch error {
case .batteryLow(let message):
print(message)
case .unKnown:
print("알 수 없는 에러입니다.")
}
// "배터리가 곧 방전됨" 만 출력됨
< 옵셔널 체이닝 >
옵셔널에 속해 있는 nil 일지도 모르는 프로퍼티, 메서드, 서브스크립션 등을 가져오거나 호출할 때 사용할 수 있는 일련의 과정
- 옵셔널에 값이 있으면 프로퍼티, 메소드 등 호출 O
- 옵셔널이 nil이면 프로퍼티, 메소드는 nil 반환
struct Developer {
let name: String
}
struct Company {
let name: String
let developer: Developer? // 옵셔널값
}
var company = Company(name:"gunter", developer: nil)
print(company.developer) // nil 출력
var developer = Developer(name: "yang")
var company2 = Company(name: "gunter", developer: developer)
print(company2.developer) // optional에 감싸진 developer 출력
print(company2.developer.name)
// 에러 발생: optional에 감싸져있기 때문에 내부프로퍼티 name에 접근 불가 -> 옵셔널을 벗겨야함(옵셔널바인딩 or 옵셔널 체이닝)
// 옵셔널 바인딩 말고 옵셔널 체이닝을 통해 값 벗겨내기
print(company2.developer?.name) // optional에 감싸진 yang
print(company2.developer!.name) // yang (강제언래핑 하므로)
< try catch 문 >
- 프로그램 내에서 에러가 발생한 상황에 대해 대응하고 복구하는 과정
- swift에서는 런타임 에러 발생한 경우 이를 처리하기 위한 4가지 일급클래스를 제공
1) 발생(throwing)
2) 감지(catching)
3) 전파(propagation)
4) 조작(manipulating)
- swift에서 error는 Error프로토콜을 따르는 타입의 값으로 표현됨
- Error프로토콜은 요구사항이 없는 빈 프로토콜이지만 오류를 표현하기 위해서는 이 프로토콜을 채택해야 함
// 오류가 발생했을 때 정상적으로 실행할 수 없다는 것을 나타내기 위해 throw 구문 사용
// 에러가 발생할 것 같은 지점에 throw를 던져 에러를 발생시킴
throw PhoneError.batteryLow(batteryLevel: 20)
- 오류 처리 방법
1) 함수에서 발생한 오류를 해당 함수를 호출한 코드에 전파
2) do-catch 구문
3) try?를 통해 오류를 optional값 으로 처리
4) 오류가 발생하지 않을것이라고 확신
// 오류가 던져진 것에 대비하여, 던져진 오류를 처리하기 위한 대비 코드
// 오류 처리 4가지 방법
// 1) 함수에서 발생한 오류를 해당 함수를 호출한 코드에 전파
// 오류가 발생할 수 있음을 나타내기 위해 함수(매개변수) 뒤에 throws
func checkPhoneBatteryStatus(batteryLevel: Int) throws -> String {
// 오류가 throwing 되면 String 변수 반환
guard batteryLevel != -1 else { throw PhoneError.unknown }
guard batteryLevel <= 20 else { throw PhoneError.batteryLow(batteryLevel: 20)}
return "배터리 상태가 정상입니다."
}
// 2) do-catch문으로 에러를 처리
/*
do {
try 오류 발생 가능 코드
} catch 오류 패턴 {
처리 코드
}
*/
// checkPhoneBatteryStatus메소드가 오류를 발생시켜도 catch문이 오류를 잡아 적절한 예외 처리 가능
do {
try checkPhoneBatteryStatus(batteryLevel: -1)
} catch PhoneError.unknown {
print("알 수 없는 에러입니다.")
} catch PhoneError.batteryLow(let batteryLevel) {
print("배터리 전원 부족, 남은 배터리: \(batteryLevel)")
} catch {
print("그 외 오류 발생: \(error)")
}
// 2) try? 옵셔널 값 변경
let status = try? checkPhoneBatteryStatus(batteryLevel: -1)
print(status) // 메소드가 unknown을 반환하므로 status는 nil
// 메소드가 에러를 던져주지 않는다면 optional값이 출력됨
let status2 = try? checkPhoneBatteryStatus(batteryLevel: 30)
print(status2)
// Optional("배터리 상태가 정상입니다.")
// try! : 오류가 절대 발생하지 않을것이라고 확신할때
// 메소드가 오류를 발생하면 런타임에러, 프로그램 종료되므로 유의해서 사용
let status3 = try! checkPhoneBatteryStatus(batteryLevel: 30)
< 클로저 >
- 코드에서 전달 및 사용할 수 있는 독립 기능 블록이며, 일급 객체의 역할을 할 수 있음
* 일급객체란? 전달 인자로 보낼 수 있고, 변수/상수 등으로 저장하거나 전달할 수 있으며 함수의 반환값이 될 수도 있음
- 보통 unnamed closure를 칭함
- unnamed라서 func표현식 사용
- closure head와 closure body를 in 키워드로 구분
let hello = { () -> () in
print("hello")
}
// 클로저 호출
hello()
let hello2 = { (name: String) -> String in
return "Hello, \(name)"
}
// 전달인자 레이블을 넣지 않고 바로 파라미터 값을 전달해야함
hello2(name: "Gunter") // 에러
hello2("Gunter")
// 클로저를 함수 파라미터로 전달
func doSomething(closure:() -> ()) {
closure()
}
doSomething(closure: { () -> () in
print("hello")
})
// 클로저를 함수 반환값으로 사용
func doSomething2() -> () -> () {
return { () -> () in
print("hello4")
}
}
doSomething2()()
- 클로저를 간단하게 표현하는 법
// 후행 클로저를 통해 읽기 쉽게 변환
// int형 3개를 매개변수로 받고 int형을 반환하는 클로저
func doSomething(closure: (Int, Int, Int) -> Int) {
closure(1,2,3)
}
doSomething(closure: { (a, b, c) in
return a+b+c
})
doSomething(closure: {
return $0+$1+$2
})
// 단일 return문은 return도 생략 가능
doSomething() {
$0+$1+$2
}
// 단 하나의 클로저만 매개변수로 전달하면 ()도 생략 가능
doSomething {
$0+$1+$2
}
< 고차함수 >
swift가 일급객체 함수라서 가능한 기능 중 하나
- 다른 함수를 전달 인자로 받거나, 함수 실행 결과를 함수로 반환하는 함수
- map, filter, reduce
// 1) map : 컨테이너 내부의 기존 데이터를 변형하여 새로운 컨테이너 생성
let numbers = [0,1,2,3]
let mapArray = numbers.map { (number) -> Int in
return number * 2
}
print(mapArray) // [0,2,4,6]
// 2) filter : 컨테이너 내부의 데이터를 걸러서 새로운 컨테이너로 추출
let intArray = [10,5,20,13,4]
let filterArray = intArray.filter{ $0 > 5 }
print(filterArray) // [10,20,13]
// 3) reduce : 컨테이너 내부 데이터를 하나로 통합
// result: 누적 값, element: 각 요소
let someArray = [1,2,3,4,5]
let reduceResult = someArray.reduce(0) { // 괄호안은 result의 초기값
(result: Int, element: Int) -> Int in
print("\(result) + \(element)")
return result + element
}
/*
0+1
1+2
3+3
6+4
10+5