brunch

You can make anything
by writing

C.S.Lewis

by MJ Apr 02. 2017

4. Swift의 함수

Swift 3.x Programming

이번에는 함수 및 폐쇄(Closure)를 알아보는데, 이는 처리를 하나로 통합하여 외부에서 실행 가능하게 할 것이다. 함수는 폐쇄의 하나이기 때문에 공통 사양이 많다.


1. 처리의 재사용

프로그래밍에서는 동일한 작업을 여러 곳에서 수행하는 경우가 있다. 함수 및 폐쇄는 하나로 묶은 처리를 잘라내거나 재사용이 가능하기 위한 것이다. 즉, 언제라도 실행할 수 있기 때문에 다시 사용할 수 있는 것 같은 처리를 알맞은 함수나 폐쇄로 처리해서 중복처리를 하나로 모아서 가독성과 유지보수성을 향상시킬 수 있다.


2. 함수

함수는 입력으로 인수, 출력으로 리턴값을 가진 이름을 가진 하나의 묶은 처리를 말한다. 함수명을 함수명이라고 하며 함수명과 인수를 합쳐 함수를 외부에서 호출할 수 있다.

정의 방법은 함수는 func 키워드로 선언하고 다음과 같은 형식으로 함수명, 인수, 리턴형을 정의한다.


func 함수명(인수명1: 형, 인수명2: 형 ...) -> 리턴형 {

    함수호출시 실행되는 문장

    필요에 따라 return 문에서 리턴값을 리턴함

}


인수는 ()에 인수명: 형 이라는 형식을 구분하며 정의하고 리턴형은 ()뒤에 -> 뒤에 정의한다. 함수명과 인수명은 변수명과 상수명의 경우 이모티콘과 같은 다양한 문자를 사용하지만 보통 실무개발시 문자만 이용한다.

리턴값을 리턴하려면 { }에서 return문을 사용한다. return문은 프로그램 제어를 함수호출에서 이동하는 문장으로 return 키워드 다음에 리턴하는 표현식을 return 식으로 설명한다. 식 형은 컴파일시 검증된 리턴값 형과 다르면 컴파일 오류가 발생한다. 리턴값이 없는 함수에서 return문을 지정하지 않고 return 키워드만 설명한다. 또한 리턴값이 없는 함수에서는 return키워드를 생략할 수 있다.

함수형은 인수와 리턴값의 형을 조합하여 (인수형) -> 리턴값형으로 표시된다.  아래 예는 Int형 인수를 하나로 2배로 Int형 값을 리턴 (Int) -> Int형의 2x(_:)함수를 정의한 것이다.


func 2x(_ a: Int) -> Int {

    return a*2

}

2x(5) //10


함수는 함수명()을 지정하고 ()에 함수에 입력되는 인수와 인수명을 구분한다. 인수명이 없다면 단순한 인수만 선언한다.


함수명(인수명1, 인수1, 인수명2: 인수2, ...)


함수는 실행후 출력되는 리턴값을 리턴한다. 리턴값은 변수와 상수를 할당할 수 있다. 인수를 전달하여 함수를 실행하고 리턴값을 상수에 대입하는 형태를 보면 아래와 같이 선언할 수 있다.


let 상수명 = 함수명(인수명1: 인수1, 인수명2: 인수2, ...)


리턴값이 정의되어 있는 함수를 호출한다면 일반적인 리턴값을 변수와 상수에 할당한다. 참고로 문법을 배우는 단계에서는 Playground를 많이 사용하는데 이 환경에서는 리턴값 대입을 생략해도 컴파일 경고가 나오지 않기 때문에 되도록이면 꼭 리턴값을 대입해서 처리하는 문법으로 익혀보자. 또는 리턴값이 불필요한 경우에는 _을 대입하여 명시적으로 리턴값을 무시하면 컴파일경고를 줄일 수있다.


func funcDiscardableResult() -> String {

    return "출력"

}

_ = funcDiscardableResult() //출력


다른 방법으로는 리턴값이 꼭 필요하지 않는 함수에 @discardableResult특성을 선언하면 컴파일 경고를 제한할 수 있다.


@discardableResult

func funcDiscardableResult() -> String {

    return "출력"

}

funcDiscardableResult() //"출력"


인수

인수는 함수에 대한 입력을 말한다. 인수는 이름과 형으로 구성되며 함수에는 여러 개의 인수를 가질 수 있다. 또한 인수는 함수 정의시 선언하는 것을과 호출시 지정하는 것이 2가지 있다. 예로 함수 정의시 선언하고 있는 i가 가인수이며 함수를 호출할 때 지정한 3이 실제인수이다.


func printValue(_ i: Int) {

    print(i)

}

printValue(3)


인수명은 함수 호출시 사용하는 외부 인자명과 함수내에서 사용되는 내부인수명 2개를 가질 수 있다. 외부 인수명과 내부 인수명을 구분하기 위해 외부인수명 내부인수명: 형 형태로 정의한다.


func setVote(user: String, to group: String) {

    print("\(user)이 \(group)에 투표했다")

}

setVote(user: "MJ", to: "Goyang-si")


제2인수 내부인수명은 group이 있기 때문에 setVote(user: to:)함수안에 group이라는 인수명을 사용할 수 있다. 외부 인수명은 함수를 이용하는쪽에서 보면 의미있는 이름을 내부인수명은 프로그램이 중복되지 않는 이름을 넣는 것이 보편적이다.

또한 외부인수명은 생략하고 사용하고 싶다면 외부인수명에 _(밑줄)을 사용한다.


func plus(_ i1: Int, _ i2: Int) -> Int {

    return i1 + i2

}

let a = plus(3, 5) //8


만약 기본값을 가지고 있는 인수는 함수 호출시 생략이 가능하다. 인수의 기본값을 기본 인수라고 말하며 기본 인수를 지정하려면 일반 인수 선언후 값 대입을 추가한다. 아래 예는 1번 인수에 기본인수가 정의되어 있어 인수를 생략할 수 있다.


func hello(user: String="Admin") {

    print("안녕하세요, \(user))

}

hello()

hello(user: "MJ")


기본인수는 어떤 인수도 정의할 수 있고 여러가지 인수에 정의할 수 있다. 기본 인수는 검색조건과 같은 다양한 인수가 필요하지만, 모든 인수 지정은 필수는 아니기 때문에 함수 정의시 유용하다. 예로 productSearch(byQuery:sortKey:ascending:)함수는 검색할 단어, 정렬, 오름차순여부를 지정하는 3개의 인수를 가지고 있지만 필수적인 부분은 검색어뿐이다. 필수가 아닌 인수는 기본 인수를 설정하고 있기 때문에 검색어만 사용하는 경우, 하나의 함수로 대응이 가능하다.


func productSearch(byQuery query: String, sortKey: String="id", ascending: Bool = false) -> [Int] {

    return [1, 2, 3]

}

// 사용하지 않는 인수값을 지정하지 않는 경우도 호출 가능

productSearch(byQuery: "제품명") // [1, 2, 3]


인아웃 함수는 함수에서 인수를 다시 대입하여 함수 외부에 반영시키고 싶을 때 인아웃 인수(inout parameters)를 이용한다. 인아웃 인수를 사용하려면 인수형 앞에 inout키워드를 추가한다. 또한 인아웃 인수를 가지는 함수를 호출하려면 인아웃 인수앞에 &을 추가한다. 아래 예는 hello(user:)함수에 빈변수 user를 리턴하지만 hello(user:)함수를 실행한 다음 user값을 "MJ"을 입력한다.


func hello(user: inout String) {

    if user.isEmpty {

        user = "MJ"

    }

    print("안녕하세요 \(user)")

}

var user: String = ""

hello(user: &user) //안녕하세요 MJ


가변인자는 임의의 갯수 값을 받을 수 있는 인자를 말한다. 배열을 인수로 사용하는 것도 여러개의 값을 받을 수 있지만, 가변인수 함수의 호출하는 쪽에 배열임을 의식하지 않아도 된다는 것이다. Swift언어에서는 하나의 함수에 최대 1개 가변인자를 정의할 수 있다. 가변 인자를 정의하려면, 인수 정의 끝에 ...을 추가한다. 가변인자를 가진 함수를 호출하는 쪽에 인수를 구분 기호로 보내진다. 가변인자로 받은 인수는 함수내부에서는 Array<Element>형으로 처리된다.

따라서 함수에서 가변인수에 접근하는 방법은 Array<Element>형 요소에 접근하는 방법과 동일하다. 다음 print(str:)함수 인수는 가변인수이고 여러개의 String 형 값을 인수로 얻을 수있다. 함수 내부에서는 가변인수형은 [String]으로 취급된다.


func print(str: String ...) {

    if str.count == 0 {

        return

    }

    print("첫번쨰 \(str[0])")

    for s in str {

        print("요소 - \(s)")

    }

}

print(str: "가", "나", "다")


이처럼 변수와 상수에 값을 대입하는 것과 같이, 실제 인수형도 컴파일시 체크된 다른 형의 실제 인수를 주는 플로그램은 컴파일 오류가 발생한다. 아래와 같이, Int형 인수를 1개를 얻거나 String형 값을 리턴 (Int) -> String형 함수 string(from:), Double형 인수 double을 선언하고 인수형이 일치하지 않기 때문에 컴파일 오류가 발생한다.


func string(from: Int) -> String {

    return "\(i)"

}

let i = 1

let double = 2.0

let s1 = string(from: i) // "1"

let s2 = string(from: double) //컴파일오류


리턴값은 함수 출력을 나타내는 값을 말한다. 리턴형은 함수 선언시 정의함수 호츨은 정위된 리턴형의 값을 결과로 받을 수 있다. 만약 함수 선언할 때 리턴형을 지정할 필요가 없는 경우 함수 선언에서 리턴형 정의를 생략할 수 있다. 아래 예는 hello(user:)함수로 print(_:)함수를 사용하여 문자를 출력하는 것인데 함수 호출시 아무것도 값을 리턴하지 않는다.


func hello(user: String) {

    print("안녕하세요 \(user)")

]

hello(user: "MJ")


함수 선언에서 리턴형 정의를 생략하면 함수 리턴값은 Void형이 아래와 같이 정의되어 있는 것과 동일하다.


func hello(user: String) -> Void {

    print("안녕하세요 \(user)")

}

hello(user: "MJ")


참고로 함수선언에서 정의된 리턴형과 실제 리턴형이 일치하는지는 컴파일러에서 확인된다. 즉, 정의된 리턴갑 형과 일치하지 않는 형을 리턴하는 프로그램은 컴파일 오류가 리턴값이 정의된데로 값을 리턴할 수 있도록 컴파일러가 보장한다는 것이다.


3. 폐쇄(Closure)

폐쇄는 재사용 가능한 묶음의 처리를 말한다. 함수도 폐쇄의 일종으로 비슷하다. 함수를 사용하려면 func키워드르로 정의해야 하지만 폐쇄는 따로 정의하는 방법이 있다. 폐쇄식명이 필요하거나 형추론에 의한 형을 선택하기 때문에 함수보다 쉽게 정의가능하다.

폐쇄식에 의한 폐쇄는 전체를 { }으로 묶고 아래와 같은 형태로 인수 및 리턴형을 정의한다.


{(인수명1: 형, 인수명2: 형 ...) -> 리턴형 in

     페쇄 실행시 실행되는 문장

     필요할 경우 return문으로 리턴값 리턴

}


인수와 리턴값 형을 작성시 함수와 비슷하지만 리턴형과 문장 in키워드로 구분한다. 리턴값을 리턴하려면 함수와 똑같이 return문을 사용한다. 그러나 폐쇄가 한개뿐이 없다면 return키워드 사용은 선택적이다. 이를 암시적 리턴(Implicit returns)라고 한다. 또한 폐쇄형은 함수와 똑같이 (인수형) -> 리턴형으로 표시한다. 아래 예는 Int형 인수를 하나가져와 2배로 늘려주는 Int형값을 리턴(Int) -> Int형 폐쇄를 정의하고 실현해 보자.


let 2x = {(x: Int) -> Int in

    x * 2

}

double(5) //10


위 예는 폐쇄문이 하나만 있었기 때문에 return키워드를 생략할 수 있지만 두문장 이상이라면 return 키워드를 생략하면 컴파일 오류가 난다.


let 2x = {(x: Int) -> Int in 

    print("입력값: \(x)")

    x * 2  // 컴파일오류 

}

2x(5)


또한 폐쇄형은 보통 형과 같이 사용할 수 있어 변수와 상수형이나 함수의 인수형으로 사용이 가능하다.


let cs: (Int) -> Int

func examFunc(x: (Int) -> Int) {}


폐쇄의 인수와 리턴형 선언은 폐쇄 대입하는 쪽의 형에서 추론하여 생략할 수 있다. 아래 예에서 (String) -> Int변수 c에 폐쇄를 대입하고 있다. 변수형은 (String) -> Int와 명확하기 때문에 이 변수에 할당하는 폐쇄형도 (String) -> Int로 형추론할 수 있다. 따라서 변수 c 에 대입하는 폐쇄인수와 리턴값 형은 생략할 수 있다.


var c: (String) -> Int

//인수 및 리턴형 명시하는 경우

c = {(s: String) -> Int in

    return s.characters.count

}

c("한글") //2

// 인수 및 리턴형 명시안하는 경우

c = {s in 

    return s.characters.count * 3

}

c("한글") //6


반대로 형이 정해지지 않은 변수 및 상수에 폐쇄를 할다하면 폐쇄형을 추론할 수 없는 경우에는 폐쇄에 인수 및 리턴값 형을 정의해야 한다.


let c = {s in

    return s.characters.count * 3

} // 폐쇄형이 정해져 있지 않기 때문에 컴파일오류


실행하는 방법은 함수와 동일하고 폐쇄로 할당되는 변수명과 상수명 끝에 ()을 지정하고 ()에 인수를 구분하여 정렬한다. 리턴값은 변수와 상수에 할당이 가능하다.


let 상수명 = 변수명(인수1, 인수2 ...)


폐쇄가 대입된 변수와 상수형은 폐쇄형으로 형추론된다. 따라서 아래와 같이 (String) -> Int형의 폐쇄가 할당된 정수 length형은 (String) -> Int형으로 형추론된다.


let length = {(s: String) -> Int in 

    return s.characters.count

}

length("문자 출력") //5


함수처럼 인수와 리턴형은 컴파일시 체크된다. 인수에 다른 형의 값을 할당하거나 다른 형의 리턴값을 리턴하려고 하면 컴파일 오류가 발생한다.

폐쇄식 인수는 함수 인수와 비슷히지만 함수와 다른 몇가지 제약이 있다.

외부 인수명 (폐쇄식 불가능)

기본인수 (페쇄식 불가능)

인아웃 인수 (폐쇄식 가능)

가변인자 (폐쇄식 가능)

간단한 인수명 (폐쇄식 가능)

함수 정의는 외부인수명과 내부 인수명을 각각 지정한다. 폐쇄식의 정의는 내부 인수명을 지정할 수 있고 외부 인수명을 지정할 수 없다. 따라서 함수에서 외부 인수명을 _으로 지정한 것과같은 상태이다. 예로 정의된 폐쇄는 인수명을 생략하고 plus(3,5)와 같이 호출한다.


let plus = {(x: Int, y: Int) -> Int in

    return x + y

}

plus(3,4) //7


또한 폐쇄식 정의는 기본 인수를 지정할 수 없다. 아래오 ㅏ같이 기본인수를 가진 폐쇄를 정의하려고 하면 컴파일 오류가 된다.


let hello = {(user: String = "MJ") -> Void in

    print("안녕하세요 \(user)")

}


정의하는 폐쇄형이 추론할 수 있는 케이스는 형을 생략할 수 있다. 이런 경우 인수명 정의를 생략하고 대신 단순 인수명(shorthand arugment name)을 사용할 수 있다. 이런 간단 인수명에는 $와 인덱스를 붙여 $0, $1, $2형태로 선언한다. 아래 예는 2개의 Int형 인수를 얻고 Bool형 값을 리턴하여 폐쇄 단순 인수명을 사용하여 접근할 수 있다.


let isEqual: (Int, Int) -> Bool = {

    return $0 == $1

}

isEqual(3, 3) // true


간단인수명을 사용한 폐쇄는 폐쇄정의에서 인수혀을 지정하지 않기 때문에 외부에서 형추론할 수 없는 경우 컴파일오류이다. 다만, 간단 인수명을 많이 사용하면 인수가 무엇을 의미하는지 모를 수 있어 코드 가독성이 떨어지지만 간단한 처리에는 알맞다. 아래 예는 Array<Element>의 filter(_:)메소드를 간단 인수명을 사용해 보았다. filter(_:)메소드는 조건에 맞는 요소만을 포함한 배열을 리턴하는 메소드에서 인수에 그 조건을 폐쇄로 제공한다.


let n = [1, 2, 3, 4]

let m = n.filter { $0 > 3 }

m // [4]


리턴값이 없는 함수를 정의하는 것과 똑같이 리턴값이 없는 폐쇄를 정의할 수 있다.


// 리턴값이 없는 폐쇄

let e: () -> Void = {}

// 한개 리턴값을 가지는 폐쇄

let s: () -> Int = {

    return 1

}


참고로 Swift언어에서는 ()와 Void형은 동일하다.그러나, 일반적으로 폐쇄형 표기에서 인수가 없는 경우 ()을 사용하고 리턴값이 존재하지 않는 경우 Void형을 사용한다. 즉, () -> (), () - > Void로 선언한다.

로컬 범위에 정의된 변수나 상수는 로컬범위에서만 사용할 수 없다. 반대로 폐쇄가 참조하는 변수와 상수는 폐쇄가 실행되는 범위가 변수나 상수가 정의된 로컬범위 이외에도 폐쇄를 실행할 때 사용할 수 있다. 이것을 폐쇄가 자신이 정의된 범위의 변수와 상수에 대한 참조를 유지하고 있기 때문이다. 이 기능을 캡쳐라고 말한다. 캡쳐 실행은 아래 예처럼 say 상수에 할당된 폐쇄를 사용해보자.


let say: (String) -> String

do { 

    let s= "@"

    say = {user in

        return "안녕하세요 \(user)\(s)"

    }

}

say("MJ")

s // s는 다른 범위에 정의되어 있기 때문에 컴파일오류


상수 say는 do문장의 범위 밖에서 선언되어 있어 do 문 밖에서도 사용할 수 있지만  상수 s 는 do문장 범위내에 선언되어 있기 때문에 do문 외부에서 사용할 없다. 그러나 상수 say에 대입된 폐쇄는 내부에서 s를 이용하고 있지만 do문 외부에서도 실행이 된다. 이는 폐쇄가 캡쳐되어 자신이 정의된 범위의 변수 및 상수에 대한 참조를 유지하는 것으로 실형된다.

캡쳐 실행은 다음 say상수에 할당된 폐쇄를 예로 설명해본다. 여기에서는 do키워드를 사용하여 새 로컬범위를 만들고 있다. 일반적으로 do키워드는 catch키워드와 같이 오류처리에 사용되지만 이와 같이 새 범위를 작성하는 용도는 단독으로 사용할 수 있다. 캡쳐 대상은 변수와 상수에 들어 있는 값이 아닌 변수나 상수 자신이다. 따라서 캡쳐된 변수에 대해 변경은 퍠쇄실행할때도 반영된다. 예로  상수 c에 할당된 폐쇄는 실행될때마다 변수 cnt값을 1증가시킨다. 변수  cnt 초기값은 0이지만 변수 자체를 캡쳐하고 있기 때문에 변수에 대한 변경도 유지된다. 따라서 실행할 때마다 다른값이 된다.


let c: () -> Int

do {

    var cnt = 0

    c = {

        cnt += 1

        return cnt

    }

}

c() //1

c() //2


폐쇄 함수와 다른 폐쇄 인수로써 이용하는 경우에만 알맞은 사양으로 특성 및 트레이링 폐쇄가 있다. 속성은 폐쇄에 대해 지정하는 추가정보로 트레이링 폐쇄는 폐쇄를 인수로 얻는 함수 가독성을 높이기 위한 사양이다.

속성은 폐쇄형 앞에 @속성명을 추가하여 지정한다. 속성은 escaping속성과 autoclosure속성이 있다.


func 함수명(인수명: @속성명 퍠쇄형명) {

    함수호출시 실행되는 문장

}



func orExam(_ lhs: Bool, _ rhs: @autoclosure() -> Bool) -> Bool {

    if lhs {

        return true

    } else {

        return rhs()

    }

}

orExam(true, false) // true


escaping속성은 함수에 인수로 전달된 폐쇄가 함수 범위 외부에서 유지될 가능성이 있다는 것을 나타내는 속성이다. 컴파일러는 escaping속성유무에 따라 폐쇄가 캡쳐를 할 필요가 있는지 결정한다. 폐쇄가 함수 범위 밖에서 유지되어야  폐쇄 실행은 함수 실행에 한정되어 있어 캡쳐가 필요하지 않는다. 반대로 폐쇄가 함수 범위밖에서 유지될 가능성이 있는 경우, 즉, escaping특성이 필요한 경우 폐쇄를 실행할 때까지 함수 범위 변수를 유지해야 하기 때문에 캡쳐가 필요하다.

다음 enqueue(operation:)함수는 인수로 주어진 폐쇄를 배열 que에 추가한다. 즉, 이 인수의 폐쇄는 함수 범위밖에서도 유지된다. 따라서 enqueue(operation:)함수 인수는 escaping속성을 지정해야 한다. 지정하지 않으면 컴파일 오류가 발생한다.


var que = [() -> Void]()

func enqueue(operation: @escaping() -> Void) {

    que.append(operation)

}

enqueue{ print("실행") }

enqueue{ print("실행") }

que.forEach {$0()}


escaping속성이 지정되지 않는 폐쇄는 함수범위밖에서 유지할 수 없다. 다라서 폐쇄의 실행은 함수 범위내에서 이루어져야 한다. 아래 예는 execute1(operation:)함수에 전달된 폐쇄는 escaping속성이 지정되어 있지 않지만 함수범위내에서만 실행되기 때문에 오류가 발생하지 않는다.


func execute1(operation: () -> Void) {

    operation()

    operation()

}

execute1 { print("실행") }


autoclouse속성은 인수를 페쇄로 감싸서 지연평가를 실행하기 위한 속성이다. Bool형 인수를 2개가지고 그 논리합을 리턴하는 함수를 만들어 보자. 즉, 논리합을 요구하는 || 연산자와 동일한 동작을 함수를 구현한다.


func orExam(_ lhs: Bool, _ rhs: Bool) -> Bool {

    if lhs {

        print("참")

        return true

    } else {

        print(rhs)

        return rhs

    }

}

orExam(true, false) //true


만약 위에서 만든 함수의 두번째 인수에 각각 다른 함수의 리턴값을 보내는 방법을 생각해 보자. 아래 예는 첫번째 인수에는 lhs()함수 리턴값, 두번째 인수에는 rhs()함수 리턴값을 보낸다.


func orExam(_ lhs: Bool, _ rhs: Bool) -> Bool {

    if lhs {

        print("참")

        return true

    } else {

        print(rhs)

        return rhs

    }

}

func lhs() -> Bool {

    print("lhs() 함수 호출")

    return true

}

func rhs() -> Bool {

    print("rhs() 함수 호출")

    rerturn false

}

orExam(lhs(), rhs())


print(_:) 함수 출력결과에서 lhs(), rhs()함수들 모두실행된다. 프로그래밍 언어에서는 함수 인수가 그 함수에 넘겨지기 전에 실행되기 때문인데 이것을 보통 정격평가라고 말한다. 그러나 사실 두번째인수를 실행될 필요가 없다. 이유는 첫번째 인수가 true이기 때문에 두번째 인수를 실행하지 않고 결과 true로 결정하기 때문이다. 그렇다면 두번째 인수를 실행하지 않도록 변경해보자. 변경하는 방법은 두번째 인수에 () -> Bool형 폐쇄로 변경하면 두번째 인수가 필요한 시기에는 처음 폐쇄가 실행하고 두번째 Bool형 값을 가져온다.


func orExam(_ lhs: Bool, _ rhs: () -> Bool) -> Bool {

    if lhs {

       print("참")

       return true

    } else {

       let rhs = rhs()

       print(rhs)

       return rhs

    }

}

func lhs() -> Bool {

    print("lhs() 함수 호출")

    return true

}

func rhs() -> Bool {

    print("rhs() 함수 호출")

    return false

}

orExam(lhs(), { return rhs() })


위 소스를 실행하면 불필요한 함수호출이 안되는 것을 알 수 있다. 이처럼 두번째 인수를 폐쇄처리하여 필요할 때까지 평가를 지연시킬 수 있다. 이를 지연평가라고 한다. 위 소스코드 스타일은 불필요한 함수 실행을 막을 수 있다는 것이지만 호출하는 측이 복잡해지는 단점이 있다. 이런 단점을 보완하기 위해서는 autoclosure속성을 사용하면 해결된다. autoclosure속성은 인수를 폐쇄로 덮어버리는 처리를 암묵적으로 한다.


func orExam(_ lhs: Bool, _ rhs: @autoclosure () -> Bool) -> Bool {

    if lhs {

       print("참")

       return true

    } else {

       let rhs = rhs()

       print(rhs)

       return rhs

    }

}

func lhs() -> Bool {

    print("lhs() 함수 호출")

    return true

}

func rhs() -> Bool {

    print("rhs() 함수 호출")

    return false

}

orExam(lhs(), rhs())


즉, autoclosure속성을 이용하면 지연평가를 간단하게 해결할 수 있다.


트레일링 폐쇄(Trailing closure)는 함수의 마지막 인수가 폐쇄의 경우 폐쇄를 ()외부에 작성할 수 있다. exec1(parameter: handler:) 함수를 트레일링 폐쇄를 사용하는 방법과 사용하지 않는 방법을 만들어보자.


func exec1(parameter: Int, handler: (String) -> Void) {

    handler("인수 = \(parameter)")

}

// 트레일링 폐쇄를 사용하지 않는 경우

exec1(parameter: 10, handler: {s in

    print(s)

})

// 트레일링 폐쇄를 사용하는 경우

exec1(parameter: 20) { s in

    print(s)

}


일반적인 코딩에서는 함수호출()이 폐쇄후까지 진행되기 때문에 페쇄가 여러줄로 겹쳐있는 경우 가독성이 떨어진다. 반대로 트레일링 폐쇄를 사용하는 경우 ()는 폐쇄 정의앞에서 종료되기 때문에 가독성이 좋다.

또한 함수는 폐쇄의 일종이기 때문에 폐쇄로 처리한다. 함수를 폐쇄로 사용하려면 함수명만 식으로 함수를 참조한다. 함수를 폐쇄로 취급하여 함수를 변수나 상수에 할당하거나 다른 함수의 인수로 전달할 수 있다.


let 상수명 = 함수명


식에는 인수명까지 포함할 수 있고 이렇게 하면 인수명으로 여라가지 함수를 구별하는데 유용하다.


let 상수명 = 함수명(인수명1:인수명2:)


아래 예는 (Int) -> Int형 함수 2x(_:)에 f상수에 대입한다. f형은 (int) -> Int형으로 추론된다.


func 2x(_ x: Int) -> Int {

   return x * 2

}

let f = 2x 


함수 인자가 되는 폐쇄를 함수로 정의하여 중복 폐쇄를 하나로 통합하거나 폐쇄에 의미가 있는 이름을 지정할 수 있다. 우선 함수 인자가 되는 폐쇄를 함수로 정의하지 않는 경우를 생각해보자. 아래 예는 Array<Element>형의 map(_:)메소드 인수로 {$0*2}라는 폐쇄를 두번 사용한다. map(_:)메소드는 폐쇄로 지정하여 각각의 요소가 변환된 새 컬렉션을 리턴한다.


let arr1 = [1, 2, 3, 4]

let 2xArr1 = arr1.map { $0 * 2 } // [2, 4, 6, 8]


함수를 폐쇄로 처리하면 아래와 같이 구현할 수 있다.


func 2x(_ x: Int) -> Int {

    return x * 2

}

let arr1 = [1, 2, 3, 4]

let 2xArr1 = arr1.map(2x) // [2, 4, 6, 8]


함수를 폐쇄로 정의하여 자주사용할 수 있는 { $0 * 2 }부분을 하나로 처리할 수 있게 구현했다. 또한 2x라는 처리내용을 명칭으로 가독성을 높였다.

폐쇄식을 이용하면 복잡한 값의 초기화도 쉽게 파악할 수 있다. 예로 5 x 5칸을 표현하는 것을 구현한다고 보면 아래 예처럼 2차원 배열을 통해 각각 리터럴을 직접 정의하는 것을 많이 사용한다.


var a = [[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1]]


위와 같이 선언하게 되면 Array<Element>형 생성식이 중첩되기 때문에 구조를 파악하기 어렵다. 이를 간단하고 가독성이 좋은 코드로 구현하기 위해서 폐쇄식을 사용하여 일련의 초기화 절차 구현을 하나의 식으로 만들 수 있다.


var a: [[Int]] = {

    let side = 5

    let row = Array(repeating: 1, count: side)

    let x = Array(repeating: row, count: side)

    return x

}()

a



지금까지 함수를 이용하여 정의하는 부분과 호출자 모두에게 편리한 인터페이스를 제공하는 것이다. 예로 기본 인수를 사용하는 것으로 호출하는 측에서 인수 지정을 선택할 수 있다. 또한 인수명을 함수 내부와 외부로 구분하여 정의하는 것이 통해 바로 알 수 있는 이름을 사용하는 것을 추천한다.

페쇄는 처리변수와 상수에 할당하거나 함수로 전달할 수 있다. 함수에 전달되는 폐쇄 실행하는 특징을 이용하여 관리할 수 있다.

매거진의 이전글 3. Swift의 제어문
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari