Swift문법 - (14)클래스의 상속과 초기화

Youth·2022년 10월 1일
0

swift문법공부

목록 보기
14/27
post-thumbnail
post-custom-banner

클래스의 상속

  • 상속(Inheritance)
  • 본질적으로 성격이 비슷한 타입을 새로만들어 저장속성을 추가하거나 메서드를 변형시켜서 사용하려는 것
  • 다중상속(하나의 클래스에 두개이상의 클래스를 상속받는건) 불가능
  • final 키워드를 붙이면 해당 클래스는 상속이 불가능
  1. 저장속성을 추가(재정의는 불가능하다)
class Person {
    var id = 0
    var name = "이름"
    var email = "abc@gmail.com"
}

class Student: Person {
		// person의 저장속성은 모두 가지고 있다
    // id, name, email을 가지고있다
    var studentId = 0
}

class Undergraduate: Student {
		// student의 저장속성은 모두 가지고 있다
    // id, name, email, studentId를 가지고 있다
    var major = "전공"
}

재정의(Overriding)

  • 오버로딩(overloading)
    • 함수에서 함수의 하나의 이름에 여러 함수를 대응시켜서 사용
  • 오버라이딩(overriding)
    • 클래스의 상속에서 상위클래스의 속성/메서드를 재정의(기능을 변형하여 사용)
💡 저장속성에 관한 재정의(overriding)은 어떤 경우에도 불가능
class Aclass {
    func doSomething() {
        print("Do something")
    }
}

// 상속을 할때 기존에있던 doSomething을 "재정의"할 수 있다
// 재정의 하지 않으면 자동으로 doSomething을 가지고있다
class Bclass: Aclass {
    override func doSomething() {
				// 상위클래스에 있는 doSomething 함수를 실행한다
				// 재정의하기전의 함수를 실행한다는 뜼
				super.doSomething()
        print("Do another job")
    }
}

💡 중요 - 재정의의 규칙 정리 1) 저장속성은 재정의 **불가** 2) 메서드는 재정의 가능(다만, 기능 확장만 가능) 3) 계산속성은 재정의 가능(실질적으로 메서드이기 때문) ~~4) 생성자는 기본적으로 상속되지 않고 재정의 원칙(추후 다룸)~~
class SomeSuperclass {
    // 저장속성
    var aValue = 0
    // 메서드
    func doSomething() {
        print("Do something")
    }
}

class SomeSubclass: SomeSuperclass {
    // 저장속성의 재정의는 원칙적 불가
    //override var aValue = 3 **error발생**
    
    // 저장속성을 계산속성으로 재정의는 가능하다
    override var aValue: Int {
        get {
            return 1
        }
        set {    
						// ⭐️**self로 쓰면 안됨**
            super.aValue = newValue
        }
    }
    
    // 메서드는 재정의 가능
    override func doSomething() {
        super.doSomething()
        print("Do something 2")
    }
}

속성의 재정의(엄격한 rule이 있음)

  1. 저장속성의 재정의는 불가능
    1. 계산속성으로의 재정의는 가능
    2. 속성 감시자를 추가하는건 가능(속성감시자는 저장속성에 대한 감시자이기 때문)
  2. 계산속성은 실질적인 메서드기때문에 재정의 가능
    1. 기능을 축소하는 형태로의 재정의는 불가능
    • 읽기전용 계산 속성을 읽기/쓰기가 가능한 가능한 속성으로 재정의 가능(확장 O)
    • 속성 감시자를 추가하는 재정의는 불가능 (읽기 전용 속성을 관찰 할 수 없음 - 논리에 안 맞음)
    • 읽기/쓰기 계산 속성을 읽기만 가능한 가능한 속성으로만 재정의 불가능(기능 제한 X)
    • 속성 감시자를 추가하는 재정의 가능 (관찰은 가능)
class Vehicle {
		// 저장속성 - 원칙적으로 재정의 불가능, 계산속성으로는 재정의 가능
    var currentSpeed = 0.0
		// 계산속성 - 유지및 확장은가능, 축소는 불가능
		// 쓰기가 있기때문에 속성감시자추가 가능
		// 계산속성을 정의하는 케이스는 두가지 기능재정의, 속성감시자 추가
		// 이 경우는 기능재정의와 속성감시자를 추가하는 재정의 두가지 모두 가능
    var halfSpeed: Double {
        get {
            return currentSpeed / 2
        }
        set {
            currentSpeed = newValue * 2
        }
    }
}

class Bicycle: Vehicle {
    // 저장 속성 추가는 당연히 가능
    var hasBasket = false
    
    // 1) 저장속성 계산속성으로 재정의(메서드 추가) 가능
    // self키워드 대신 super키워드 사용해야함 - 메모리주소를 가지고 있기 때문에
    override var currentSpeed: Double {
        // 상위 속성이기 때문에 super키워드 필요
        get {
            return super.currentSpeed
        }
        set {
            super.currentSpeed = newValue
        }
    }

		// 1) 저장속성에 속성감시자를 추가하는 재정의(메서드 추가)는 가능
    override var currentSpeed: Double {
        // 상위 속성이기 때문에 super키워드 필요
        willSet {
            print("값이 \(currentSpeed)에서 \(newValue)로 변경 예정")
        }
        didSet {
            print("값이 \(oldValue)에서 \(currentSpeed)로 변경 예정")
        }
    }
    
     ⭐️ 계산속성을 재정의 가능 (super키워드 주의)
		// 계산속성에서 set블럭이 속성감시자의 역할을 하기때문에 **속성감시자 추가 불가
		// 계산속성의 재정의 첫번째 케이스인데 이미 set이 있어서 속성감시자 추가 불가**
    override var halfSpeed: Double {
        get {
            return super.currentSpeed / 2
        }
        set {
            super.currentSpeed = newValue * 2
        }
    }
    
     ⭐️ 계산속성을 재정의 하면서, 속성감시자 추가 가능 (속성감시자 부분 참고)
		// 계산속성에서 set블럭이 속성감시자의 역할을 하기때문에 **속성감시자 추가 불가**
		// 하지만 읽기/쓰기 계산속성을 재정의하는 케이스에서는 **예외**
    override var halfSpeed: Double {
        willSet {
            print("값이 \(halfSpeed)에서 \(newValue)로 변경 예정")
        }
        didSet {
            print("값이 \(oldValue)에서 \(halfSpeed)로 변경 예정")
        }
    }
}
  • 계산속성의 경우 재정의가 가능
  • 계산속성의 재정의 케이스는 두가지이다 기능을 재정의 or 속성감시자를 추가
  • 기존의 읽기전용 계산속성은 기능을 유지/확장하는 경우로 재정의 가능
  • 기존의 읽기전용 계산속성은 속성감시자를 추가하는 재정의는 불가능(의미가 없음)
  • 기존의 읽기/쓰기전용 계산속성은 기능을 유지하는 경우로 재정의 가능
  • 기존의 읽기/쓰기전용 계산속성은(쓰기기능존재) 속성감시자 추가하는 재정의 가능(이미 쓰기기능이 속성감시자의 역할을 하긴하지만 예외적으로 가능)

타입 속성의 재정의 원칙

  • 타입 저장 속성은 재정의 불가 - static키워드 (계산속성으로 재정의하거나, 속성 감시자를 추가하는 것도 불가능)
  • 타입 계산 속성 - class 키워드인 경우 계산 속성 재정의 (확장방식) 가능.
  • 재정의한 타입 저장/계산 속성에는 감시자 추가 원칙적으로 불가

초기화(initalization)

  • 초기화는 클래스, 구조체, 열거형의 인스턴스를 생성하는 과정임
  • 각 "저장 속성"에 대한 초기값을 설정하여 인스턴스를 사용가능한 상태로 만드는
  • 열거형은 저장속성이 존재하지 않으므로, case중에 한가지를 선택 및 생성
class Color {
    //let red, green, blue: Double    
    // 동일한 타입일때, 한줄에 작성가능
    let red: Double
    let green: Double
    let blue: Double
    
    // **생성자도 오버로딩(Overloading)을 지원**
    // (파리미터의 수, 아규먼트 레이블, 자료형으로 구분)
    // 기본 생성자 -> 저장 속성의 기본값을 설정하면 "자동" 구현이 제공됨
    init() {      
        red, green, blue = 0.0
    }

    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
}
var color = Color.init()Color() 과 똑같은거임

초기화의 방법( = 저장속성이 초기값을 가지게 하는 방법)

  • 저장 속성의 선언과 동시에 값을 저장
  • 저장 속성을 옵셔널로 선언 (초기값이 없어도 nil로 초기화됨)
  • 생성자에서 값을 초기화

구조체의 특별한 생성자(멤버와이즈 이니셜라이저)

struct Color1 {
    var red: Double = 1.0
    var green: Double = 1.0
    var blue: Double

}
// 두개의 이니셜라이즈를 자동으로 제공
var c2 = Color1(blue: Double)
var c2 = Color1(red: Double, green: Double, blue: Double)

생성자(initializer)

구조체 생성자

case1) 올바르지 않은 방법

이유 : 새로운 생성자를 매번 만들어줘야함

struct Color {
    let red, green, blue: Double

    init(white: Double) {
        red   = white
        green = white
        blue  = white
    }
    
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
}

case2) 올바른방법

이유 : 재사용성을 고려한 방법

struct Color1 {
    let red, green, blue: Double
    
	   // 구조체는 다른 생성자를 호출하는 방식도 가능 ⭐️
    init() {
        self.init(red: 0.0, green: 0.0, blue: 0.0)
    }

    init(white: Double) {
        self.init(red: white, green: white, blue: white)
    }
    
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
}

클래스의 생성자

class Color2 {
    let red, green, blue: Double
	    
		// 편의생성자 : **생성자의 재사용성을 고려해 다른 생성자를 호출할 수 있음**
		// **class는 생성자의 재정의를 고려해야하는데 편의생성자는 고려를 안해도 됨**
    convenience init() {
        self.init(red: 0.0, green: 0.0, blue: 0.0)
    }

    convenience init(white: Double) {
        self.init(red: white, green: white, blue: white)
    }
    
		// 지정생성자
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
}
💡 **생성자를 만들때 고려해야하는게 재사용성이다(오버로딩이 되기때문에) 근데 여러개의 생성자를 만들때 다른 생성자를 호출할수있는데 구조체의 경우엔 그냥 생성자안에 다른생성자를 사용할 수 있지만 클래스의 경우엔 편의생성자를 만들어야 다른생성자를 사용(호출)할 수 있다.**

(이미 모든 속성을 초기화하는 지정생성자가 있다면)
모든 속성을 초기화하지 않는 경우 편의생성자로 구현을 권장

클래스의 상속과 지정/편의 생성자 사용 예시

class Aclass {
    var x: Int
    var y: Int

    // 지정생성자 - 모든 저장 속성 설정
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
    
		// 편의생성자 - (조금 편리하게 생성) 모든 저장 속성을 설정하지 않음
    convenience init() {
        self.init(x: 0, y: 0)
    }
}

var a = Aclass()

// 상속이 일어나는 경우 ⭐️
// **init**을 해야 메모리에 초기화가 된다(convinence X)

class Bclass: Aclass {
    
    var z: Int
	   
		// 생성자의 역할
		// 나의단계에있는 저장속성(z) 찍어내고
		// 상위지정생성자 호출해서 상위단계 저장속성(x, y)을 찍어냄
    init(x: Int, y: Int, z: Int) {
				// ⭐️ (필수) - 새로운 저장속성을 초기화 해준다
        self.z = z

				// error발생 : 상위의 지정생성자 호출하기전이기 때문에
        //self.y = y          

				// ⭐️ (필수) 상위의 지정생성자 호출 - **어떤거든**
        super.init(x: x, y: y)

				// 새로운지정생성자 호출 -> 상위의지정생성자 호출 -> 하고싶은거
        // self.z = 7
    }
    
    convenience init(z: Int) {
				//self로 접근이 불가능함 -> 아직 메모리를 초기화 시키지 않았음
        //self.z = 7
        self.init(x: 0, y: 0, z: z)
    }
    
    convenience init() {
        self.init(z: 2)
    }
}

⭐️생성자의 상속과 재정의⭐️

  1. 생성자는 기본적으로 상속되지 않고 재정의를 하는게 원칙
  • 왜냐? 하위클래스에 최적화가 되어있지 않기 때문에
    1. 상위 지정생성자를 고려해야함
    2. 현재단계의 저장속성을 고려해서 구현해야함

[1단계 - 상위 생성자에 대한 고려]

  • 상위에 어떤 지정 생성자가 존재하는지?

  • (상위) 지정 생성자

1) 하위클래스에서 지정 생성자로 구현 (재정의)

2) 하위클래스에서 편의 생성자로 구현 가능 (재정의)

3) 구현 안해도됨(반드시 재정의하지 않아도 됨), 새로운생성자를 만듦

  • (상위) 편의 생성자

1) 재정의를 하지 않아도 됨 (호출 불가가 원칙이기 때문에 재정의 제공 안함)

2) (만약에 동일한 이름을 구현했다면) 그냥 새로 정의한 것임

SuperClass

class Aclass {
    var x = 0   
    // init() {}                // 기본 생성자가 자동으로 제공됨
}
1단계 - 상위생성자에 대한 고려
⭐️상위에는 "지정"생성자가 존재함
선택지가 3가지 존재함(지정생성자 재정의, 편의생성자 재정의, 새로운 생성자)

class Bclass: Aclass {
		var y: Int
		첫번째 선택지 - 지정생성자 재정의
		override init() {
				2단계 - 현재단계(Bclass)의 저장속성고려하여 구현
				재정의이긴하지만 결국은 지정생성자이고 "하위클래스에서의 지정생성자"
        -> 현재단계 저장속성 초기화 -> 상위클래스 지정생성자 호출
				self.y = 0
				super.init()
		}
}
1단계 - 상위생성자에 대한 고려
⭐️상위에는 "지정"생성자가 존재함
선택지가 3가지 존재함(지정생성자로 재정의, 편의생성자로 재정의, 새로운 생성자)

class Bclass: Aclass {
		var y: Int
		두번째 선택지 - 편의생성자로(똑같은 이름 + convenience)
    편의 생성자는(델리게이트 크로스) 현재단계의 지정생성자를 호출해야함
		override convenience init() {
				self.init(y: 0)
		}
		현재단계의 하위클래스의 지정생성자
		init(y: Int) {
				self.y = y
				super.init() // x초기화 시켜줌 
		}
}
1단계 - 상위생성자에 대한 고려
⭐️상위에는 "지정"생성자가 존재함
선택지가 3가지 존재함(지정생성자로 재정의, 편의생성자로 재정의, 새로운 생성자)

class Bclass: Aclass {
		var y: Int

		세번째 선택지 - 새로운 생성자 생성
		init(y: Int) {
			self.y = y
			super.init() // x초기화 시켜줌 
		}
}

생성자 상속의 예외사항

  • 예외적으로 자동상속되는 경우
  1. 지정생성자 자동상속
    1. 새로운 저장속성이 아얘 없거나, 새로운저장속성의 기본값이 존재하는경우
  2. 편의생성자 자동상속
    1. 상위 지정생성자를 모두 상속하는 경우 편의생성자를 자동상속한다
    2. 상위 지정생성자를 모두 재정의해서 구현하는 경우 편의생성자를 자동상속한다
SuperClass

class Food {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

⭐️상위의 생성자

지정생성자, 편의생성자

class RecipeIngredient: Food {
    var quantity: Int
    
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    
		// 편의생성자로 재정의함
		// 상위클래스의 지정생성자를 모두 재정의한경우로 볼 수 있다
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
    // convenience init() { }      // 자동 상속 (예외 규칙)
}

필수생성자(required) - 상속관련

class Aclass {
    var x: Int
    required init(x: Int) {
        self.x = x
    }
}

class Bclass: Aclass {
//    required init(x: Int) {
//        super.init(x: x)
//    }
}

class Cclass: Aclass {
    init() {
        super.init(x: 0)
    }
    // 다른생성자를 구현했기때문에 required init이 자동상속되지 않음
    required init(x: Int) {
				// 호출시 required init으로 호출하지 않음
        super.init(x: x)       
    }
}

실패가능생성자 - init?() {}

  • 실패가능 생성자는 인스턴스 생성에 실패할 수도 있는 가능성을 가진 생성자
struct Animal {
    let species: String
    
    // 실패가능 생성자
    init?(species: String) {
        if species.isEmpty {
						**// 생성자 내에서 실패 가능 부분에 nil을 리턴하면 됨(약속)**
            return nil         
        }
        self.species = species
    }
		
    // 실패가능한생성자가 있을 때 같은 이름의 지정생성자 구현 불가
		init(species: String) {}
}
 
let a = Animal(species: "Giraffe")
let b = Animal(species: "")
enum TemperatureUnit {
    case kelvin
    case celsius
    case fahrenheit
    
		// 실패가능생성자 enum에서 생성자를 구현할경우 고민해봐야함
    init?(symbol: Character) {
        switch symbol {
        case "K":
            self = TemperatureUnit.kelvin
        case "C":
            self = TemperatureUnit.celsius
        case "F":
            self = TemperatureUnit.fahrenheit
				// return이 nil인 경우가 있기때문에 실패가능생성자로 구현해야함
        default:
            return nil
        }
    }
}
let c: TemperatureUnit = TemperatureUnit.celsius
let f: TemperatureUnit? = TemperatureUnit(symbol: "F")

유사한방식 - 열거형의 원시값 설정
enum TemperatureUnit1: Character {
    case kelvin = "K"
    case celsius = "C"
    case fahrenheit = "F"
}
let f1: TemperatureUnit1? = TemperatureUnit1(rawValue: "F") // .fahrenheit
let u: TemperatureUnit1? = TemperatureUnit1(rawValue: "X") // nil

같은 구조체나 클래스 내에서 (델리게이트 어크로스)

  • 실패가능 생성자(넓음)는 실패불가능 생성자(좁음)를 호출할 수 있음
  • 실패불가능 생성자(좁음)는 실패가능 생성자(넓음)를 호출할 수 없음

클래스의 상속관계에서

  • 하위의 실패 불가능생성자(좁음)에서 상위의 실패가능생성자(넓은)은 호출 불가능
  • 하위의 실패가능생성자(넓음)에서 상위의 실패불가능생성자(좁음)은 호출 가능

클래스의 재정의에서

  • 상위의 실패가능생성자(넓음)을 하위에서 실패불가능한생성자(좁음)로 재정의 가능
  • 상위의 실패불가능생성자(좁음)을 하위에서 실패가능생성자(넓음)으로 재정의 불가능
profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료
post-custom-banner

0개의 댓글