[부스트코스] iOS 프로그래밍을 위한 스위프트 기초 #2

백승호·2020년 8월 12일
0

링크: https://www.edwith.org/boostcamp_ios/


13. 구조체

선언 방식

구조체는 타입을 정의하기때문에 대문자 카멜케이스를적용

struct 이름 {
	멤버
	타입 멤버
	인스턴스 메서드
	타입 메서드
}
어떤 타입안의 멤버를 프로퍼티, 함수를 메소드 라고 부름
타입 프로퍼티는 뭐고 타입 메서드는 머지? 무슨 차이가 있는 걸까?
타입이 붙으면 static이 앞에 붙넹

struct Sample {
	var propertyOne: Int = 100
	let propertyTwo: Int = 42
	static var typeProperty: Int = 1
	
	func instanceMethod() {
		print("instance method")
	}
	static func typeMethod() {
		print("type method")
	}
}

static 키워드를 붙여주게 되면 Sample이라는 타입에서
사용할수있는 타입 프로프티가 됩니다.
static이 없다면 인스턴스
함수도 마찬가지

구조체 사용법

struct Sample {
	var propertyOne: Int = 100
	let propertyTwo: Int = 42
	static var typeProperty: Int = 1
	
	func instanceMethod() {
		print("instance method")
	}
	static func typeMethod() {
		print("type method")
	}
}

var 이름: Sample = Sample()
이렇게 구조체가 하나의 타입이 되었음
하나의 인스턴스가 생성이 된다

앞에 var가 붙었기 때문에 내부 프로퍼티 값들을 변경 할 수있다
이름.propertyOne = 200
이름.instanceMethod()
// let으로 선언된 놈은 바꿀수 없다
// 불가능  -> 이름.propertyTwo = 200
타입 자체가 사용할수 있는 프로퍼티, 타입 자체가 사용할 수 있는 메소드
구조체 선언부에 static을 붙여 선언한 것들
Sample.typeProperty = 10
Sample.typeMethod()
-> 인스턴스를 거치는게 아니라 타입 자체로 사용할 수 있음!

// 이름.typeProperty = 400
// 이름.typeMethod()
// -> 이렇게 인스턴스가 타입 프로퍼티나 메소드를 사용하는건 불가능

struct Student {
	var name: String = "unknown"
	var `class`: String = "Swift" // ``로 묶어주면 class라는 이름도 사용가능해짐
	
	static func selfIntroduce() {
		print("학생타입입니다.")
	}
	func selfIntroduce() {
		print("저는 \(self.class)반 \(name)입니다")
	}
}

class는 이미 정의된 키워드이기 때문에 이를 변수이름으로
사용하고싶다면 ``으로 감싸줘야 하고
사용할때도 self.class로, 명시적으로 앞에 self.을 붙여줘야 사용할 수 있다

14. 클래스

선언 방식

클래스는 구조체와 매우 유사
구조체는 값 타입인 반면 클래스는 참조타입이라는 점이 큰 차이
'다중 상속'이 되지 않는다는점 유의(상속은 됨)

class 이름 {
	구현부
}

class Sample {
	var propertyOne: Int = 100
	let propertyTwo: Int = 10
	static var typeProperty: Int = 42
	
	func instanceMethod() {
		print("instance method")
	}
	static func typeMethod() {
		print("type method - static")
	class func classMethod() {
		print("type method - class")
	}
}
구조체와 매우 유사함을 볼 수 있음
다만, 차이점은 인스턴스메서드, 타입메서드에 추가로 클래스 메서드라는게 있다
나중에 상속을 받았을때 재정의 불가능한 static
재정의가 가능한 class
둘다 타입 메서드라고 부르긴하는데 선언부에 어떤 키워드가 붙냐에 따라 다르다

사용 방식

class Sample {
	var propertyOne: Int = 100
	let propertyTwo: Int = 10
	static var typeProperty: Int = 42
	
	func instanceMethod() {
		print("instance method")
	}
	static func typeMethod() {
		print("type method - static")
	class func classMethod() {
		print("type method - class")
	}
}

var sample: Sample = Sample()
sample.propertyOne = 200
// let 으로 선언된 프로퍼티 변경 불가능 
// sample.propertyTwo = 100
let sample2: Sample = Sample()
sample2.propertyOne = 200
// let으로 선언된 클래스라도 프로퍼티가 변수로 선언되어있으면
// 수정 가능!!!, let, var 인스턴스의 키워드 상관없이 내부 var 프로퍼티는 변경해줄 수 있음

15. 열거형

열거형 enum
굉장히 강력한 기능을 가질수있음!
잘알면 훨씬 도움됨

각각의 케이스가 고유의 값으로 취급된다
C경우 정수값이 자동할당되었음 Swift에선 전혀 그렇지않다
enum은 타입이므로 대문자 카멜케이스를 사용해서 이름을 정의
각 케이스는 소문자 카멜케이스로 정의한다
케이스는 그 자체가 고유의 값이다(무슨뜻?)

enum 이름 {
	case 이름1
	case 이름2
	case 이름3, 이름4, 이름5
	...
	메서드
}

enum Weekday {
	case mon
	case tue
	case wed
	case thu, fri, sat, sun
}
요일들을 만들어보았다

각자 한줄씩 넣어줄수도, 한줄에 다 넣어줘도 됨

열거형의 케이스를 나타내는 문법은?
var day: Weekday = Weekday.mon
var day = Weekday.mon
day = .tue

열거형은 스위치 케이스문에 자주 사용됨
switch day {
case .mon, .tue, .wed, .thu:
	print("It is weekday")
case Weekday.fri:
	print("TGIF")
case .sat, .sun:
	print("Happy weekend")
}
이렇게 case 뒤에 타입을 명시하고 값을 줄수도, .으로 생략하고
값을 줄수도 있다
케이스를 전부 구현해주면 default를 구현해줄 필요없다
그러나 하나라도 빼주면 default를 구현 해줘야한다

.rawValue

C처럼 각 케이스에 값을 주고 싶다면?
열거형 정의시 rawValue(원시값)을 줄 수 있다.

enum 이름 {
	case 이름1
	case 이름2
	case 이름3, 이름4, 이름5
	...
}

이 포맷에 원시값의 자료형과 값을 넣어주되,
케이스별로 각각 다른 값을 가져야 한다

enum 이름: 자료형 {
	case 이름1 = 0
	case 이름2 = 1
	case 이름3 (자동으로 1씩 증가함)
	...
}

enum Fruit: Int {
	case apple = 0
	case grape = 1
	case peach
}

print("peach's rawvalue is \(Fruit.peach.rawValue)")

정수 타입 뿐만 아니라 Hashable프로토콜을 따르는 모든 타입이 원시값의 타입으로 지정될 수 있다

enum School: String {
	case elementary = "초등"
	case middle = "중등"
	case high = "고등"
	case university
}
print("university rawvalue is \(School.university.rawValue)")
// university rawvalue is university
// 숫자는 1씩 증가해서 예상되지만 문자열의 경우는 케이스의 이름 자체를 가져옴

rawValue를 통해 초기화 할 수 있다(?무슨뜻)
rawValue가 case에 해당하지 않을 수 있으므로
rawValue를 통해 초기화 한 인스턴스는 옵셔널 타입이다

let apple: Fruit = Fruit(rawValue: 1)
이런식으로 초기화 할수 있다고~ 아하! 그런데

let apple: Fruit? = Fruit(rawValue: 0)
이렇게 케이스가 정의되지 않은 rawValue값이 들어올 수도 있으니까
그런 경우를 대비하기 위해 옵셔널인 ?로 써야한다는거구나
값을 쓰고싶으면 if let으로 옵셔널 바인딩을 거쳐 사용할 수 있겠네

let apple: Fruit? = Fruit(rawValue: 0)
if let orange: Fruit = Fruit(rawValue: 5) {
	print("rawValue 5에 해당하는 케이스는 \(orange)입니다")
	} else {
	print("rawValue 5에 해당하는 케이스가 없습니다")
}

열거형 메서드

enum Month {
	case dec, jan, feb
	case mar, apr, may
	case jun, jul, aug
	case sep, oct, nov
	
	func printMessage() {
		switch self {
		case .mar, .apr, .may:
			print("봄")
		case .jun, .jul, .aug:
			print("여름")
		case .sep, .oct, .nov:
			print("가을")
		case .dec, .jan, .feb:
			print("겨울")
		}
	}
}
Month.mar.printMessage()
인스턴스를 생성하지 않더라도 함수호출을 할 수 있다...!

16. 클래스 vs 구조체/열거형

클래스는 참조타입. 열거형과구조체는 값 타입이라는 것이 가장 큰 차이점이다
클래스는 상속이 가능하지만 열거형과 구조체는 상속이 불가능하다
  • 값 타입: 데이터를 전달할 때 값을 복사해서 전달
  • 참조타입: 데이터를 전달할 때 값의 메모리 위치를 전달
struct ValueType {
	var property = 1
}
let structFirst = ValueType()
var structSecond = structFirst
// 두번째 구조체 인스턴스에 첫번째 인스턴스 값을 복사
structSecond.property = 2
// 값 수정가능해짐. 원래 구조체 인스턴스가 상수로 선언되면 내부 var값 수정못함
print("first struct property: \(structFirst.property)\n")
print("second struct property: \(structSecond.property)")
// 1
// 2

이렇게 되는 이유?
두번째 구조체 인스턴스는 첫번째 구조체 인스턴스를 똑같이 복사한 별도의 인스턴스이기 때문에 두번째 구조체 인스턴스의 프로퍼티 값을 변경해도 첫번째 구조체 인스턴스의 프로퍼티 값에는 영향이 없음!

class ReferenceType {
	var property = 1
}
let classFirst = ReferenceType()
let classSecond = classFirst
classSecond.property = 2
// 클래스는 let으로 선언되어도 var프로퍼티 수정가능
print("first class property: \(classFirst.property)\n")
print("second class property: \(classSecond.property)\n")
// 2
// 2

이렇게 되는 이유?

두번째 클래스 참조는 첫번째 클래스 인스턴스를 참조하기 때문에 두번째 참조를 통해 인스턴스의 프로퍼티 값을 변경하면

첫 번째 클래스 인스턴스의 프로퍼티 값을 변경하게 된다

값 타입을 사용하는 경우

  • 연관된 몇몇의 값들을 모아 하나의 데이터 타입으로 표현하고 싶은 경우
  • 다른 객체 또는 함수등으로 전달될 때 참조가 아니라 값을 복사할 경우
  • 자신을 상속할 필요가 없거나 다른 타입을 상속 받을 필요가 없는 경우

스위프트에서의 사용

  • 스위프트의 기본 데이터타입은 모두 구조체로 구현되어있다
  • 스위프트는 구조체와 열거형 사용을 선호한다
  • Apple프레임워크는 대부분 클래스를 사용한다
  • 구조체/클래스 선택과 사용은 개발자의 몫
struct SomeStruct {
	var someProperty: String = "Property"
}
var someStructInstance: SomeStruct = SomeStruct()

func someFunction(structInstance: SomeStruct) {
	var localVar: SomeStruct = structInstance
	lovalVar.someProperty = "ABC"
}
someFunction(someStructInstance)
print(someStructInstance.someProperty)
// 결과는 Property 왜냐면 참조형식이 아니기때문에 원본에 변화 없음

17. 클로저 기본

코드의 블럭!

1급 시민이기때문에 변수 상수로도 저장될 수 있고 함수에 인자로 전달되기도 한다

함수는 이름이있는 클로저. 그러니까 클로저의 일종이라고 보면된다

{ (매개변수 목록) -> 반환타입 in
		실행 코드
		return 
}
매개변수 필요없으면 ()
// 함수 사용 예
func sumFunction(a: Int, b: Int) -> Int {
	return a+b
}
var sumResult: Int = sumFunction(a: 1, b: 2)
print(sumResult)

// 클로저 사용 예. 함수타입 변수에 클로저{ (매개변수목록) -> 반환형 in  코드 } 담아 사용하기
var sum: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
	return a+b
}
sumResult = sum(1, 2)
print(sumResult) 

함수는 클로저의 일종이므로 sum 변수에는 당연히 함수도 할당할 수 있다
sum = sumFunction(a:b:)
클로저는 함수의 전달인자로서 주로 사용된다

let add: (Int, Int) -> Int
add = { (a: Int, b: Int) -> Int in
	return a + b
}

let subtract: (Int, Int) -> Int
subtract = { (a: Int, b: Int) -> Int in
	return a - b
}

let divide: (Int, Int) -> Int
divide = { (a: Int, b: Int) -> Int in
	return a / b
}

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
	return method(a, b)
}

calculate라는 함수에 method라는 이름으로 클로저를 넘겨줄거고 함수에서 전달받은 클로저를 호출해줄거임

var calculated: Int
calculated = calculate(a: 50, b: 10, method: add)
calculated = calculate(a: 50, b: 10, method: substract)
calculated = calculate(a: 50, b: 10, method: divide)

calculated = calculate(a: 50, b: 10, method: { (left: Int, right: Int) -> Int
	return left * right	
})

상수에 넣어두고 호출할거면 함수쓰는거랑 뭐가달라?
그냥 바로 코드블럭을 넘겨주면 바로 실행가능한 코드들을 묶어서 함수로 전달해줄수있음

18. 클로저 고급

클로저를 손쉽게 표현할수있는 방법!
주의점: 너무 다양하기때문에 남들이 이해하기 적당한 선에서 축약형 사용하자

후행 클로저, 반환타입 생략, 단축 인자이름, 암시적 반환 표현
import Swift

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
	return method(a, b)
}
var result: Int

//클로저가 함수의 마지막 전달인자라면 마지막 매개변수 이름을 생략한 후
//함수 소괄호 외부에 클로저를 구현할 수 있다.
[ 후행 클로저 ] method: 함수타입변수 대신, 괄호 닫고 중괄호 열어줌
result = calculate(a: 10, b: 10) { (left: Int, right: Int) -> Int in
	return left + right
}
print(result)

[ 반환 타입 생략 ] -> Int 도 생략
result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) -> Int in
	return left + right
})
클로저의 반환형은 어짜피 calculate함수 정의시 어떤 타입의 반환값을 갖는 함수가 올지 알고있기 때문에
굳이 명시해주지 않아도 작동한다 ( -> Int 는 생략가능!)

+ 후행 클로저에도 같은 개념으로 적용가능!
result = calculate(a: 10, b: 10) { (left: Int, right: Int) in
	return left + right
}

[ 단축인자 이름 ] (left: Int, right: Int) in
클로저에 매개변수이름 left, right 굳이 필요해?
이것도 calculate 정의할 때 함수타입 작성하면서 알게되잖어 필요없어
result = calculate(a: 10, b: 10, method: {
	return $0 + $1
})
+ 후행 클로저로 적용가능
result = calculate(a: 10, b: 10) {
	return $0 + $1
}

[ 암시적 반환 표현 ] return 키워드도 제거!
result = calculate(a: 10, b: 10) {
	$0 + $1
}
이렇게 쓸거면 그냥 한줄로도 가능하겠다?
맞음
result = calculate(a: 10, b: 10) { $0 + $1 }
이렇게도 표현 가능함

  축약하지 않은 클로저 문법과 축약 후의 문법 비교
result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) -> Int in
	return left + right
})

result = calculate(a: 10, b: 10) { $0 + $1 }

19. 프로퍼티

구조체 클래스 열거형 등등 타입과 연관된 표현을 할때 사용함
어떤 역할에 따라 인스턴스 프로퍼티, 타입 프로퍼티로 나뉨
+ 저장 프로퍼티, 연산 프로퍼티

struct Student {
	// 인스턴스 저장 프로퍼티
	var name: String = ""
	var `class`: String = "Swift"
	var koreanAge: Int = 0
	
	// 인스턴스 연산 프로퍼티
	var westernAge: Int {
		get {
			return koreanAge - 1
		} // westernAge 값을 꺼내가려 한다면
		
		set(inputValue) {
			koreanAge = inputValue + 1
		} // westernAge 값을 넣으려고 한다면
	}

	// 타입저장 프로퍼티
	static var typeDescription: String = "학생"

	// 인스턴스 메서드
	func selfIntroduce() -> String {
		return ("I am \(self.class) class, \(name)")
	}
	// get만 구현된 읽기 전용 인스턴스 연산 프로퍼티.
	// 값을 쓸수는 없음! set(inputValue) 규칙이 없기 때문 
	var selfIntroduction: String {
		get {
			return "I am \(self.class) class, \(name)"
		}
	}
	// 프로퍼티로 인스턴스 메소드를 대체할 수 있다!
	// 저렇게 String타입의 변수를 선언하고, 값을 읽으려 할때 문자열을 넘겨주도록 하면 됨
	// 쓰기 전용 프로퍼티는 없음! 즉, set(inputValue)만 달랑 구현된건 없다
	
	// 타입 메서드
	static func selfIntroduce() {
	print("학생타입입니다")
	}
	// 읽기전용 타입 연산 프로퍼티
	// 읽기전용엔 get을 생략 가능
	static var selfIntroduction: String {
		return "학생타입입니다"
	}
}

------------------사용---------------------
print(Student.selfIntroduction)
// 학생입니다
-> 이런식으로 연산 프로퍼티를 사용해서 인스턴스 생성안하고 구조체 타입에서 타입프로퍼티의 값을 얻어올 수 있음

var sebaek: Student = Student()
sebaek.koreanAge = 10
sebaek.name = "sebaek"
print(sebaek.name)
//인스턴스의 연산 프로퍼티 사용(get)
print(sebaek.selfIntroduction)
print(sebaek.westernAge)
응용해보자
환율에 따라 돈을 계산해야한다면?
달라를 가져다가 원으로 환산해야한다면?

struct Money {
	var currencyRate: Double = 1100
	var dollar: Double = 0
	var won: Double {
		get {
			return dollar * currencyRate
		}
		// set(inputValue)가 특별히 명시되어 있지 않다면 newValue로 인풋값이 들어옴
		set {
			dollar = newValue / currencyRate
		}
	}
}

var money: Money = Money()
money.dollar = 10
print(money.won)

money.won = 1200
print(money.dollar)
저장 프로퍼티와 연산 프로퍼티의 기능은 함수, 메서드, 클로저, 타입 등
외부에 위치한 지역/전역 변수에도 모두 사용하능하다

var a: Int = 100
var b: Int = 200
var sum: Int {
	return a + b
}

20. 프로퍼티 감시자

프로퍼티 감시자는 값이 변경될 때 그것들을 감시하고 있다가 우리가 원하는 동작을 수행할수있도록 도와주는 녀석. 로그를 찍어줌!

struct Money {
	// 프로퍼티 감시자 사용. 명시적 매개변수()
	var currencyRate: Double = 1100 {
		willSet(newRate) { // 바뀔 값
			print("환율이 \(currencyRate)에서 \(newRate)로 변경될 예정입니다")
		}
		didSet(oldRate) { // 바뀌기 이전의 값
			print("환율이 \(oldRate)에서 \(currencyRate)으로 변경되었습니다")
		}
	}
	// 프로퍼티 감시자 사용. 디폴트 매개변수
	var doller: Double = 0 {
		willSet {
			print("\(dollar)달러에서 \(newValue)달러로 변경될 예정입니다")
		}
		didSet {
			print("\(oldValue)달러에서 \(dollar)달러로 변경되었습니다")
		}
	}
	
	// 연산 프로퍼티
	var won: Double {
		get {
			return dollar * currencyRate
		}
		set {
			dollar = newValue / currencyRate
		}
	}
	// 프로퍼티 감시자와 연산 프로퍼티 기능을 동시에 사용할수 없음
	willSet은 저장되는 값이 변경될때를 감시하기 때문
}
	
var moneyInMyPocket: Money = Money()
moneyInMyPocket.currencyRate = 1150
// 환율이 1100에서 1150으로 변경될 예정입니다
// 환율이 1100에서 1150으로 변경되었습니다
moneyInMyPocket.dollar = 10
//0달러에서 10달러로 변경될 예정입니다
//0달러에서 10달러로 변경되었습니다
print(moneyInMyPocket.won)
프로퍼티 감시자의 기능은
함수, 메서드, 클로저, 타입등의 외부에 위치한 지역/전역 변수 모두에 사용가능하다
var a: Int = 100 {
	willSet {
		print("\(a)에서 \(newValue)로 변경될 예정입니다")
	}
	didSet {
		print("\(oldValue)에서 \(a)로 변경되었습니다")
	}
}
a = 200
// 100에서 200로 변경될 예정입니다
// 100에서 200로 변경되었습니다

상속

클래스의 상속? 스위프트에서 상속은 클래스, 프로토콜에서 사용가능
클래스는 단일상속이기때문에 여러클래스로부터 상속받는 다중상속은 불가능함

class 이름: 상속받을 클래스 이름 {
		구현
}

class Persion {
	var name: String = ""
	
	func selfIntroduce() {
		print("저는 \(name)입니다")
	}

	// 일반 함수의 경우 final 키워드를 주게되면 재정의를 방지할 수 있다. 오버라이드 금지!
	final func sayHello() {
		print("hello")
	}
	// 타입 메서드. static의 경우 재정의 불가능한 타입메서드이다.
	static func typeMethod() {
		print("type method - static")
	}
	// 타입 메서드. class의 경우 재정의 가능한 타입메서드이다.
	class func classMethod() {
		print("type method - class")
	}
	// 재정의 가능한 class메서드라도 final 키워드를 사용하면 재정의 할 수 없다
	// 메서드 앞의 static과 final class 는 똑같은 역할을 한다
	final class func finalClassMethod() {
		print("type method - final class")
	}
}

// Person을 상속받는 Student클래스 정의
class Student: Person {
	var major: String = ""
	// var name: String = ""
	// 상속받아온 저장 프로퍼티는 재정의를 할 수없다.

	//override 키워드로 재정의 가능
	override func selfIntroduce() {
		print("저는 \(name)이고, 전공은 \(major)입니다")
	}
	override class func classMethod() {
		print("overriden type method - class")
	}
	// static을 사용한 타입메서드는 재정의 불가.
	// final을 사용한 메서드, 프로퍼티는 재정의 불가
	// 만약 상속받아온 부모 클래스의 함수를 사용하도록 하고 싶다면
	// 재정의하면서 super.함수() 를 하면 된다.

let sebaek: Person = Person()
let hana: Student = Student()

sebaek.name = "sebaek"
hana.name = "hana"
hana.major = "Swift"

sebaek.selfIntroduce()
hana.selfIntroduce()

Person.classMethod()
Person.typeMethod()

21. 인스턴스의 생성과 소멸 init, deinit

인스턴스의 생성과 소멸. 이제까지 클래스 구조체 생성할때 프로퍼티 기본값을 꼭 너어줬었어
저장 프로퍼티에 값들을 할당해줬었지. 인스턴스 생성되었을때, 그 안의 프로퍼티는 모두 정상적인 
값으로 초기화되어있는 상태여야한다는 규칙이 있기 때문이다.
인스턴스를 생성해준 다음에 프로퍼티에 적절한 값을 넣어줬지

그런데 만약 초기화와 동시에 프로퍼티의 값들에 할당해주고 싶다면?
그래서 이니셜라이저를 사용하게 되면 기본값을 사용하지 않더라도 초기화될때 값 할당가능

init(매개 변수이름: 타입, 매개 변수이름: 타입, 매개 변수이름: 타입 ...) {
	self.저장프로퍼티이름 = 매개변수이름
	self.저장프로퍼티이름 = 매개변수이름
	...
}

class PersonB {
	var name: String
	var age: Int
	var nickName: String

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

let hana: PersonB = PersonB(name: "hana", age: 20, nickName: "하나")
// 의도대로 인스턴스 생성가능!
// 별명이 없는 사람도 있을 수 있다면?
프로퍼티의 초기값이 꼭 필요하지 않은 경우 -> 옵셔널을 사용한다

[ 옵셔널 ? 사용시 ]

class PersonC {
	var name: String
	var age: Int
	var nickName: String?

	convenience init(name: String, age: Int, nickName: String) {
		// 계속 self쓰기 귀찮으면 밑에 정의한 함수를 사용할수도 있다.
		self.init(name: name, age: age)
		self.nickName = nickName
	}
	// 옵셔널로 정의되어있기 때문에 초기화하지 않아줘도 가능. nil
	init(name: String, age: Int) {
		self.name = name
		self.age = age
	}
}
이렇게 정의하면 initializer를 다양한 버전으로 사용할 수 있게 됨
let jenny: PersonC = PersonC(name: "jenny", age: 10)
let mike: PersonC = PersonC(name: "mike", age: 5, nickName: "k")

[ 암시적 추출 옵셔널 ! 사용시 ]

class Puppy {
	var name: String
	var owner: PersonC!
	
	init(name: String) {
		 self.name = name
	}
	func goOut() {
		 print("\(name)가 주인 \(owner.name)와 산책을 합니다")
	}
}

암시적 추출 옵셔널 ! 은 인스턴스 사용에 꼭 필요하지만 초기값을 할당하지 않고자 할 때 사용한다
문제는 이니셜라이즈할때 전달되기 어려운 값일때 이름만 먼저 받아두고 owner를 세팅하겠다!

let happy: Puppy = Puppy(name: "happy")
// happy.goOut() 주인인 PersonC가 없어서 런타임 오류!
happy.owner = jenny
happy.goOut()

[ 실패가능한 이니셜라이저 ] init과 인스턴스에 ? 붙이기
매개변수로 전달되는 값들이 정상범주를 넘어간다거나 원하는 범주의 값이 아닐경우를
대비해서 실패 가능한 이니셜라이저를 둘수가 있다.
실패하면 nil을 반환됨. 반환되는 타입 자체가 옵셔널이 된다
그래서 ?를 갖다 붙이게 되는데,

init?(매개변수이름: 타입, 매개변수이름: 타입) {
		if 조건 {
			return nil
		}
		if 조건 {
			return nil
		}
		self.클래스 저장프로퍼티 이름 = 매개변수이름
		self.클래스 저장프로퍼티 이름 = 매개변수이름
}

이니셜라이저에 조건을 다는 실패가능한 이니셜라이저는 이렇게 사용한다

class PersonD {
	var name: String
	var age: Int
	var nickName: String?

	init?(name: String, age: Int) {
		if (0...120).contains(age) == false {
			return nil
		}
		if name.characters.count == 0 {
			return nil
		}
		self.name = name
		self.age = age
	}
}

// let john: PersonD = PersonD(name: "john", age: 23)
// 불가능. init?로 초기화했기 때문에 옵셔널인 ?를 붙여줘야 컴파일가능
let john: PersonD? = PersonD(name: "john", age: 23)
let jocker: PersonD? = PersonD(name: "jocker", age: 123)
let batman: PersonD? = PersonD(name: "", age: 10)

print(jocker) // nil
print(batman) //nil

deinit은 클래스의 인스턴스가 메모리에서 해제되는 시점에 호출된다
인스턴스가 해제되는 시점에 해야할 일을 구현할 수 있다.
class PersonE {
	var name: String
	var pet: Puppy?
	var child: PersonC

	init(name: String, child: PersonC) {
		self.name = name
		self.child = child
	}
	deinit {
		if let petName = pet?.name {
			print("\(name)가 \(child.name)에게 \(petName)를 인도합니다")
			self.pet?.owner = child
		}
	}
}

사람이 강아지를 가지고있는데, 사람이 deinit될때...사람이 강아지를 기르는 상태라면
자신의 자식에게 인도하는거임
var donald: PersonE? = PersonE(name: "donald", child: jenny)
donald?.pet = happy
donald = nil // donald 인스턴스가 더이상 필요없으므로 메모리에서 해제됨
// donald가 jenny에게 happy를 인도합니다

23. 옵셔널 체이닝과 nil 병합

구조체나 클래스를 선언할때 안에 또다른 구조체 인스턴스가 들어올수도 있고
연결되어 프로퍼티를 타고 타고 타고 들어갈때
옵셔널이 들어가면 자꾸 체크해야하는 경우가 생겨!

class Person {
	var name: String
	var job: String?
	var home: Apartment?
	
	init(name: String) {
		self.name = name
	}
}

class Apartment {
	var buildingNumber: String
	var roomNumber: String
	var `guard`: Person?
	var owner: Person?

	init(dong: String, ho: String) {
		buildingNumber = dong
		roomNumber = ho
	}
}

let sebaek: Person? = Person(name: "sebaek")
let apart: Apartment? = Apartment(dong: "101", ho: "202")
let superman: Person? = Person(name: "superman")
// 인스턴스 생성시 모두 옵셔널 프로퍼티는 nil상태!
// 꼭 필요했던 프로퍼티만 가진 상태.

옵셔널 체이닝을 사용하지 않은상태에서 우리집의 경비원 직업이 궁금하다면?

func guardJob(owner: Person?) {
	if let owner = owner {
		if let home = owner.home {
			if let `guard` = home.guard {
				if let guardJob = `guard`.job {
					print("우리집 경비원의 직업은 \(guardJob)입니다")
				} else {
					print("우리집 경비원은 직업이 없어요")
				}
			}
		}
	}
}
뭐가 있는지 없는지를 if let으로 자꾸 확인하는 과정이 필요함

굉장히 복잡함...

옵셔널 체이닝을 사용한다면

func guardJobWithOptionalChaining(owner: Person?) {
	if let guardJob = owner?.home?.guard?.job {
		print("우리집 경비원 직업은 \(guardJob)입니다")
	} else {
			print("우리집 경비원은 직업이 없어요")
	}
}

guardJobWithOptionalChaining(owner: sebaek)
쭉 한번에 연결해서 써볼수있음..
사람이 있습니까?.집이있습니까?.집에경비원이있습니까?.직업은뭡니까?
각 단계별로 체크가 됨!

sebaek?.home?.guard?.job // nil
// 집과 경비원을 할당해줌
sebaek?.home = apart
sebaek?.home?.guard = superman

sebaek?.home?.guard // Optional(Person)
sebaek?.home?.guard?.name //superman
sebaek?.home?.guard?.job //nil
sebaek?.home?.guard?.job = "경비원"

[ nil 병합 연산자 ?? ]
앞의 값 결과가 nil이라면 뒤의값을 반환해라

guardJob = sebaek?.home?.guard?.job ?? "슈퍼맨"
결과가 nil이라면 ?? 뒤의 "슈퍼맨"을 guardJob에 넣어준다

print(guardJob) // 경비원. 아까 위에 이미 넣어줌 

24. 타입 캐스팅

스위프트의 타입 캐스팅?
1. 인스턴스의 타입을 확인하는 용도
2. 클래스의 인스턴스를 자신의 부모 혹은 자식 클래스의 타입으로 사용할수 있는지 확인하는 용도

is, as를 사용한다

let someInt: Int = 100
let someDouble: Double = Double(someInt)
// 이건 실질적으로 타입캐스팅이 아니다
// 그저 someInt의 값을 가지는 Double자료형의 인스턴스를 만들어준것 뿐

주로 클래스 인스턴스에서 많이 사용. 혹은, 딕셔너리에서 애니 애니오브젝트 타입 쓸때도 많이 사용
클래스를 일단 만들어봤다.
class Person {
	var name: String = ""
	func breath() {
		print("breaths")
	}
}

class Student: Person {
	var school: String = ""
	func goToSchool() {
		print("went to school")
	}
}

class UniversityStudent: Student {
	var major: String = ""
	func goToMT() {
		print("membership training")
	}
}

사람 클래스
사람 상속받는 학생클래스
학생 상속받는 대학생클래스
var sebaek: Person = Person()
var hana: Student = Student()
var jason: UniversityStudent = UniversityStudent()

is 연산자를 통해 타입을 확인

var result: Bool
result = sebaek is Person // true
result = sebaek is Student // false
result = sebaek is UniversityStudent // false

result = hana is Person // true
result = hana is Student // true
result = hana is UniversityStudent // false

result = jason is Person // true
result = jason is Student // true
result = jason is UniversityStudent // true

if sebaek is UniversityStudent {
	print("sebaek is University student")
} else if sebaek is Student {
	print("sebaek is Student")
} else if sebaek is Person {
	print("sebaek is Person")
}

switch jason {
case is Person:
	print("jason is Person");
case is Student:
	print("jason is Student");
case is UniversityStudent:
	print("jason is UniversityStudent")
default:
	print("jason is none")
}

이런 조건문으로 사용할 수 있음. 먼저 걸리는대로 빠져나오기때문에 유의할것
as 를 사용한 업 캐스팅
as 를 사용해서 상속받아온 부모클래스의 인스턴스로 사용할수 있도록
컴파일러에게 타입정보를 전환해준다. 많이 쓰진않어..
  부모타입 = 자식() as 부모타입
Any혹은 AnyObject로도 타입정보를 변환할 수 있다
암시적으로 처리되므로 생략해도 무방!

var mike: Person = UniversityStudent() as Person
var jenny: Student = Student()
var jina: Any = Person() //as Any 생략 가능
// var jina: UniversityStudent = Person() as UniversityStudent
// 컴파일 오류 발생. 부모클래스는 자식클래스로 캐스팅할수없음

as 를 사용한 다운 캐스팅
as? 또는 as!를 사용하여 자식 클래스의 인스턴스로 사용할 수 있도록
사람타입으로 지정되어있는데 학생인척 할 수 있느냐? 업캐스팅되어있는 상태면 가능하겠지
컴파일러에게 인스턴스의 타입정보를 전환해준다

조건부 다운 캐스팅 as?

var mike: Person = UniversityStudent() as Person
var jenny: Student = Student()
var jina: Any = Person()

var optionalCasted: Student?

optionalCasted = mike as? UniversityStudent
// mike는 원래 UniversityStudent인데 업캐스팅되어 Person인 상태
// 따라서 UniversityStudent로 캐스팅이 될 수있어!
optionalCasted = jenny as? UniversityStudent
optionalCasted = jina as? UniversityStudent
optionalCasted = jina as? Student
// 나머지는 학생이거나 Person이였기 때문에 다운캐스팅 불가!

캐스팅에 성공하면 결과값이 옵셔널로 리턴. 불가능한건 nil이 리턴됨

강제 다운 캐스팅 as!
다운 캐스팅을 강제함! 못하면 런타임오류발생 
var forcedCasted: Student
optionalCasted = mike as! UniversityStudent
// mike 는 universityStudent가될 수 있기 때문에 ㄱㅊ
optionalCasted = jenny as? UniversityStudent
optionalCasted = jina as? UniversityStudent
optionalCasted = jina as? Student
// 나머지는 런타임오류발생
// 반환타입이 옵셔널이 아니기때문에 좀 더 편하게 활용할수는 있다

활용
주로 함수로 전달될경우 확인을 해볼 수 있음
가장 부모가되는 클래스를 받았을때 어디까지 내려갈수있는지 확인해볼수있음

[ switch - case is ~:]
func doSomethingWithSwitch(someone: Person) {
	switch someone {
	case is UniversityStudent:
		(someone as! UniversityStudent).goToMT()
	case is Student:
		(someone as! Student).goToSchool()
	case is Person:
		(someone as! Person).breath()
	}
}

case istrue false 여부를 가리기 때문에 실질적으로 사용하기 위해선
as가 필요해진다

[ if let ]
func doSomething(someone: Person) {
	if let universityStudent = someone as? UniversityStudent {
		universityStudent.goToMT()
	} else if let student = someone as? Student {
		student.goToSchool()
	} else if let person = someone as? Person {
		person.breath()
	}
}
as?로 캐스팅과 동시에 추출해서 사용할 수있음

굉장히 중요함. 딕셔너리에서도 많이 사용됨

25. assert/ guard

애프리케이션이 동작 도중 생성하는 다양한 결과값을 '동적으로'확인하고, 안전하게 처리할수있도록 도와주는 친구들이다.

Assertion

어떤 결과, 조건들을 가지고 확인해보는데 사용할수있다

assert( : :file:line:)함수를 사용한다

디버깅 모드에서만 동작한다

배포하는 애플리케이션에서는 제외된다

주로 디버깅 중 조건의 검증을 위해 사용한다

Assert

var someInt: Int = 0
assert(someInt == 0, "someInt != 0")
맞으면 지나치고 그렇지 않으면 메세지 출력! 그리고 동작을 중지!!
여기가 잘못되었어요! 디버거가 알려준다. 메세지는 생략해도댐.

func functionWithAssert(age: Int?) {
	assert(age != nil, "age == nil")
	// nil이 들어오게되면 멈추게!
}

Guard

Early Exit하기 위해 가드를 사용한다. 잘못된 값의 전달 시 특정 실행 구문을 빠르게 종료한다. 디버깅 모드 뿐 아니라 어떤 조건에서도 동작한다

guard의 else 블럭 내부에는 특정 코드블럭을 종료하는 지시어 (return, break 등)이 꼭 있어야 한다

타입캐스팅, 옵셔널과도 자주 사용된다

그 외 단순 조건 판단후 빠르게 종료할 때도 용이하다

func functionWithGuard(age: Int?) {
	guard let unwrappedAge = age, //guard let으로 먼저 옵셔널 바인딩. nil이면 빠져나올거야
		unwrappedAge < 130,
		unwrappedAge >= 0 else {
		print("나이값 입력이 잘못되었습니다")
		return
	}
	print("당신의 나이는 \(unwrappedAge)세 입니다") //가드에서 쓴옵셔널 바인딩을 아래에서도 사용할수있다
}
var count = 1
while true {
	guard count < 3 else {
		break
	}
	print(count)
	count += 1
}
이렇게 해줄수도 있다
딕셔너리를 받아왔을때 굉장히 많이 활용하게 된다
func someFunction(info: [String: Any]) {
	guard let name = info["name"] as? String else {
		return
	}
	guard let age = info["age"] as? Int, age >= 0 else {
		return
	}
	print("\(name): \(age)")
}
// 딕셔너리는 모두 옵셔널타입임. 있을지 없을지 모르기 때문. 따라서
// guard let으로 바인딩해서 값을 가져오고 가져온걸 String으로 타입캐스팅해서
// String으로 안된다면 빠르게 종료

someFunction(info: ["name": "jenny", "age": "10"])
someFunction(info: ["name": "mike"])
someFunction(info: ["name": "yagom", "age": 10])
// yagom: 10

26. 프로토콜

프로토콜은 특정 역할을 수행하기 위한 메서드, 프로퍼티, 이니셜라이저등
요구사항을 정의한다
구조체나 클래스나 열거형에다가 너는 이기능이 꼭 필요해! 그러니까 이 기능을
꼭 구현해 놨어야해 라고 강요하는것과 같다
프로토콜을 구현할거에요 = 채택
실제 구현 = 준수한다

protocol 이름 {
	정의부분
}
protocol Talkable {
	// 1. 프로퍼티 요구할수있다
	//  - 항상 var 키워드 사용
	//  - get은 읽기만 가능해도 상관없다는 뜻
	//  - get set 명시하면 읽기 쓰기 모두 가능한 프로퍼티여야 한다
	var topic: String { get set }
	var language: String { get }
	
	// 2. 메서드 요구
	func talk()
	// 3. 이니셜라이저 요구
	init(topic: String, language: String)
}
Talkable이라는 프로토콜은 특정 타입에 채태되었을때 이런 프로퍼티와 메소드
이니셜라이저를 구현해야한다는것.
프로토콜에선 구현을 직접하는게 아니라 이런게 필요하다는걸 알려주는것 뿐

// 구조체가 채택한 경우
struct Person: Talkable {
	var topic: String
	// 프로토콜은 get set 이므로 읽기 쓰기 가능한 var가 들어가야한다
	let language: String
	// 읽기만 가능해도 상관없으므로 let 해도 되고, var로 선언해도 문제없다
	
	읽기전용 프로퍼티 요구는 연산 프로퍼티로 대체 가능하다
	var language: String { 
			get {
				return "한국어" 
			}
	}
	물론 읽기 쓰기 프로퍼티도 연산 프로퍼티로 대체할 수 있다.
	var subject: String = ""	
	var topic: String {
		get {
			return self.subject
		}
		set {
			self.subject = newValue
		}
	}

	func talk() {
		print("\(topic)에 대해 \(language)로 말합니다")
	}
	
	init(topic: String, language: String) {
		self.topic = topic
		self.language = language
	}
}
프로토콜 끼리 상속! 다중 상속이 가능하다
protocol 프로토콜이름: 부모프로토콜 이름 목록 {
 정의부
}

protocol Readable {
	func read()
}
protocol Writeable {
	func write()
}
protocol ReadSpeakable: Readable {
	func speak()
}
protocol ReadWriteSpeakable: Readable, Writeable {
	func speak()
}

struct SomeType: ReadWriteSpeakable {
	// ReadWriteSpeakable 프로토콜을 채택하고있기 때문에
	// 읽기 쓰기 말하기를 모두 구현해야 프로토콜을 준수하는것임.
	func read() {
		print("Read")
	}
	func write() {
		print("Write")
	}
	func speak() {
		print("Speak")
	}
}

클래스 상속과 프로토콜
클래스 상속할때 프로코콜과 부모클래스 동시에 하려면 어떻게 해?
1. 상속받으려는 클래스 먼저 명시하고 2. 그 뒤에 채택할 프로토콜 목록을 작성한다

class SuperClass: Readable {
	func read() {
		print("read")
	}
}

class SubClass: SuperClass, Writeable, ReadSpeakable {
	func write() {
		print("write")
	}
	func speak() {
		print("speak")
	}
}

프로토콜 준수 확인
인스턴스가 특정 프로토콜을 준수하는지 확인해보기 is, as연산자 활용

let sup: SuperClass = SuperClass()
let sub: SubClass = SubClass()

var someAny: Any = sup
someAny is Readable // true
someAny is ReadSpeakable // false

someAny = sub
someAny is Readable //true
someAny is ReadSpeakable //true

someAny = sup
// Any 타입으로 매개변수를 받아온경우..딕셔너리에서 자주있는일
// Any 자료형인 someAny가 Readable로 다운 캐스팅이 가능한가?
// 가능하면 someReadable에 담기고 read()실행
if let someReadable: Readable = someAny as? Readable {
	someReadable.read()
}
if let someReadSpeakable: ReadSpeakable = someAny as? ReadSpeakable {
	someReadSpeakable.speak()
}
//ReadSpeakable프로토콜은 채택하지 않기 때문에 이건 동작하지않음

someAny = sub
if let someReadable: Readable = someAny as? Readable {
	someReadable.read()
}

27. 익스텐션

익스텐션은 구조체, 클래스, 열거형, 프로토콜 타입에 새로운 기능을 추가할 수 있는 기능
스위프트 표준라이브러리에 구현되어있는 소스를 직접 볼수없어도
타입만 알고있으면 그 타입의 기능을 확장할 수 있다.
메서드라든지 이니셜라이저를 더 추가해주고싶을때 유용함

익스텐션으로 추가할 수 있는 기능

  • 연산타입 프로퍼티 / 연산 인스턴스 프로퍼티
  • 타입 메서드 / 인스턴스 메서드
  • 이니셜라이저
  • 서브스크립트
  • 중첩 타입
  • 특정 프로토콜을 준수할 수 있도록 기능 추가

기존에 존재하는 기능을 재정의 할 수는 없다

extension 확장할 타입 이름 {

}
기존에 존재하는 타입이 추가적으로 다른 프로토콜을 채택할 수 있도록 확장가능
extension 확장할 타입 이름: 프로토콜1, 프로토콜2, 프로토콜3... {
	프로토콜 요구사항 구현
}

// 연산 프로퍼티 추가
extension Int {
	var isEven: Bool {
		return self % 2 == 0
	}
	var isOdd: Bool {
		return self % 2 == 1
	}
}
print(1.isEven) // false
print(1.isOdd) // true

// 메서드 추가
extension Int {
	func multiply(by n: Int) -> Int {
		return self * n
	}
}
print(3.multiply(by: 2)) // 6
print(4.multiply(by: 5)) // 20

// 이니셜라이저 추가
extension String {
	init(intTypeNumber: Int) {
		self = "\(intTypeNumber)"
	}
	init(doubleTypeNumber: Double) {
		self = "\(doubleTypeNumber)"
	}
}
익스텐션이 어떤 프로토콜을 따르도록 할 수있다..프로토콜중심 프로그래밍 개념에 대해
조금 더 살펴보면 더 큰 파급 효과가 있을것
profile
삽질하는 개발자 지망생

0개의 댓글