Swift 2. 심화 문법 part 1

박건희·2022년 3월 7일
0

Swift

목록 보기
2/10

swift 문법을 기록하고 복습하기 위해 쓰는 글이며, 모든 내용은 boost course의 야곰님의 강의를 기반으로 합니다. 후에 문제가 될 시 모든 내용을 삭제하겠습니다

Reference:

https://www.boostcourse.org/mo122

📌 목차

  • 옵셔널 (Optional)
  • 구조체 (Struct)
  • 열거형 (enum)
  • 클로져 (closure)
  • 프로퍼티 (property)

📌 문법 설명

1. Optional

값이 있을수도, 없을 수도 있는 상태 (???: 있을 수도 없을 수도 있습니다~)

null safety를 보장하는 언어를 써봤다면 어려울 것 없다. nil의 가능성을 가지고 있는 변수를 나타내기 위해 사용하고, readability와 code efficiency에 도움을 준다. 가장 큰 장점으로, compile time에서 잡을 수 있는 에러의 종류를 늘려주니 개발자가 싫어할 이유가 없다.

유의 사항:

  • Dart와는 다르게 '!'와 '?' 둘 다 nullable variable을 의미한다
  • Optinal은 enum type과 generic type을 섞어놓은 타입이다
var mightBeNull: Optional<Int> = nil 
let mightBeNull_2: Int? = nil

와 같은 형태로 사용한다.

'!'와 '?'의 차이점

! : Implicitly Unwrapped Optional (암시적 추출 옵셔널)
? : Optional

!는 Optional이 아닌 일반 값과 연산이 가능하다. 즉 !로 옵셔널을 선언한 변수의 경우 개발자가 알아서 nil 이 아닌 경우에만 이를 사용할 것이라고 믿는 것이다. 따라서 !는 runtime error의 가능성이 있다.
?로 선언한 경우는 다른 일반 값들과 연산이 불가능하다.

e.g.

var implicitOptional: Int! = nil
implicitOptional = implicitOptional + 1 // <- 컴파일 에러가 나지 않음! 대신 런타임 에러가 발생

var optional: Int? = nil
optional = optional + 1  // <- 컴파일 단계부터 타입 미스매칭으로 인한 애러가 남

Optional 사용 방법 1: Optional Binding

다른 null safety 문법들에서 nullable variable을 처리하는 방법과 동일하다. null인지 확인하고 아니라면 값을 사용하는 것이다

다만 평범한 if-else statement 가 아니라 if-let이란 문법을 사용한다.

var myName: String! = nil // <- nullable 변수 선언

if let name: String = MyName {
	someFunctionWithStringParam(name)
} else {
	print("myName은 nil입니다")
}

if let 문법은
1. if-let문 안에서만 사용가능한 상수 하나를 만들고 (위 예시의 'name)
2. 그 상수를 non-nullable type으로 선언해서
3. 만약 Optional 변수와 타입이 매칭되면 첫번째 중괄호를 실행하고, 아니라면 두번째 else 중괄호를 실행하는 식으로 null check를 한다. (이 부분은 꼭 필요한 부분은 아니지만 콘솔 로그를 찍기 위해서 해주는 것이 좋다)

이 과정에서 name과 MyName이 서로 binding 된다고 해서 Optional Binding이라고 부른다.
if let 블록 안에 여러 개의 Optional 변수를 바인딩 할 수도 있다.

Optional 사용 방법 2: Force Unwrapping

이 방법은 Dart와 형태가 거의 똑같다!

위에 배웠듯이 !(implicitly unwrapped optional)은 Optional이 아닌 것처럼 사용 가능하기에, ?로 선언된 optional 뒤에 !를 붙여서 사용하면 된다!
방법 그대로 강제로 Optional이란 껍질을 언랩핑해버린다!

var myName: String? = "Sydney"
someFunctionWithStringParam(myName!)

당연히 컴파일 에러는 안나지만, 만약 nil이 들어간 상태로 코드가 진행되면 런타임 에러가 발생한다.

Struct (구조체)

c++의 구조체와 비슷한 느낌이다. 클래스와 뭐가 다른지 잘 모르겠는 부분이...

struct SAMPLE {
	...
}

로 표현하고, 이 안에 property와 method를 넣는다. 당연히 var로 선언한 프로퍼티는 변경 가능 (mutable), let으로 선언한 프로퍼티는 변경 불가능 (immutable) 이다.

메소드나 프로퍼티 앞에 static을 붙여주면 instance를 생성하지 않고도 쓸 수 있는 타입 메소드나 타입 프로퍼티가 되는 것은 자바와 동일하다.

구조체를 선언할 때도 var 냐 let이냐에 따라 수정 여부가 결정된다.

var mutable: Sample = Sample() // <- 내용 수정 가능
let immutable: Sample = Sample() // <- 수정 불가능

Class

Swift의 클래스는 다중 상속이 되지 않는다고 한다. (은근 불편할 듯)

Struct와 Class의 차이점 1: class function

스위프트의 class는 대부분 struct와 유사하지만, 타입 메서드가 두 종류가 있다는 것이 약간의 차이점이다.

class SAMPLE {
	...
    
    static func typeMethod1() {
    	print('이 메소드는 다른 클래스가 상속해도 못 바꾸지!')
    }
    
    class func typeMethod2() {
    	print('이 메소드는 다른 클래스가 상속하면 바꿀 수 있지!')
    }
}

위 예시에 나와 있듯이 static으로 선언 시 재정의가 불가능, class로 선언한 class function은 상속한 클래스가 재정의 할 수 있다.

어찌됐든 둘 다 타입 메소드라고 부른다고 한다.

Struct와 Class의 차이점 2: 타입

구조체는 value type, 클래스는 reference type이라는 것이 다르다. (역시 자바는 근본...) 어쨌든 그로 인해 발생하는 차이가 하나 더 있다.
클래스는 var로 선언해도 let으로 선언해도 프로퍼티를 바꿀 수 있다는 것이다

구조체의 경우 let으로 지정된 경우 모든 데이터가 픽스되지만, 클래스는 let으로 선언해도 해당 클래스의 주소만 고정되는 reference type이기 때문에 그렇다(고 생각한다. 만약 아니라면 댓글로 알려주세요)

enum (열거형)

보통 대부분의 언어에서 enum은 계륵같은 느낌이 있다. 헌데 Swift에선 강력한 기능들이 있어서 꼭 배울 필요가 있다고 한다.

enum Weekday {
    case mon
    case tue
    case wed
    case thu, fri, sat, sun
}

위와 같이 사용할 수 있다. 각 case 별로 정수가 할당되는 C언어의 열거형과 다르게 각 case 자체가 고유의 값을 가진다.

위 예시에서 보이듯이 연속해서 case들을 한 줄에 넣어도 된다.

var day: Weekday = Weekday.mon
day = .tue

위와 같이 최초 선언 시 타입을 명시하면 후에 .differentCase와 같이 간단하게 사용가능하다.

그 외엔..

  • Swift에서 switch-case문을 쓸 경우 무조건 default case를 만들어야 하지만, enum type으로 사용한다면 (모든 case를 핸들한 경우에 한해서) default case를 만들어 줄 필요 없다.
  • enum의 각 case를 원하는 타입의 raw value로 매칭해줄 수 있다.

e.g.

enum Fruit: Int {
    case apple = 0
    case grape  // 이 경우 자동으로 grape의 raw value는 1
    case peach  // peach.rawValue는 2
}

enum School: String {
    case elementary = "초등"
    case middle = "중등"
    case high = "고등"
    case university // <- 이 경우 'university'가 자동으로 raw value가 됨
}
  • enum type 안에 메소드를 넣을 수 있다
    - 이 기능 덕분에 클래스나 구조체 대신 enum을 사용할 수 있다.

Class VS struct VS enum

class는 참조 타입, 나머지는 값 타입...
상속은 class만 가능하지만 extension은 모두 가능..

이렇게 겹치는 기능들이 많아지다 보니 어떤 것을 사용할지 애매해진다. 어떤 상황에 어떤 것을 택하는 것이 좋을까?

구조체

  • 참조가 아니라 값의 복사를 원할 때 (value type이기 때문에)
  • 상속 받을 필요가 없을 때

Closure

Closure는 상수나 변수에 할당이 가능한 코드블록이다.
파이썬이나 자바의 lambda식과 비슷한 개념인 것 같다.

{ (매개변수 목록) -> 반환타입 in
    실행 코드
}

라는 형식을 가진다. 함수 역시도 closure의 한 종류며, '이름이 있는 closure'라고 할 수 있다.

스위프트는 함수를 따로 정의해서 할당하는 방법도 있지만, 아래와 같이 closure를 할당할 수도 있다.

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

let sumResult: Int = sum(1, 2)

이렇게 쓸꺼면 그냥 함수를 쓰는게 낫지 않냐는 생각이 들지만, 함수를 parameter로 사용하거나 return value에 대신 들어갈 callback function을 작성할 때 편리하다.

Closure의 다양한 사용방법

후행 클로져 (Trailing clousure)

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

var result: Int

result = calculate(a: 10, b: 10) { (left: Int, right: Int) -> Int in
    return left + right
}

closure가 함수의 마지막 parameter라면, 해당 함수를 사용할 때 parameter가 들어갈 소괄호 밖에 closure를 빼줄 수 있다.

반환타입 생략

result = calculate(a: 10, b: 10) { (left: Int, right: Int) in
    return left + right
}

위와 동일하게 closure를 사용할 때, 이미 calculate라는 function definition에 클로져의 타입을 정의했으므로 실제 사용할 때는 closure의 return type을 적어줄 필요가 없다.

파라미터 생략

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

한 단계 더 나아가서 파라미터까지 생략할 수 있다. 첫번째 파라미터는 $0 두번째는 $1,... 의 식으로 아예 파라미터 없이 function body만 적으면 되고, 다시 말하지만 이는 function에서 이미 클로져의 파라미터와 return type을 정의했기 때문에 가능한 것이다

implicit return

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

더 나아가 return이라는 표현까지 생략할 수 있다. 리턴문이 없으면 마지막에 쓴 줄이 리턴 타입일 것이라 컴파일러가 인식하기 때문이다


마치 순차적인 것처럼 설명했지만 후행 클로져, 반환타입 생략, 파라미터 생략 등 모두 개별적으로 사용할 수 있는 테크닉이다. 이 말고도 더 있다고 하지만, 그 때부턴 가독성이 현저하게 떨어지는 뇌절이 되기 쉽다고 한다.

Property

우리가 class, enum, struct에서 사용했던 property는 기본적인 값을 저장하기 위한 저장 프로퍼티며, 그 외의 것들은 아래와 같다:

연산 프로퍼티

var westernAge: Int {
        get {
            return koreanAge - 1
        }
        
        set(inputValue) {
            koreanAge = inputValue + 1
        }
}

getter와 setter로 구분되어 있으며, 위 예시에서 koreanAge는 저장 프로퍼티다. set 옆에 파라미티러를 설정하지 않고 그냥


        set {
            koreanAge = newValue + 1
        }

이렇게 newValue라는 implicit parameter를 사용해줘도 된다. (newValue말고 다른 이름을 넣으면 안된다! 일종의 예약어다!)

원래라면 함수를 하나 더 만들고, constructor 안에 넣어 실행해야 하는 귀찮은 작업을 연산 프로퍼티 덕분에 쉽게 할 수 있을 것 같은 생각이 들었다. 또한 async 작업이 연산 프로퍼티에 가능하다면 lazy initialization도 될 것 같아 편리할 것 같아 보인다.

getter만 설정해놓으면 읽기 전용 프로퍼티로 활용할 수 있다.

Property Observer (프로퍼티 감시자)

프로퍼티의 값을 감시하고 있다가 새로운 변화 (i.e. 새로운 set)이 감지 되면 특정 행동을 할 수 있는 property observer라는 게 있다.

struct Money {
  var currencyRate: Double = 1100 {
    willSet {
      print("환율이 \(currencyRate)에서 \(newValue)으로 변경 되었습니다.")
    }

    didSet {
      print("환율이 \(oldValue)에서 \(currencyRate)으로 변경 되었습니다.")
    }
  }
  
}


var money: Money = Money()

money.currencyRate = 1150

위 코드에서 1100의 default 값이 1150으로 변환되는 순간 이런 로그가 콘솔에 찍힌다.

log에서 볼 수 있듯이 willSet은 값이 저장되기 직전에 실행, didSet은 저장된 직후에 실행된다.

주의
연산 프로퍼티에는 property observer를 사용할 수 없음!

profile
CJ ENM iOS 주니어 개발자

0개의 댓글