구조체와 클래스를 생성할 때 지금까지 기본 이니셜라이저를 사용해 인스턴스를 생성함
초기화 과정은 새로운 인스턴스를 사용할 준비를 하기 위해 저장 프로퍼티의 초깃값을 성정하는 등의 일을 한다.
이니셜라이저를 정의하면 초기화 과정을 직접 구현할 수 있다.
init
키워드를 사용하여 이니셜라이저 메소드임을 표현class SomeClass {
init() {
// 초기화할 때 필요한 코드
}
}
struct SomeStruct {
init() {
// 초기화할 때 필요한 코드
}
}
enum SomeEnum {
case someCase
init() {
// 열거형은 초기화할 때 반드시 case중 하나가 되어야 한다.
self = .someCase
// 초기화할 때 필요한 코드
}
}
💡 초기화와 프로퍼티 감시자
이니셜라이저를 통해 초깃값을 할당하거나 프로퍼티 기본값을 통해 처음의 저장 프로퍼티가 초기화될 때는 프로퍼티 감시자 메서드가 호출되지 않음
struct Area {
var squareMeter: Double
init() {
squareMeter = 0.0 // squareMeter의 초깃값 할당
}
}
let room: Area = Area()
print(room.squareMeter) // 0.0
Area 구조체는 squareMeter라는 Double 타입의 저장 프로퍼티를 가지고 있다.
init 이니셜 라이저로 인스턴스를 초기화하여 square의 초깃값은 0.0이 된다.
이니셜라이저로 저장 프로퍼티에 초깃값을 설정하는 방식도 있지만, 프로퍼티를 정의할 때 기본값을 할당하는 방식을 사용할 수도 있다.
struct Area {
var squareMeter: Double = 0.0 // 프로퍼티 기본값 할당
}
let room: Area = Area()
print(room.squareMeter) // 0.0
struct Area {
var squareMeter: Double
init(fromPy py: Double) { // 첫 번째 이니셜라이저
squareMeter = py * 3.3058
}
init(fromSquareMeter squareMeter: Double) { // 두 번째 이니셜라이저
self.squareMeter = squareMeter
}
init(value: Double) { // 세 번째 이니셜라이저
squareMeter = value
}
init(_ value: Double) { // 네 번째 이니셜라이저
squareMeter = value
}
}
let roomeOne: Area = Area(fromPy: 15.0)
print(roomeOne.squareMeter) // 49.587
let roomTwo: Area = Area(fromSquareMeter: 33.06)
print(roomTwo.squareMeter) // 33.06
let roomThree: Area = Area(value: 30.0)
let roomFour: Area = Area(55.0)
Area() // 오류 발생!!
class Person {
var name: String
var age: Int?
init(name: String) {
self.name = name
}
}
let yagom: Person = Person(name: "yagom")
print(yagom.name) // "yagom"
print(yagom.age) // nil
yagom.age = 99
print(yagom.age) // 99
yagom.name = "Eric"
print(yagom.name) // "Eric"
사람의 이름은 아는데 나이는 민감한 부분이므로 모를 수 있기 때문에 age 프로퍼티를 옵셔널로 선언함
이니셜라이저에서 특별히 초기화하지 않았지만 자동으로 nil이 할당
나중에 나이를 알게 되는 시점에 제대로 된 값을 할당할 수 있음
name 프로퍼티를 상수가 아닌 변수로 선언해둔다면 Eric
이라는 이름을 할당하고 난 후에 전혀 다른 사람으로 변할 수 있다.
이런 상황을 방지하려면 name 프로퍼티를 상수로 선언해야한다.
이때 고려해야할 점은 상수로 선언된 저장 프로퍼티는 인스터스를 초기화하는 과정에서만 값을 할당할 수 있으며, 처음 할당된 이후로는 값을 변경할 수 없다.
💡 상수 프로퍼티와 상속
클래스 인스턴스의 상수 프로퍼티는 프로퍼티가 정의된 클래스에서만 초기화할 수 있다.
해당 클래스를 상속받은 자식의 클래스나 이니셜라이저에서는 부모클래스의 상수 프로퍼티의 값을 초기화 할 수 없다.
class Person {
let name: String
var age: Int?
init(name: String) {
self.name = name
}
}
let yagom: Person = Person(name: "yagom")
//yagom.name = "Eric" // 오류 발생!!
struct Point {
var x: Double = 0.0
var y: Double = 0.0
}
struct Size {
var width: Double = 0.0
var height: Double = 0.0
}
let point: Point = Point(x: 0, y: 0)
let size: Size = Size(width: 50.0, height: 50.0)
// 구조체에 저장 프로퍼티에 기본값이 있는 경우
// 필요한 매개변수만 사용하여 초기화할 수도 있다.
let somePoint: Point = Point()
let someSize: Size = Size(width: 50)
let anotherPoint: Point = Point(y: 100)
클래스는 멤버와이즈 이니셜라이저를 지원하지 않으므로 멤버와이즈 이니셜라이즈는 구조체만의 특권
값 타입인 구조체와 열거형은 코드의 중복을 피하기 위하여 이니셜라이저가 다른 이니셜라이저에게 일부 초기화를 위임을 간단하게 구현할 수 있다.
하지만 클래스는 상속을 지원하는 터라 간단한 초기화도 위임할 수 없음
값 타입에서 이니셜라이저가 다른 이니셜라이저를 호출하려면 self.init를 사용한다
당연히 self.init은 이니셜라이저 안에서만 사용할 수 있는데 self.init을 사용한다는 것 자체가 사용자 정의 이니셜라이저를 정의하고 있다는 뜻이다.
그런데 사용자 정의 이니셜라이저를 정의하면 기본 이니셜라이저와 멤버와이즈 이니셜라이저를 사용할 수 없다고 했다. 따라서 초기화 위임을 하려면 최소 두 개 이상의 이니셜라이저를 정의해야 한다
💡 기본 이니셜라이저를 지키고 싶다면
사용자 정의 이니셜라이저를 정의할 때도 기본 이니셜라이저나 멤버와이즈 이니셜라이저를 사용하고 싶다면 익스텐션을 사용하여 사용자 정의 이니셜라이저를 구현하면 된다.
enum Student {
case elementary, middle, high
case none
// 사용자 정의 이니셜라이저가 있는 경우, init() 메서드를 구현해주어야
// 기본 이니셜라이저를 사용할 수 있다.
init() {
self = .none
}
// 첫 번째 사용자 정의 이니셜라이저
init(koreanAge: Int) {
switch koreanAge {
case 8...13:
self = .elementary
case 14...16:
self = .middle
case 17...19:
self = .high
default:
self = .none
}
}
// 두 번째 사용자 정의 이니셜라이저
init(bornAt: Int, currentYear: Int) {
self.init(koreanAge: currentYear - bornAt + 1)
}
}
var younger: Student = Student(koreanAge: 16)
print(younger) // middle
younger = Student(bornAt: 1998, currentYear: 2016)
print(younger) // high
init
대신 init?
키워드 사용💡 이니셜라이저의 매개변수
실패하지 않는 이니셜라이저와 실패 가능한 이니셜라이저를 같은 이름과 같은 매개변수 타입을 갖도록 정의할 수 없다.
실패 가능한 이시녈라이저는 실제로 특정 값을 반환하지 않는다. 초기화를 실패했을 때는 nil을, 성공했을 때는 return을 적어 초기화의 성공과 실패를 표현할 뿐 실제 값은 반환하지는 않는다.
실패 가능한 이니셜라이저를 사용하면 잘못된 전달인자를 전달받았을 때 초기화하지 않을 수 있다.
class Person {
let name: String
var age: Int?
init?(name: String) {
if name.isEmpty {
return nil
}
self.name = name
}
init?(name: String, age: Int) {
if name.isEmpty || age < 0 {
return nil
}
self.name = name
self.age = age
}
}
let yagom: Person? = Person(name: "yagom", age: 99)
if let person: Person = yagom {
print(person.name)
} else {
print("Person wasn't initialized")
}
// yagom
let chope: Person? = Person(name: "chope", age: -10)
if let person: Person = chope {
print(person.name)
} else {
print("Person wasn't initialized")
}
// Person wasn't initialized
실패 가능한 이니셜라이저는 특히 열거형에서 유용하게 사용할 수 있다.
특정 case에 맞지 않는 값이 들어오면 생성에 실패할 수 있다.
혹은 rawValue로 초기화할 때, 잘못된 rawValue가 전달되어 들어온다면 열거형 인스턴스를 생성하지 못할 수 있다.
따라서 rawValue를 통한 이니셜라이저는 기본적으로 실패가능한 이니셜라이저로 제공된다.
enum Student: String {
case elementary = "초등학생", middle = "중학생", high = "고등학생"
init?(koreanAge: Int) {
switch koreanAge {
case 8...13:
self = .elementary
case 14...16:
self = .middle
case 17...19:
self = .high
default:
return nil
}
}
init?(bornAt: Int, currentYear: Int) {
self.init(koreanAge: currentYear - bornAt + 1)
}
}
var younger: Student? = Student(koreanAge: 20)
print(younger) // nil
younger = Student(bornAt: 2020, currentYear: 2016)
print(younger) // nil
younger = Student(rawValue: "고등학생")
만약 사용자 정의 연산을 통해 저장 프로퍼티 기본값을 설정하고자 한다면 클로저나 함수를 사용하여 프로퍼티 기본값을 제공할 수 있다.
인스턴스를 초기화할 때 함수나 클로저가 호출되면서 연산 결괏값을 프로퍼티 기본값으로 제공해준다. 그렇기 때문에 클로저나 함수의 반환 타입은 프로퍼티의 타입과 일치해야한다.
프로퍼티 기본값을 설정해주기 위해서 클로저를 사용한다면 클로저가 실행되는 시점은 초기화할 때 인스턴스의 다른 프로퍼티 값이 설정되기 전이기때문에, 다른 프로퍼티를 사용하여 연산할 수 없다.
다른 프로퍼티에 기본값이 있다고해도 안됨. 또한 클로저 내부에서 self 프로퍼티도 사용할 수 없고, 인스턴스 메서드를 호출할 수도 없음
class SomeClass {
let someProperty: SomeType = {
// 새로운 인스턴스를 생성하고 사용자 정의 연산을 통한 후 반환해준다.
// 반환되는 값의 타입은 SomeType과 같은 타입이어야 한다.
return someValue
}()
}
클로저 뒤에 소괄호가 붙은 이유는 클로저를 실행하기 위해서이다. 클로저 뒤에 소괄호가 붙어 클로저가 실행한 결괏값은 프로퍼티의 기본값이 된다.
만약 소괄호가 없다면 프로퍼티의 기본값은 클로저 그 자체가 된다.
struct Student {
var name: String?
var number: Int?
}
class SchoolClass {
var students: [Student] = {
// 새로운 인스턴스를 생성하고 사용자 정의 연산을 통한 후 반환해준다.
// 반환되는 값의 타입은 [Student] 타입이어야 한다.
var arr: [Student] = [Student]()
for num in 1...15 {
var student: Student = Student(name: nil, number: num)
arr.append(student)
}
return arr
}()
}
let myClass: SchoolClass = SchoolClass()
print(myClass.students.count) // 15
class SomeClass {
deinit {
print("Instance will be deallocated immediately")
}
}
var instance: SomeClass? = SomeClass()
instance = nil // Instance will be deallocated immediately
class FileManager {
var fileName: String
init(fileName: String) {
self.fileName = fileName
}
func openFile() {
print("Open File: \(self.fileName)")
}
func modifyFile() {
print("Modify File: \(self.fileName)")
}
func writeFile() {
print("Write File: \(self.fileName)")
}
func closeFile() {
print("Close File: \(self.fileName)")
}
deinit {
print("Deinit instance")
self.writeFile()
self.closeFile()
}
}
var fileManager: FileManager? = FileManager(fileName: "abc.txt")
if let manager: FileManager = fileManager {
manager.openFile() // Open File: abc.txt
manager.modifyFile() // Modify File: abc.txt
}
fileManager = nil
// Deinit instance
// Write File: abc.txt
// Close File: abc.txt
디이니셜라이저를 잘 활용하면, 메모리 관리 측면 외에도 프로그래머가 설계한 로직에 따라 인스턴스가 메모리에서 해제되기 직전에 적절한 작업을 하도록 할 수 있다.