< 2단원. 다양한 표현 및 확장 >
1. 구조체
- Swift 대부분의 타입은 구조체로 이루어져 있다.
- 구조체는 값(value) 타입이다.
- 타입명은 대문자 Camel Case를 사용하여 정의한다.
struct 이름 {
/* 구현부 */
}
e.g.)
struct Sample {
// 가변 프로퍼티(값 변경 가능)
var mutableProperty: Int = 100
// 불변 프로퍼티(값 변경 불가능)
let immutableProperty: Int = 100
// 타입 프로퍼티(static 키워드 사용: 타입 자체가 사용하는 프로퍼티)
static var typeProperty: Int = 100
// 인스턴스 메서드(인스턴스가 사용하는 메서드)
func instanceMethod() {
print("instance method")
}
// 타입 메서드(static 키워드 사용: 타입 자체가 사용하는 메서드)
static func typeMethod() {
print("type method")
}
}
// 가변 인스턴스 생성
var mutable: Sample = Sample()
mutable.mutableProperty = 200
mutable.immutableProperty = 200 // 불변 프로퍼티는 인스턴스 생성 후 수정 불가능. 컴파일 오류발생
// 불변 인스턴스 생성
let immutable: Sample = Sample()
// 불변 인스턴스는 가변 프로퍼티라도 인스턴스 생성 후에 수정 불가능
immutable.mutableProperty = 200 // 컴파일 오류발생
immutable.immutableProperty = 200 // 컴파일 오류발생
// 타입 프로퍼티 및 메서드
Sample.typeProperty = 300
Sample.typeMethod()
mutable.typeProperty = 400 // 인스턴스에서는 타입 프로퍼티 사용 불가능. 컴파일 오류발생
mutable.typeMethod() // 인스턴스에서는 타입 메서드 사용 불가능. 컴파일 오류발생
2. 클래스
- 클래스는 참조(reference) 타입이다.
- 타입명은 대문자 Camel Case를 사용하여 정의한다.
- Swift의 클래스는 다중 상속이 불가능하다.
class 이름 {
/* 구현부 */
}
e.g.)
class Sample {
// 가변 프로퍼티
var mutableProperty: Int = 100
// 불변 프로퍼티
let immutableProperty: Int = 100
// 타입 프로퍼티
static var typeProperty: Int = 100
// 인스턴스 메서드
func instanceMethod() {
print("instance method")
}
// 타입 메서드
// 상속 시 재정의 불가 타입 메서드 - static
static func typeMethod() {
print("type method - static")
}
// 상속 시 재정의 가능 타입 메서드 - class
class func classMethod() {
print("type method - class")
}
}
// 인스턴스 생성 - 참조정보 수정 가능
var mutableReference: Sample = Sample()
mutableReference.mutableProperty = 200
// 불변 프로퍼티는 인스턴스 생성 후 수정할 수 없습니다.
mutableReference.immutableProperty = 200 // 컴파일 오류발생
// 인스턴스 생성 - 참조정보 수정 불가
let immutableReference: Sample = Sample()
// ** 클래스의 인스턴스는 참조 타입이므로 let으로 선언되었더라도 인스턴스 프로퍼티의 값 변경이 가능합니다.
immutableReference.mutableProperty = 200
// 다만 참조정보를 변경할 수는 없습니다.
immutableReference = mutableReference // 컴파일 오류발생
// 참조 타입이라도 불변 인스턴스는 인스턴스 생성 후에 수정할 수 없습니다.
immutableReference.immutableProperty = 200 // 컴파일 오류발생
// 타입 프로퍼티 및 메서드
Sample.typeProperty = 300
Sample.typeMethod()
// 인스턴스에서는 타입 프로퍼티나 타입 메서드를 사용할 수 없습니다.
mutableReference.typeProperty = 400 // 컴파일 오류발생
mutableReference.typeMethod() // 컴파일 오류발생
3. 열거형
* Swift의 열거형은 다른 언어와는 다르게 강력한 기능을 지니고 있다.
- 유사한 종류의 여러 값을 한 곳에 모아서 정의한 것이다.
- enum 자체가 하나의 데이터 타입으로 대문자 Camel Case를 사용하여 이름을 정의한다.
- 각 case는 소문자 Camel Case로 정의한다.
- 각 case는 그 자체가 고유의 값이다. (각 case에 자동으로 정수값이 할당되지 않는다.)
- 각 case는 한줄에 개별로도, 한줄에 여러개도 정의할 수 있다.
enum 이름 {
case 이름1
case 이름2
case 이름3, 이름4, 이름5
}
- 타입이 명확할 경우 열거형 이름은 생략 가능하다.
enum Weekday {
case mon
case tue
case wed
case thu, fri, sat, sun
}
// 열거형 타입과 케이스 모두 사용 가능
var day: Weekday = Weekday.mon
// 타입이 명확할 경우 케이스처럼 표현 가능
day = .tue
print(day) // tue
// switch의 비교값에 열거형 타입이 위치할 때 모든 열거형 케이스를 포함한다면 default 생략 가능
switch day {
case .mon, .tue, .wed, .thu:
print("평일입니다")
case Weekday.fri:
print("불금입니다")
case .sat, .sun:
print("신나는 주말입니다")
}
- rawValue(원시값) : C언어의 enum처럼 정수값을 가질 수 있다. case별로 각각 다른 값을 가져야 하며, 지정되지 않은 경우 자동으로 1이 증가된 값이 할당된다.
enum Fruit: Int {
case apple = 0
case grape = 1
case peach
case mango = 0 // mango와 apple의 원시값이 같으므로 컴파일 오류발생
}
print("Fruit.peach.rawValue == \(Fruit.peach.rawValue)") // Fruit.peach.rawValue == 2
// Hashable 프로토콜을 따르는 모든 타입을 원시값의 타입으로 지정할 수 있다.
enum School: String {
case elementary = "초등"
case middle = "중등"
case high = "고등"
case university
}
// 열거형의 원시값 타입이 String일 때, 원시값이 지정되지 않았다면 case의 이름을 원시값으로 사용
print("School.university.rawValue == \(School.university.rawValue)") // School.university.rawValue == university
- rawValue를 통해 초기화 할 수 있으며, rawValue가 case에 해당하지 않을 수 있으므로 rawValue를 통해 초기화 한 인스턴스는 Optional 타입이다.
let apple: Fruit = Fruit(rawValue: 0) // rawValue를 통해 초기화 한 열거형 값은 optional type 컴파일 오류발생
let apple: Fruit? = Fruit(rawValue: 0)
// if let 구문을 사용하면 rawValue에 해당하는 케이스를 바로 사용할 수 있다.
if let orange: Fruit = Fruit(rawValue: 5) {
print("rawValue 5에 해당하는 케이스는 \(orange)입니다")
} else {
print("rawValue 5에 해당하는 케이스가 없습니다")
} // rawValue 5에 해당하는 케이스가 없습니다
- Swift의 열거형에는 메서드도 추가할 수 있다.
enum Month {
case dec, jan, feb
case mar, apr, may
case jun, jul, aug
case sep, oct, nov
func printMessage() {
switch self {
case .mar, .apr, .may:
print("따스한 봄")
case .jun, .jul, aug:
print("무더운 여름")
case .sep, .oct, .nov:
print("선선한 가을")
case .dec, .jan, .feb:
print("추운 겨울")
}
}
}
Month.mar.printMessage() // 따스한 봄
4. 클래스 vs 구조체/열거형
- 클래스는 참조 타입, 열거형과 구조체는 값 타입
- 클래스는 상속(subclassing)이 가능하지만 열거형과 구조체는 불가능
* 값 타입(Value Type) : 데이터를 전달할 때 값을 복사하여 전달
* 참조 타입(Reference Type) : 데이터를 전달할 때 값의 메모리 위치를 전달
struct ValueType {
var property = 1
}
class ReferenceType {
var property = 1
}
// 첫번째 구조체 인스턴스
let firstStructInstance = ValueType()
// 두번째 구조체 인스턴스에 첫번째 인스턴스 값 복사
var secondStructInstance = firstStructInstance
// 두번째 구조체 인스턴스 프로퍼티 값 수정
secondStructInstance.property = 2
// 두번째 구조체 인스턴스는 첫번째 구조체를 똑같이 복사한 별도의 인스턴스이기 때문에 두번째 구조체 인스턴스의 프로퍼티 값을 변경해도 첫번째 구조체 인스턴스의 프로퍼티 값에는 영향이 없음
print("first struct instance property : \(firstStructInstance.property)") // 1
print("second struct instance property : \(secondStructInstance.property)") // 2
// 클래스 인스턴스 생성 후 첫번째 참조 생성
let firstClassReference = ReferenceType()
// 두번째 참조 변수에 첫번째 참조 할당
let secondClassReference = firstClassReference
secondClassReference.property = 2
// 두번째 클래스 참조는 첫번째 클래스 인스턴스를 참조하기 때문에 두번째 참조를 통해 인스턴스의 프로퍼티 값을 변경하면, 첫번째 클래스 인스턴스의 프로퍼티 값을 변경하게 됨
print("first class reference property : \(firstClassReference.property)") // 2
print("second class reference property : \(secondClassReference.property)") // 2
* 값 타입(Value Type)을 사용하는 경우
- 연관된 몇몇의 값들을 모아서 하나의 데이터 타입으로 표현하고 싶은 경우
- 다른 객체 또는 함수 등으로 전달될 때 참조가 아니라 복사할 경우
- 자신을 상속할 필요가 없거나, 다른 타입을 상속 받을 필요가 없는 경우
* Swift에서의 사용
- Swift의 기본 데이터 타입은 모두 구조체로 구현되어 있다. (Swift는 구조체와 열거형 사용을 선호함)
- Apple 프레임 워크의 대부분의 큰 뼈대는 모두 클래스로 구성되어 있다.
5. 클로저 기본
- 클로저(Closure) : 실행 가능한 코드 블럭. 이름 정의가 필요하지 않다는 것 외에 함수와 동일하다. (함수는 클로저의 일종으로 이름이 있는 클로저라고 한다. 일급 객체)
{ (매개변수 목록) -> 반환 타입 in
실행 코드
}
e.g.)
let sum: (Int, Int) -> Int = { (a: Int, b: Int) in
return a + b
}
let sumResult: Int = sum(1, 2)
print(sumResult) // 3
- 클로저는 함수 내부에서 원하는 코드 블럭을 실행할 수 있기 때문에 주로 함수의 전달인자로 많이 사용된다.
let add: (Int, Int) -> Int
add = { (a: Int, b: Int) in
return a + b
}
let substract: (Int, Int) -> Int
substract = { (a: Int, b: Int) in
return a - b
}
let divide: (Int, Int) -> Int
divide = { (a: Int, b: Int) in
return a / b
}
func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
return method(a, b)
}
var calculated: Int
calculated = calculate(a: 50, b: 10, method: add)
print(calculated) // 60
calculated = calculate(a: 50, b: 10, method: substract)
print(calculated) // 40
calculated = calculate(a: 50, b: 10, method: divide)
print(calculated) // 5
// 따로 클로저를 상수/변수에 넣어 전달하지 않고, 함수를 호출할 때 클로저를 작성하여 전달할 수도 있습니다.
calculated = calculate(a: 50, b: 10, method: { (left: Int, right: Int) -> Int in
return left * right
})
print(calculated) // 500
6. 클로저 심화
- 클로저는 아래 규칙을 통해 다양한 모습으로 표현 가능하다.
1) 후행 클로저(trailing closure) : 클로저가 함수의 마지막 전달인자일 때, 마지막 매개변수 이름을 생략한 후 함수 소괄호 외부에 클로저를 구현할 수 있다.
result = calculate(a: 10, b: 10) { (left: Int, right: Int) -> Int In
return left + right
}
print(result) // 20
2) 반환 타입 생략 : 컴파일러가 클로저의 타입을 유추할 수 있는 경우 매개변수, 반환 타입은 생략이 가능하다. (단, in 키워드는 생략 불가능)
result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) in
return left + right
})
print(result) // 20
// 후행 클로저와 함께 사용할 수 있다.
result = calculate(a: 10, b: 10) { (left: Int, right: Int) in
return left + right
}
print(result) // 20
3) 단축 인자 이름 : 전달인자의 이름이 굳이 필요없고, 컴파일러가 타입을 유추할 수 있는 경우 축약된 전달인자 이름($0, $1, $2...)을 사용할 수 있다.
result = calculate(a: 10, b: 10, method: {
return $0 + $1
})
print(result) // 20
// 당연히 후행 클로저와 함께 사용할 수 있다.
result = calculate(a: 10, b: 10) {
return $0 + $1
}
print(result) // 20
4) 암시적 반환 표현 : 반환값이 있는 경우, 암시적으로 클로저의 맨 마지막 줄은 return 키워드를 생략하더라도 반환값으로 취급한다.
result = calculate(a: 10, b: 10) {
$0 + $1
}
print(result) // 20
// 간결하게 한 줄로 표현해 줄 수도 있다.
result = calculate(a: 10, b: 10) { $0 + $1 }
print(result) // 20
7. 프로퍼티
- 종류 : 인스턴스 저장 프로퍼티, 타입 저장 프로퍼티, 인스턴스 연산 프로퍼티, 타입 연산 프로퍼티, 지연 저장 프로퍼티
- 구조체, 클래스, 열거형 내부에 구현 가능하다. (다만, 열거형 내부에는 연산 프로퍼티만 구현 가능)
- 연산 프로퍼티는 var로만 선언 가능하며, 읽기 전용으로는 구현할 수 있지만, 쓰기 전용은 구현할 수 없다.
- 읽기 전용으로 구현하려면 get 블럭만 작성해주면 된다. (생략 가능)
- 읽기, 쓰기 모두 가능하게 하려면 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 = "학생"
/*
// 인스턴스 메서드
func selfIntroduce() {
print("저는 \(self.class)반 \(name)입니다")
}
*/
// 읽기 전용 인스턴스 연산 프로퍼티
// 간단히 위의 selfIntroduce() 메서드를 대체할 수 있다.
var selfIntroduction: String {
get {
return "저는 \(self.class)반 \(name)입니다"
}
}
/*
// 타입 메서드
static func selfIntroduce() {
print("학생타입입니다")
}
*/
// 읽기 전용 타입 연산 프로퍼티
// 읽기 전용에서는 get을 생략할 수 있다.
static var selfIntroduction: String {
return "학생타입입니다"
}
}
// 타입 연산 프로퍼티 사용
print(Student.selfIntroduction) // 학생타입입니다
// 인스턴스 생성
var noey: Student = Student()
noey.koreanAge = 10
// 인스턴스 저장 프로퍼티 사용
noey.name = "noey"
print(noey.name) // noey
// 인스턴스 연산 프로퍼티 사용
print(noey.selfIntroduction) // 저는 Swift반 noey입니다
print("제 한국나이는 \(noey.koreanAge)살이고, 미국나이는 \(noey.westernAge)살입니다.") // 제 한국나이는 10살이고, 미국나이는 9살입니다.
e.g.)
struct Money {
var currencyRate: Double = 1100
var dollar: Double = 0
var won: Double {
get {
return dollar * currencyRate
}
set {
dollar = newValue / currencyRate
}
}
}
var moneyInMyPocket = Money()
moneyInMyPocket.won = 11000
print(moneyInMyPocket.won) // 11000
moneyInMyPocket.dollar = 10
print(moneyInMyPocket.won) // 11000
- 저장 프로퍼티와 연산 프로퍼티의 기능은 함수, 메서드, 클로저, 타입 등의 외부에 위치한 지역/전역 변수에도 모두 사용이 가능하다.
var a: Int = 100
var b: Int = 200
var sum: Int {
return a + b
}
print(sum) // 300
8. 프로퍼티 감시자(Property Observer)
- 프로퍼티의 값이 변경될 때 원하는 동작을 수행할 수 있다.
- 값이 변경되기 직전에 willSet 블럭이, 값이 변경된 직후에 didSet 블럭이 호출된다. (둘 중 하나만 구현해도 무방)
- 변경되려는 값이 기존값과 동일해도 프로퍼티 감시자는 항상 동작한다.
- willSet 블럭에서는 암시적 매개변수 newValue, didSet 블럭에서는 oldValue를 사용할 수 있다.
- 프로퍼티 감시자는 연산 프로퍼티에는 사용할 수 없다. (저장된 값 변경 시 사용되기 때문에)
- 함수, 메서드, 클로저, 타입 등의 지역/전역 변수에 모두 사용 가능하다.
struct Money {
// 프로퍼티 감시자 사용
var currencyRate: Double = 1100 {
willSet(newRate) {
print("환율이 \(currencyRate)에서 \(newRate)으로 변경될 예정입니다")
}
didSet(oldRate) {
print("환율이 \(oldRate)에서 \(currencyRate)으로 변경되었습니다")
}
}
// 프로퍼티 감시자 사용
var dollar: Double = 0 {
// willSet의 암시적 매개변수 이름 newValue
willSet {
print("\(dollar)달러에서 \(newValue)달러로 변경될 예정입니다")
}
// didSet의 암시적 매개변수 이름 oldValue
didSet {
print("\(oldValue)달러에서 \(dollar)달러로 변경되었습니다")
}
}
// 연산 프로퍼티
var won: Double {
get {
return dollar * currencyRate
}
set {
dollar = newValue / currencyRate
}
/* 프로퍼티 감시자와 연산 프로퍼티 기능은 동시에 사용할 수 없다.
willSet {
}
*/
}
}
var moneyInMyPocket: Money = Money()
// 환율이 1100.0에서 1150.0으로 변경될 예정입니다
moneyInMyPocket.currencyRate = 1150
// 환율이 1100.0에서 1150.0으로 변경되었습니다
// 0.0달러에서 10.0달러로 변경될 예정입니다
moneyInMyPocket.dollar = 10
// 0.0달러에서 10.0달러로 변경되었습니다
print(moneyInMyPocket.won) // 11500.0
9. 상속(Inheritance)
- 클래스, 프로토콜 등에서 가능하며, 열거형, 구조체는 상속이 불가능하다.
- Swift의 클래스는 단일 상속으로 다중 상속을 지원하지 않는다.
class 이름: 상속받을 클래스 이름 {
/* 구현부 */
}
- final 키워드를 사용하면 재정의(override)를 방지할 수 있다.
- static 키워드를 사용해 타입 메서드를 만들면 재정의가 불가능하다.
- class 키워드를 사용해 타입 메서드를 만들면 재정의가 가능하다.
- final class는 static 키워드를 사용한 것과 동일하게 동작한다. (재정의가 불가능하다)
- override 키워드를 사용해 부모 클래스의 메서드를 재정의 할 수 있다.
// 기반 클래스 Person
class Person {
var name: String = ""
func selfIntroduce() {
print("저는 \(name)입니다")
}
// final 키워드를 사용하여 재정의를 방지할 수 있다.
final func sayHello() {
print("hello")
}
// 타입 메서드
// 재정의 불가 타입 메서드 - static
static func typeMethod() {
print("type method - static")
}
// 재정의 가능 타입 메서드 - class
class func classMethod() {
print("type method - class")
}
// 재정의 가능한 class 메서드라도 final 키워드를 사용하면 재정의 할 수 없다.
// 메서드 앞의 'static'과 'final class'는 똑같은 역할을 한다.
final class func finalClassMethod() {
print("type method - final class")
}
}
// Person을 상속받는 Student
class Student: Person {
var major: String = ""
override func selfIntroduce() {
print("저는 \(name)이고, 전공은 \(major)입니다")
}
override class func classMethod() {
print("overriden type method - class")
}
// static을 사용한 타입 메서드는 재정의 할 수 없다.
override static func typeMethod() { } // 컴파일 오류발생
// final 키워드를 사용한 메서드, 프로퍼티는 재정의 할 수 없다.
override func sayHello() { } // 컴파일 오류발생
override class func finalClassMethod() { } // 컴파일 오류발생
}
let noey: Person = Person()
let hana: Student = Student()
noey.name = "noey"
hana.name = "hana"
hana.major = "Swift"
noey.selfIntroduce() // 저는 noey입니다
hana. selfIntroduce() // 저는 hana이고, 전공은 Swift입니다
Person.classMethod() // type method - class
Person.typeMethod() // type method - static
Person.finalClassMethod() // type method - final class
Student.classMethod() // overriden type method - class
Student.typeMethod() // type method - static
Student.finalClassMethod() // type method - final class
10. 인스턴스 생성(init) / 소멸(deinit)
- Swift의 모든 인스턴스는 초기화와 동시에 모든 프로퍼티에 유효한 값이 할당되어 있어야 한다.
- 프로퍼티에 미리 기본값을 할당해두면 인스턴스가 생성됨과 동시에 초기값을 지니게 된다.
class PersonA {
// 모든 저장 프로퍼티에 기본값 할당
var name: String = "unknown"
var age: Int = 0
var nickName: String = "nick"
}
// 인스턴스 생성
let jason: PersonA = PersonA()
// 기본값이 인스턴스가 지녀야 할 값과 맞지 않다면 생성된 인스턴스의 프로퍼티에 각각 값 할당
jason.name = "jason"
jason.age = 30
jason.nickName = "j"
- 이니셜라이저(Initializer) : 프로퍼티 초기값을 지정하기 어려운 경우, init을 통해 인스턴스가 가져야 할 초기값을 전달할 수 있다.
class PersonB {
var name: String
var age: Int
var nickName: String
// 이니셜라이저
init(name: String, age:Int, nickName: String) {
self.name = name
self.age = age
self.nickName = nickName
}
}
let hana: PersonB = PersonB(name: "hana", age: 20, nickName: "하나")
// 프로퍼티의 초기값이 꼭 필요 없을 때
// - 옵셔널을 사용
// - class 내부의 init을 사용할 때는 convenience 키워드 사용
class PersonC {
var name: String
var age: Int
var nickName: String?
init(name: String, age: Int, nickName: String) {
self.name = name
self.age = age
self.nickName = nickName
}
// 위와 동일한 기능 수행
convenience init(name: String, age:Int, nickName: String) {
self.init(name: name, age: age)
self.nickName = nickName
}
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
let jenny: PersonC = PersonC(name: "jenny", age: 10)
let mike: PersonC = PersonC(name: "mike", age: 15, nickName: "m")
// 암시적 추출 옵셔널은 인스턴스 사용에 꼭 필요하지만 초기값을 할당하지 않고자 할 때 사용
class Puppy {
var name: String
var owner: PersonC!
init(name: String) {
self.name = name
}
func goOut() {
print("\(name)가 주인 \(owner.name)와 산책을 합니다")
}
}
let happy: Puppy = Puppy(name: "happy")
// happy.goOut() // owner가 없는 상태라 컴파일 오류발생
happy.owner = jenny
happy.goOut() // happy가 주인 jenny와 산책을 합니다
- 이니셜라이저 매개변수로 전달되는 초기값이 잘못된 경우 인스턴스 생성에 실패할 수 있으며, nil을 반환한다.
- 실패 가능한 이니셜라이저의 반환 타입은 옵셔널 타입이다. (init?을 사용)
class PersonD {
var name: String
var age: Int
var nickName: String?
init?(name: String, age: Int) {
if (0...120).contains(age) == false {
return nil
}
if name.count == 0 {
return nil
}
self.name = name
self.age = age
}
}
// let john: PersonD = PersonD(name: "john", age: 23) // 컴파일 오류발생
let john: PersonD? = PersonD(name: "john", age: 23)
let john: PersonD? = PersonD(name: "joker", age: 123)
let batman: PersonD? = PersonD(name: "", age: 10)
print(joker) // nil
print(batman) // nil
- 디이니셜라이저(deinitializer) : deinit은 클래스의 인스턴스가 메모리에서 해제되는 시점에 자동으로 호출된다. 매개변수를 지닐 수 없으며, 클래스 타입에만 구현 가능하다.
- 인스턴스가 메모리에서 해제되는 시점은 ARC(Automatic Reference Counting)의 규칙에 따라 결정.
class PersonE {
var name: String
var pet: Puppy?
var child: PersonC
init(name: String, child: PersonC) {
self.name = name
self.child = child
}
// 인스턴스가 메모리에서 해제되는 시점에 자동 호출
deinit {
if let petName = pet?.name {
print("\(name)가 \(child.name)에게 \(petName)을 인도합니다")
self.pet?.owner = child
}
}
}
var donald: PersonE? = PersonE(name: "donald", child: jenny)
donald?.pet = happy
donald = nil // donald 인스턴스가 더이상 필요없으므로 메모리에서 해제된다.
// donald가 jenny에게 happy를 인도합니다.
11. 옵셔널 체이닝(Optional Chaining) & nil 병합 연산자
- 옵셔널 체이닝(Optional Chaining) : 옵셔널 내부의 내부의 내부로 옵셔널이 연결되어 있을 때 매번 nil 확인을 하지 않고 최종적으로 원하는 값이 있는지 없는지 확인할 수 있다.
class Person {
var name: String
var job: String?
var home: Apartment?
init(name: String) {
self.name = name
}
}
class Apartment {
var buildingNumber: String
var roomNumber: String
var 'guard': Person?
var owner: Person?
init(dong: String, ho: String) {
buildingNumber = dong
roomNumber = ho
}
}
// 옵셔널 체이닝 사용
let noey: Person? = Person(name: "noey")
let apart: Apartment? = Apartment(dong: "101", ho: "202")
let superman: Person? = Person(name: "superman")
// 옵셔널 체이닝이 실행 후 결과값이 nil일 수 있으므로 결과 타입도 옵셔널입니다.
// 옵셔널 체이닝을 사용하지 않는 경우
func guardJob(owner: Person?) {
if let owner = owner {
if let home = owner.home {
if let 'guard' = home.guard {
if let guardJob = 'guard'job {
print("우리집 경비원의 직업은 \(guardJob)입니다")
} else {
print("우리집 경비원은 직업이 없어요")
}
}
}
}
}
guardJob(owner.noey)
// 옵셔널 체이닝을 사용할 경우
func guardJobWithOptionalChaining(owner: Person?) {
if let guardJob = owner?.home?.guard?.job {
print("우리집 경비원의 직업은 \(guardJob)입니다")
} else {
print("우리집 경비원은 직업이 없어요")
}
}
guardJobWithOptionalChaining(owner: noey) // 우리집 경비원은 직업이 없어요
noey?.home?.guard?.job // nil
noey?.home = apart
noey?.home // Optional(Apartment)
noey?.home?.guard // nil
// 경비원 할당
noey?.home?.guard = superman
noey?.home?.guard // Optional(Person)
noey?.home?.guard?.name // superman
noey?.home?.guard?.job // nil
noey?.home?.guard?.job = "경비원"
- nil 병합 연산자 : 중위 연산자. Optional ?? Value 옵셔널 값이 nil일 경우, 우측의 값을 반환한다. (띄어쓰기 주의)
var guardJob: String
guardJob = noey?.home?.guard?.job ?? "슈퍼맨"
print(guardJob) // 경비원
noey?.home?.guard?.job = nil
guardJob = noey?.home?.guard?.job ?? "슈퍼맨"
print(guardJob) // 슈퍼맨
12. 타입 캐스팅(Type Casting)
- 클래스의 인스턴스를 부모 혹은 자식 클래스의 타입으로 사용할 수 있는지 확인하는 용도이다. is, as를 사용한다.
* 형변환은 타입 캐스팅이 아니라 새로운 값을 생성하는 것. *
class Person {
var name: String = ""
func breath() {
print("숨을 쉽니다")
}
}
class Student: Person {
var school: String = ""
func goToSchool() {
print("등교를 합니다")
}
}
class UniversityStudent: Student {
var major: String = ""
func goToMT() {
print("멤버쉽 트레이닝을 갑니다")
}
}
// 인스턴스 생성
var noey: Person = Person()
var hana: Student = Student()
var jason: UniversityStudent = UniversityStudent()
- is를 사용하여 타입을 확인한다.
var result: Bool
result = noey is Person // true
result = noey is Student // false
result = noey is UniversityStudent // false
result = hana is Person // true
result = hana is Student // true
result = hana is UniversityStudent // false
result = jason is Person // true
result = jason is Student // true
result = jason is UniversityStudent // true
if noey is UniversityStudent {
print("noey는 대학생입니다")
} else if noey is Student {
print("noey는 학생입니다")
} else if noey is Person {
print("noey는 사람입니다")
} // noey는 사람입니다
switch jason {
case is Person:
print("jason은 사람입니다")
case is Student:
print("jason은 학생입니다")
case is UniversityStudent:
print("jason은 대학생입니다")
default:
print("jason은 사람도, 학생도, 대학생도 아닙니다")
} // jason은 사람입니다
switch jason {
case is UniversityStudent:
print("jason은 대학생입니다")
case is Student:
print("jason은 학생입니다")
case is Person:
print("jason은 사람입니다")
default:
print("jason은 사람도, 학생도, 대학생도 아닙니다")
} // jason은 대학생입니다
- 업 캐스팅(Up Casting) : as를 사용하여 부모 클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 타입 정보를 전환해준다. Any 혹은 AnyObject로도 타입 정보를 변환할 수 있다. (암시적으로 처리되므로 생략 가능)
// UniversityStudent 인스턴스를 생성하여 Person 행세를 할 수 있도록 업 캐스팅
var mike: Person = UniversityStudent() as Person
var jenny: Student = Student()
// var jina: UniversityStudent = Person() as UniversityStudent // 컴파일 오류발생
// UniversityStudent 인스턴스를 생성하여 Any 행세를 할 수 있도록 업 캐스팅
var jina: Any = Person() // as Any 생략가능
- 다운 캐스팅(Down Casting) : as? 또는 as!를 사용하여 자식 클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 인스턴스의 타입 정보를 전환해준다.
// A. 조건부 다운 캐스팅
// as?를 사용한다.
// 캐스팅에 실패하면, 즉 캐스팅하려는 타입에 부합하지 않는 인스턴스라면 nil을 반환하기 때문에 결과의 타입은 옵셔널 타입이다.
var optionalCasted: Student?
optionalCasted = mike as? UniversityStudent
optionalCasted = jenny as? UniversityStudent // nil
optionalCasted = jina as? UniversityStudent // nil
optionalCasted = jina as? Student // nil
// B. 강제 다운 캐스팅
// as!를 사용한다.
// 캐스팅에 실패하면, 즉 캐스팅하려는 타입에 부합하지 않는 인스턴스라면 런타임 오류가 발생한다.
// 캐스팅에 성공하면, 옵셔널이 아닌 일반 타입을 반환한다.
var forcedCasted: Student
forcedCasted = mike as! UniversityStudent
// forcedCasted = jenny as! UniversityStudent // 런타임 오류발생
// forcedCasted = jina as! UniversityStudent // 런타임 오류발생
// forcedCasted = jina as! Student // 런타임 오류발생
e.g.)
func doSomethingWithSwitch(someone: Person) {
switch someone {
case is UniversityStudent:
(someone as! UniversityStudent).goToMT()
case is Student:
(someone as! Student).goToSchool()
case is Person:
(someone as! Person).breath()
}
}
doSomethingWithSwitch(someone: mike as Person) // 멤버쉽 트레이닝을 갑니다
doSomethingWithSwitch(someone: mike) // 멤버쉽 트레이닝을 갑니다
doSomethingWithSwitch(someone: jenny) // 등교를 합니다
doSomethingWithSwitch(someone: noey) // 숨을 쉽니다
func doSomething(someone: Person) {
if let universityStudent = someone as? UniversityStudent {
universityStudent.goToMT()
} else if let student = someone as? Student {
student.goToSchool()
} else if let person = someone as? Person {
person.breath()
}
}
doSomething(someone: mike as Person) // 멤버쉽 트레이닝을 갑니다
doSomething(someone: mike) // 멤버쉽 트레이닝을 갑니다
doSomething(someone: jenny) // 등교를 합니다
doSomething(someone: noey) // 숨을 쉽니다
13. assert & guard
- 애플리케이션이 동작 도중에 생성하는 다양한 연산 결과값을 동적으로 확인하고 안전하게 처리할 수 있도록 확인하고 빠르게 처리할 수 있다.
- Assertion : assert(_:_file:line:) 함수를 사용한다. 디버깅 모드에서만 동작하여 예상했던 조건의 검증을 위해 사용한다.
var someInt: Int = 0
// 검증 조건과 실패시 나타날 문구를 작성해준다.
// 검증 조건에 부합하므로 지나간다.
assert(someInt == 0, "someInt != 0")
someInt = 1
// assert(someInt == 0) // 동작 중지, 검증 실패
// assert(someInt == 0, "someInt != 0") // 동작 중지, 검증 실패
func functionWithAssert(age: Int?) {
assert(age != nil, "age == nil")
assert((age! >= 0) && (age! <= 130), "나이값 입력이 잘못되었습니다")
print("당신의 나이는 \(age!)세입니다")
}
functionWithAssert(age: 50)
// functionWithAssert(age: -1) // 동작 중지, 검증 실패
// functionWithAssert(age: nil) // 동작 중지, 검증 실패
* assert(_:_:file:line:)와 같은 역할을 하지만 실제 배포 환경에서도 동작하는 precondition(_:_:file:line:) 함수도 있다.
- guard(Early Exit) : 잘못된 값의 전달 시 특정 실행구문을 빠르게 종료한다. 디버깅 모드뿐만 아니라 어떤 조건에서도 동작한다. else 블럭 내부에는 특정 코드블럭을 종료하는 지시어(return, break 등)가 꼭 있어야 한다. 타입 캐스팅, 옵셔널과도 자주 사용된다. 그 외에도 단순 조건 판단 후 빠르게 종료할 때도 용이하다.
func functionWithGuard(age: Int?) {
guard let unwrappedAge = age,
unwrappedAge < 130,
unwrappedAge >= 0 else {
print("나이값 입력이 잘못되었습니다")
return
}
print("당신의 나이는 \(unwrappedAge)세입니다")
}
var count = 1
while true {
guard count < 3 else {
break
}
print(count)
count += 1
}
// 1
// 2
func someFunction(info: [String: Any]) {
guard let name = info["name"] as? String else {
return
}
guard let age = info["age"] as? Int, age >= 0 else {
return
}
print("\(name): \(age)")
}
someFunction(info: ["name": "jenny", "age": "10"])
someFunction(info: ["name": "mike"])
someFunction(info: ["name": "noey", "age": 10]) // noey: 10
// ** if let / guard를 이용한 옵셔널 바인딩 비교 **
// 1. if let 옵셔널 바인딩
if let unwrapped: Int = someValue {
// do something
unwrapped = 3
}
// if 구문 외부에서는 unwrapped 사용이 불가능하다.
// unwrapped = 5
// 2. guard 옵셔널 바인딩
// guard 구문 이후에도 unwrapped 사용 가능하다.
guard let unwrapped: Int = someValue else {
return
}
unwrapped = 3
14. 프로토콜(Protocol)
- 특정 역할을 수행하기 위한 메서드, 프로퍼티, 이니셜라이저 등의 청사진을 정의한다.
- 구조체, 클래스, 열거형은 프로토콜을 채택(Adopted)해서 특정 기능을 수행하기 위한 프로토콜의 요구사항을 실제로 구현할 수 있다.
- 어떤 프로토콜의 요구사항을 모두 따르는 타입은 그 프로토콜을 준수한다(Conform)고 표현한다.
- 타입에서 프로토콜의 요구사항을 충족시키려면 프로토콜이 제시하는 청사진의 기능을 모두 구현해야 한다. (즉, 프로토콜은 기능을 정의하고 제시할 뿐이지 스스로 기능을 구현하지는 않는다.)
protocol 프로토콜 이름 {
/* 정의부 */
}
// << 프로퍼티 요구 >>
// 프로퍼티 요구는 항상 var 키워드를 사용한다.
// get은 읽기만 가능해도 상관 없다는 뜻이며, get과 set을 모두 명시하면 읽기 쓰기 모두 가능한 프로퍼티여야 한다.
protocol Talkable {
// 프로퍼티 요구
var topic: String { get set }
var language: String { get }
// 메서드 요구
func talk()
// 이니셜라이저 요구
init(topic: String, language: String)
}
// << 프로토콜 채택 >>
// 타입명: 프로토콜 이름
// Person 구조체는 Talkable 프로토콜을 채택한다.
struct Person: Talkable {
// 프로퍼티 요구 준수
var topic: String
let language: String
// 읽기 전용 프로퍼티 요구는 연산 프로퍼티로 대체가 가능하다.
var language: String { return "한국어" }
// 물론 읽기, 쓰기 프로퍼티도 연산 프로퍼티로 대체할 수 있다.
// var subject: String = ""
// var topic: String {
// set {
// self.subject = newValue
// }
// get {
// return self.subject
// }
// }
// 메서드 요구 준수
func talk() {
print("\(topic)에 대해 \(language)로 말합니다")
}
// 이니셜라이저 요구 준수
init(topic: String, language: String) {
self.topic = topic
self.language = language
}
}
// 프로퍼티 요구는 다양한 방법으로 해석, 구현할 수 있다.
struct Person: Talkable {
var subject: String = ""
// 프로퍼티 요구는 연산 프로퍼티로 대체가 가능하다.
var topic: String {
set {
self.subject = newValue
}
get {
return self.subject
}
}
var language: String { return "한국어" }
func talk() {
print("\(topic)에 대해 \(language)로 말합니다")
}
init(topic: String, language: String) {
self.topic = topic
}
}
- 프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항보다 더 많은 요구사항을 추가할 수 있다.
- 프로토콜 상속 문법은 클래스의 상속 문법과 유사하지만, 프로토콜은 클래스와 다르게 다중상속이 가능하다.
protocol 프로토콜 이름: 부모 프로토콜 이름 목록 {
/* 정의부 */
}
e.g.)
protocol Readable {
func read()
}
protocol Writeable {
runc write()
}
protocol ReadSpeakable: Readable {
func speak()
}
protocol ReadWriteSpeakable: Readable, Writeable {
func speak()
}
struct SomeType: ReadWriteSpeakable {
func read() {
print("Read")
}
func write() {
print("Write")
}
func speak() {
print("Speak")
}
}
// << 클래스 상속과 프로토콜 >>
// 클래스에서 상속과 프로토콜 채택을 동시에 하려면 상속받으려는 클래스를 먼저 명시하고 그 뒤에 채택할 프로토콜 목록을 작성한다.
class SuperClass: Readable {
func read() { }
}
class SubClass: SuperClass, Writeable, ReadSpeakable {
func write() { }
func speak() { }
}
- is, as 연산자를 사용해서 인스턴스가 특정 프로토콜을 준수하는지 확인 가능하다.
let sup: SuperClass = SuperClass()
let sub: SubClass = SubClass()
var someAny: Any = sup
someAny is Readable // true
someAny is ReadSpeakable // false
someAny = sub
someAny is Readable // true
someAny is ReadSpeakable // true
someAny = sup
if let someReadable: Readable = someAny as? Readable {
someReadable.read()
} // read
if let someReadSpeakable: ReadSpeakable = someAny as? ReadSpeakable {
someReadSpeakable.speak()
} // 동작하지 않음
someAny = sub
if let someReadable: Readable = someAny as? Readable {
someReadable.read()
} // read
15. 익스텐션(Extension)
- 구조체, 클래스, 열거형, 프로토콜 타입에 새로운 기능을 추가할 수 있는 기능이다.
- 기능을 추가하려는 타입의 구현된 소스 코드를 알지 못하거나 볼 수 없다 해도, 타입만 알고 있다면 그 타입의 기능을 확장할 수 있다.
- 외부 라이브러리나 프레임워크 등 외부에서 가져온 타입에 원하는 기능을 추가하고자 할 때 유용하게 사용 가능하다.
<< Swift의 익스텐션이 타입에 추가할 수 있는 기능 >>
1) 연산 타입 프로퍼티 / 연산 인스턴스 프로퍼티
2) 타입 메서드 / 인스턴스 메서드
3) 이니셜라이저
4) 서브스크립트
5) 중첩 타입
6) 특정 프로토콜을 준수할 수 있도록 기능 추가
** 익스텐션은 타입에 새로운 기능을 추가할 수는 있지만, 기존에 존재하는 기능을 재정의할 수는 없다. **
<< 클래스의 상속과 익스텐션 비교 >>
// extension 키워드를 사용하여 정의
extension 확장할 타입 이름 {
/* 타입에 추가될 새로운 기능 구현 */
}
// 익스텐션은 기존에 존재하는 타입이 추가적으로 다른 프로토콜을 채택할 수 있도록 확장할 수 있다.
// 이런 경우에는 클래스나 구조체에서 사용하던 것과 똑같은 방법으로 프로토콜 이름을 나열해준다.
extension 확장할 타입 이름: 프로토콜1, 프로토콜2, 프로토콜3... {
/* 프로토콜 요구사항 구현 */
}
e.g.)
// << 연산 프로퍼티 추가 >>
// 아래 익스텐션은 Int 타입에 두 개의 연산 프로퍼티를 추가한 것이다.
// Int 타입의 인스턴스가 홀수인지 짝수인지 판별하여 Bool 타입으로 알려주는 연산 프로퍼티
// 익스텐션으로 Int 타입에 추가해준 연산 프로퍼티는 Int 타입의 어떤 인스턴스에도 사용이 가능하다.
// 인스턴스 연산 프로퍼티를 추가할 수도 있으며, static 키워드를 사용하여 타입 연산 프로퍼티도 추가할 수 있다.
extension Int {
var isEven: Bool {
return self % 2 == 0
}
var isOdd: Bool {
return self % 2 == 1
}
}
print(1.isEven) // false
print(2.isEven) // true
print(1.isOdd) // true
print(2.isOdd) // false
var number: Int = 3
print(number.isEven) // false
print(number.isOdd) // true
number = 2
print(number.isEven) // true
print(number.isOdd) // false
// << 메서드 추가 >>
// 메서드 익스텐션을 통해 Int 타입에 인스턴스 메서드인 multiply(by:) 메서드를 추가
// 여러 기능을 여러 익스텐션 블록으로 나눠서 구현해도 전혀 문제가 없다.
// 관련된 기능별로 하나의 익스텐션 블록에 묶어주는 것도 좋다.
extension Int {
func multiply(by n: Int) -> Int {
return self * n
}
}
print(3.multiply(by: 2)) // 6
print(4.multiply(by: 5)) // 20
number = 3
print(number.multiply(by: 2)) // 6
print(number.multiply(by: 3)) // 9
// << 이니셜라이저 추가 >>
// 인스턴스를 초기화(이니셜라이즈)할 때 인스턴스 초기화에 필요한 다양한 데이터를 전달받을 수 있도록 여러 종류의 이니셜라이저를 만들 수 있다.
// 타입의 정의부에 이니셜라이저를 추가하지 않더라도 익스텐션을 통해 추가할 수 있다.
// 익스텐션으로 클래스 타입에 편의 이니셜라이저는 추가할 수 있지만, 지정 이니셜라이저는 추가할 수 없다.
// 지정 이니셜라이저와 디이니셜라이저는 반드시 클래스 타입의 구현부에 위치해야 한다. (값 타입은 상관없음)
extension String {
init(int: Int) {
self = "\(int)"
}
init(double: Double) {
self = "\(double)"
}
}
let stringFromInt: String = String(int: 100) // "100"
let stringFromDouble: String = String(double: 100.0) // "100.0"
'프로그래밍' 카테고리의 다른 글
[Swift] boostcourse로 시작하는 swift 문법 - 3 (0) | 2024.04.02 |
---|---|
[Swift] boostcourse로 시작하는 swift 문법 - 1 (2) | 2024.03.29 |
[Swift] Window 11로 Swift 공부하기 (0) | 2024.03.28 |
[Python] postech - Python 프로그래밍 II 7-8주차 (0) | 2021.06.12 |
[Python] postech - Python 프로그래밍 II 5-6주차 (0) | 2021.06.11 |