본문 바로가기

iOS

[iOS] 옵셔널 , 함수, 클로저

옵셔널, 함수


INDEX

1. 옵셔널

2. 함수

3. 클로저

1. 옵셔널


옵셔널(Optional)은 스위프트에서 도입된 새로운 개념으로서 언어 차원에서 프로그램의 안정성을 높이고자 사용하는 개념이다.

옵셔널은 성공적으로 값을 반환한다는 보장이 없는, 즉 값을 처리하는 과정에서 오류가 발생할 가능성이 있는 값을 옵셔널 타입이라는 객체로 감싼 후 반환한다.

이를 옵셔널 래핑 (Optional Wrapping) 이라고 한다.

여기서 중요한 점은 "오류가 발생할 가능성"이다.

즉, 오류가 발생할 가능성이 조금이라도 있는 값은 모두 옵셔널 타입으로 감싸 전달한다.

스위프트는 언어의 안정성을 위해 가급적 오류를 발생시키지 않으려고 노력한다. 오류가 발생하면 프로그램의 실행 흐름이 중단되고 경우에 따라 앱이 죽어버릴 수 있으므로, 언어의 안정성을 위해서는 될 수 있으면 피해야 하는 상황일 수 밖에 없다.

타 언어에서 Null, null 로 표현되기도 하는 nil값이 없다 라는 것을 표현하기 위한 특수 값이다.

기존의 objective-c 에서는 빈 메모리 주소를 가리키는 값이었으나 스위프트에서는 단순히 값이 없음을 의미하게 되었다.

또한, 스위프트는 일반적인 자료형은 nil 값을 가질 수 없도록 제약을 걸어두었다.

즉, 문자열이나 정수 등과 같은 일반 자료형은 값이 없음 이라는 이라는 값이 저장될 수 없도록 차단되어 있기 때문에 억지로 nil값을 대입하려 해도 대입이 불가능하다.

함수나 메소드를 통한 값 반환에도 마찬가지다. 반환 타입이 정해져 있기 때문에 그 타입에 맞게 값을 반환해야 하는데, 처리 과정이 실패했을 경우에는 nil 을 반환하게 된다.

옵셔널 타입이 실제로 가질 수 있는 값의 종류는 두 가지다.

  1. 오류가 발생할 가능성이 있으나 실제 실행 결과에서 오류가 발생하지 않았을 때 반환되는 nil이 아닌 값, -> Optional 타입에 wrapping 되어 있는건 동일하다.

  2. 실제 실행 결과에서 오류가 발생했을 때 반환되는 nil

만일 처리 결과가 성공이라면 특수한 처리 과정을 통해 옵셔널 타입을 해제 (Unwrapping) 하고 실제 값을 추출하여 사용해야 한다.

만일 처리 결과가 실패라면 옵셔널 타입의 값은 nil 값을 반환하므로 옵셔널 타입을 해제해서는 안된다.

모든 값을 옵셔널로 선언하는 것은, 일반 자료형에 nil 값을 사용하는 것과 다를 바가 없다. 그렇게 되면 모든 값을 사용할 때 마다 일일이 nil인지 아닌지를 체크하여 사용해야 한다. -> 프로그래밍 로직을 복잡하게 만들 뿐만 아니라 처리 과정 또한 어렵게 만든다.

따라서, 꼭 필요한 경우에만 제한적으로 옵셔널 타입을 적용하는 것이 좋다.



1.1. 옵셔널 타입의 선언과 정의


일반 자료형을 옵셔널 타입으로 정의하는 방법은 매우 단순하다.

우리가 사용하는 자료형 뒤에 ? 만 붙이면 된다.

String? 은 옵셔널 String 타입을 의미하고, Int?는 옵셔널 Int 타입을 의미한다.

옵셔널 타입의 변수에 값을 할당할 때에는 옵셔널 타입임을 인지할 필요가 거의 없다.

일반 변수처럼 생각하고 값을 대입해도 된다.



1.2. 옵셔널 값 처리


옵셔널 타입의 결과값은 그 자체로는 아무것도 할 수 없다.

옵셔널 타입은 애초에 연산을 지원하지 않는 타입이다.

따라서 옵셔널 타입과 일반 타입은 서로 연산이 불가능하며 옵셔널 타입끼리의 연산이나 결합 또한 지원하지 않는다.

이러한 옵셔널 값을 사용하는 방법에 대해 살펴본다.

우리가 전달받은 것은 Optional 이라는 객체이다.

그 객체 내부에 우리가 원하는 값이 들어있다.

이 값을 우리가 원하는 대로 사용하기 위해서는 실제 값을 둘러싸고 있는 옵셔널 객체를 해제해야한다.

옵셔널 객체를 해제하면 일반 타입의 값이 되는데, 그 값이 우리가 직접 사용할 수 있는 값이다.

이처럼 옵셔널 객체를 해제하고 내부에 있는 값을 추출하는 과정을 옵셔널 해제 (Optional Unwrapping) 이라고 한다.

옵셔널 해제 방식은 명시적 해제묵시적 해제로 나뉜다.

명시적 해제는 강제 해제와 비강제 해제로 나뉘고 묵시적 해제는 자동 해제와 ! 연산자를 사용한 자동 해제로 나눌 수 있다.

강제 해제는 옵셔널 값의 nil 여부와는 상관없이 그냥 옵셔널을 무조건 해제하는 방식으로, 스위프트 공식 문서에서는 Forced Unwrapping 이라는 용어를 사용한다.

강제 해제 방법은 매우 간단하다. 옵셔널 변수 뒤에 ! 연산자만 붙이면 옵셔널 객체가 해제되고 내부에 저장된 값을 추출할 수 있다.

아래 예시를 살펴본다.

var optInt:Int? = 3

print("Optional value : \(optInt)")
// Optional(3)

print("Forced Unwrapping value : \(optInt!)")
// 3

이처럼 옵셔널 타입으로부터 값을 강제 추출하기 위해서는 옵셔널 값 뒤에 ! 연산자를 붙이면 된다.

이를 사용하면 옵셔널 타입끼리의 연산 또한 처리할 수 있다.

옵셔널 변수의 값이 nil 일 때, 강제 해제를 진행하면 오류가 발생한다.

그래서 옵셔널 변수나 상수등을 안전하게 사용하려면 조건이 따른다.

강제 해제 연산자를 사용할 때에는 옵셔널 값이 nil인지 우선 점검해야 한다.

이후 옵셔널 값이 nil이 아닐 때에만 강제 해제 연산자를 통해 값을 추출해야 한다.


var str = "123"
var intFromStr = Int(str)

if intFromStr != nil {
    print("값이 변환되었음, 변환값은 \(intFromStr)입니다.")
} else {
    print("값 변환에 실패하였음.")
}

옵셔널은 값이 없는 nil 이거나 정상적인 값을 옵셔널 객체로 둘러싼 두 가지 경우만 존재하므로 옵셔널 값이 nil인지를 if, guard등을 통해 점검한 이후 사용해야한다.



1.3. 옵셔널 바인딩 (옵셔널 비강제 해제)

앞에서 우리는 nil 여부를 체크하여 안전하게 옵셔널 타입을 해제할 수 있었다.

위에서 진행한 내용을 비강제적인 해제를 이용해 작성이 가능하다.

비강제 해재 구문은 if 문과 같은 구문 내에서 조건식 대신에 옵셔널 타입의

값을 변수 또는 상수에 할당하는 구문을 사용하는 방식으로, 옵셔널 바인딩

(Optional Binding) 이라고 한다.

if문의 조건절을 이용해 옵셔널 바인딩을 진행하는 예제를 살펴본다.


var str = "Swift"

if let intFromStr = Int(Str) {
    print("값이 변환되었음, 변환값은 \(intFromStr)입니다.")
}   else {
    print("값 변환에 실패하였음.")
}

앞서 살펴봤던 강제 해제와 비슷하지만, intFromStrif 문의 조건절에서 상수로 선언되었다는 차이가 존재한다.

옵셔널 타입의 값이 만일 존재할 경우, 상수 또는 변수에 할당하는 과정을 거치며

자연스럽게 옵셔널 타입이 해제되지만, 옵셔널 타입의 값이 nil일 경우에는 값의

할당이 실패되어 실패 분기로 진행되도록 한다.

func intStr(str:String) {

    guard let intFromStr = Int(Str) else {
        print("값 변환에 실패하였음.")
        return
    }

    print("값이 변환되었음. 변환값은 \(intFromStr)입니다.")
}

위 구문은 guard 구문을 이용해 옵셔널 바인딩을 구현한 예제이다.

guard 구문 또한 작동 방식은 동일하다. guard 구문은 조건에 맞지 않으면 무조건 함수 실행을 종료시키기 때문에, 실행 흐름상 옵셔널 값이 해제되지 않으면 더이상 진행이 불가한 경우에 이용된다.

만일, 형식상 옵셔널로 정의해야 하지만, 실제로 사용할 때에는 절대 nil 값이 대입될 가능성이 없는 변수인 경우가 있다.

guard 구문은 함수 또는 메소드에서만 사용이 가능하다.

if 구문을 이용한 옵셔널 바인딩은 조건에 따라서 다른 피드백을 나타낼 때 이용한다.

guard 구문을 이용한 옵셔널 바인딩은 옵셔널 해제가 정상적으로 이루어지지 않았을 때, 프로그램의 흐름상 문제가 될 경우 사용한다.

if 구문을 사용한 옵셔널 바인딩은 단순히 옵셔널 값의 처리 결과에 따라 서로 다른
피드백을 주고 싶을때 사용한다. 하지만 guard 구문은 조건에 맞지 않으면 무조건
함수의 실행을 종료시키는 특성이 있기 때문에, 실행 흐름상 옵셔널 값이 해제되지
않으면 더이상 진행이 불가능할 정도의 오류가 발생할때만 사용하는 것이 좋다.

var value01: Int? = 10

위처럼 Optional Int 타입이지만 우리가 정확히 10을 대입해줘서 값이 있음을 확신할 때에는 묵시적 옵셔널 해제를 이용한다.

묵시적 해제는 컴파일러에 의한 옵셔널 자동 해제와 '!' 연산자를 통한 자동 해제 방법이 있다.

let optInt = Int("123")

// 옵셔널 강제 해제 후 비교
if ((optInt!) == 123) {
    print("optInt == 123")
} else {
    print("optInt != 123")
}

// 컴파일러에 의한 옵셔널 자동 해제
if(optInt == 123) {
    print("optInt == 123")
} else {
    print("optInt != 123")
}

두번쨰 if문에서는 옵셔널을 해제하지 않아 optInt 값이 Optional(123)일 것으로 예상하고 조건이 맞지않아 else문이 실행될 것으로 예상되지만, 컴파일러에서 자동으로 옵셔널을 해제해준다.

이를 묵시적 해제라고 한다.

var value01 : Int! = 10
value01 + 5 // 15

위 방법은 형식상 옵셔널로 정의해야 하지만 변수의 값이 nil이 될 가능성이 없을 경우 사용하는 방법이다.

이처럼 nil이 될 가능성이 없는 옵셔널 타입 선언시 ! 연산자를 사용하여 일반 변수처럼 사용이 가능하다.

형식상 옵셔널로 정의해야 하지만, 실제로 사용할 때에는 절대 nil 값이 대입될 가능성이 없는 변수일 떄 묵시적 옵셔널 해제를 사용한다.



2. 함수


함수란, 프로그램 실행 과정 중 독립적으로 처리될 수 있는 부분을 분리하여 구조화한 객체를 의미한다.

함수는 일반 함수와 사용자 정의 함수로 나눌 수 있다.

일반 함수는 프로그래밍 언어 또는 프레임워크에서 제공하는 함수로 기본적 연산 또는 처리 등을 수행하기 위한 목적으로 사용된다.

대표적으로 표준출력에 사용되는 print()가 있다.

그 외의 필요에 따라 사용자가 직접 만들어 사용하는 함수를 사용자 정의 함수 라고한다.

함수를 만들어서 사용하는 이유는 다음과 같은 이점 때문이다.

함수를 사용하는 이유

  • 동일한 코드가 여러 곳에서 사용될 떄 이를 함수화 하면 재작성할 필요 없이 호출만으로 처리할 수 있다.

  • 전체 프로세스를 하나의 소스 코드에서 연속적으로 작성하는 것보다 기능 단위로 함수화하면 가독성이 좋아지고, 코드의 흐름과 로직을 이해하기 쉽다.

  • 비즈니스 로직을 변경해야 할 때 함수 내부만 수정하면 되므로 유지보수에 용이하다.

2.1. 사용자 정의 함수

사용자 정의 함수를 만드는 형식은 아래와 같다.


func 함수명 (매개변수1: 타입, 매개변수2: 타입...) -> 반환 타입 {

    함수 몸체

    return 반환값
}

타 언어에서 함수를 만드는 방법과 유사하다.

스위프트에서는 명시적으로 func 라는 키워드를 통해 함수를 선언한다.

만일 함수의 인자값이 필요 없는 경우라면 매개변수는 당연히 생략될 수 있다.

또한, 함수의 반환 타입을 표시할 때에는 -> 기호와 함께 사용한다.

이 기호 다음에 작성된 자료형은 이 함수가 반환하는 값의 타입을 의미한다.

함수의 반환 타입 자료형에는 제약이 없다, 일반적인 String, Int, DOuble, Bool 등과 같은 기본 자료형 외에도 AnyObject, UITableCell 등 클래스의 객체도 사용할 수 있다.

또한, nil을 반환하려면 함수의 반호나 타입이 반드시 옵셔널 타입으로 정의되어 있어야 한다.

반환값이 없을 경우에는 -> 기호를 생략하면 된다.

2.2. 함수 호출

이제 함수를 호출하는 방법에 대해 살펴보자.


func incrementBy(amount: Int, numberOfTimes: Int) {
    var cnt = 0
    cnt = amount * numberOfTimes
}

위와 같은 함수가 존재한다고 해보자.

해당 함수는 두개의 매개변수를 받기 때문에 이 함수를 호출할 때는 다음과 같이 인자값 앞에 해당 매개변수의 레이블을 기재해야한다.

incrementBy(amount: 5, numberOfTimes: 20)

함수를 호출할 때에도 반드시 레이블을 포함하도록 강제하는 것은 스위프트 측이 강조하는 장점 즉, 레이블 미표기로 인한 혼란이나 불편함을 방지하기 위함이라고 한다.

( 솔직히 잘 모르겠다; 타 언어에서 함수호출시 위에서 언급한 불편함을 겪어본 경험이 없다. )



Typealias

typealias 는 새로운 축약형 타입을 정의하는 문법이다.

이름이 길거나 사용하기 복잡한 타입 표현을 새로운 타입명으로 정의해주는 문법으로, typealias 키워드를 사용하여 정의한다.

이를 사용하면 길고 복잡한 형태의 타입 표현도 짧게 줄일 수 있어 전체적으로 소스 코드가 간결해지는 효과를 가져올 수 있다.

typealias <새로운 타입 이름> = <타입 표현>

Typealias를 정의하고 나면 컴파일러는 새로운 타입 이름을 타입 표현과 동일하게 간주한다.

typealias infoResult = (Int, Character, String)

func getUserInfo() -> infoResult {
    let gender: Character = "M"
    let height = 180
    let name = "여정수"

    return (height, gender, name)
}

위와 같이 (Int, Character, Name) 형태로 정의된 튜플을 infoResult 라는 새로운 타입 이름으로 정의한 이후로는 infoResult라는 단어는 (Int, Character, Name) 튜플과 동일하게 취급된다.

2.3. 가변 인자

일반적으로 함수는 미리 정의된 형식과 개수에 맞는 인자값만 처리하는 것이 일반적이지만, 때에 따라서는 가변적 개수의 인자값을 입력받아야 할 때도 있다.

스위프트는 이러한 가변 인자 입력 방식을 지원하는데, 아래와 같이 함수를 정의할 때 매개변수명 다음에 ... 연산자를 추가한다.

func 함수명(매개변수명 : 매개변수 타입 ...)

이렇게 정의된 매개변수는 가변 인자로 인식되어 인자의 개수를 제한하지 않고 인자값을 입력받으며, 입력된 인자값을 배열로 저장한다.

func avg(score: Int...) -> Double {

    var total = 0
    for r in score {        // score 배열 원소 순회
        total += r
    }

    return (Double(total) / Double(score.count))    // 총합값을 배열 길이로 나눔
}

이처럼 가변 인자값은 입력 개수를 특정할 수 없는 형태의 매개변수에서 사용된다.

빈번히 사용되기는 않지만, 가변 인자가 아니면 같은 결과를 얻기 위해 꽤 복잡한 과정을 거쳐야 할 경우가 있다고 하니 꼭 기억해두자.


기본값을 갖는 매개변수

함수의 매개변수는 기본값을 지정할 수 있는 기능이 있다.

작성 형식은 아래와 같다.

func 함수명(매개변수: 타입 = 기본값) {
    실행 내용
}

함수 정의시 매개변수의 이름과 매개변수 타입 다음 대입연산자 =를 추가하고, 이어서 기본값을 작성한다.

이렇게 기본값이 입력된 매개변수는 인자값 생략이 가능하다.

func echo(message: String, newline: Bool = true) {
    if newline == true {
        print(message, true)
    } else {
        print(message, false)
    }
}

echo(message: "안녕하세요")
echo(message: "안녕하세요", newline: false)

위 echo 함수는 첫 번째 인자값으로 출력할 메시지를 입력받고, 두 번쨰 인자값으로 줄 바꿈 처리 여부를 결정한다.

두 번쨰 인자값이 false라면 줄 바꿈을 진행하지 않고, 기본값인 true일 때만 줄 바꿈 처리를 진행한다.


변수의 생존 범위 및 생명 주기

타 언어와 같이 변수와 상수들은 정의된 위치에 따라 사용할 수 있고, 생존할 수 있는 일정 범위를 부여받는다.

이를 변수의 생존 범위, 또는 스코프라고 한다.

범위를 기준으로 변수를 구분하면 크게 전역변수 & 지역변수로 나눌수 있다.

전역변수는 global 변수라고도 하며, 프로그램 최상위 레벨에서 작성된 변수를 의미한다.

이 변수는 일반적으로 프로그램 내 모든 위치에서 참조할 수 있으며, 특별한 경우를 제외하고는 프로그램이 종료되기 전까지는 삭제되지 않는다.

반면 로컬 변수라고 부르는 지역 변수는 특정 범위 내에서만 참조하거나 사용할 수 있는 변수를 의미한다.


do {
    do {
        var ccnt = 3
        ccnt += 1
        print(ccnt)     // 4
    }

    ccnt += 1
    print(ccnt)     // Use of unresolved identifier 'ccnt'
}

do 블록은 일반적으로 에러 처리를 위해 do~catch 구문 형식으로 사용되지만, 단독으로 사용할 때는 단순히 실행 블록을 구분하는 역할을 한다.



2.4. 일급 객체로서의 함수

스위프트는 객체지향 언어이자 동시에 함수형 언어이다.

함수형 언어들을 공부하다보면 일급 시민, 일급 객체 라는 용어를 접하게 된다.

이는 프로그램 언어 안에서 특정 조류의 객체가 일급의 지위를 가지는가에 대한 의미이다.

일급 함수의 특성

객체가 다음 조건을 만족하는 경우 이 객체를 일급 객체로 간주한다.

  1. 객체가 런타임에도 생성이 가능해야 한다.
  2. 인자값으로 객체를 전달할 수 있어야 한다.
  3. 반환값으로 객체를 사용할 수 있어야 한다.
  4. 변수나 데이터 구조 안에 저장할 수 있어야 한다.
  5. 할당에 사용된 이름과 관계없이 고유한 구별이 가능해야 한다.

함수가 위 조건을 만족하면 이를 일급 함수라고 하고 그 언어를 함수형 언어로 분류한다.

즉, 함수형 언어에서는 함수가 일급 객체로 대우받는다는 의미이다.

지금부터 일급 함수의 특성에 대해 단계적으로 하나씩 살펴보도록 한다.

  1. 변수나 상수에 함수를 대입할 수 있음
func foo(base: Int) -> String {
    return "결과값: \(base + 1)"
}

let fn1 = foo(base: 5)

foo 라는 함수에 인자값을 넣어 실행하고 이를 fn1 상수에 할당하고 있다.

변수나 상수에 함수를 대입할 수 있다는 성질은 위 의미와는 다르다.

위 내용은 함수의 결과값을 변수나 상수에 대입하는 것이지, 함수 자체를 대입하는것이 아니다.

아래 예를 살펴보자.

let fn2 = foo       // fn2 상수에 foo 함수가 할당됨
fn2(5)              // 결과값: 6

상수 fn2에 foo 함수 자체를 대입하고 있다.

함수 자체가 대입되었으므로 이제 fn2는 foo와 이름만 다를 뿐 인자값, 같은 기능, 같은 반환값을 가지는 함수가 된다.

  1. 함수의 반환 타입으로 함수를 사용할 수 있음

일급 함수의 특성 중 두 번쨰는 함수의 반환 타입으로 함수를 사용한다는 것이다.

일급 객체로 대우받는 함수는 실행 결과로 정수, 실수, 문자열 등 기본 자료형 또는 클래스, 구조체 등의 객체를 반환할 수 있을 뿐만 아니라 함수 자체를 반환할 수도 있다.

함수가 함수를 반환하다는 의미를 아래 예제를 통해 이해해보도록 한다.

func desc() -> String {
    return "this is desc()"
}

func pass() -> (void) -> String {
    return desc
}

let p = pass()
p()             // "this is desc()"

가장 위에 작성된 desc함수는 인자값 없이 문자열을 반환하는 함수 형식으로 정의되어 있다.

그 이후 작성된 함수는 pass 함수다. 이 함수의 내부 블럭을 살펴보면 다른 실행 구문 없이 desc 함수 자체를 반환한다.

즉, 함수가 함수를 반환하고 있으며 반환하는 함수의 반환값은 String이다.

이는 pass 함수가 desc 함수를 반환하므로 (void) -> String 으로 반환값을 표기한다.

이를 응용한 예시를 살펴본다


func plus(a: Int, b: Int) -> Int {
    return a+b
}

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

func times(a: Int, b:Int) -> Int {
    return a*b
}

func divide(a: Int, b:Int) -> Int {
    guard b != 0 else {     // divide by zero exception
        return 0
    }
    return a/b
}

func call(_ operand: String) -> (Int, Int) -> Int {

    switch operand {
        case "+":
            return plus

        case "-":
            return minus

        case "*":
            return times

        case "/":
            return divide

        default :
            return plus
    }
}


let c = calc('+')
c(3,4)          // plus 3+4 = 7 

call 함수를 보면 1차 인자값으로 operand를 전달받는다. 이후 2차 인자값으로 피연산자들의 값을 전달받아 연산을 진행한다.

  1. 함수의 인자값으로 함수를 사용할 수 있음

일급 함수는 반환값으로 함수를 사용할 수 있을 뿐만 아니라 다른 함수의 인자값으로 함수를 전달할 수 있는 특성을 가지고 있다.

이를 이해하기 위해서는 콜백 함수 개념을 이해해야 한다.

콜백 함수는 특정 구문의 실행이 끝나면 시스템이 호출하도록 처리된 함수를 의미한다.

아래 예시를 살펴보자


func incr(param: Int) -> Int {
    return param + 1
}

func broker(base: Int, function fn: (Int) -> Int) -> Int {
    return fn(base)
}

broker(base:3, function:incr)

위 예시는 함수의 인자로 함수를 사용하는 예시이다.

broker 함수의 인자로 함수를 받고 있으며 인자 함수는 Int값을 매개변수로 갖고 반환값 또한 Int값을 갖는다.

이번에는 콜백 함수를 사용하는 예시를 살펴보도록 한다.


func successThrough() {
    print("연산 처리 성공")
}

func failThrough() {
    print("연산 처리 실패")
}

func divide(base: Int, success sCallBack: (Void)-> Void, fail fCallBack: (Void) -> Void) -> Int {

    guard base != 0 else {
        fCallBack()
        return 0
    }

    defer {
        sCallBack()
    }

    return 100
}

divide(base: 30, success: successThrough, fail: failThrough)

위 예제는 함수 인자를 사용하여 콜백을 처리하고 있다.

함수의 두번쨰 인자는 내부 연산 과정이 성공적으로 완료되었을 때 실행할 함수이며, 세번째 인자는 내부 연산 과정이 실패했을 때 실행할 함수이다.



3. 클로저

클로저 표현식은 일반 함수의 선언 형식에서 func 키워드와 함수명을 제외한 나머지 부분만 작성하는 경량 문법을 사용한다.

{ (매개변수) -> 반환 타입 in
    실행 구문
}

클로저 표현식에서는 시작 부분을 in 키워드를 사용하여 실행 블록의 시작을 표현한다.

즉, in 키워드 다음부터 클로저 표현식의 실행 블록이 작성된다.

{ () -> () in
    print("클로저 실행")
}

클로저 표현식은 그 자체로 함수라고 할 수 있다.

클로저 표현식은 대부분 인자값으로 함수를 넘겨주어야 할 때 사용하지만, 직접 실행 또한 가능하다.

이 또한 일급 함수로서의 특성을 활용하여 상수나 변수에 클로저 표현식을 할당하여 실행이 가능하다.

let f= { () -> Void in
    print("클로저 실행")
}

위 구문은 실제로 함수의 인자값으로 전달된 클로저 표현식이 함수 내에서 실행되는 방식이다.

상수 f에 클로저 표현식으로 작성된 함수 전체가 할당되고, 이 상수에 함수 호출 연산자()를 추가함으로써 클로저 표현식이 실행된다.

({ () -> Void in
    print("클로저 실행")
})()

이번에는 매개변수가 있는 형태의 클로저 표현식에 대해 알아보도록 한다.

함수를 선언할 때 처럼 매개변수와 함수의 이름만 적절히 작성하면 된다.


let c = { (s1:Int, s2:String) -> Void in
    print("s1: \(s1), s2: \(s2)")
}

c1(1, "closure")        // s1: 1, s2: closure

위 예제에서 매개변수는 클로저의 실행 블록 내부에서 상수로 선언되므로 실행 구문 범위 내에서 사용이 가능하다.

따라서, 아래와 같이 더욱 간결하게 작성이 가능하다.


let c= { (s1:int, s2:String) -> Void in
    print("s1:\(s1), s2: \(s2)")
}(1, "closure")



3.1. 클로저 표현식과 경량 문법


클로저 표현식은 주로 인자값으로 사용되는 객체인 만큼, 간결성을 극대화 하기 위한 구문들로 이루어져 있다.

배열의 정렬 메소드 예제를 통해 실제로 클로저 표현식에 적용되는 경량 문법에 대해 조금 더 알아보도록 한다.

이해를 돕기 위한 배열 하나를 아래와 같이 만든다.

var value = [1, 9, 5, 7, 3, 2]

이 배열은 정렬 함수인 sort(by:) 를 이용하여 큰 순서나 작은 순서대로, 또는 임의의 순서대로 정렬할 수 있다.

정렬 기준을 잡기 위해서는 특정 형식을 따르는 함수를 정의하여 인자값으로 이를 넣어주어야 한다.

기본적으로 정렬이란 두 값의 비교를 반복하는 알고리즘이다.

순서대로 인자값을 받아 첫 번째 인자값이 두 번째 인자값보다 앞쪽에 와야한다고 판단하면 true, 이외에는 false를 반환함으로써 비교 결과를 전달한다.


// Int형 파라미터 2개를 입력받아 Bool값을 반환하는 함수
func order(s1: Int, s2: Int) -> Bool {
    if s1>s2: {
        return true
    } else {
        return false
    }
}

value.sort(by: order)
// [9, 7, 5, 3, 2, 1]

작성된 함수 order는 입력된 두 인자값의 크기를 비교하여 첫번째 인자값이 더 크면 true를, 이외에는 false를 반환한다.

이 기준에 따라 정렬이 실행된 결과 가장 큰 값 9가 앞으로, 가장 작은 1이 뒤로 배치되는 내림차순 정렬이 완성되었다.

이제 위 함수 order를 클로저 표현식으로 바꾸어 작성해보도록 한다.

{
    (s1:Int, s2:Int) -> Bool in
    if s1>s2 {
        return true
    } else {
        return false
    }
}

위 클로저 표현식으로 sort 메소드의 by 인자값으로 바로 사용이 가능하다.


value.sort(by: {
    (s1:Int, s2:Int) -> Bool in
    if s1>s2 {
        return true
    } else {
        return false
    }
})

이를 더욱 간결화시키면 아래와 같다.


value.sort(by: {(s1:Int, s2:Int) -> Bool in return s1 > s2})
value.sort(by: {(s1, s2 in return s1 > s2})

-> Bool 이라는 반환값 표현과 변수의 타입이 생략된 형태로, 위 구문에서는 반환 구문이 s1>s2 인데, 이는 비교 구문이다.

따라서 그 결과는 true 혹은 false로 나뉘어지며, 이 과정을 거쳐 클로저 표현식의 반환값 타입이 Bool이라는 것과 전달받은 매개변수의 타입을 컴파일러가 추론하게된다.

이제는 매개변수를 생략해보도록 한다.

매개변수를 생략하게 되면 매개변수명 대신 $0, $1, $2.. 와 같이 이름으로 할당된 내부 변수를 이용할 수 있으며 이 값은 입력받은 인자값의 순서대로 매칭된다.

매개변수가 생략되면 남는 구문은 실행 구문 뿐이다.

이 때문에 in 키워드로 기존처럼 실행 구문과 클로저 선언 부분을 분리할 필요가 없어지므로 in 키워드 역시 생략할 수 있다.

결국 남는것은 아래와 같다.

{return $0 > $1}

따라서 이 클로저 표현식을 인자로 전달해보면 아래와 같다.


value.sort(by: { $0 > $1 })


정리


  • 옵셔널은 프로그램의 안정성을 위한 개념이다.

  • nil은 기존의 objective-c 에서는 빈 메모리 주소를 가리키는 값이었으나 스위프트에서는 단순히 값이 없음을 의미하게 되었다.

  • 일반 자료형에는 nil값을 할당할 수 없다. 하지만 각각의 일반 자료형들을 기반으로 옵셔널 타입이 만들어진다. (ex: Optional Int, Optional String)

  • 옵셔널 타입이 가질 수 있는 값은 nil || 옵셔널 타입에 쌓여진 nil이 아닌 값

  • 당장에 오류가 발생하지 않더라도 잠재적으로 오류가 발생할 가능성이 있는 상황

  • 결국 옵셔널 타입이란, 반환하고자 하는 값을 옵셔널 객체로 다시 한 번 감싼 형태를 의미한다.

  • 옵셔널 객체를 해제하고 내부에 있는 값을 추출하는 과정을 **옵셔널 해제 (Optional Unwrapping) 이라고 한다.**

  • 옵셔널 타입으로부터 값을 강제 추출하기 위해서는 옵셔널 값 뒤에 ! 연산자를 붙이면 된다.

  • 옵셔널은 값이 없는 nil 이거나 정상적인 값을 옵셔널 객체로 둘러싼 두 가지 경우만 존재하므로 옵셔널 값이 nil인지를 if, guard등을 통해 점검한 이후 사용해야한다.

  • if 문과 같은 구문 내에서 조건식 대신에 옵셔널 타입의 값을 변수 또는 상수에 할당하는 구문을 사용하는 방식으로, 옵셔널 바인딩 (Optional Binding) 이라고 한다. ex ) if let intFromStr = Int(Str) { }

  • 옵셔널 바인딩의 경우, if 또는 guard 구문을 이용하여 진행할 수 있다, guard문은 함수나 메소드에서만 사용이 가능하다.

  • if 구문을 사용한 옵셔널 바인딩은 단순히 옵셔널 값의 처리 결과에 따라 서로 다른 피드백을 주고 싶을때 사용한다. 하지만 guard 구문은 조건에 맞지 않으면 무조건 함수의 실행을 종료시키는 특성이 있기 때문에, 실행 흐름상 옵셔널 값이 해제되지 않으면 더이상 진행이 불가능할 정도의 오류가 발생할때만 사용하는 것이 좋다.

  • 형식상 옵셔널로 정의해야 하지만, 실제로 사용할 때에는 절대 nil 값이 대입될 가능성이 없는 변수일 떄 묵시적 옵셔널 해제를 사용한다.

  • 함수란, 프로그램 실행 과정 중 독립적으로 처리될 수 있는 부분을 분리하여 구조화한 객체를 의미한다.

  • 함수를 사용하는 이유

  • 함수를 호출할 떄에는 매개변수 레이블을 기재하여 호출해야 한다.

  • 가변인자 받는법 :func avg(score: Int ...) -> Double {}

'iOS' 카테고리의 다른 글

[iOS] MyWebBrowser 정리  (0) 2020.04.16
[iOS] TableView 정리  (0) 2020.04.16
[iOS] 내 소개 어플리케이션  (0) 2020.04.14
[iOS] Web Browser Project  (0) 2020.04.12
[iOS] 배열, 집합, 튜플 자료형  (0) 2020.04.11