iOS/iOS 자료실

Swift 스터디. 4월 둘째주

Chipmunks 2018. 4. 15.
728x90

CHAPTER 5. 집단 자료형 : 연관된 데이터를 손쉽게 다루기

1. 배열

배열(Arrays)은 일련의 순서를 가지는 리스트 형식의 값을 저장하는 데에 사용되는 자료형

배열에서 인덱스는 순서대로 할당되며, 중간에 값을 생략하거나 건너뛰는 경우는 없다. 연결된 아이템이 삭제되더라도 인접한 다음 아이템들이 차례대로 앞으로 이동하며 빈 인덱스를 채워 놓음. 실제로 사라지는 인덱스는 가장 마지막 인덱스다.

배열 자료형의 특징
  • 저장할 아이템의 타입에 제약이 없음. 하나의 배열에 저장하는 아이템 타입은 모두 같아야 함
  • 선언 시 배열에 저장할 아이템 타입을 명확히 정의해야 함
  • 배열의 크기는 동적으로 확장할 수 있음

순회 방법

1. 상수 length에 할당하고 카운터 변수로 순회
1
2
3
4
5
6
var cities = ["서울""인천""부산"]
let length = cities.count
 
for i in 0..<length {
    print("\(i)번째 인덱스 : \(cities[i])")
}
cs


< 결과화면 >

1
2
3
0번째 인덱스 : 서울
1번째 인덱스 : 인천
2번째 인덱스 : 부산
cs


배열 크기를 상수에 할당한 이유

for ~ in 구문에 직접 넣으면, 매 반복문 실행 시 매번 배열의 크기를 다시 계산하므로 전체적인 실행 시간을 떨어트리는 원인이 된다. 배열의 크기를 한 번만 읽어 상수에 저장한 다음 쓰는 편이 좋다.


2. 배열값의 순회 특성을 사용하여 탐색

1
2
3
4
5
var cities = ["서울""인천""부산"]
 
for row in cities {
    print("배월 원소는 \(row)입니다")
}
cs

< 결과화면 >
1
2
3
0번째 인덱스 : 서울
1번째 인덱스 : 인천
2번째 인덱스 : 부산
cs


아이템을 통해 인덱스 값을 역으로 찾는 배열 메소드

배열.index(of: 아이템)

배열 아이템 동적 추가

  1. append(_:) : 입력된 값을 배열의 맨 뒤에 추가
  2. insert(_:at:) : 원하는 인덱스 위치에 넣고, 이를 기준으로 뒤 인덱스는 하나씩 다음으로 밀려난다.
  3. append(contentsOf:) : 배열의 맨 마지막에 '배열'을 추가한다. 메소드의 인자값은 배열이다.

공간 미리 할당하기

1
2
3
extension Array : RangeReplaceableCollection {
    public int(repeating repeatedValue: Element, count: Int)
}
cs


코코아 터치 프레임워크에서, 배열을 생성하는 여러 가지 방법 중 초기화할 때 배열의 크기를 지정할 수 있는 구문이다.


1
2
3
var cities = Array(repeating: "None", count: 3)
 
var cities = [String](repeating: "None", count: 3)
cs

범위 연사자를 이용한 인덱스 참조

1
2
3
4
5
var alphabet = ["a""b""c""d""e"]
 
alphabet[0...2// ["a", "b", "c"]
alphabet[2...3// ["c", "d"]
alphabet[1..<3// ["b", "c"]
cs



2. 집합

서로 다른 값을 중복 없이 저장하고자할 때 사용하는 집단 자료형
내부적으로 해시(Hash) 연산의 결과값을 이용해 데이터를 저장하기 때문에, 집합에 저장할 데이터 타입은 해시 연산을 할 수 있는 타입이어야 한다. 스위프트의 기본 타입은 기본적으로 해시 연산이 가능하다. 임의로 만든 타입으로 집합의 아이템으로 저장한다면, 스위프트 표준 라이브러리에서 제공하는 Hashable 프로토콜을 구현해야 한다. hashValue 라고 불리는 해시값을 만들어 낼 수 있는 기능을 정의해야 한다.

해시 연산 : 임의의 입력된 메시지를 고정 길이의 데이터 크기로 변환해주는 알고리즘. 아무리 긴 데이터나 짧은 길이의 데이터라 할지라도 고정 길이의 데이터를 변환한다. 입력 값이 조금이라도 변경되면 연산의 결과값도 달라진다. 이 때문에 데이터의 무결성 검증이나 메시지 인증 등에 사용된다. 해시 연산값을 이용한 자료 탐색은 현존하는 자료 탐색 기술 중 가장 빠른 기술이다. 메모리를 낭비한다는 단점에도 볼구하고 많이 사용된다.


집합의 정의

1
var genres : Set = ["클래식""락""발라드"]
cs


Set<데이터 타입>() 으로 데이터 타입을 명시적으로 지정해 줄 수 있다.


  1. insert(_:) : 집합에 데이터 추가. 이미 있는 경우에는 아무 처리도 하지 않는다.
  2. remove(_:) : 아이템을 삭제하고, 그 아이템을 반환한다. 없다면 nil을 반환한다.
  3. removeAll() : 집합의 모든 아이템을 삭제한다.
  4. contains(_:) : 해당 집합 내에 아이템이 있으면 true, 없다면 false를 반환한다.


집합 연산

1. intersection(_:) : 교집합
2. union(_:) : 합집합
3. subtract(_:) : 차집합
4. symmetricDifference(_:) : 합집합에서 교집합을 뺀 것


부분집합 포함관계 판단 연산

  1. isSubset(of_:) : 주어진 집합의 값 전체가 특정 집합에 포함되는지를 판단
  2. isSuperset(of:) : 주어진 집합이 특정 집합의 모든 값을 포함하는지를 판단
  3. isStrictSubset(of:), isStrictSuperset(of:) : 위 두 메소드오 차이점은 서로 일치하는 집합은 true로 반환 안함
  4. isDisjoint(with:) : 공통 값이 없을 때 판단


중복을 제거할 수 있다는 기능으로, 배열(Arrays)에서 중복된 값을 제거하고자 할 때, 집합으로 바꾼 뒤 다시 배열로 바꾼다.

: Array(Set(arr))

3. 튜플

하나의 튜플에 여러 가지 타입의 아이템을 저장할 수 있다. 선언 후 상수적 성격을 띠므로 더 이상 값을 추가하거나 삭제하는 등의 변경이 불가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let tupleValue = ("a""b"12.5true)
 
tupleValue.0
tupleValue.1
 
var tpl01 : (IntInt= (100200)
 
let (a, b, c, d, e) = tupleValue
print(a)
print(b)
 
func getTupleValue() -> (StringStringInt) {
    return ("t""v"100)
}
 
let (var1, var2, var3) = getTupleValue()
 
print(var1)
print(var2)
print(var3)
 
let (var4, var5, _) = getTupleValue()
print(var4)
print(var5)
 
cs


점(.) 연산자로 인덱스로 접근이 가능하다. 바인딩(binding) 기능으로 튜플 값을 각각의 변수로 넣어줄 수 있다.

이 기능은 함수가 여러 타입의 데이터들을 반환하고자 할 때 진가를 발휘한다.


순회 처리를 지원하지 않는다.

4. 딕셔너리

고유 키(Key)와 그에 대응하는 값(Value)을 연결하여 데이터를 저장하는 자료형이다.
  • 하나의 키는 하나의 데이터에만 연결된다.
  • 키는 중복될 수 없다. 중복 선언 시 기존 키에 연결된 데이터가 제거된다.
  • 저장할 수 있는 데이터 타입에는 제한이 없지만, 하나의 딕셔너리에 저장하는 데이터 타입은 모두 일치해야 한다.
  • 아이템에는 순서가 없지만 키에는 내부적으로 순서가 있으므로 for~in 구문으로 순회가 가능하다
  • 키의 타입은 거의 제한이 없으나 해시(Hash) 연산이 가능한 타입이어야 한다.

딕셔너리 선언

1
2
3
4
var capital1 = ["KR":"Seoul""EN":"London""FR":"Paris"]
var capital2 : [String : String= ["KR":"Seoul""EN":"London""FR":"Paris"]
var captial3 : Dictionary<StringString> = ["KR":"Seoul""EN":"London""FR":"Paris"]
 
cs


1. updateValue(<저장할 데이터>, forKey: <키>) : 해당 키에 데이터를 수정함. 또는 없다면 추가함

2. removeValue(forKey:) : 해당 키 데이터를 삭제하고 그 데이터를 반환. 없다면 nil을 반환


순회 탐색

1
2
3
4
5
6
7
8
9
10
11
var capital1 = ["KR":"Seoul""EN":"London""FR":"Paris"]
 
for row in capital1 {
    let (key, value) = row
    print("현재 데이터는 \(key) : \(value)입니다.")
}
 
for (key, value) in capital1 {
    print("현재 데이터는 \(key) : \(value)입니다.")
}
 
cs



CHAPTER 6. 옵셔널 : 스위프트가 잠재적 오류를 다루는 방법 

옵셔널(Optional)은 스위프트에서 도입한 개념으로 프로그램의 안정성을 높이고자 사용하는 개념.


값을 처리하는 과정에서 오류가 발생할 가능성이 있느 값을 옵셔널 타입이라는 객체로 감싼 후 반환하는 개념. 이를 옵셔널 래핑(Optional Wrapping)이라고 함.


 

1
2
3
let num = Int("123")        => 123
 
let num2 = Int("swift")        => nil
cs


문자열을 숫자로 바꾸는 경우이다. 첫 번째의 경우는 Int(문자열)로 성공적으로 반환하지만,

두 번째의 경우는 nil을 반환한다.


보통 다른 언어에서는 두 번째의 경우 예외를 발생시키거나, 자바스크립트의 경우 NaN을 반환한다. 그러나 스위프트는 언어의 안정성을 위해 가급적 오류를 발생시키지 않으려 한다. 따라서 '값이 없음'을 뜻하는 nil을 반환한다. ( 오브젝티브-C에서는 빈 메모리 주소를 가리켰다. )


스위프트에서 또 다른 안정성을 위해 nil 사용을 제약했다. 일반 데이터 타입에는 nil 값을 가질 수 없다.

nil 값을 저장하기 위해서 옵셔널 타입을 사용해야 한다. 오류가 발생할 가능성이 있다면, 타입을 옵셔널 타입으로 설정해야 한다.


옵셔널 타입이 실제로 가질 수 있는 값은 nil이 아닌 값과 nil 이다. 그리고 Optional() 으로 둘러싼 결과값이 나오게 된다.


Int("123") => Optional(123)

Int("swift") => nil


옵셔널 타입을 사용할 때 매 번 nil인지 아닌지 체크해줘야 한다.


선언

1
2
3
4
5
6
7
var optInt : Int?
var optStr : String?
var optDouble : Double?
var optrr : [String]?
var optDic : Dictionary<StringString>?
var optDic2: [String:String]?
var optClass : AnyObject?
cs


옵셔널 값 처리

옵셔널 타입은 그 자체로 연산이 불가능함. 옵셔널 타입을 해제한 다음 연산을 해야 함.

명시적 해제와 묵시적 해제로 나뉨.

명시적 해제는, 강제적인 해제와 비강제적인 해제로 나눌 수 있다.
묵시적 해제는, 컴파일러에 의한 자동 해제와 ! 연산자를 사용한 자동 해제로 나뉜다.

옵셔널 강제 해제 (Forced Unwrapping)

1
2
3
4
5
6
7
var optInt : Int= 3
 
print("옵셔널 자체의 값 : \(optInt)")
print("!로 강제 해제한 값 : \(optInt)")
 
옵셔널 자체의 값 : Optional(3)
!로 강제해제한 값 : 3
cs


그러나 nil 타입에 강제 연산자를 붙여버리면 오류가 발생한다. 옵셔널 변수나 상수를 안전하게 사용하려면 조건문으로 꼭 확인을 해주어야 한다.


1
2
3
4
5
6
7
8
var str = "123"
var intFromStr = Int(str)
 
if intFromStr != nil {
    print("값이 변환됨. 변환 값 : \(intFromStr!)")
else {
    print("값 변환 실퍠")
}
cs


intFromStr 과 != 연산자 사이에 문법의 오류를 막기 위해 꼭 공백이 있어야한다.


(intFromStr)! = (nil)

(intFromStr!) = (nil)


으로 모호하므로, 컴파일러는 구문 분석 오류를 발생한다.


옵셔널 바인딩

if 구문 내에 조건식 대신, 옵셔널 타입의 값을 일반 타입의 변수나 상수에 할당하는 구문으로 옵셔널 바인딩(Optional Binding)이라고 한다. 반드시 조건문에서 사용해야 한다.

1
2
3
4
5
6
var str = "Swift"
if let intFromStr = Int(str) {
    print("값이 변환됨. 변환 값 : \(intFromStr)")
else {
    print("값 변환 실패")
}
cs


1
2
3
4
5
6
7
8
9
func intStr(str:String) {
 
    guard let intFromStr = Int(str) else {
        print("값 변환 실패")
        return
    }
 
    print("값 변환 됨. 변환 값 \(intFromStr)")
}
cs


guard 구문을 이용해 옵셔널 바인딩을 구현했다. guard 구문 특성상 함수나 메소드에만 사용한다. 앱을 만드는 과정에서 거의 대부분 함수로 이루어지기 때문에 guard 구문을 사용할 기회가 많다.


guard 구문은 함수를 종료시키는 특성이 있어, 옵셔널 값이 반드시 해제되어야 할 때 사용한다.

옵셔널 타이비지만, 절대 nil 값이 들어오지 않을 것이라는 보장이 있을 때는, 강제 해제 연산자 ! 를 사용하는 것이 더 효율적이다.


1
2
3
4
var capital = ["KR""서울""EN""런던""FR""파리"]
 
print(capital["KR"])    // Optionl("서울")
print(capital["KR"]!)   // 서울
cs


컴파일러에 의한 옵셔널 자동 해제

컴파일러가 자동으로 해제해주어, 굳이 개발자가 해제하지 않아도 될 때가 있다. 바로 옵셔널 객체의 값을 비교 연산자를 이용하여 비교하는 경우가 그에 해당된다. 명시적으로 옵셔널 객체를 강제 해제하지 않아도 한쪽이 옵셔널, 다른 한쪽이 일반 타입이라면 자동으로 옵셔널 타입을 해제하여 비교 연산을 수행함.

옵셔널의 묵시적 해제

묵시적 해제(Implicitly Unwrapped Optional)은 값을 사용하려고 할 때에 자동으로 옵셔너리 해제된 값을 제공하기 때문에 굳이 ! 연산자를 사용하여 해제할 필요가 없는 편리한 구문이다.

선언 시 ? 대신 ! 를 붙여준다.

1
2
3
4
5
var strOpt : String= "Swift Optional"
var str : String! = "Swift Optional"
 
print(strOpt) => Optional("Swift Optional")
print(str) => Swift Optional
cs

! 선언에 nil 을 대입해도 아무런 문제가 없다. print() 함수 시 nil 이 할당된 변수를 출력하려고 시도해서 발생하는 오류일 뿐이다.


묵시적 옵셔널은 컴파일러에 의해 자동으로 옵셔널이 해제된다. 따라서 nil 이 대입되어 있다면 오류를 피할 수가 없다. 이는 옵셔널 타입을 사용하는 메리트가 없어진다. 형식상 옵셔널로 정의해야 하지만, 실제로 사요할 때 절대 nil 값이 대입될 가능성이 없는 변수일 때 사용하면 편리하다.


var value : Int! = Int("123")


Int(문자열)이 반환하는 값이 옵셔널 타입이기 때문에, 어쩔 수 없이 value 변수를 옵셔널 타입으로 선언한다. Int("123")은 누가봐도 정수로 반환될 것이기 때문에, 묵시적 해제를 써준다.


또 다른 경우에는 클래스 또는 구조체 내에서다. 주로 멤버 변수를 정의할 때, 선언과 초기화를 분리시켜줘야 하는 경우다.


옵셔널의 강점은 안전성뿐 아니라 안전성을 담보하는 과정에서 표현되는 코드의 간결성이다.

옵셔널 체인(Optional Chain)으로, 코드를 간결하게 만들 수 있다.


1
2
3
4
5
if (myDelegate != nil) {
    if ([myDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
        [myDelegate scrollViewDidScroll:myScrollView];
    }
}
cs


1
myDelegate?.scrollViewDidScroll?(myScrollView)
cs


CHAPTER 7 : 함수 : 함수가 갑입니다

함수의 장점

  • 동일한 코드들을 함수 호출만으로 재사용
  • 기능 단위로 함수화 시 가독성이 조항지고, 코드와 로직을 이해하기 쉬움
  • 비즈니스 로직을 변경할 때, 함수 내부만 수정하면 되므로 유지보수가 용이

함수 정의 방법

1
2
3
4
func 함수 이름(매개변수1 : 타입, 매개변수2 : 타입, ...) -> 반환 타입 {
    실행 내용
    return 반환값
}
cs


함수 네이밍 시, 숫자는 자제한다.


함수 호출 방법

함수 이름(매개변수1: 값, 매개변수2: 값, ...)

1
2
3
4
5
6
7
func times(x:Int, y:Int-> Int {
    return x*y
}
 
times(x: 5, y: 10)  // 함수의 이름만으로 호출한 구문
times(x:y:)(510)  // 함수의 식별자를 사용하여 호출한 구문
 
cs


함수의 반환값과 튜플

앞서 살펴본 것처럼, 함수 반환값으로 튜플을 반환해 줄 수 있다.

특정 튜플 타입이 여러 곳에서 사용될 경우 타입 알리어스를 통해 새로운 축약형 타입을 정의하는 것이 좋다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typealias infoResult = (Int, Character, String)
 
func getUserInfo() -> infoResult {
    let gender : Character = "M"
    let height = 180
    let name = "다람쥐"
    
    return (height, gender, name)
}
 
let info = getUserInfo()
 
info.0
info.1
info.2
 
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typealias infoResult = (h: Int, g: Character, n: String)
 
func getUserInfo() -> infoResult {
    let gender : Character = "M"
    let height = 180
    let name = "다람쥐"
    
    return (height, gender, name)
}
 
let info = getUserInfo()
 
info.0
info.1
info.2
 
info.h
info.g
info.n
 
cs


매개 변수

함수의 매개변수는 내부 매개변수와 외부 매개변수가 있다.
  • 내부 매개변수 : 함수가 내부적으로 인자값을 참조하기 위해 사용하는 변수.
  • 외부 매개변수 : 함수 외부에서 함수나 인자값을 구분하기 위해 사용하는 변수.

1
2
3
4
func 함수이름(<외부 매개변수명> <내부 매개변수명> : <타입><외부 매개변수명> <내부 매개변수명> : <타입> ...) {
    // 함수 내용
}
 
cs


1
2
3
4
5
6
func printHello(to name : String, welcomeMessage msg : String) {
    print("\(name)님, \(msg)")
}
 
printHello(to: "홍길동", welcomeMessage: "안녕하세요.")
 
cs


1
2
3
4
5
6
func printHello(_ name : String, _ msg : String) {
    print("\(name)님, \(msg)")
}
 
printHello("홍길동""안녕하세요.")
 
cs


외부 매개변수 자리에 언더바(_)를 넣어주면 함수 호출 시 매개변수를 사용하지 않아도 된다.


가변 인자

1
2
3
4
5
6
7
8
9
10
11
func avg(score : Int...) -> Double {
    var total = 0
    for r in score {
        total += r
    }
    
    return (Double(total) / Double(score.count))
}
 
print(avg(score: 10203040))
 
cs


< 실행 결과 >

1
2
25.0
 
cs


기본값을 갖는 매개변수

1
2
3
4
5
6
7
8
9
10
func echo(message : String, newline : Bool = true) {
    if newline {
        print(message, true)
    } else {
        print(message, false)
    }
}
 
echo(message: "안녕하세요.")
echo(message: "안녕하세요.", newline: false)
cs


< 실행 결과 >

1
2
안녕하세요. true
안녕하세요. false
cs


매개변수의 수정

스위프트에서 매개변수는 상수이다. 값을 변경할 수가 없다.
함수 내부에서 var 변수로 상수를 저장하여, 함수 내부에서 사용해야 한다.

InOut 매개변수

함수 내부는 함수 외부에 영향을 미칠 수 없다.

1
2
3
4
5
6
7
8
9
10
11
var cnt = 30
 
func autoIncrement(value : Int-> Int {
    var value = value
    value += 1
    
    return value
}
 
print(autoIncrement(value: cnt)) // 31
print(cnt) // 30
cs


inout 키워드로 함수 외부에 영향을 미칠 수 있다. 값 자체를 전달하는 것이 아니라, 값이 저장된 메모리 주소를 전달한다는 의미다. 인자값에는 주소 추출 연산자 &를 붙여주어야 정상적으로 전달이 가능하다.


& 연산자는 C언어와 유사하고, inout은 포인터 개념과 유사하다.


매개변수 뒤에 클론(:)과 'inout'  키워드를 붙인다.


1
2
3
4
5
6
7
8
func foo(paramCount:inout Int-> Int {
    paramCount += 1
    return paramCount
}
 
var count = 30
print(foo(paramCount: &count))  // 31
print(count)    // 31
cs


이처럼 주소를 전달하는 것을 프로그래밍 용어로 '참조(Reference)에 의한 전달'이라고 한다. 기존처럼 값을 복사하여 전달하는 것을 '값(Value)에 의한 전달'이라고 한다.


값에 의한 전달과 참조에 의한 전달

값에 의한 전달 : 인자 값의 수정이 발생하더라도 원본 데이터에는 영향을 미치지 않는다.

참조에 의한 전달 : 외부 인자값 원본에도 영향을 미친다. 스위프트에서 inout 키워드를 사용한다. 예외적으로 클래스(Class)로 구현된 인스턴스는 inout 키워드를 사용하지 않아도 항상 참조에 의해 전달된다. 따라서 함수 내부에서 값이 수정되면 원본 객체에도 영향을 미치므로 주의해야 한다. inout 키워드를 붙인 매개변수에 상수, 리터럴 상수를 넣어서는 안된다.

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

스코프(Scope) : 변수가 생존할 수 있느 범위이다.
범위를 기준으로, 전역(Global) 변수와 지역(Local) 변수로 나뉨.

전역 변수는 프로그램 내 모든 위치에서 참조가 가능하고, 프로그램이 종료되기 전까지 특별한 경우를 제외하고 삭제되지 않는다.

로컬 변수는 특정 범위 내에서만 참조하거나 사용할 수 있다. 조건절, 함수 구문 등 특정 실행 블록 낸부에서 선언된 변수는 모두 지역변수이다. 그 블록이 끝나면 제거된다. 이를 변수의 생명 주기(Life Cycle)ㅣㅇ라고 한다.

컴파일러가 변수를 찾는 순서

  1. 함수 내부에서 정의된 변수를 찾음
  2. 하수 외부에서 정의된 변수를 찾음
  3. 글로벌 범위에서 정의된 변수를 찾음
  4. import 된 라이브러리 범위

일급 함수

스위프트는 객체지향 언어이자 동시에 함수형 언어이다. 함수형 언어 시 일급객체(First-Class Object)라는 용어를 접한다.

객체가 다음의 조건을 만족하는 경우, 이 객체를 일급 객체로 간주한다.
  • 객체가 런타임에도 생성이 가능
  • 인자값으로 객체를 전달 가능
  • 반환값으로 객체를 사용 가능
  • 변수나 데이터 구조 안에 저장 가능
  • 할당에 사용된 이름과 관계없이 고유한 구별이 가능

1. 변수나 상수에 함수를 대입할 수 있음

1
2
3
4
5
6
func foo(base:Int-> String {
    return "결과값은 \(base + 1)입니다"
}
 
let fn1 = foo
print(fn1(5))
cs


변수나 상수에 함수 대입시, 함수 타입(Function Type)을 가진다.


함수 타입은 일반적으로 함수의 형태를 축약한 형태로 사용

( 인자타입1, 인자타입2, ...) -> 반환 타입


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func boo(age : Int-> String {
    return "\(age)"
}
 
func boo(age : Int, name : String-> String {
    return "\(name)의 나이는 \(age)세 입니다."
}
 
let s : (IntString-> String = boo               // 함수의 이름
let s2 : (IntString-> String = boo(age:name:)   // 함수의 식별자
 
// let t = boo (오류)
let t1 : (IntString-> String = boo
let t2 = boo(age:name:)
let t3 : (Int-> String = boo
cs


함수 타입을 표시할 때, 인자값이 없거나 반환값이 없는 경우 빈 괄호를 사용하는 대신 Void를 사용해 명시적으로 "값이 없음"을 표시한다. Void는 빈 튜플 값을 나타내는 값으로 타입 알리어스로 정의된 단어다.


public typealias Void = ()


() -> String ==> (Void) -> String

Int -> () ==> (Int) -> Void

() -> () ==> (Void) -> Void


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

1
2
3
4
5
6
7
8
9
10
func desc() -> String {
    return "this is desc()"
}
 
func pass() -> () -> String {
    return desc
}
 
let p = pass()
p()
cs


< 실행 결과 >

1
"this is desc()"
cs


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

흔히 자바스크립트 같은 웹 프로그래밍 언어에서 콜백 함수(Callback Function)이라고 한다. 인자값으로 함수로 넘겨줄 수 있다.

1
2
3
4
5
6
7
8
9
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) // 4
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func successThrough() {
    print("연산 처리가 성공했습니다.")
}
 
func failThrough() {
    print("처리 과정에 오류가 발생했습니다.")
}
 
func divide(base : Int, success sCallBack : () -> Void, fail fCallBack: () -> Void-> Int {
    
    guard base != 0 else {
        fCallBack() // 실패 함수 실행
        return 0;
    }
    
    defer {
        sCallBack() // 성공 함수 실행
    }
    
    return 100 / base
}
 
 
divide(base: 30, success: successThrough, fail: failThrough)
cs


defer 블록은 함수나 메소드에서 코드의 흐름과 상관없이 가장 마지막에 실행되는 블록이다.

지연 블록이라고 부르기도 한다. 작성된 위치에 상관없이 항상 함수의 종료 직전에 실행된다. 종료 시점에 맞추어 처리해야 할 구문이 있다면 어디에 작성해야 할지 고민하지 않고 defer 블록에 넣어두면 된다.


사용된 각종 리소스의 처리나 해제, 연결 종료 등의 구문을 처리하는 용도로 유용하게 사용된다.

다음은 defer 블록의 특성이다.


  1. 작성된 위치와 순서에 상관없이 함수가 종료되기 직전에 실행된다.
  2. 블록을 읽기 전에 함수의 실행이 종료될 경우 defer 블록은 실행되지 않는다.
  3. defer 블록을 여러번 사용할 수 있다. 가장 마지막에 작성된 defer 블록부터 역순으로 실행된다.
  4. defer 블록을 중첩해서 사용할 수 있다. 바깥쪽 defer 부터 실행되며 가장 안쪽에 있는 defer 블록은 가장 마지막에 실행된다.

인자값으로 재사용하지 않는 코드를 함수로 만들어 대입하는 것은 번거로운 일이다. 이를 해결하고자 함수형 언어에서는 익명 함수를 지원한다. 일회용 함수라고 생각하면 된다.

스위프트도 지원하며, 이를 클로저(Closure)라고 부른다.

1
2
3
4
5
6
7
8
9
10
divide(base: 30,
       success: {
            () -> Void in
            print("연산 처리 성공!")
        },
       fail: {
            () -> Void in
            print("연산 처리 실패..")
        }
)
cs


자바(Java) 같은 경우, 일급 함수를 다루지 않으므로, 함수나 메소드를 직접 전달하지 못한다. 이 때문에 Invoke라는 번거로운 방식을 채택했었고 최근에는 Java 8 을 기준으로 람다 기능이 추가되어 편해지긴 했다. 그러나 스위프트에 비해 번거로운건 마찬가지다.


함수의 중첩

중첩 함수(Nested Function), 외부 함수(Outer Function), 내부 함수(Inner Function)

함수 내에 작성할 수 있는 내부 함수의 수는 제한이 없다.
내부 함수는 외부 함수가 실행되는 순간 생성되고, 종료되는 순간 소멸한다.
외부 함수는 프로그램이 실행될 때 생성되고 프로그램이 종료될 때 소멸한다.
외부 함수가 종료되면 내부 함수도 더는 존재하지 않는다. 이것이 내부 함수의 생명 주기(Life Cyle)다.

내부 함수는 일반적으로 외부 함수를 거치지 않으면 접근할 수 없다. 이를 함수의 은닉성이라고 한다. 중첩된 함수는 함수의 은닉성을 높여준다.


1
2
3
4
5
6
7
8
9
10
11
12
// 외부 함수
func outer(base : Int-> String {
    // 내부 함수
    func inner(inc : Int-> String {
        return "\(inc)를 반환"
    }
    
    let result = inner(inc: base + 1)
    return result
}
 
outer(base: 3)
cs


< 결과 값 >

1
"4를 반환"
cs


일반적으로 함수는 자신을 참조하는 곳이 있으면 생성되었다가 참조하는 곳이 사라지면 제거되는 생명 주기를 가진다. 참조 카운트와 관련이 있다. 참조 카운트가 0에서 1이 되는 순간 생성되어, 1 이상인 동안 유지되어 0이 되면 소멸한다. 내부 함수는 참조할 수 있는 곳이 외부 함수이므로, 외부 함수에 의존한다.


내부 함수를 반환값으로 제공할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 외부 함수
func outer(param : Int-> (Int-> String {
 
    // 내부 함수
    func inner(inc : Int-> String {
        return "\(inc)를 리턴"
    }
 
    return inner
 
}
 
let fn1 = outer(param: 3// outer() 실행, 그 결과로 inner
let fn2 = fn1(30// inner(inc: 30)
cs


이제 내부 함수를 외부에서도 접근할 수 있게 됐다. outer(param: 3) 구문이 실행한 후 이제 외부 함수의 참조 카운트가 0이 되었기 때문에 소멸한다. 그러나, 내부 함수인 inner은 상수 fn1에 참조되었으므로 참조 카운트가 존재한다. 내부 함수 inner 혼자만 살아 남은 셈이다.


만약 내부 함수에 외부 함수의 지역 상수, 또는 지역 변수가 참조되면 어떤 일이 일어날까


1
2
3
4
5
6
7
8
9
10
11
12
13
14
func basic(param: Int-> (Int-> Int {
    let value = param + 20
    
    func append(add : Int-> Int {
        return value + add
    }
    
    return append
}
 
let result = basic(param: 10)    // 1
result(10)    // 2
 
// 40
cs


 basic 외부 함수가 사라져도, 그 안에 있는 지역 변수 value 는, 여전히 내부 함수 append 에서 사용하고 있다! 이는 클로저(Closure) 때문이다. append 함수가 클로저를 갖기 떄문이다.


  • 클로저는 두 가지로 이루어진 객체다. 하나는 내부 함수이며 또 다른 하나는 내부 함수가 만들어진 주변 환경이다.
  • 클로저는 외부 함수 내에서 내부 함수를 반환하고, 내부 함수가 외부 함수의 지역 변수나 상수를 참조할 때 만들어진다.

클로저란 내부 함수와 내부 함수에 영향을 미치는 주변 환경(Context)를 모두 포함한 객체다.

주변 환경은, 내부 함수에서 참조하는 모든 외부 변수나 상수의 값, 내부 함수에서 참조하는 다른 객체까지 말한다. 이를 문맥(Context)라고 한다.


내부 함수를 둘러싼 주변 환경 객체가 값으로 바뀌어 저장된다. 클로저에 의해 내부 함수 주변의 지역 변수나 상수도 함께 값이 저장된다. 이를 캡처(Capture) 되었다라고 한다. 변수나 상수의 타입이 기본 자료형이나 구조체 자료형일 때 발생한다.


1
2
3
4
5
6
7
8
9
10
11
12
let result = basic(param: 10)    // 1
let result2 = basic(param: 15)    // 2
 
// result1에 할당된 클로저 정의
func append(add : Int-> Int {
    return 30 + add
}
 
// result2에 할당된 클로저 정의
func append(add : Int-> Int {
    return 35 + add
}
cs


클로저

앞서 말한 클로저는 많은 함수형 언어에서 공통으로 가지는 소프트웨어 아키텍처적인 개념이다.
스위프트는 클로저를 일회용 함수로 작성할 수 있는 구문으로 사요한다. 함수의 이름을 작성하지 않아도 되어 익명(Anonymous) 함수라고 부른다.

다양한 언어에서 지원하는 일회용 함수

Objective-C : 블록(Block)
자바스크립트 : 익명(Anonymous) 함수
자바 : 람다(Lambda) 함수
파이썬 : 람다(Lambda) 함수
Lisp : 람다(Lambda) 함수
스위프트 : 클로저(Closure)


스위프트에서 클로저는 다음 세 가지 경우 중 하나에 대부분 해당된다.


1. 전역 함수 : 이름이 있으며, 주변 환경에서 캡처할 어떤 값도 없는 클로저

2. 중첩 함수 : 이름이 있으며 자신을 둘러싼 함수로부터 값을 캡처할 수 있는 클로저

3. 클로저 표현식 : 이름이 없으며 주변 환경으로부터 값을 캡처할 수 있는 경량 문법으로 작성된 클로저

클로저 표현식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{ (매개변수) -> 반환 타입 in
    실행할 구문
}
 
let f = { () -> Void in
    print("클로저 실행")
}
f()
 
let c = { (s1:Int, s2:String-> Void in
    print("s1:\(s1), s2:\(s2)")
}
c(1"closure")
 
({ (s1:Int, s2:String-> Void in
    print("s1:\(s1), s2:\(s2)")
})(1"closure")
cs


클로저 표현식과 경량 문법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var value = [195732]
 
func order(s1: Int, s2: Int-> Bool {
    if s1 > s2 {
        return true
    }
    
    return false
}
 
value.sort(by: order)
// [9, 7, 5, 3, 2, 1]
 
value.sort(by: {
    (s1:Int, s2:Int-> Bool in
    if s1 > 2 {
        return true
    }
    
    return false
})
 
value.sort(by: {(s1:Int, s2:Int-> Bool in return s1 > s2})
 
value.sort(by: {(s1:Int, s2:Intin return s1 > s2 })   // 반환값 추론
 
value.sort(by: { s1, s2 in return s1 > s2 }) // 인자값도 추론
 
value.sort(by: { $0 > $1 }) // 매개변수 생략, in 키워드 생략, return 구문 생략
 
value.sort(by: >// 연산자 함수(Operator Functions)
cs


'iOS > iOS 자료실' 카테고리의 다른 글

[오류] Could not insert new outlet connection 해결법  (2) 2018.08.03
Swift(iOS) 스터디. 5번째 SQLite  (0) 2018.06.03
Swift 스터디. 4번째  (0) 2018.05.19
Swift 스터디. 3번째  (0) 2018.05.19
Swift 스터디. 4월 첫째주  (0) 2018.04.08

댓글