📱 Mobile/🚀 Swift

[Swift] 구조체와 클래스 - (2)

exception_log 2021. 1. 27. 18:57

<!— 본 포스트는 "꼼꼼한 재은씨의 Swift : 문법편"을 공부하며 직접 정리한 포스트입니다. —>

 

🚀작성자의 Swift 정리 원본인 Notion 구경하기 -> www.notion.so/Swift-59150070adb0467ea11d4f69090dbb24

 

상속

한 클래스가 다른 클래스에서 정의된 프로퍼티나 메소드를 물려받아 사용하는 것
  • 프로퍼티와 메소드를 물려준 클래스는 부모 클래스 = 상위 클래스 = 슈퍼 클래스 = 기본 클래스
  • 프로퍼티와 메소드를 물려받은 클래스는 자식 클래스 = 하위 클래스 = 서브 클래스 = 파생 클래스

서브 클래싱

먼저 기본 클래스 A 를 정의한다.

class A {
	var name = "Class A"
	

	var description: String {
		return "This class name is \(self.name)"
	}
	
	func foo() {
		print("\(self.name)'s method foo is called")
	}

}
  • 서브클래싱을 위한 문법 구조
class <클래스 이름> : <부모 클래스> {
	// 추가 구현 내용
}
  • 서브클래싱 문법은 클래스를 정의할 때 콜론으로 구분하여 왼쪽에 작성하고자 하는 클래스, 오른쪽에는 상속받고자 하는 클래스의 이름을 각각 나열하는 것이다. 이 때 주의할 점은 스위프트에서는 단일 상속만 지원된다는 점이다.
  • 가끔 코코아 터치 프레임워크의 클래스 정의 구문을 보면 콜론 다음에 나열된 여러 개의 클래스를 보게될 때도 있는데, 이는 대부분 가장 첫번째만 상속일 뿐 나머지는 '구현 (implements)'이라고 하는 또 다른 기능이다.
앞의 클래스 A를 사용하여 클래스 B를 서브클래싱 한다.

class B: A {
	var prop = "Class B"
	

	func boo() -> String {
		return "Class B prop = \(self.prop)"
	}

}

let b = B()
b.prop // "Class B"
b.boo() // Class B prop = Class B
b.name // "Class A"
b.foo() // Class A's method foo is called

오버라이딩

부모 클래스로부터 물려받은 내용을 덮어쓰는 것 (재정의)

🚀 오버라이딩한 내용은 자기 자신 또는 자신을 서브클래싱한 하위 클래스에만 정의된다. 즉, 자식 클래스에서 프로퍼티나 메소드의 내용을 변경한다고 해서 변경된 내용이 부모 클래스에까지 적용되지는 않는다는 뜻이다.

🚀 만약 특정 클래스에서 메소드를 오버라이딩 했다면 이 클래스를 상속받는 하위 클래스들은 모두 수정된 메소드를 상속받지만, 부모 클래스나 부모 클래스를 상속받은 다른 클래스는 기존 메소드를 그대로 사용한다.

🚀스위프트에서는 오버라이딩하려는 메소드나 프로퍼티의 선언 앞에 override 키워드를 붙여야 한다.

🚀 override 키워드가 붙으면 컴파일러는 이 프로퍼티 혹은 메소드가 상위 클래스에서 정의된 것인지를 검사한다. 여기에서 부모 클래스라고 하지 않고 굳이 상위 클래스라고 표현한 것은, 직접적인 부모 클래스 뿐 아니라 부모 클래스의 부모클래스, 또 그 위의 부모 클래스 등 아무것도 상속받지 않은 기본 클래스가 나타날 때까지 클래스의 계층을 따라 계속해서 탐색하기 때문이다.

→ 가장 상위 클래스까지 탐색했음에도 정의된 내역을 발견하지 못하면 오류 발생

[ 프로퍼티 오버라이딩 시 허용되는 것 ]

  1. 저장 프로퍼티를 get, set 구문이 모두 있는 연산 프로퍼티로 오버라이딩 하는 것
  1. get, set 구문이 모두 제공되는 연산 프로퍼티를 get, set 구문이 모두 제공되는 연산 프로퍼티로 오버라이딩 하는 것
  1. get 구문만 제공되는 연산 프로퍼티를 get, set 구문이 모두 제공되는 연산 프로퍼티로 오버라이딩 하는 것
  1. get 구문만 제공되는 연산 프로퍼티를 get 구문만 제공되는 연산 프로퍼티로 오버라이딩하는 것

[ 프로퍼티 오버라이딩 시 허용되지 않는 것 ]

  1. 저장 프로퍼티를 저장 프로퍼티로 오버라이딩 하는 것
  1. get, set 구문과 관계없이 연산 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것
  1. 저장 프로퍼티를 get 구문만 제공되는 연산 프로퍼티 (=읽기 전용)로 오버라이딩 하는 것
  1. get, set 구문을 모두 제공하는 연산 프로퍼티를 get 구문만 제공되는 연산 프로퍼티로 오버라이딩 하는 것

즉, 프로퍼티 오버라이딩은 상위 클래스의 기능을 하위 클래스가 확장, 또는 변경하는 방식으로 진행되어야지, 제한하는 방식으로 진행되어서는 안된다는 것이다.

class Vehicle {
	var currentSpeed = 0.0
	

	var description: String {
		return "시간당 \(self.currentSpeed) 의 속도로 이동하고 있습니다."
	}
	
	func makeNoise() {
		
	}

class Car: Vehicle {
	var gear = 0
	var engineLevel = 0
	

	override var currentSpeed: Double {
		get {
			return Double(self.engineLevel * 50)
		}
		set {
			// 아무런 일을 하지 않더라도 반드시 있어야 한다. currentSpeed가 원래 저장 프로퍼티였기 때문!
		}
	}
	
	override var description: String {
		get {
			return "Car: engine = \(self.engineLevel), so currentSpeed = \(self.currentSpeed)"
		}
		set {
			print("New Value is \(newValue)")
		}
	}

}
	
let c = Car()
c.engineLevel = 5
c.currentSpeed // 250
c.description = "New Class Car"

print(c.description) 

실행 결과 >>
New Value is New Class Car
Car : engineLevel = 5, so currentSpeed = 250.0
  • 메소드 오버라이딩에서 오버라이딩 대상이 되는 메소드의 매개변수 개수나 타입, 그리고 반환 타입은 변경할 수 없다. 메소드 오버라이딩을 통해 변경할 수 있는 것은 오로지 내부 구문 뿐이다.
  • 메소드 오버라이딩은 상위 클래스의 프로퍼티나 메소드를 수정할 수 있다는 점에서 매우 강한 생산성을 가진다.
  • 하지만 보안상의 이유로 오버라이딩을 허용하고 싶지 않을 때 final 키워드를 이용하면 메소드나 프로퍼티가 상수적인 성격으로 변하므로 더는 하위 클래스에서 오버라이드 할 수 없다.
    final class Vehicle {
    	...
    }

오버로딩

하나의 메소드 이름으로 여러가지 메소드를 만들어 쌓는 것. 같은 이름의 메소드라도 정의된 매개변수의 타입이 다르면 서로 다른 메소드로 처리하는 것이 오버로딩이다.
  • 스위프트에서 메소드는 이름뿐만 아니라 매개변수의 개수와 타입을 기준으로 하여 유일성 여부를 구분한다. 따라서 이름이 같고 매개변수의 개수까지 일치하더라도 타입이 다르면 서로 다른 메소드로 구분한다.

타입 캐스팅

  • 자식 클래스는 본래의 타입 대신 부모 클래스 타입으로 선언하여 사용할 수 있다.
class Vehicle {
	var currentSpeed = 0.0

	func accelerate() {
		self.currentSpeed += 1
	}

}

class Car: Vehicle {
	var gear: Int {
		return Int(self.currentSpeed / 20) + 1
	}

	func wiper() {
	
	}

}

// Car 클래스의 인스턴스를 할당받지만 Vehicle 타입으로 선언되었다. 
let trans: Vehicle = Car()
  • 자식 클래스는 상위 클래스에 해당하는 타입은 모두 사용할 수 있다.

타입 비교 연산

인스턴스(또는 상수, 변수) is 비교 대상 타입
  • 이 연산자는 아래의 연산 법칙을 따른다.
    1. 연산자 왼쪽 인스턴스의 타입이 연산자 오른쪽 비교대상 타입과 일치할 경우 true
    1. 연산자 왼쪽 인스턴스의 타입이 연산자 오른쪽 비교대상 타입의 하위 클래스일 경우 true
    1. 그 외 false

타입 캐스팅 연산

타입을 변환할 수 있는 기능 (형변환)
  • 관계없는 모든 타입으로 변환이 가능한 것이 아니라 상속 관계에 있는 타입들 사이에서 허용된다.

[업 캐스팅]

  • 하위 클래스 타입을 상위 클래스 타입으로 변환할 때
  • 캐스팅하기 전 타입이 하위 클래스, 캐스팅한 후 타입이 상위 클래스일 때
  • 캐스팅한 결과, 캐스팅하기 전 타입보다 추상화될 때
  • 일반적으로 캐스팅 과정에서 오류가 발생할 가능성이 없음
객체 as 반환할 타입

[다운 캐스팅]

  • 상위 클래스 타입을 하위 클래스 타입으로 캐스팅할 때
  • 캐스팅하기 전 타입이 상위 클래스, 캐스팅한 후 타입이 하위 클래스
  • 캐스팅한 결과, 캐스팅하기 전 타입보다 구체화 될 때
  • 캐스팅 과정에서 오류가 발생할 가능성이 있음
  • 오류에 대한 처리 방식에 따라 옵셔널 캐스팅과 강제 캐스팅으로 나누어짐
객체 as? 변환할 타입 (결과는 옵셔널 타입)
객체 as! 변환할 타입 (결과는 일반 타입)

예시 >>
if let anySUV = anyCar as? SUV {
	pritn("\(anySUV) 캐스팅 성공")
}

Any, AnyObject

  • 상속 관계에 있지 않아도 타입 캐스팅할 수 있는 예외
  • Any와 AnyObject 타입은 무엇이든 다 받아들일 수 있는 일종의 범용 타입이다.
  • AnyObject 타입으로 선언된 값은 타입 캐스팅을 통해 구체적인 타입으로 변환할 수 있다. AnObject는 클래스 특성상 항상 다운 캐스팅만 수행되는데, 상속 관계가 성립하지 않아도 예외로 타입 캐스팅할 수 있지만 실제로 저장된 인스턴스 타입과 관계없는 타입으로 캐스팅하고자 하면 오류가 발생한다.
  • Any 타입은 매우 극단적으로 추상화된 타입이어서 Any 타입에 할당된 객체가 사용할 수 있는 프로퍼티나 메소드는 아예 제공되지 않는다. Any 라는 타입으로 정의하면 모든 값을 제한 없이 할당받을 수 있지만, 그 값을 이용해서 할 수 있는 것은 거의 없어지는 셈이다.
  • 여기에 더하여 Any 타입의 남용은 스위프트에서 사용되는 정적인 타입들을 모두 동적인 타입으로 바꾸어버리는 결과를 가져온다. 이는 결국 앱의 전체적인 생산성을 낮추게 된다.

초기화 구문

🚀 초기화 : 항상 인스턴스를 생성해서 메모리 공간을 할당받은 다음에 사용하는 것

init 초기화 메소드

init(<매개변수> : <타입> , ...) {
	1. 매개변수 초기화
	2. 인스턴스 생성 시 기타 처리할 내용
}
  1. 초기화 메소드의 이름은 init으로 통일된다.
  1. 매개변수의 개수, 이름, 타입은 임의로 정의할 수 있다.
  1. 매개변수틔 이름과 개수, 타입이 서로 다른 여러 개의 초기화 메소드를 정의할 수 있다.
  1. 정의된 초기화 메소드는 직접 호출되기도 하지만, 대부분 인스턴스 생성 시 간접적으로 호출된다.
struct Resolution {
	var width = 0
	var height = 0

	// 초기화 메서드 : Width를 인자값으로 받음
	init(width: Int) {
		self.width = width
	}

}

class VideoMode {
	var resolution = Resolution(width: 2048)
	var interlaced = false
	var frameRate = 0.0
	var name: String?

	// 초기화 메소드 : interlaced, frameRate 두 개의 인자값을 받음
	init(interlaced: Bool, frameRate: Double) {
		self.interlaced = interlaced
		self.frameRate = frameRate
	}

}

// 인스턴스 생성 구문은 init 메소드가 생략된 형태이다
let resolution = Resolution(width: 4096)
let videoMode = VideoMode(interlaced: true, frameRate: 40.0)

// 하나의 인자값만 받고 싶다면 init 메소드를 여러개 선언해준다. (오버로딩)
class VideoMode {
	var resolution = Resolution(width: 2048)
	var interlaced = false
	var frameRate = 0.0
	var name: String?

	// 초기화 메소드 : interlaced, frameRate 두 개의 인자값을 받음
	init(interlaced: Bool, frameRate: Double) {
		self.interlaced = interlaced
		self.frameRate = frameRate
	}
	
	// 초기화 메소드 : interlaced 한 개의 인자값을 받음
	init(interlaced: Bool) {
		self.interlaced = interlaced
	}

}

// 인스턴스 생성 및 상수 할당
let videoMode = VideoMode(interlaced: true)
let vm = VideoMode() //error!

// 아래와 같이 init 메소드를 정의하면 에러가 발생하지 않는다. 
class VideoMode {
    var interlaced = false
    var frameRate = 0.0
    var name: String?

		//초기화 될 때 interlaced 인자값을 받는 구문
	init(interlaced: Bool = true) {
	    self.interlaced = interlaced
	}

}

// 인스턴스 생성 및 상수 할당
let vm = VideoMode() // (O)
let videoMode = VideoMode(interlaced: true) // (O)
  • init 메소드를 정의하고 나면 기본 초기화 구문 제공되지 않는다.

초기화 구문의 오버라이딩

class Base {
	var baseValue: Double
	init(inputValue: Double) {
		self.baseValue = inputValue
	}
}

class ExBase: Base {
	override init(inputValue: Double) {
		super.init(inputValue: 10.5)
	}
}

초기화 구문 델리게이션

상위 초기화 구문의 호출이 연속으로 이어지면 최상위에 있는 초기화 구문까지 호출이 이어지면서, 모든 초기화 구문이 누락되는 일 없이 실행된다. 이렇게 연쇄적으로 부모 크래스의 초기화 구문에 대한 호출이 발생하는 것을 초기화 구문 델리게이션 이라고 한다.

  • 기본 초기화 구문을 제외한 나머지 초기화 구문을 오버라이딩 할 때에는 반드시 부모 크래스 초기화 구문을 호출함으로써 델리게이션 처리를 해주어야 한다.
  • 부모 클래스에 기본 초기화 구문만 정의되어 있거나 기본 초기화 구문이 아예 명시적으로 정의되어 있지 않은 상태에서 자식 클래스가 오버라이딩 할 때는 super.init() 구문을 호출해주지 않아도 자동으로 부모클래스의 초기화 구문이 호출된다. 이 때 초기화 구문 호출은 자식 클래스부터 역순으로 이루어진다.
  • 부모 클래스에 기본 초기화 구문 이외에 다른 형식의 초기화 구문이 추가되어 있다면 자식 클래스에서 기본 초기화 구문을 오버라이딩 할 때 명시적으로 부모 클래스의 기본 초기화 구문을 호출해야 한다.
    class Base {
    	var baseValue: Double
    	init() {
    		self.baseValue = 0.0
    		print("Base Init")
    	}
    
    	init(baseValue: Double) {
    		self.baseValue = baseValue
    	}
    
    }
    
    class ExBase: Base {
    	override init() {
    		super.init()
    		print("ExBase Init")
    	}
    }

옵셔널 체인

옵셔널 타입의 문제점

옵셔널 타입은 항상 nil 여부를 검사하여 정상적인 값이 저장된 것을 확인한 후에 사용하는 것이 안전하므로 if 구문을 통해 옵셔널 타입을 처리하는 경우가 많다. 여기에서 옵셔널에 대한 이슈가 발생하는데, if 구문을 통해 값의 안전성 여부를 검사해야 한다는 것이다.

클래스나 구조체 등의 객체는 내부적으로 프로퍼티나 메소드를 소유하는데, 이때 클래스나 구조체의 인스턴스가 옵셔널 타입으로 선언될 경우 프로퍼티와 메소드를 호출하기 위해서는 매번 if 구문을 통해 옵셔널 인스턴스의 정상값 여부를 검사해야 한다.

struct Human {
	var name: String?
	var man: Bool = ture
}

var boy: Human? = Human(name: "홍길동", man: true)

일단 옵셔널 타입으로 선언된 이상, 변수 boy를 사용하려면 옵셔널 타입에 대한 안전성 검사가 필요하다.
if boy != nil {
	if boy!.name != nil {
		print("이름은 \(boy!.name!) 입니다.")
	}
}

혹은 옵셔널 비강제 해제 구문을 사용하여 아래와 같이 작성할 수도 있다.
if let b = boy {
	if let name = b.name {
		print("이름은 \(name)입니다.")
	}
}
  • 어느 방식이든 안전성을 담보하려면 if 구문의 처리를 피할 수 없다. 그리고 옵셔널 타입이 중첩되어 있을 때 if 구문을 중첩해서 사용하는 방식은 코드를 작성하는 입장에서 상당히 부담이 된다. 이러한 단점을 극복하고 복잡한 코드를 간단하게 줄여주는 방법으로 도입된 것이 바로 옵셔널 체인이다.

옵셔널 체인

옵셔널 타입으로 정의된 값이 하위 프로퍼티나 메소드를 가지고 있을 때, 이 요소들을 if 구문을 쓰지 않고도 간결하게 사용할 수 있는 코드를 작성하기 위해 도입되었다.
if let name = startup?.ceo?.name {
	print("대표 이사의 이름은 \(name)입니다.")
}
  • 이 때 만약 startup 변수나 ceo 프로퍼티가 빈 값이라면 아무런 값도 할당되지 않은 채로 구문은 종료된다. nil 객체의 프로퍼티에 값을 할당해줄 수는 없기 때문이다. 하지만 오류는 결코 발생하지 않으므로 안전하게 값을 할당할 수 있다.
  • 일반적으로 옵셔널 체인에는 다음과 같은 특징이 있다.
    1. 옵셔널 체인으로 참조된 값은 무조건 옵셔널 타입으로 반환된다.
    1. 옵셔널 체인 과정에서 옵셔널 타입들이 여러 번 겹쳐 있더라도 중첩되지 않고 한 번만 처리된다.
  • 옵셔널 타입을 몇 번 중첩하더라도 결국 반환할 수 있는 값은 nil 또는 정상값 두 개로 나누어지므로 단순히 하나의 옵셔널 객체로 감싼 값일 뿐이다.
    Optional(Optional(Optional(123))) = Optional(123)
반응형

'📱 Mobile > 🚀 Swift' 카테고리의 다른 글

[Swift] 프로토콜  (0) 2021.01.29
[Swift] 열거형과 익스텐션  (0) 2021.01.28
[Swift] 구조체와 클래스 - (1)  (0) 2021.01.26
[Swift] 함수 - (3)  (0) 2021.01.25
[Swift] 함수 - (2)  (0) 2021.01.25