-Today's Learning Content-

  • switch - fallthrough
  • class와 struct

1. switch - fallthrough

개념정리

  1. switch
    switch는 조건문의 일종으로 스위치문에 사용된 멤버를 case라는 경우의 수로 나누어 멤버의 값과 동일한 케이스를 반환하는 조건문이다.
  2. fallthrough
    switch를 사용할 때 사용하는 메서드로 스위치문의 케이스 내에서 선언할 수 있으며, 선언할 경우 케이스가 반환되어도 코드를 끝내지 않고 다음 케이스까지 이어서 실행할 수 있다.

1) fallthrough 사용해보기

switch문에서 fallthrough를 사용하기 위해 먼저 enum타입의 값을 생성해준다.

enum Animal {
case dog, cat, rabbit

let maltese: Animar = .dog

위에서 Animal타입은 dog, cat, rabbit의 3가지 case를 가지고 있다.
상수로 선언한 malteseAnimal타입이며 dog라는 값을 가지고 있다.
이제 switch문을 사용하여 상수가 가진 값을 출력한다.

switch maltese {
case .dog:
	print("이 동물은 강아지 입니다.")
case .cat:
    print("이 동물은 고양이 입니다.")
case .rabbit:
    print("이 동물은 토끼 입니다.")
}
// 출력: 이 동물은 강아지 입니다.

보통은 멤버에 해당하는 케이스의 값을 반환하며 스위치문이 종료된다.
하지만 여기에 fallthrough를 사용하면...

switch maltese {
case .dog:
    print("이 동물은 강아지 입니다.")
    fallthrough
case .cat:
    print("이 동물은 고양이 입니다.")
case .rabbit:
    print("이 동물은 토끼 입니다.")
}
// 출력: 이 동물은 강아지 입니다.
//	 	이 동물은 고양이 입니다.

위와 같이 값을 반환하고도 끝나지 않고 다음 케이스의 값까지 반환하는 것을 알 수 있다.
이를 활용하면 아래와 같은 코드도 작성할 수 있다.

switch maltese {
case .dog:
    print("이 동물은 강아지 입니다.")
    fallthrough
case .cat:
	print("고양이는 아니군요...")
    break
    print("이 동물은 고양이 입니다.")
case .rabbit:
    print("이 동물은 토끼 입니다.")
}
// 출력: 이 동물은 강아지 입니다.
//	 	고양이는 아니군요...

단, fallthrough는 최하단의 case에서는 사용할 수 없다.
다음 case가 존재하지 않기 때문에 코드를 이어서 실행할 수 없기 때문이다.

switch maltese {
case .dog:
    print("이 동물은 강아지 입니다.")
    fallthrough
case .cat:
    print("이 동물은 고양이 입니다.")
    fallthrough
case .rabbit:
    print("이 동물은 토끼 입니다.")
    fallthrough // Error!! 'fallthrough' without a following 'case' or 'default' block
}

2. class와 struct

1. class

class참조타입(Reference Type)로, 타입을 정의하고 프로퍼티와 메서드를 가질 수 있다.
같은 클래스타입끼리 상속이 가능하며, 인스턴스를 생성할 때 클래스 내부의 프로퍼티가 모두 초기화 되어야 한다는 특징을 가지고 있다.

convenience initializer란?

class에서 프로퍼티 초기화를 위해 init을 사용하는 방법은 3가지가 있다.

  • 지정초기화
// 일반적인 init

class Person {
	var name: String
    var age: Int
    
    init(name: String, age: Int) {
    	self.name = name
        self.age = age
	}
}
// 사용: 
let person: Person = Person(name: "crois", age: 20)
  • 기본값 초기화
// 1. 기본값을 지정하기
class Person {
	var name: String = "crois"
    var age: Int = 20
}

// 2. init으로 기본값 지정하기
class Person {
	var name: String
    var age: Int
    
    init() {
    	self.name = "crois"
        self.age = 20
	}
}
// 사용: 
let person: Person = Person()
  • 보조 초기화(convenience initializer)
class Person {
	// 프로퍼티 기본값 없음
	var name: String
    var age: Int 
    
    // 지정 초기화
    init(name: String, age: Int) {
    	self.name = name
        self.age = age
	}
    
    convenience init(name: String) {
    	// 위에서 정의한 지정 초기화를 self로 불러오기
        // 인스턴스 생성시 age를 지정할 필요 없음
    	self.init(name: name, age: 20)
	}
}
// 사용:
let person: Person = Person(name: "crois") // age = 20

2. struct

struct값 타입(Value Type)으로, 타입을 정의하고 프로퍼티와 메서드를 가질 수 있다.
class와 유사하지만 상속은 사용할 수 없고, 클래스와 달리 Memberwise Initializer(멤버와이즈 이니셜라이저)를 제공하기 때문에 프로퍼티를 꼭 초기화해 줄 필요가 없다.

mutating이란?

struct는 값을 복사하는 타입이기 때문에 인스턴스화 하여 프로퍼티값을 변경하더라도 원본(struct)의 프로퍼티에는 영향을 미치지 않는다.

struct Person {
    var name: String
    var age: Int = 20 // age 기본값 설정
}

var person: Person = Person(name: "crois")
person1.age = 30
print(person.age) // 30

print(Person().age) // 20 - 원본 프로퍼티는 영향을 받지 않음

또, struct는 내부 프로퍼티의 변화에 민감하기 때문에 일반적인 메서드 선언으로는 내부 프로퍼티 값을 변경할 수 없다.
이 때, 내부 프로퍼티 값을 변경할 수 있도록 도와주는 코드가 mutating이다.

struct Person {
    var name: String
    var age: Int
    
    mutating func changeAge(_ age: Int) {
    	self.age = age
    }
}
var person: Person = Person(name: "crois", age: 20)
person.changeAge(30)
print(person.age) // 30

structclass와 다르게 인스턴스를 상수(let)으로 선언할 경우 프로퍼티의 값을 변경할 수 없게 된다.
메서드를 사용해도 마찬가지이다.

struct Person {
    var name: String
    var age: Int = 20 // age 기본값 설정
    
    mutating func changeAge(_ age: Int) {
    	self.age = age
    }
}

let person: Person = Person(name: "crois")
person.age = 30 // Error - Cannot assign to property: 'person' is a 'let' constant
person.changeAge(30) // Error - Cannot assign to property: 'person' is a 'let' constant

3. clas와 struct 비교

\ classstruct
선언classstruct
타입참조 타입값 타입
상속가능불가능
초기화필수멤버와이즈 이니셜라이저로 대체 가능
인스턴스화 후 프로퍼티 변경가능상수인 경우 불가능
보조 초기화convenience initializer 사용 가능없음
내부에서 프로퍼티 값 변경가능mutating 사용시 가능

3. Troubleshooting

iOS 5기, Swift문법 종합반 기초 주차 과제로 Calculator를 PlayGround로 제작하는 과제가 주어졌다.
최근 iOS 앱 개발을 위해 UIKit이나 SwiftUI만 다루다가 오랜만에 Swift만으로 동작을 만드려고 하니 헷갈리고 어려운 부분이 몇가지 있었다.
그 중 class의 상속과 관련하여 트러블이 있었기에 그에 대해 다뤄보려고 한다.

1. 상속하기

주어진 과제의 Lv.3 로 Calculator 클래스를 부모클래스로 가지는 자식클래스를 연산자별로 생성하는 문제가 주어졌다.
먼저, Calculator 클래스의 프로퍼티 및 메서드는 아래와 같다.

class Calculator {
    // Lv.3 Calculator 코드 수정하기
    // 프로퍼티 선언
    var firstNumber: Int
    var secondNumber: Int
    
    // Lv.3 Calculator 코드 수정하기
    // 이니셜라이저 추가
    init(firstNumber: Int, secondNumber: Int) {
        self.firstNumber = firstNumber
        self.secondNumber = secondNumber
    }
    
    // 사칙연산 계산하기
    // Lv.3 Calculator 코드 수정하기
    // 매개변수 제거
    func calculate(operatorSymbol: String) -> Int {
        var result: Int = 0
        
        // 더하기 구현
        if operatorSymbol == "+" {
            result = firstNumber + secondNumber
        // 빼기 구현
        } else if operatorSymbol == "-" {
            result = firstNumber - secondNumber
        // 곱하기 구현
        } else if operatorSymbol == "*" {
            result = firstNumber * secondNumber
        // 나누기 구현
        } else if operatorSymbol == "/" {
            result = firstNumber / secondNumber
        // Lv.2 나머지 연산
        } else if operatorSymbol == "%" {
            result = firstNumber % secondNumber
        // Lv.2 예외처리 상황 처리하기
        // 지정되지 않은 연산자가 operatorSymbol에 들어올 경우
        } else {
            print("사용할 수 없는 연산자입니다.\n사용가능 연산자: +, -, *, /, %")
            result = 0
        }
        
        return result
    }
}

// Calculator 선언
let calculator: Calculator = Calculator(firstNumber: 10, secondNumber: 20)

위 코드와 같이 Calculator 타입은 생성자를 통해 계산할 숫자를 입력받고, 메서드를 선언하면 operatorSymbol을 입력하여 해당하는 계산을 실행하도록 만들었다.
그리고 더하기, 빼기, 곱하기, 나누기, 나머지 라는 이름의 새로운 클래스를 만들어 Calculator 클래스를 상속해주었다.

// 더하기 연산자 클래스 생성
class AddOperation: Calculator { ... }

// 빼기 연산자 클래스 생성
class SubstractOperation: Calculator { ... }

// 곱하기 연산자 클래스 생성
class MultiplyOperation: Calculator { ... }

// 나누기 연산자 클래스 생성
class DivideOperation: Calculator { ... }

// 나머지 연산자 클래스 생성
class RemainingOperation: Calculator { ... }

자식 클래스의 내부는 operatorSymbol과 return 값만 다를 뿐 같은 구조를 공유할 수 있다고 생각했고, 우선 더하기 연산자 클래스를 완성하여 제대로 작동하면 내부 코드를 복사하여 다른 클래스에도 사용하려고 했다.
그러나 생각대로 잘 되지 않았다.

2. super로 초기화하기

class AddOperation: Calculator {
	let operatorSymbol: String = "+" // 더하기를 미리 선언
    
    init(firstNumber: Int, secondNumber: Int) {
    	// 부모 클래스의 프로퍼티를 초기화(1차 초기화)
    	super.init(firstNumber: firstNumber, secondNumber: secondNumber)
        
        // 인스턴스화 할 때 부모 클래스의 메서드 매개변수를 자식클래스의 프로퍼티로 선언
        super.calculate(operatorSymbol: String = self.operatorSymbol)
    }
}

let addOperation: AddOperation = AddOperation(firstNumber: 10, secondNumber: 20)
addOperation.calculate()

그러나 위의 코드에서 에러가 발생했다.

class AddOperation: Calculator {
	let operatorSymbol: String = "+"
    
    init(firstNumber: Int, secondNumber: Int) { // Error: Overriding declaration requires an 'override' keyword
    	super.init(firstNumber: firstNumber, secondNumber: secondNumber)
        
        super.calculate(operatorSymbol: String = self.operatorSymbol) // Error: Cannot assign to immutable expression of type 'String.Type'
    }
}

let addOperation: AddOperation = AddOperation(firstNumber: 10, secondNumber: 20)
addOperation.calculate() // Error: Missing argument for parameter 'operatorSymbol' in call

에러 코드를 하나하나 살펴보자면 이렇다.

  • Error: Overriding declaration requires an 'override' keyword : 선언을 재정의 하려면 'override' 키워드가 필요하다.
  • Error: Cannot assign to immutable expression of type 'String.Type' : 'String.Type'유형의 불변 표현식에 할당할 수 없다.
  • Error: Missing argument for parameter 'operatorSymbol' in call : 호출 중 매개변수가 누락되었다.

처음에는 에러를 보고 왜 이러는지 알 수 없었는데, 첫 번째 에러는 잘 들여다보니 부모클래스인 Calculator의 선언자와 자식클래스인 AddOperation의 선언자가 완전히 같은 탓에 발생하는 문제였다.
내 의도는 AddOperation이 초기화될 때 초기화 값을 부모클래스인 Calculator에 전달하여 초기화 시키는 것이었는데 선언자를 완전히 똑같이 쓴 탓에 재정의(override)가 필요하다는 오류가 뜬 것이다...
그래서 AddOperation의 선언자를 수정해주었다.

init(_ first: Int,_ second: Int) {
	super.init(firstNumber: first, secondNumber: second)
}

두 번째 에러의 경우 잘 이해가 안되었는데, 이것도 내가 실수를 한 것이었다.
본래 메서드는 calculate(operatorSymbol: Stirng) -> Int { ... }의 형태인데, 여기서 매개변수에 Stirng: self.operatorSymbol을 넣으려고 하니까 String타입에 String.Type을 선언할 수 없다는 에러가 발생한 것이다...
아무 생각도 없이 함수를 처음 만드는 것처럼 매개변수에 타입을 지정해주고 값을 넣어주었던 것이 문제였다.

super.calculator(operatorSymbol: self.operatorSymbol)

그러나 이렇게 해도 마지막 에러는 여전했다.
선언자에 함수를 넣어두면 인스턴스화 하며 자동으로 매개변수 값이 채워져서 매개변수 선언 없이 메서드를 사용할 수 있을 줄 알았는데, 방법이 잘못된 것 같았다.
그래서 메서드를 재정의하는 것으로 방법을 변경하였다.

3. override로 재정의하기

override func calculate(operatorSymbol: String = "+") -> Int {
	// 메서드를 재정의하여 매개변수에 "+"를 지정
    // 부모클래스의 메서트를 호출하여 매개변수로 재정의한 메서드의 매개변수를 지정
	super.calculate(operatorSymbol: operatorSymbol)
}

let addOperation: AddOperation = AddOperation(10, 20)
addOperation.calculate() // 30

재정의를 하니 무사히 원하던 대로 매개변수 선언없이 메서드를 사용할 수 있게 되었다.
그래서 더이상 필요 없어진 프로퍼티인 operatorSymbol을 지우고 다른 연산자 클래스도 똑같이 구현하여 작업을 마무리 하였다.


-Today's Lesson Review-

오늘은 스파르타에서 제공해준 Swift 기초문법 강의를 보며 기초를 다시 배우는 시간을 가졌다.
쉬운 내용이기도 하고 다 아는 내용이라고 생각했었는데, 보다보니 몰랐던 코드도 몇가지 있었고 잊고있던 코드도 보였다.
그 중 특히 인상깊었던 내용 몇 가지를 TIL로 정리해 보았는데, 정리를 하면서도 이런 것도 잊거나 모르고 있었으면서 기초를 뒤로했던게 부끄럽게 느껴졌다.
무엇이든 기초를 탄탄히 하는 것이 중요하다는 것을 다시금 깨닫는 순간이었다.
과제로 주어진 미션을 해결하면서도 헷갈리거나 어려웠던 부분들이 있어서 더욱 기초의 중요함을 알게되었던 것 같다.
앞으로도 틈틈히 기초를 다지며 자만하지 않도록 주의해야겠다고 생각했다.
profile
이유있는 코드를 쓰자!!

0개의 댓글