어우 어제는 분량이 너무 많아서 오늘도 걱정이 되네요
알고 있던 부분을 다시 한번 짚고 디테일한 부분까지 알려줘서 긴가봐요
오늘은 주제가 좀 적으니까 내용도 적으려나...
힘들지만 또 열심히 해야죠
오늘도 화이팅입니다
특정한 기능을 수행하는 코드를 다시 사용할 수 있게 해주는 것이 함수라고 했습니다. 그리고 값을 받을수도 안 받을수도 있다고 했죠
먼저 한 예시를 볼까요
func favoriteAlbum() {
print("My favorite is Fearless")
}
간단한 함수를 작성해봤습니다. 근데 이 함수는 작동하지 않을거에요
왜냐하면 함수 호출을 안했기 때문입니다
모든 함수들은 우리가 호출을 하지 않는이상 작동하지 않습니다
예를 들어 위의 함수를 호출하려면 favoriteAlbum()
을 작성하면 됩니다
호출을 해주니 함수가 작동합니다
위 예시에서 봤듯이 함수는 func
키워드로 작성합니다. 그리고 함수의 이름, 괄호, 괄호안의 코드로 구성되죠 그리고 호출할 때는 함수이름()
형식으로 호출합니다
위의 코드는 어떤 상황에서도 같은 행동을 하기 때문에 사실 큰 의미를 가지지 않습니다.
만약 우리가 매번 다른 이름의 앨범을 출력하고 싶다면 함수가 인자를 받도록 하며 됩니다
func favoriteAlbum(name: String) {
print("My favorite is \(name)")
}
이런 식으로 말이죠 name
이라는 String
변수를 받아 출력하는 함수입니다
이 함수를 호출하려면 어떻게 해야할까요? 위에서 했던 방식과 동일합니다. 다만 변수만 추가해주면 됩니다
favoriteAlbum(name: "Fearless")
이 예시들을 보면 함수를 꼭 써야할까 라는 생각이 들수도 있지만
이게 수십개로 늘어나고 각각 다른곳에 있다고 생각해보죠
함수라면 간단히 값을 업데이트 해줌으로써 상황에 맞게 내용을 바꿀수 있지만
그게 아니라면 매번 코드를 찾아서 직접 수정을 해줘야 할겁니다
어떤 경우에 함수 인자의 이름을 함수가 호출될 때와, 함수 내부에서 다뤄질 때에 다르게 쓰고싶을 때가 있죠
이 방법은 호출할 때는 자연스럽게 읽히도록, 함수 내부에서는 분별이 되도록 해줍니다
스위프트에서 자주 사용되므로 이번 기회에 이해를 하고 넘어가죠
문자열의 문자 개수를 세는 함수를 만들어보죠
func countLettersInString(string: String) {
print("The string \(string) has \(string.count) letters.")
}
호출할 때도 작성해볼까요?
countLettersInString(string: "Hello")
이제 내부이름와 외부이름을 사용해볼까요?
쉽습니다 인자 이름을 두번 쓰면 됩니다. 하나는 외부, 하나는 내부용으로요
func countLettersInString(myString str: String) {
print("The string \(str) has \(str.count) letters.")
}
countLettersInString(myString: "Hello")
myString
을 외부, str
을 내부용으로 작성했습니다
이름의 변경에 따라 호출할 때와 함수 내부 코드에서 사용되는 이름이 변경되었어요
만약 외부이름을 작성할 필요가 없다면 그냥 _
을 써주면 됩니다
func countLettersInString(_ str: String) {
print("The string \(str) has \(str.count) letters.")
}
countLettersInString("Hello")
"Hello"라는 문자열의 문자 개수를 세어달라. 좀 더 직관적인 호출문이 된거같죠?
좀 더 스위프트스러운 방법을 쓰고싶다면 in
, for
, with
같은 이름을 써줘도 됩니다
func countLetters(in string: String) {
print("The string \(string) has \(string.count) letters.")
}
countLetters(in: "Hello")
"count letters in Hello" 좀 더 읽기 쉬운 구문을 바뀌었네요
함수는 ->
를 통해 값을 반환할 수 있습니다.
예시로 테일러 스위프트의 앨범 중 하나이면 참을 반환하는 함수를 작성해보죠
func albumIsTaylor(name: String) -> Bool {
if name == "Taylor Swift" { return true }
if name == "Fearless" { return true }
if name == "Speak Now" { return true }
if name == "Red" { return true }
if name == "1989" { return true }
return false
}
if albumIsTaylor(name: "Red") {
print("That's one of hers!")
} else {
print("Who made that?!")
}
if albumIsTaylor(name: "Blue") {
print("That's one of hers!")
} else {
print("Who made that?!")
}
아 그리고 스위프트는 이 함수가 값을 반환한다는 사실을 인지하고 있기 때문에
return
키워드를 생략할 수 있습니다
func getMeaningOfLife() -> Int {
return 42
}
위 함수에서 생략을 해보면
func getMeaningOfLife() -> Int {
42
}
이렇게 됩니다.
이방법은 SwiftUI에서 자주 사용됩니다
그건 또 언제 배우나...
스위프트는 여러모로 안전한 언어이지만 반대로 그만큼 신경을 써야합니다
가장 흔한 경우는 없어지거나 나쁜 데이터를 사용하는 경우죠
func getHaterStatus() -> String {
return "Hate"
}
위 함수는 아무런 인자를 받지않고 "Hate" 문자열을 반환합니다.
근데 Hater들이 어느날은 기분이 좋아서 "Hate"를 반환하지 않아도 된다고 하면 어떻게 해야할까요? 아마 몇가지 방법들이 떠오를 겁니다. 빈 문자열을 반환한다던지 말이죠
그러나 각자만의 방법을 사용하기전에 스위프트에서 내놓은 해결책이 있습니다.
바로 옵셔널이죠.
옵셔널 변수는 값을 가질수도 안 가질수도 있습니다
이해를 좀 더 쉽게 해보자면 10점 만점의 어떤 설문에 대해서
1점을 줄 수도 있고 7점을 줄수도 있겠죠
그러나 그 문항에 대해서 대답하기 싫거나 아예 모르는 경우가 있을 수 도 있습니다
이런경우에는 "모른다"라고 적거나 아예 공란으로 둘 수도 있겠죠
옵셔널이 이와 비슷한 성격입니다
-> String
은 무조건 문자열을 반환받고 값이 없을수가 없다는 의미입니다
여기서 옵셔널을 사용하려먼 ?
기호를 데이터타입 뒤에 붙여주면 됩니다.
func getHaterStatus() -> String? {
return "Hate"
}
조금 변경한 코드를 볼까요?
func getHaterStatus(weather: String) -> String? {
if weather == "sunny" {
return nil
} else {
return "Hate"
}
}
여기선 nil
이라는 요소를 추가했습니다. 값이 없다라는 뜻이에요
위 함수는 날씨가 "sunny"라면 아무것도 반환하지 않고
그 외의 날씨에는 "Hate"를 반환합니다.
이번에는 아래의 코드를 실행해볼까요?
var status: String
status = getHaterStatus(weather: "rainy")
아마 에러가 뜰껍니다.
에러가 뜨는 이유는 String
타입의 변수에 nil
이 할당되려 하기 때문입니다.
스위프트는 굉장히 안정성을 추구하기 때문에 위와 같은 상황의 경우 에러메시지를 출력하고 앱 빌딩을 중단합니다.
너무 민감하다고 생각할 수 있지만 미연에 실수를 방지한다고 생각하죠
이유는 알겠고 위의 코드가 정상적으로 작동하려면 어떻게 해야할까요?
status
를 String?
타입으로 해주면 됩니다
var status: String?
status = getHaterStatus(weather: "rainy")
좀 더 간략히 적을 수도 있겠죠
var status = getHaterStatus(weather: "rainy")
하지만 스위프트는 이 옵셔널 값을 그냥 사용하게 두지 않습니다
아래 함수를 볼까요?
func takeHaterAction(status: String) {
if status == "Hate" {
print("Hating")
}
}
문자열을 받아서 메시지를 출력하는 함수입니다.
이 함수는 옵셔널 변수가 아니라 그냥 변수를 필요로 하죠
그말인 즉슨 위에서 만든 status
를 못 사용한다는 말입니다.
스위프트에서는 두가지 방법을 제시하는데요 둘 다 사용되긴 하지만 한가지가 특히 선호되고 있습니다.
일단 첫번째는 옵셔널 언래핑입니다. 특별한 구문을 사용해서 조건문으로 풀어냅니다
옵셔널이 값이 있는지 확인하고 있다면 언래핑해서 non-optional로 바꾸어서 코드를 실행하게 합니다.
구문은 아래와같이 구성됩니다
if let unwrappedStatus = status {
// unwrappedStatus contains a non-optional value!
} else {
// in case you want an else block, here you go…
}
이 if let
구문은 한번에 체크하고 언래핑해줍니다.
이 방법을 사용해서 getHaterStatus()
의 반환값을 안전하게 언래핑 해줄 수 있겠네요. 또한 non-optional 문자열을 사용할 수 있습니다.
func getHaterStatus(weather: String) -> String? {
if weather == "sunny" {
return nil
} else {
return "Hate"
}
}
func takeHaterAction(status: String) {
if status == "Hate" {
print("Hating")
}
}
if let haterStatus = getHaterStatus(weather: "rainy") {
takeHaterAction(status: haterStatus)
}
강제 언래핑은 !
기호로 사용할 수 있습니다.
사용자가 옵셔널이 값을 가진다는걸 확신하면 강제 언래핑을해서 사용할 수 있죠
위 함수를 보면 옵셔널 값이 들어가기 때문에 경고를 합니다
왜냐면 함수에 없는 앨범 이름이 들어가면 nil
을 반환하기 때문이죠
이때 강제 언래핑을 사용해서 해결할 수 있습니다
print("It was released in \(year!)")
그럼 경고문이 사라진 것을 확인할 수 있습니다.
이 !
기호를 사용해서 암시적으로 언래핑된 옵셔널을 만들 수도 있습니다.
이 옵셔널은 다른 일반적인 옵셔널들과 같습니다
값을 가질수도 있고 nil
을 가질 수도 있죠
다만 언래핑을 할 필요가 없다는 차이점이 있습니다.
스위프트도 따로 이것을 검사하지도 않구요. 때문에 이것을 사용함에 있어서 신중을 가해야합니다
스위프트에는 코드가 덜 복잡해지도록 해주는 2가지 방법이 있습니다
첫 번째를 옵셔널 체이닝이라고 합니다.
옵셔널에 값이 있을때만 코드가 동작하도록 해주는 것이지요
예시의 결과를 보면 "The album is Optional("Taylor Swift")"라고 되어있습니다
만약 우리가 albumReleased()
의 반환값을 대문자로 바꾸고 싶다면 uppercased()
를 사용하면 될겁니다. 하지만 이는 문자열에는 적용되지만 우리가 가지는 반환값은 옵셔널입니다.
문자열일수도 nil
일 수도 있다는 말이죠
그래서 우리는 옵셔널 체이닝을 통해서 문자열을 받을 때만 동작하고 그 외의 경우는 동작하지 않도록 해줄겁니다.
let album = albumReleased(year: 2006)?.uppercased()
print("The album is \(album)")
?
를 통해서 사용하는걸 기억해둬야 합니다. 물음표 기호 뒤의 구문은 앞에서 값을 가질 경우에만 동작합니다.
nil 병합 연산자는 코드를 더 심플하고 안전하게 만들어 줍니다.
let album = albumReleased(year: 2006) ?? "unknown"
print("The album is \(album)")
??
기호가 nil병합 연산자임을 알려줍니다.
albumReleased()
가 값을 가진다면 album
에 할당하고 nil
이라면
뒤의 "unknown"을 할당합니다.
그리고 눈치챘는지 모르겠지만 결과창에 더이상 옵셔널이 나오지 않죠
nil병합 연산자를 통해 어떤 경우든 값을 가지게 되었기 떄문이죠
따라서 언래핑하거나 앱 크래시가 날 위험을 감수하지 않아도 됩니다.
Enumerations은 주로 enum으로 불립니다.
func getHaterStatus(weather: String) -> String? {
if weather == "sunny" {
return nil
} else {
return "Hate"
}
}
날씨라는 문자열을 받는 함수네요.
근데 문자열을 인자로 받는건 별로 좋은 선택이 아닙니다. "rain","rainy","raining", "Rain" 등 표현하는 방식이너무 많죠
열거형으로 이 문제를 해결할 수 있습니다. 새로운 데이터 타입을 정의해서요
enum
으로 sun, cloud, rain 같이 날씨에 대한 값들을 정해놓으면
스위프트는 이 정해진 값들만 받을겁니다.
그리고 사실 동작할 때는 보이지 않는곳에서 숫자로 작동합니다.
덕분에 속도가 빨라요
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)
enum
작성하는 방식은 다음과 같습니다
enum `타입이름` {
case 값1, 값2
}
어렵지 않죠? 그리고 사용할 때는 타입이름.값1
같은 식으로 사용합니다.
위의 코드에서는 WeatherType.cloud
가 예시가 되네요
다음은 변화를 줘서 코드를 다시 작성해보죠
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.sun
이었지요
스위프트는 사용자가 WeatherType변수를 사용하는걸 알기때문에 타입추론을 사용했습니다
enum은 특히 switch문에서 효과적입니다.
func getHaterStatus(weather: WeatherType) -> String? {
switch weather {
case .sun:
return nil
case .cloud, .wind:
return "dislike"
case .rain:
return "hate"
}
}
근데 이 코드는 작동하지 않습니다
왜냐면 모든 경우에 대해서 설정을 해놓지 않았기 때문이죠
.snow에 대한 case를 추가하거나 default구문을 추가해주면 작동할겁니다
정상 작동하도록 바꿔줬습니다.
enum의 가장 강력한 기능중 하나는 값을 붙여줄 수 있다는 겁니다.
예시에서 .wind
에 값을 붙여보죠
enum WeatherType {
case sun
case cloud
case rain
case wind(speed: Int)
case snow
}
그리고 스위프트는 switch문에 조건을 추가할 수 있게 해줍니다.
let
키워드로 case의 변수에 접근하고 where로 조건을 만듭니다
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, .snow:
return "hate"
}
}
getHaterStatus(weather: WeatherType.wind(speed: 5))
.wind
가 두번 쓰여진게 보이시죠?
speed 변수가 10 이하이기 때문에 두번째 .wind
의 "meh"를 출력하네요
스위프트는 switch문에서 위에서 아래로 검사를 합니다. 그리고 조건에 맞는 case를 찾으면 멈추죠.
만약 speed가 10 이상이라면 "meh"는 출력되지 않겠죠?
그래서 case 순서를 정할 때는 신경을 써야합니다.
스위프트에서 구조체는 매우 만들기 쉽습니다
왜냐면 memberwise initializer라는 것을 자동으로 생성해주거든요
여기에 Person 구조체가 있습니다
struct Person {
var clothes: String
var shoes: String
}
2개의 인스턴스를 생성한다면 아래처럼 작성하면 됩니다.
let taylor = Person(clothes: "T-shirts", shoes: "sneakers")
let other = Person(clothes: "short skirts", shoes: "high heels")
한번 인스턴스를 생성하면 간단하게 프로퍼티들을 읽어낼 수 있습니다
print(taylor.clothes)
print(other.shoes)
만약 구조체를 다른것에 복사를 한다면 그것은 완전히 다른 별개의 것이 됩니다.
엄밀히 따지면 조금은 다르지만 일단은 넘어갈게요
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)
print(taylorCopy)
taylor
을 복사한 taylorcopy
의 값을 바꾸어도 서로 영향을 끼치지 않습니다.
구조체 안에 함수를 선언할 수도 있습니다.
struct Person {
var clothes: String
var shoes: String
func describe() {
print("I like wearing \(clothes) with \(shoes)")
}
}
참고로 구조체 내부의 함수는 메서드라고 불립니다.
클래스는 함수와 비슷하지만 몇가지 차이점이 있습니다
우선 초기화 생성자부터 알아보죠
앞서 구조체에서 썼던 것을 클래스로 바꿔보죠
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
}
}
클래스는 상속을 받을 수 있다고 했습니다.
UIKit이나 AppKit에서도 유용하게 쓰이기 때문에 알아둬야합니다.
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
를 작성해보죠
뭐 단순히 Singer
의 코드를 복사 붙여넣기 할 수도 있겠지만
상속은 좀 더 쉽게 할 수 있습니다. 자식클래스: 부모클래스
식으로 작성하면 됩니다.
class CountrySinger: Singer {
}
이렇게 말이죠
그리고 이 클래스만의 sing()
메서드를 가지도록 할겁니다.
단순히 override
키워드를 추가해서 작성해주면 됩니다.
class CountrySinger: Singer {
override func sing() {
print("Trucks, guitars, and liquor")
}
}
이제 새로 객체를 생성해서 확인해보죠
var taylor = CountrySinger(name: "Taylor", age: 25)
taylor.sing()
이제 좀 더 복잡한 걸 해보죠 HeavyMetalSinger
클래스를 만들어 봅시다
이번에는 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("Grrrrr rargh rargh rarrrrgh!")
}
}
noiseLevel
이 앞선 Singer
클래스에 없었습니다.
따라서 새로운 초기화를 통해서 만들어주고 name
과 age
프로퍼티는 부모 클래스의 것을 그대로 사용하죠