map함수(메서드)는 컨테이너 내부의 기존 데이터를 변형(transform)하여 새로운 컨테이너를 생성합니다.
> 변형하고자 하는 numbers와 변형 결과를 받을 doubledNumbers, strings
let numbers: [Int] = [0, 1, 2, 3, 4]
var doubledNumbers: [Int]
var strings: [String]
> for 구문 사용
doubledNumbers = [Int]()
strings = [String]()
for number in numbers {
doubledNumbers.append(number * 2)
strings.append("\(number)")
}
print(doubledNumbers) // [0, 2, 4, 6, 8]
print(strings) // ["0", "1", "2", "3", "4"]
> map 메서드 사용
// numbers의 각 요소를 2배하여 새로운 배열 반환
doubledNumbers = numbers.map({ (number: Int) -> Int in
return number * 2
})
// numbers의 각 요소를 문자열로 변환하여 새로운 배열 반환
strings = numbers.map({ (number: Int) -> String in
return "\(number)"
})
print(doubledNumbers) // [0, 2, 4, 6, 8]
print(strings) // ["0", "1", "2", "3", "4"]
// 매개변수, 반환 타입, 반환 키워드(return) 생략, 후행 클로저
doubledNumbers = numbers.map { $0 * 2 }
print(doubledNumbers) // [0, 2, 4, 6, 8]
filter
filter함수(메서드)는 컨테이너 내부의 값을 걸러서 새로운 컨테이너로 추출합니다.
> for 구문 사용
// 변수 사용에 주목하세요
var filtered: [Int] = [Int]()
for number in numbers {
if number % 2 == 0 {
filtered.append(number)
}
}
print(filtered) // [0, 2, 4]
> filter 메서드 사용
// numbers의 요소 중 짝수를 걸러내어 새로운 배열로 반환
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
reduce함수(메서드)는 컨테이너 내부의 콘텐츠를 하나로 통합합니다.
> 통합하고자 하는 someNumbers
let someNumbers: [Int] = [2, 8, 15]
> for 구문 사용
// 변수 사용에 주목하세요
var result: Int = 0
// someNumbers의 모든 요소를 더합니다
for number in someNumbers {
result += number
}
print(result) // 25
> reduce 메서드 사용
// 초깃값이 0이고 someNumbers 내부의 모든 값을 더합니다.
let sum: Int = someNumbers.reduce(0, { (first: Int, second: Int) -> Int in
//print("\(first) + \(second)") //어떻게 동작하는지 확인해보세요
return first + second
})
print(sum) // 25
// 초깃값이 0이고 someNumbers 내부의 모든 값을 뺍니다.
var subtract: Int = someNumbers.reduce(0, { (first: Int, second: Int) -> Int in
//print("\(first) - \(second)") //어떻게 동작하는지 확인해보세요
return first - second
})
print(subtract) // -25
// 초깃값이 3이고 someNumbers 내부의 모든 값을 더합니다.
let sumFromThree = someNumbers.reduce(3) { $0 + $1 }
print(sumFromThree) // 28
익스텐션(Extension) 은 스위프트의 강력한 기능 중 하나입니다. 익스텐션은 구조체, 클래스, 열거형, 프로토콜 타입에 새로운 기능을 추가 할 수 있는 기능입니다.
기능을 추가하려는 타입의 구현된 소스 코드를 알지 못하거나 볼 수 없다 해도, 타입만 알고 있다면 그 타입의 기능을 확장할 수도 있습니다.
스위프트의 익스텐션이 타입에 추가할 수 있는 기능
연산 타입 프로퍼티 / 연산 인스턴스 프로퍼티
타입 메서드 / 인스턴스 메서드
이니셜라이저
서브스크립트
중첩 타입
특정 프로토콜을 준수할 수 있도록 기능 추가
익스텐션은 타입에 새로운 기능을 추가할 수는 있지만, 기존에 존재하는 기능을 재정의 할 수는 없습니다.
클래스의 상속과 익스텐션을 비교해보겠습니다. 이 둘은 비슷해보이지만 실제 성격은 많이 다릅니다.
클래스의 상속은 클래스 타입에서만 가능하지만 익스텐션은 구조체, 클래스, 프로토콜 등에 적용이 가능합니다. 또 클래스의 상속은 특정 타입을 물려받아 하나의 새로운 타입을 정의하고 추가 기능을 구현하는 수직 확장이지만, 익스텐션은 기존의 타입에 기능을 추가하는 수평 확장입니다. 또, 상속을 받으면 기존 기능을 재정의할 수 있지만, 익스텐션은 재정의할 수 없다는 것도 큰 차이 중 하나입니다. 상황과 용도에 맞게 상속과 익스텐션을 선택하여 사용하면 됩니다.
상속
익스텐션
확장
수직확장
수평확장
사용
클래스 타입
클래스, 구조체, 프로토콜, 제네릭 등 모든 타입
재정의
가능
불가능
익스텐션을 사용하는 대신 원래 타입을 정의한 소스에 기능을 추가하는 방법도 있겠지만, 외부 라이브러리나 프레임워크를 가져다 썼다면 원본 소스를 수정하지 못합니다. 이처럼 외부에서 가져온 타입에 내가 원하는 기능을 추가하고자 할 때 익스텐션을 사용합니다. 따로 상속을 받지 않아도 되며, 구조체와 열거형에도 기능을 추가할 수 있으므로 익스텐션은 매우 편리한 기능입니다.
익스텐션은 모든 타입에 적용할 수 있습니다. 모든 타입이라 함은 구조체, 열거형, 클래스, 프로토콜, 제네릭 타입 등을 뜻합니다. 즉, 익스텐션을 통해 모든 타입에 연산 프로퍼티, 메서드, 이니셜라이저, 서브스크립트, 중첩 데이터 타입 등을 추가할 수 있습니다.
더불어 익스텐션은 프로토콜과 함께 사용하면 굉장히 강력한 기능을 선사합니다. 이 부분은 프로토콜 중심 프로그래밍(Protocol Oriented Programming)에 대해 더 알아보면 좋습니다.
익스텐션은 기존에 존재하는 타입이 추가적으로 다른 프로토콜을 채택할 수 있도록 확장할 수도 있습니다. 이런 경우에는 클래스나 구조체에서 사용하던 것과 똑같은 방법으로 프로토콜 이름을 나열해줍니다.
extension 확장할 타입 이름: 프로토콜1, 프로토콜2, 프로토콜3... {
/* 프로토콜 요구사항 구현 */
}
스위프트 라이브러리를 살펴보면 실제로 익스텐션이 굉장히 많이 사용되고 있음을 알 수 있습니다. Double 타입에는 수많은 프로퍼티와 메서드, 이니셜라이저가 정의되어 있으며 수많은 프로토콜을 채택하고 있을 것이라고 예상되지만, 실제로 Double 타입의 정의를 살펴보면 그 모든것이 다 정의되어 있지는 않습니다.
그러면 Double 타입이 채택하고 준수해야 하는 수많은 프로토콜은 어디로 갔을까요? 어디에서 채택하고 어디에서 준수하도록 정의되어 있을까요? 당연히 답은 익스텐션입니다. 이처럼 스위프트 표준 라이브러리 타입의 기능은 대부분 익스텐션으로 구현되어 있습니다. Double 외에도 다른 타입들의 정의와 익스텐션을 찾아보면 더 많은 예를 보실 수 있습니다. 꼭 한 번 찾아보세요!
익스텐션 구현
연산 프로퍼티 추가
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 타입에 두 개의 연산 프로퍼티를 추가한 것입니다. Int 타입의 인스턴스가 홀수인지 짝수인지 판별하여 Bool 타입으로 알려주는 연산 프로퍼티입니다. 익스텐션으로 Int 타입에 추가해준 연산 프로퍼티는 Int 타입의 어떤 인스턴스에도 사용이 가능합니다. 위의 코드처럼 인스턴스 연산 프로퍼티를 추가할 수도 있으며, static 키워드를 사용하여 타입 연산 프로퍼티도 추가할 수 있습니다.
메서드 추가
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
위 코드의 익스텐션을 통해 Int 타입에 인스턴스 메서드인 multiply(by:) 메서드를 추가했습니다. 여러 기능을 여러 익스텐션 블록으로 나눠서 구현해도 전혀 문제가 없습니다. 관련된 기능별로 하나의 익스텐션 블록에 묶어주는 것도 좋습니다.
프로토콜(Protocol)은 특정 역할을 수행하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진을 정의합니다. 구조체, 클래스, 열거형은 프로토콜을 채택(Adopted)해서 특정 기능을 수행하기 위한 프로토콜의 요구사항을 실제로 구현할 수 있습니다. 어떤 프로토콜의 요구사항을 모두 따르는 타입은 그 프로토콜을 준수한다(Conform)고 표현합니다. 타입에서 프로토콜의 요구사항을 충족시키려면 프로토콜이 제시하는 청사진의 기능을 모두 구현해야 합니다.
protocol Talkable {
// 프로퍼티 요구
// 프로퍼티 요구는 항상 var 키워드를 사용합니다
// get은 읽기만 가능해도 상관 없다는 뜻이며
// get과 set을 모두 명시하면
// 읽기 쓰기 모두 가능한 프로퍼티여야 합니다
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
}
}
프로토콜 상속
프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항보다 더 많은 요구사항을 추가할 수 있습니다. 프로토콜 상속 문법은 클래스의 상속 문법과 유사하지만, 프로토콜은 클래스와 다르게 다중상속이 가능합니다.
assert(_:_:file:line:) 함수를 사용합니다. assert 함수는 디버깅 모드에서만 동작합니다. 배포하는 애플리케이션에서는 제외됩니다.
주로 디버깅 중 조건의 검증을 위하여 사용합니다.
var someInt: Int = 0
// 검증 조건에 부합하므로 지나갑니다
assert(someInt == 0, "someInt != 0")
someInt = 1
//assert(someInt == 0) // 동작 중지, 검증 실패
//assert(someInt == 0, "someInt != 0") // 동작 중지, 검증 실패
// assertion failed: someInt != 0: file guard_assert.swift, line 26
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:) 함수도 있습니다. 함께 살펴보세요.
빠른 종료 - Early Exit
guard를 사용하여 잘못된 값의 전달 시 특정 실행구문을 빠르게 종료합니다. 디버깅 모드 뿐만 아니라 어떤 조건에서도 동작합니다. guard의 else 블럭 내부에는 특정 코드블럭을 종료하는 지시어(return, break 등)가 꼭 있어야 합니다. 타입 캐스팅, 옵셔널과도 자주 사용됩니다. 그 외에도 단순 조건 판단 후 빠르게 종료할 때도 용이합니다.
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 yagom: Person = Person()
var hana: Student = Student()
var jason: UniversityStudent = UniversityStudent()
타입 확인
is를 사용하여 타입을 확인합니다.
var result: Bool
result = yagom is Person // true
result = yagom is Student // false
result = yagom 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 yagom is UniversityStudent {
print("yagom은 대학생입니다")
} else if yagom is Student {
print("yagom은 학생입니다")
} else if yagom is Person {
print("yagom은 사람입니다")
} // yagom은 사람입니다
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은 대학생입니다
업 캐스팅
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 생략가능
다운 캐스팅
as? 또는 as!를 사용하여 자식 클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 인스턴스의 타입정보를 전환해줍니다.
조건부 다운 캐스팅
as?를 사용합니다. 캐스팅에 실패하면, 즉 캐스팅하려는 타입에 부합하지 않는 인스턴스라면 nil을 반환하기 때문에 결과의 타입은 옵셔널 타입입니다.
var optionalCasted: Student?
optionalCasted = mike as? UniversityStudent
optionalCasted = jenny as? UniversityStudent // nil
optionalCasted = jina as? UniversityStudent // nil
optionalCasted = jina as? Student // nil
강제 다운 캐스팅
as!를 사용합니다. 캐스팅에 실패하면, 즉 캐스팅하려는 타입에 부합하지 않는 인스턴스라면 런타임 오류가 발생합니다. 캐스팅에 성공하면 옵셔널이 아닌 일반 타입을 반환합니다.
var forcedCasted: Student
optionalCasted = mike as! UniversityStudent
//optionalCasted = jenny as! UniversityStudent // 런타임 오류
//optionalCasted = jina as! UniversityStudent // 런타임 오류
//optionalCasted = jina as! Student // 런타임 오류
활용
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: yagom) // 숨을 쉽니다
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: yagom) // 숨을 쉽니다
1. 글쎄요... 해당 기기에서 수신하는 데이터의 크기와 포멧을 알아야 할 것 같습니다. 혹은, 데이터 전송의 단위 때문일 수도 있겠습니다.
2. 해당 내용은 애플의 문서를 잘 살펴보시면 도움이 될 것 같습니다. [Using Swift with Cocoa and Objective-C : Interacting with C APIs] (https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithCAPIs.html)
3. 아닙니다. 위의 문서에서 확인해보시면 Swift Type은 UInt8이 아니고 CChar인 것을 확인해보실 수 있습니다. 또, CChar는 Int8과 같은 타입입니다.
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 yagom: Person? = Person(name: "yagom")
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: yagom)
// 옵셔널 체이닝을 사용한다면
func guardJobWithOptionalChaining(owner: Person?) {
if let guardJob = owner?.home?.guard?.job {
print("우리집 경비원의 직업은 \(guardJob)입니다")
} else {
print("우리집 경비원은 직업이 없어요")
}
}
guardJobWithOptionalChaining(owner: yagom)
// 우리집 경비원은 직업이 없어요
yagom?.home?.guard?.job // nil
yagom?.home = apart
yagom?.home // Optional(Apartment)
yagom?.home?.guard // nil
yagom?.home?.guard = superman
yagom?.home?.guard // Optional(Person)
yagom?.home?.guard?.name // superman
yagom?.home?.guard?.job // nil
yagom?.home?.guard?.job = "경비원"
안녕하세요 야곰님 다름이 아니라 하나 이해가 되지 않는 것이 있어서 이렇게 글을 남깁니다.
yagom?.home 으로 접근하는 것과
yagom!.home 으로 접근하는 것의 차이점이 무엇인가요!?
!를 사용한 것은 강제로 언랩핑하여 내부 프로퍼티에 접근하는 방법인데
위의 예제에선 yagom.home 이 아닌 yagom?.home이 되는 것인가요?
그리고
type(of: yagom)
type(of: yagom!)
이 둘의 출력 결과물은 각각
Optional(Person)
Person
인데
type(of: yagom?)
은 에러가 나더라구요...!
이유가 무엇인가요?
[자문자답]
일단 첫번째에서 ? 와 ! 로 값을 얻어내는 것에선 만일 값이 존재하지 않을 때 ? 는 nil을 반환하는 반면 !는 에러를 발생시키더라구요.
?로 접근하는 것이 더 안전할 것 같습니다!
두번째의 이유는 저것이 타입이 아니라 ?을 붙임으로써 문법의 종류가 되버려서 같은데 그 맞는 이유인가요?
스위프트의 모든 인스턴스는 초기화와 동시에 모든 프로퍼티에 유효한 값이 할당되어 있어야 합니다. 프로퍼티에 미리 기본값을 할당해두면 인스턴스가 생성됨과 동시에 초기값을 지니게 됩니다.
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"
이니셜라이저
프로퍼티 기본값을 지정하기 어려운 경우에는 이니셜라이저 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 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
}
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() // 주인이 없는 상태라 오류 발생
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.characters.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 joker: PersonD? = PersonD(name: "joker", age: 123)
let batman: PersonD? = PersonD(name: "", age: 10)
print(joker) // nil
print(batman) // nil
디이니셜라이저
deinit은 클래스의 인스턴스가 메모리에서 해제되는 시점에 호출됩니다. 인스턴스가 해제되는 시점에 해야할 일을 구현할 수 있습니다. 자동으로 호출되므로 직접 호출할 수 없습니다. 인스턴스가 메모리에서 해제되는 시점은 ARC(Automatic Reference Counting) 의 규칙에 따라 결정됩니다. ARC에 대해 더 자세한 사항은 ARC 문서를 참고하세요.
디이니셜라이저는 클래스 타입에만 구현할 수 있습니다.
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를 인도합니다
// 기반 클래스 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 finalCalssMethod() {
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 yagom: Person = Person()
let hana: Student = Student()
yagom.name = "yagom"
hana.name = "hana"
hana.major = "Swift"
yagom.selfIntroduce()
// 저는 yagom입니다
hana.selfIntroduce()
// 저는 hana이고, 전공은 Swift입니다
Person.classMethod()
// type method - class
Person.typeMethod()
// type method - static
Person.finalCalssMethod()
// type method - final class
Student.classMethod()
// overriden type method - class
Student.typeMethod()
// type method - static
Student.finalCalssMethod()
// type method - final class
이번 파트에서는 지연 저장 프로퍼티를 제외한 저장 프로퍼티와 연산 프로퍼티에 대해 알아봅니다.
정의와 사용
프로퍼티는 구조체, 클래스, 열거형 내부에 구현할 수 있습니다. 다만 열거형 내부에는 연산 프로퍼티만 구현할 수 있습니다. 연산 프로퍼티는 var로만 선언할 수 있습니다.
연산프로퍼티를 읽기전용으로는 구현할 수 있지만, 쓰기 전용으로는 구현할 수 없습니다. 읽기전용으로 구현하려면 get 블럭만 작성해주면 됩니다. 읽기전용은 get블럭을 생략할 수 있습니다. 읽기, 쓰기 모두 가능하게 하려면 get 블럭과 set블럭을 모두 구현해주면 됩니다.
set 블럭에서 암시적 매개변수 newValue를 사용할 수 있습니다.
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 yagom: Student = Student()
yagom.koreanAge = 10
// 인스턴스 저장 프로퍼티 사용
yagom.name = "yagom"
print(yagom.name)
// yagom
// 인스턴스 연산 프로퍼티 사용
print(yagom.selfIntroduction)
// 저는 Swift반 yagom입니다
print("제 한국나이는 \(yagom.koreanAge)살이고, 미쿡나이는 \(yagom.westernAge)살입니다.")
// 제 한국나이는 10살이고, 미쿡나이는 9살입니다.
응용
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
프로퍼티 감시자
프로퍼티 감시자를 사용하면 프로퍼티 값이 변경될 때 원하는 동작을 수행할 수 있습니다. 값이 변경되기 직전에 willSet블럭이, 값이 변경된 직후에 didSet블럭이 호출됩니다. 둘 중 필요한 하나만 구현해 주어도 무관합니다. 변경되려는 값이 기존 값과 똑같더라도 프로퍼티 감시자는 항상 동작합니다.
willSet 블럭에서 암시적 매개변수 newValue를 사용할 수 있고, didSet 블럭에서 암시적 매개변수 oldValue를 사용할 수 있습니다.
클로저 매개변수를 갖는 함수 calculate(a:b:method:)와 결과값을 저장할 변수 result를 먼저 선언해둡니다.
func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
return method(a, b)
}
var result: Int
후행 클로저
클로저가 함수의 마지막 전달인자라면 마지막 매개변수 이름을 생략한 후 함수 소괄호 외부에 클로저를 구현할 수 있습니다.
result = calculate(a: 10, b: 10) { (left: Int, right: Int) -> Int in
return left + right
}
print(result) // 20
반환타입 생략
calculate(a:b:method:) 함수의 method 매개변수는 Int 타입을 반환할 것이라는 사실을 컴파일러도 알기 때문에 굳이 클로저에서 반환타입을 명시해 주지 않아도 됩니다.
대신 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
단축 인자이름
클로저의 매개변수 이름이 굳이 불필요하다면 단축 인자이름을 활용할 수 있습니다. 단축 인자이름은 클로저의 매개변수의 순서대로 $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
암시적 반환 표현
클로저가 반환하는 값이 있다면 클로저의 마지막 줄의 결과값은 암시적으로 반환값으로 취급합니다.
result = calculate(a: 10, b: 10) {
$0 + $1
}
print(result) // 20
// 간결하게 한 줄로 표현해 줄 수도 있습니다
result = calculate(a: 10, b: 10) { $0 + $1 }
print(result) // 20
축약 전과 후 비교
//축약 전
result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) -> Int in
return left + right
})
//축약 후
result = calculate(a: 10, b: 10) { $0 + $1 }
print(result) // 20
스위프트의 열거형은 다른 언어의 열거형과는 많이 다릅니다. 잘 살펴보아야 할 스위프트의 기능 중 하나입니다.
enum은 타입이므로 대문자 카멜케이스를 사용하여 이름을 정의합니다
각 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("신나는 주말!!")
}
원시값(Raw value)
C 언어의 enum처럼 정수값을 가질 수도 있습니다. rawValue를 사용하면 됩니다.
case별로 각각 다른 값을 가져야합니다.
enum Fruit: Int {
case apple = 0
case grape = 1
case peach
// mango와 apple의 원시값이 같으므로
// mango 케이스의 원시값을 0으로 정의할 수 없습니다
// case mango = 0
}
print("Fruit.peach.rawValue == \(Fruit.peach.rawValue)")
// Fruit.peach.rawValue == 2
정수 타입 뿐만 아니라 Hashable 프로토콜을 따르는 모든 타입이 원시값의 타입으로 지정될 수 있습니다.
enum School: String {
case elementary = "초등"
case middle = "중등"
case high = "고등"
case university
}
print("School.middle.rawValue == \(School.middle.rawValue)")
// School.middle.rawValue == 중등
// 열거형의 원시값 타입이 String일 때, 원시값이 지정되지 않았다면
// case의 이름을 원시값으로 사용합니다
print("School.university.rawValue == \(School.university.rawValue)")
// School.middle.rawValue == university
원시값을 통한 초기화
rawValue를 통해 초기화 할 수 있습니다. rawValue가 case에 해당하지 않을 수 있으므로 rawValue를 통해 초기화 한 인스턴스는 옵셔널 타입입니다.
// rawValue를 통해 초기화 한 열거형 값은 옵셔널 타입이므로 Fruit 타입이 아닙니다
//let apple: Fruit = Fruit(rawValue: 0)
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에 해당하는 케이스가 없습니다
메서드
스위프트의 열거형에는 메서드도 추가할 수 있습니다.
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()
클래스의 타입 메서드는 두 종류가 있습니다. 상속 후 재정의가 가능한 class 타입메서드, 상속 후 재정의가 불가능한 static 타입메서드가 있습니다. 자세한 내용은 상속 부분에서 다시 다룹니다.
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() // type method
// 인스턴스에서는 타입 프로퍼티나 타입 메서드를
// 사용할 수 없습니다
// 컴파일 오류 발생
//mutableReference.typeProperty = 400
//mutableReference.typeMethod()
학생 클래스 만들어보기
class Student {
// 가변 프로퍼티
var name: String = "unknown"
// 키워드도 `로 묶어주면 이름으로 사용할 수 있습니다
var `class`: String = "Swift"
// 타입 메서드
class func selfIntroduce() {
print("학생타입입니다")
}
// 인스턴스 메서드
// self는 인스턴스 자신을 지칭하며, 몇몇 경우를 제외하고 사용은 선택사항입니다
func selfIntroduce() {
print("저는 \(self.class)반 \(name)입니다")
}
}
// 타입 메서드 사용
Student.selfIntroduce() // 학생타입입니다
// 인스턴스 생성
var yagom: Student = Student()
yagom.name = "yagom"
yagom.class = "스위프트"
yagom.selfIntroduce() // 저는 스위프트반 yagom입니다
// 인스턴스 생성
let jina: Student = Student()
jina.name = "jina"
jina.selfIntroduce() // 저는 Swift반 jina입니다
지난 번에는 if 조건문에 대해 알아보았습니다.
이번에는 또다른 조건문인 switch문에 대해 알아보도록 할게요 ㅎ
스따뚜~~~~~~~~~~~~
# switch
자, 일단 만들어 봅시다. 지난번 if문 할 때 만들어 놓았던 프로젝트를 열어 봅시다.
이번에는 switchNumbers() 라는 함수를 만들거예요.
빨간네모를 따라 코드를 작성해 봅시다.ㅎㅎ
위의 코드의 실행 결과 화면입니다.
switch문의 작성법은 이렇습니다.
switch(대상 변수)
{
case 정수 혹은 문자 :
실행할 명령문
break;
case 정수 혹은 문자 :
실행할 명령문
break;
case 정수 혹은 문자 :
실행할 명령문
break;
default :
실행할 명령문
break;
}
자, switch문은 주로 정수형의 변수가 어느 특정 조건을 만족하는지 검사할 때 쓰입니다.
if문과 다른 점은 크거나 작다에 대한 비교를 하기 보다는, 딱 어떤 변수가 어떤 수일때 실행. 을 목표로 하는 조건문입니다.
눈치 채신 분들도 계시겠지만, case 안에 해당되는 수가 없으면 default에 있는 명령어를 실행합니다.
그리고 모든 case아래에 break 명령어가 있는 것이 보이실 텐데요, break문이 없으면 아래에 있는 모든 것들을 다 실행하라는 의미로 받아들여집니다.
한 번 break 없애고 실행해 보세요 ㅎㅎㅎ 어떤 참사하 일어나는지..^^
위의 코드에서는 i가 어떤 수인지 알아보는 코드가 되겠네요.
i가 5 또는 10 또는 17 중에 하나에 해당되면 거기에 맞는 명령이 실행될테구요, 만약 해당되는 것이 없으면 default에 있는 것을 실행합니다.
if조건문과 switch조건문의 특징을 잘 숙지하시고 알앚게 효율적으로 사용하시면 될 것 같습니다^^
사실 if문 보다 switch문의 사용법이나 규제가 좀 더 까다롭지만, if - else if - else if - else...를 한없이 늘어놓는 것 보다 훨씬 간편하게 사용하실 수도 있습니다.
예전에 이부분을 공부하면서 case 내부에 if문도 집어넣는 대형 참사를 경험한 적도 있다지요 ... ㅎㅎ
본문에 쓰신것처럼 한번은 또 break를 적지 않는 대형참사도 겪었구요 ㅠㅠ
그런데 마지막 default에서
printf문장에 5. 10. 15 중에 없습니다라고 하셨는데!!!!
case 는 17을 가르키네요 ?!
5 10 17중에 없습니다 아닐까요 ?! 하는 의문을 품으면서 다음강좌로 !!
댓글을 달아 주세요