[Swift 문법] - 옵셔널 체이닝, 열거형, 구조체, 클래스

sun02·2021년 8월 19일
0

100 days of Swift 

목록 보기
15/40

3. Optional Chaining

옵셔널 체이닝은 옵셔널이 값을 가질 때만 코드를 실행시킨다


func albumReleased(year: Int) -> String? {
    switch year {
    case 2006 : return “Taylor Swift”
    case 2008 : return “Fearless”
    case 2010 : return “Speak Now”
    case 2012 : return “Red”
    case 2014 : return “1989”
    default : return nil
    }
}

let album = albumReleased(year: 2006)
print(“The album is \(album)”)

이것은 결과 판넬에 “The album is Optional(“Taylor Swift”)”라고 출력한다.

만약 albumReleased의 리턴 값을 대문자로 바꾸고 싶다면, string의 uppercased() 메서드를 호출하면 된다. 예를 들어 :

let str = “Hello world”
print(str.uppercased())

문제는, albumReleased()는 옵셔널 문자열을 반환한다는 것이다.
따라서 옵셔널 체이닝을 사용한다. 왜냐하면 옵셔널 체이닝은 “문자열을 받으면 그것을 대문자로 바꾸고, 그렇지않으면 아무것도 하지 마라” 라는 정확히 이 행동을 하기 때문이다.

위 두 줄의 코드를 이것으로 바꿔보자:


let album = albumReleased(year: 2006)?.uppercased()
print(“The album is \(album)”)

여기 있는 물음표가 옵셔널 체이닝이다. 물음표 뒤에 있는 모든 것들은 물음표 앞의 모든 것들이 값을 가질 때만 실행된다.

옵셔널 체이닝은 네가 원하는 만큼 길어질 수 있다, 예를 들어:


Let album = albumReleased(year: 2006)?.someOptionalValue?.someOtherOptionalValue?.whatever

스위프트는 왼쪽에서 오른쪽으로 nil을 찾을 때까지 확인하고, 확인하면 멈춘다.

The nil coalescing operator

Nil coalescing 연산자는 “가능한 경우 값 A를 사용하지만 만약 A의 값이 nil이라면 값 B를 사용해라” 라는 의미이다.
따라서 만약 A가 옵셔널이고 값을 가진다면 A가 사용되고, 값이 없으면 B가 사용된다. 어느 경우든, 우리는 확실히 값을 가진다.


let album = albumReleased(year: 2006) ?? “unknown”
print(“The album is \(album)”)

이 두개의 물음표가 nil coalescing 연산자이다. 여기서 이 물음표들은 “만약 albumReleased()가 값을 반환하면 이것을 album 변수에 넣지만 만약 albumReleased()가 nil을 반환한다면 “Unknown”을 대신 사용해라” 를 의미한다.

이제 결과 판넬을 본다면, “The album is Taylor Swift”가 출력된다 – 더이상 옵셔널이 없다. 값을 반환하거나 “unknown”을 반환했기 때문에 스위프트가 실제 값을 가진다고 확신할 수 있기 때문이다.
이것은 결과적으로 아무것도 언래핑 할 필요가 없고 충돌을 일으킬 위험이 없다는 것이다 - 작업할 실제 데이터가 보장되므로 너의 코드는 더 안전하고 쉽게 작업될 수 있다.

4. Enumerations


func getHaterStatus(weather: String) -> String? {
    if weather == “sunny” {
        return nil
    } else {
        return “Hate”
    }
}

이 함수는 현재 날씨를 정의하는 문자열을 받는다. 문제는, 이런 데이터를 다룰 때 문자열은 최악은 선택이라는 것이다 – “rain”, “rainy”, “raining” 중에 무엇인가? 또는 “showering”, “drizzly”, “stormy”등

열거형(enums)은 새로운 데이터 타입을 정의함으로써 이러한 문제를 해결한다.
예를 들어, 우리는 날씨에는 다섯 가지 종류가 있다고 말할 수 있다 : sun, cloud, rain, wind, snow. 만약 우리가 열거형을 만든다면, 스위프트는 오직 이 다섯 값만 받을 수 있다 – 다른 것은 오류를 유발할 것이다.

코드로 작성해 보자:


enum WeatherType {
    case sun, cloud, rain, wind, snow
}

func getHaterStatus(weather: WeatherType) -> String? {
    if weather == WeatherType.sun {
        return nil
    } else {
        return “Hate”
    }
}

getHaterStatus(weather: WeatherType.cloud)

처음 세 줄을 보자 : 첫번째 줄은 타입의 이름 WeatherType을 제공한다. 두 번째 줄은 열겨형이 가질 수 있는 다섯 가지 가능한 경우를 정의한다. 그리고 세 번째 줄은 닫는 중괄호로 열거형을 끝낸다.

이제 중요한 두 가지 변화로 코드를 재작성 할 것이다 :


enum WeatherType {
    case sun
case cloud 
case rain
case wind
case snow
}

func getHaterStatus(weather: WeatherType) -> String? {
    if weather == .sun {
        return nil
    } else {
        return “Hate”
    }
}

getHaterStatus(weather: .cloud)

첫 번째 변화는, 각 날씨 타입들이 이제 각자의 줄 위에 있다는 것이고 두 번째 변화는, if weather ==.sun 이다. – WeatherType 을 이제 적지 않아도 된다. 왜냐하면 스위프트는 내가 WeatherType의 변수 중에서 비교한다는 것을 타입 추론을 사용하여 이미 알기 떄문이다.

열거형은 switch/case 문에서 특히 유용하다.


func getHaterStatus(weather: WeatherType) -> String? {
    switch weather {
    case .sun:
        return nil
case .cloud, .wind:
        return “dislike”
case .rain:
        return “hate”

}

이 코드는 빌드되지 않는다 : .snow인 경우를 다루지 않기 때문이고, 스위프트는 모든 케이스가 다루어지길 원하기 떄문이다. 너는 snow 케이스를 추가하거나 default 케이스를 추가해야한다.

Enums with additional values

열거형에 사용자가 정의한 값이 첨부될 수 있다. .wind케이스에 값을 추가하여 바람이 얼마나 빠른지 알 수 있게 해보자


enum WeatherType {
    case sun
case cloud 
case rain
case wind(speed: Int)
case snow
}

다른 케이스들은 speed 값을 필요로 하지 않고, wind만 필요하다. 스위프트는 switch/case 블록에 조건을 추가하여 그 조건이 참일 때만 케이스가 일치하도록 한다.


func getHaterStatus(weather: WeatherType) -> String? {
    switch weather {
    case .sun:
        return nil
    case .wind(let speed) where speed < 10:
        return “meh”
case .cloud, .wind:
        return “dislike”
case .rain:
        return “hate”

}

.wind 가 두 번 등장하였지만 첫 번째 wind는 10보다 적을 떄만 참이다. 중요한 것은 let을 사용하여 열거형 내부의 값을 확보한 다음 where 조건을 사용하여 확인하는 것이다

스위프트는 switch/case 문을 위에서 아래로 평가하고 일치하는 항목을 찾는 즉시 중지한다. 따라서 만약 case .cloud, .wind 가 case .wind(let speed) where speed < 10 보다 먼저 나타나면 실행되고 출력이 변경된다

참고 : 스위프트의 옵셔널은 실제로 열겨형을 사용하여 실행된다. 여기엔 두 가지 케이스가 있다 : none, some. Some은 옵셔널안에 무엇이든 값이 있는 것이다.

5. Struct

구조체는 여러 값으로 구성된 복잡한 데이터 타입이다. 우리는 옷과 신발이라는 두 가지 프로퍼티를 가지는 Person 구조체를 정의할 수 있다 :


struct Person {
    var clothes: String
    var shoes: String
}

구조체를 정의할 때 스위프트는 멤버별 이니셜라이저라는 것을 자동으로 생성하기 때문에 매우 쉽게 생성할 수 있다. 이것은 다음과 같이 두 프로퍼티에 대한 초기 값을 전달하여 구조체를 생성한다는 의미이다 :

let taylor = Person(clothes: “T-shirts”, shoes: “sneakers”)
let other = Person(clothes: “short skirts”, shoes: “high heels”)

구조체의 인스턴스를 한 번 생성하면, 이 프로퍼티를 구조체이름, 점, 그리고 읽으려는 프로퍼티의 이름을 적어서 읽을 수 있다.


print(taylor.clothes)
print(other.shoes)

하나의 구조체를 다른 구조체에 할당하면, 스위프트는 원본과 완전히 독립된 복제본이 되도록 뒤에서 복사한다. 그러나 엄격히 말하자면, “copy on write” 라는 기술을 사용한다. 이는 데이터를 변경하려고할 때만 데이터를 실제로 복사한다는 의미이다.

구조체가 어떻게 복사를 하는지 보기 위해 다음 예시를 보자:


struct Person {
    var clothes: String
    var shoes: String
}

let taylor = Person(clothes: “T-shirts”, shoes: “sneakers”)
let other = Person(clothes: “short skirts”, shoes: “high heels”)

var taylorCopy = taylor
taylorCopy.shoes = “flip flops”

print(taylor).  // -> Person(clothes: “T-shirts”, shoes: “flip flops”) 가 출력됨
print(taylorCopy) // -> Person(clothes: “T-shirts”, shoes: “sneakers”) 가 출력됨

Functions inside structs

구조체 안에 함수를 넣을 수 있고 구조체 안의 데이터를 읽거나 변화시키는 모든 함수에 대해 그렇게 하는 것이 좋다.


struct Person {
    var clothes: String
    var shoes: String
 
    func describe() {
        print(“I like wearing \(clothes) with \(shoes)”)
    }
}

구조체 내부에 작성된 함수는 메서드라 불린다. 스위프트에서 함수와 메서드 둘 다 func로 작성되지만 부를 때는 구별된다.

6. Classes

복잡한 데이터 타입을 빌드하는 클래스라는 방법이 있다. 이것은 구조체와 유사해보이지만 몇 가지 차이점을 가진다.

  • 클래스에서는 맴버별 이니셜라이저가 자동으로 생기지 않는다, 너가 작성해야한다.
  • 다른 클래스를 기반으로 하는 클래스를 정의할 수 있고 원한다면 새로운 것을 추가할 수 있다.
  • 클래스의 인스턴스는 object라 불린다. 오브젝트를 복사하면 두 복사본 모두 기본적으로 동일한 데이터를 가리킨다 – 하나를 변경하면 그 복사본도 변경된다.

Initializing an object

앞서 적었던 Person 구조체를 Person 클래스로 변환시킬 때 다음과 같이 작성할 수는 없다:

class Person {
    var clothes: String
    var shoes: String
}

왜냐하면 우리는 두 개의 문자열 프로퍼티를 선언하고 있기 때문이다. 이 들은 반드시 값을 가지고 있어야한다. 구조체에서는 스위프트가 자동으로 두 프로퍼티에 값을 제공하는 멤버별 이니셜라이저를 생성했기 때문에 괜찮았지만, 클래스에서는 멤버별 이니셜라이저가 생성되지 않기 때문에 다음과 같이 작성할 수 없다.

세 가지 해결 방안이 있다. 두 값을 옵셔널 문자열로 만들거나, 디폴트 값을 제공하거나, 이니셜라이저를 작성하는 것이다.

이니셜라이저를 작성하기 위해서는 클래스 안에 init()이라 불리는 메서드를 만들어야한다:


class Person {
    var clothes: String
    var shoes: String
 
    init(clothes: String, shoes: String) {
        self.clothes = clothes
        self.shoes = shoes
    }
}

위 코드에서 주목할 점 두가지가 있다.
먼저, init() 메서드 앞에는 func 를 작성하지 않는다. 두 번째로, 전달하려는 매개변수의 이름이 우리가 할당하려는 프로퍼티의 이름과 같기 때문에 self를 사용한다. 이 self는 “이 객체의 clothes 프로퍼티는 전달된 clothes 매개변수로 설정되어야한다.”라는 의미이다.

추가로, 스위프트는 모든 논옵셔널 프로퍼티들이 이니셜라이저가 끝날 때 혹은 이니셜라이저가 다른 메서드를 호출할 때 중 먼저 오는 시점까지 값을 가지도록 요구한다.

Class inheritance

클래스와 구조체의 두 번째 차이점은 클래스는 서로를 기반으로 더 큰 것을 생성할 수 있다는 것이고 이는 클래스 상속(class inheritance)이다.


class Singer {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    func sing() {
        print(“La la la la”)
    }
}

이제 해당 이니셜라이저를 호출하여 해당 객체의 인스턴스를 만든 다음, 이것의 프로퍼트와 메서드를 읽을 수 있다.


var taylor = Singer(name: “Taylor”, age: 25)
taylor.name
taylor.age
taylor.sing()

이것을 기본 클래스로 하고, 우리는 이것을 기반으로 CountrySinger 클래스를 생성할 것이다.
CountrySinger 클래스는 Singer 클래스의 모든 것을 가지지만 내가 sing()을 호출 할 때 “Trucks, guitars, and liguor” 라고 출력하길 원한다.

class CountrySinger : Singer {

}

콜론은 “CountrySinger가 Singer를 확장한다” 라는 의미이다.
Override 는 “이 메서드가 부모 클래스에서 실행되었다는 것을 알지만 자식클래스에 대해 변경하고 싶다” 라는 의미이다.

Override func를 다음과 같이 사용한다:


class CountrySinger : Singer {
    override func sing() {
        print(“Trucks, guitars, and liguor”)
    }
}

이제 taylor 객체가 생성되는 방식을 수정해보자:


var taylor = CountrySinger(name: “Taylor”, age: 25)
taylor.sing()

CountrySinger를 Singer로 변경하면 결과 판넬에 다른 메세지가 출력되는 것을 볼 수 있다.

이제 noiseLevel 이라는 새로운 프로퍼티를 가지는 HeavyMetalSinger 클래스를 정의해 볼 것이다.
그러나 이것은 문제를 야기하고 특별한 방법으로 해결될 수 있다

  • 스위프트는 논옵셔널 프로퍼티가 값을 가지길 바란다
  • Singer 클래스에는 noiseLevel 프로퍼티가 없다
  • 따라서, 우리는 noise level를 받도록 HeavyMetalSinger 를 위한 이니셜라이저를 만들어야한다.
  • 새로운 이니셜라이저는 헤티메탈가수의 name 과 age를 알야아한다 그래야 상위클래스 Singer에 전달할 수 있기 때문이다.
  • 상위클래스에 데이터를 전달하는 것은 메서드 호출을 통해 이루어지며 모든 프로퍼티의 값을 제공할 떄까지 메서드를 호출할 수 없다.
  • 따라서, 우리는 먼저 고유한 프로퍼티(noiseLevel)을 설정한 다음 슈퍼클래스가 사용할 다른 매개변수를 전달해야한다.

class HeavyMetalSinger: Singer {
    var noiseLevel: Int
    init(name: String, age: Int, noiseLevel: Int) {
        self.noiseLevel = noiseLevel
        super.init(name: name, age:age)
    }

    override func sing() {
        print(“Grrrr rargh rargh rarrrrgh!”)
    }
}

이니셜라이저가 세 매개변수를 받은 다음, super.init()을 호출하여 Singer 슈퍼클래스에 이름과 나이를 전달한다.

Super는 객체로 작업할 때 자주 볼 수 있는데 이것은 “상속받은 클래스의 메서드를 호출한다” 라는 의미이고 일반적으로 이것은 “부모 클래스가 먼저 해야할 작업을 수행하도록 하고 그 후의 나의 추가 작업을 진행하겠다” 를 의미하는데 사용된다.

Values vs References

구조체를 복사할 때는 값을 포함하여 모든 것이 복제된다. 이것은 구조체의 복사본을 변화시키는 것이 다른 복사본에 전혀 영향을 미치지 않는다는 것이다.
클래스에서는 모든 객체의 복사본은 오리지널 객체를 가리키기 때문에 하나를 변경시키면 모든 것이 바뀐다.

따라서 스위프트는 구조체는 “value types”라 부르고 클래스는 “reference types”라 부른다.

출처

0개의 댓글