이 글은 스위프트 프로그래밍(3판, 야곰 지음)을 읽고 간단하게 정리한 글입니다. 책에 친절한 설명과 관련 예제 코드들이 있으므로 직접 사서 읽기를 추천합니다.
어떤 클래스로부터 속성들을 물려받는 것을 의미한다.
클래스는 다른 클래스로부터 메소드나 프로퍼티 등을 상속받을 수 있다. 자식 클래스는 상속받은 클래스를, 부모클래스는 상속한 클래스를 말한다. 기반 클래스란 상속받지 않은 클래스를 의미한다.
자식클래스는 부모클래스의 메서드, 프로퍼티, 프로퍼티 감시자, 서브스크립트, 이니셜라이저 등을 사용하거나 재정의할 수 있다.
자식클래스 이름 뒤에 콜론을 붙이고 상속 받을 부모클래스 이름을 쓴다.
class 자식클래스: 부모클래스 {
// 프로퍼티와 메서드들
}
class Person {
var name: String = "사람"
var age: Int = 0
}
Student 클래스는 Person 클래스를 상속받는다.
Person의 name, age 프로퍼티를 사용할 수 있다.
자신의 메서드인 study()를 추가할 수 있다.
class Student: Person {
var grade: Int = 100
func study() {
print("I'm studying...")
}
}
let student = Student()
student.name
student.age
student.grade
student.study()
class UniversityStudent: Student {
var major: String = "전공"
override func study() {
print("I'm dying...")
}
}
let universityStudent = UniversityStudent()
universityStudent.name
universityStudent.age
universityStudent.grade
universityStudent.major
universityStudent.study()
📌키워드: override
아까 살펴봤던 메서드 study를 재정의했었다.
자식클래스가 부모클래스로부터 상속받은 특성을 그대로 사용하지 않고 변경하여 사용하는 것을 재정의라고 한다. 만약 부모클래스에 해당 특성이 없다면 컴파일 오류가 발생한다.
class UniversityStudent: Student {
var major: String = "전공"
override func study() {
print("I'm dying...")
}
override func work() { // ⛔️ Method does not override any method from its superclass
print("I'm working...")
}
}
📌키워드: super
super 프로퍼티를 사용하면 된다.
class UniversityStudent: Student {
var major: String = "전공"
override func study() {
super.study()
print("I'm dying...")
}
}
I'm studying...
I'm dying...
지금까지 사용했던 메서드는 모두 인스턴스 메서드이다. 인스턴스를 먼저 생성한 후 점(.)을 통해 해당 메서드에 접근한다
📌키워드: static, class
인스턴스 생성 없이 타입을 통해 호출된다. static은 오버라이딩을 금지하지만 class는 오버라이딩을 허용한다.
class Student: Person {
static func play() {
print("I'm playing...")
}
class func introduce() {
print("I'm student")
}
}
class UniversityStudent: Student {
override class func introduce() {
print("I'm universityStudent")
}
}
let student = Student()
student.study() // 인스턴스
Student.study(student) // 인스턴스
Student.play() // 타입
Student.introduce()
let universityStudent = UniversityStudent()
universityStudent.study() // 인스턴스
UniversityStudent.introduce() // 타입
메서드는 반환 타입이나 매개변수가 다르면 다른 메서드로 취급한다.
play 메서드를 보면 메서드 이름이 동일하지만 반환 타입이 다른, 다른 메서드이다. 재정의없이 사용한다.
introduce 메서드를 보면 매개변수가 다르기에 다른 메서드이다.
class Student: Person {
func play() {
print("I'm playing...")
}
class func introduce() {
print("I'm student")
}
}
class UniversityStudent: Student {
func play() -> String {
"I'm playing..."
}
func introduce(_ name: String) {
print("I'm \(name)")
}
override class func introduce() {
print("I'm universityStudent")
}
}
universityStudent의 play라는 이름을 가진 메서드가 두개이기에, 타입캐스팅을 하여 사용한다.
타입캐스팅을 하지 않으면 Ambiguous use of 'play()'라는 에러 메세지가 뜬다. 타입캐스팅은 뒤에서...
let student = Student()
student.play() // 인스턴스 메서드
Student.introduce() // 타입 메서드
let universityStudent = UniversityStudent()
universityStudent.play() // ⛔️ Ambiguous use of 'play()'
universityStudent.play() as Void
universityStudent.play() as String
universityStudent.introduce("대학생")
UniversityStudent.introduce()
프로퍼티를 재정의한다는 것은 프로퍼티 자체가 아니라, 접근자, 설정자, 감시자 등을 재정의하는 것을 의미한다.
읽기 전용 프로퍼티를 읽고 쓰기가 가능하도록은 할 수 있지만, 읽고 쓰기가 모두 가능했던 걸 읽기 전용으로 재정의할 수는 없다.
상속 관계 모식도 332p에서 보면, 자식클래스는 부모클래스의 특성을 다 사용할 수 있어야 하기에, 부모에서 읽고 쓸수 있었다면 자식에서도 둘 다 가능해야 한다.
Person 클래스에서는 읽기 전용 프로퍼티인 koreanAge가 있다.
class Person {
var name: String = "사람"
var age: Int = 0
var koreanAge: Int {
self.age + 1
}
}
name이라는 프로퍼티 자체를 재정의할 수 없다.
자식클래스인 Student 클래스에서 읽기 전용 프로퍼티인 koreanAge를 읽고 쓰기가 가능하도록 재정의할 수 있다. (반대는 불가능)
class Student: Person {
override var name: String = "학생" // ⛔️ Cannot override with a stored property 'name'
var grade: Int = 100
override var koreanAge: Int {
get {
super.koreanAge
}
set(newAge) {
self.age = newAge - 1 // newAge 대신 newValue로 접근가능
}
}
}
Person에서 koreanAge는 읽기 전용이었지만 student에서는 쓰는 것도 가능하다.
let person = Person()
person.koreanAge
let student = Student()
student.koreanAge = 10
student.age // 9
부모클래스의 koreanAge를 읽고 쓰기가 가능한데 자식클래스에서는 읽기 전용으로 한다면 아래와 같은 에러가 발생한다.
⛔️ Cannot override mutable property with read-only property 'koreanAge'
부모클래스인 Person으로부터 get을 가져오고 싶다면 super 키워드를 사용한다.
class Person {
var age: Int = 0
var koreanAge: Int {
get {
self.age + 1
}
set(newAge) {
self.age = newAge - 1 // newAge 대신 newValue로 접근가능
}
}
}
class Student: Person {
override var koreanAge: Int {
get {
super.koreanAge
}
set {
self.age = newValue - 2
}
}
}
프로퍼티 감시자도 재정의할 수 있다. 주의할 점은 재정의하더라도 부모클래스의 프로퍼티 감시자도 동작한다는 점이다.
class Person {
var age: Int = 0 {
didSet {
print("I'm \(self.age)")
}
}
}
class Student: Person {
override var age: Int {
didSet {
print("I'm \(self.age)~~")
}
}
}
let student = Student()
student.age = 10
I'm 10
I'm 10~~
School의 서브스크립트는 grades 배열에서, University의 서브스크립트는 universityGrades에서 해당 값을 가져오도록 재졍의하였다.
class School {
var grades: [String] = ["elementary", "middle", "high"]
subscript(index: Int) -> String {
(index < grades.count) ? grades[index] : ""
}
}
class University: School {
var universityGrades: [String] = ["freshman", "sophomore", "junior", "senior"]
override subscript(index: Int) -> String {
(index < universityGrades.count) ? universityGrades[index] : ""
}
}
let school = School()
let university = University()
school[0] // elementary
university[0] // override하지 않았다면? elementary
university[0] // freshman
📌키워드: final
자식클래스에서 재정의하는 것을 방지하려면?
클래스 자체를 상속하거나 재정의하는 것을 막는다면 클래스 앞에 final을 붙인다. 또는 부모클래스 특성을 재정의하는 걸 막는다면 해당 특성 앞에 final을 붙이면 된다.
class Person {
final func study() {
print("...")
}
}
class Student: Person {
override func study() { // ⛔️ Instance method overrides a 'final' instance method
print("I'm studying...")
}
}
final class School {
}
class University: School { // ⛔️ Inheritance from a final class 'School'
}
메소드 디스패치란 어떤 연산을 실행해야 하는지 결정하도록 돕는 메커니즘이다. 더 정확히 말하면 어떤 메소드 구현이 사용되어야 하는지 결정하는 것이다.
정적 디스패치는 값, 참조타입에서 모두 지원된다. 하지만 동적 디스패치는 참조타입에서만 지원된다. 동적 디스패치를 위해서는 상속이 필요한데 값 타입은 상속을 지원하지 않기 때문이다. 일단 디스패치는 정적/동적 보다 더 세분화해서 나눌 수 있다. Dynamic으로 갈수록 느리다.
정적 디스패치는 컴파일러가 명령(instructions)들이 어디에 있는지 안다는 측면에서 동적 디스패치보다 더 빠르다. 그러므로 함수가 호출되면 컴파일러는 수행할 함수의 메모리 주소로 바로 이동한다. 이는 성능을 크게 향상시킨다.
클래스의 경우 정적 디스패치를 위해서 final(재정의, 상속 방지), static 키워드(타입 메서드에서 메서드 재정의 방지)를 통해 추가적인 상속이나 재정의가 불가능하도록 만든다.
1. final 은 오버라이딩을 방지하기 위해 사용한다.
2. static과 class의 쓰임은 메소드와 프로퍼티를 인스턴스화 하지 않고 하나만 존재하도록 하기 위해 사용된다. static은 class와 달리 상속이 불가능하다.
모든 프로퍼티를 초기화해야 한다.
클래스는 하나 이상의 지정 이니셜라이저를 갖는다.
자식클래스는 다른 저장 프로퍼티가 없을 경우 이니셜라이저를 갖지 않을 수 있다.
초기화를 좀 더 손쉽게 도와주는 역할을 한다. 지정 이니셜라이저를 자신 내부에서 호출한다.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
convenience init(name: String, koreanAge: Int) {
self.init(name: name, age: koreanAge - 1)
}
}
let korean = Person(name: "철수", koreanAge: 10)
korean.name // 철수
korean.age // 9
세 가지 규칙이 있다.
궁극적으로 기반 클래스(Base class)의 지정 이니셜라이저를 반드시 호출하는 것을 확인할 수 있다.
클래스의 2단계 초기화는 프로퍼티를 초기화하기 전에 프로퍼티 값에 접근하는 것을 막아 초기화를 안전하게 할 수 있도록 한다.
기본적으로 스위프트의 이니셜라이저는 부모클래스의 이니셜라이저를 상속받지 않는다. 자식클래스에 최적화되어있지 않기 때문이다. 안전하다고 판단되는 상황에서만 상속한다.
부모클래스의 편의 이니셜라이저를 자식클래스에서 호출할 수 없다. 따라서 재정의하지 않는다.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
convenience init(name: String, koreanAge: Int) {
self.init(name: name, age: koreanAge - 1)
}
}
class Student: Person {
var major: String
override init(name: String, age: Int) {
self.major = "iOS"
super.init(name: name, age: age)
}
// override convenience 안함
convenience init(name: String, koreanAge: Int) {
self.init(name: name, age: koreanAge - 1)
}
}
부모클래스의 실패 가능한 이니셜라이저를 필요에 따라 실패하지 않도록 재정의할 수 있다.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
init?(age: Int) {
guard age <= 0, age > 150 else { return nil }
self.name = "이름"
self.age = age
}
}
class Student: Person {
var major: String
override init(age: Int) {
self.major = "iOS"
super.init(name: "이름", age: age)
}
}
let student = Student(age: 100)
student.name // 이름
student.age // 100
student.major // iOS
자식클래스에서 프로퍼티 기본값을 모두 제공하였을 때 다음의 두 가지 규칙일 때 이니셜라이저가 자동으로 상속된다.
부모클래스의 지정 이니셜라이저를 모두 재정의한 경우로 규칙 2에 해당한다. 따라서 부모의 편의 이니셜라이저가 자동으로 상속되었다.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
convenience init(name: String, koreanAge: Int) {
self.init(name: name, age: koreanAge - 1)
}
}
class Student: Person {
var major: String
override init(name: String, age: Int) {
self.major = "iOS"
super.init(name: name, age: age)
}
}
let student = Student(name: "학생", koreanAge: 10) // 부모클래스의 편의 이니셜라이저가 자동 상속됨
student.age // 9
📌키워드: required
required 수식어를 클래스의 이니셜라이저 앞에 명시해주면, 이 클래스를 상속받은 자식클래스에서 반드시 해당 이니셜라이저를 구현해야 한다. 다만 자식클래스에서 요구 이니셜라이저를 재정의할 때는 override 대신 required를 사용한다.
Interface Builder란 간단하게 말해서는 xib, nib, storyboard를 사용하여 VIew를 작업하는 것이다. 여기에서 생성되는 초기화 구문이다.
스토리보드나 Xib 파일을 이용해 UI를 구성하여 이를 그대로 사용하기 위해 NSCoding가 필요하다. 인터페이스 파일은 UI의 구성을 xml 형태로 저장하고 있는데, 이 저장한 형태를 사용자의 화면으로 그대로 가져오기 위해 init(coder: NSCoder)를 통해서 객체가 생성된다.
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
NSCoding이란 보관 및 배포를 위해 개체를 인코딩 및 디코딩할 수 있도록 하는 프로토콜이다.
public protocol NSCoding {
public func encode(with aCoder: NSCoder)
public init?(coder aDecoder: NSCoder)
}
class Person {
var name: String
var age: Int
required init() {
self.name = "이름"
self.age = 0
}
}
class Student: Person {
var major: String
required init() {
self.major = "iOS"
super.init()
}
}
let student = Student()
Person을 상속받는 Student에서 요구 이니셜라이저를 재정의할 때 매개변수를 추가하면 아래와 같은 에러가 발생한다. fatalError 부분을 추가하면 된다.
⛔️ 'required' initializer 'init()' must be provided by subclass of 'Person'
class Person {
var name: String
var age: Int
required init() {
self.name = "이름"
self.age = 0
}
}
class Student: Person {
var major: String
required init(major: String) {
self.major = major
super.init()
}
required init() {
fatalError("init() has not been implemented")
}
}
let student = Student(major: "iOS")
student.major // iOS
student.name // 이름
student.age // 0
또는 다음과 같이 작성한다.
class Student: Person {
var major: String
init(major: String) {
self.major = major
super.init()
}
required init() {
self.major = "iOS"
super.init()
}
}