1월 25일 TIL (Swift 기본 문법 정리)

이승원·2024년 1월 25일
0

TIL

목록 보기
9/75
post-thumbnail

반복문 ( repeat ~ while )

  • 다른 언어에서의 do ~ while 이랑 똑같은 구문이다.
  • repeat 안에 있는 구문을 우선적으로 1회 실행하고 while문을 실행한다.
	repeat {
		< 실행할 구문 >
	}
	while < 조건식 > 

조건문 ( guard )

  • guard는 if 문이랑 똑같이 주어진 조건을 만족하는지를 확인하는 조건문이지만, if 랑 다르게 조건이 참일때 실행되는 코드가 없다. 하지만 else는 필수다
  • guard를 사용하는 이유는 당연히 특정조건을 만족하지 않는 경우를 대비 하기 위해서 이지만, guard를 통해 심각한 오류가 발생할 경우를 대비해서 조기 종료를 위한 구문이라고 한다.
  • 간단한 예시를 들면, a / b 인경우, b가 0 인경우 에러로 인해 runtime error 가 발생하는데, 이를 대비 하기 위해 guard b == 0 else {}로 대비 할 수 있다.
	guard <조건식 또는 표현식> else {
    	<조건식의 또는 표현식의 결과가 false 일때 실행 될 코드>
    }

조건문 ( #available )

  • 이 구문은 평상시에는 사용되지 않을꺼 같은데, iOS 버전에 따라서 제공하는 API가 다르기 때문에 iOS 버전에 따라서 조건문을 수행하는 구문이다.
	if #available(<플랫폼 이름 버전>,<플랫폼 이름 버전>,<플랫폼 이름 버전>) {
    	<해당 버전에서 사용할 수 있는 API 구문>
    }else{
    	<API를 사용할 수 없는 환경에 대한 처리>
    }

조건문 ( switch )

  • switch 구문은 다른 언어에서 사용되는거랑 똑같은데, 신경써야 될꺼는 비교대상 즉 switch 뒤에 오는 변수의 타입이랑 같은 비교패턴을 갖는다는것이다.
	switch <비교 대상> {
    	case <비교 패턴1> :
        	<비교 패턴1이 일치했을 때 실행할 구문>
    	case <비교 패턴2>, <비교 패턴3> :
        	<비교 패턴2 또는 3이 일치했을 때 실행할 구문>
    	default : 
        	<어느 비교 패턴과도 일치하지 않았을 때 실행할 구문>
	}
  • 그리고 만약 비교대상이 1개 이상의 비교 패턴이랑 일치하고, 각 비교패턴마다 실행하고 싶은 구문이 있을때는 fallthrough를 사용한다.
	switch val {
    	case 10 / 5 :
        	print("10/5 와 값이 일치합니다.")
            fallthrough
    	case 2 :
        	print("2 와 값이 일치합니다.")
    	default : 
        	print("일치 하지 않습니다.")
	}
  • 예시가 조금 너무 억지인데, 어쨋든 이렇게 사용할 수 있다는것 정도는 알고 있자.

제어 전달문 (break, continue)

  • 이것도 다른 언어에서 사용했던것과 똑같은 구문인데 outer, inner 개념을 사용할 수 있다.
outer : for i in 1...3 {
    inner : for j in 1...3 {
        if (j==2) {
            continue outer
        }
        
        print("i = \(i) , j = \(j)")
    }
}

// i = 1 , j = 1
// i = 2 , j = 1
// i = 3 , j = 1
  • 이중 for 문을 실행할때, 특정 조건에서 밖에 for 문을 break 하던가 continue하고 싶을때가 있는데, 이중 for문을 outer:for {inner: for} 이런식으로 선언하면 continue break뒤에 outer, inner만 같이 써주면 된다.

InOut

  • 함수에 전달되는 인자값은 그 변수 자체가 아니라 변수의 값을 복사해서 전달되는 것이기 때문에 함수 내부에서는 수정이 안된다. 그러면 만약에 나는 어떤 함수가 외부에 있는 값을 수정하고 싶다면? 두가지 방법이 있다
  • InOut, 이 방법은 C언에서는 주소값 전달이랑 똑같다. 함수를 선언할때 매개변수 타입 앞에 inout를 붙이고, 함수를 호출할때 변수앞에 & 를 쓰면 된다.

	var value = 0 
    
	func increase(_ value : inout Int ) -> Int {
		value += value
        return value
	}
    
    print(increase(&value))		// 1
    print(value)				// 1
    
  • 아니면 단순하게 전역 범위로 변수를 설정하면 된다. (어떤 함수에서도 사용 가능하게 함수 밖에서 선언), 단 이건 당연히 문제가 있을 수 있다. 어느 함수에서도 접근이 가능할 수 있다는것은 그만큼 문제가 발생할 확률이 높아진다는 것이기 때문이다.

일급 함수 (First-class function)

  • 일급 함수는 아래의 특성을 갖는다.
    1. 함수를 변수에 할당할 수 있어야 한다.
    2. 함수를 다른 함수의 인자로 전달할 수 있어야 한다.
    3. 함수를 다른 함수의 반환값을 사용할 수 있어야 한다.
  • 일급 함수의 장점은 함수를 일반 값처럼 다룰 수 있기 때문에, 함수를 보다 유연하게 조합하고 활용할 수 있다는 것이다. 그럼 정확히 특성들이 어떤 의미를 갖는지 예시로 보자.

1. 함수를 변수에 할당 할 수 있어야 한다.

  • 아래와 같은 함수가 있다고 가정해보자.
func foo(base: Int) -> Int {
    print("함수 foo가 실행됩니다")
    return base + 1
}

let a = foo(5) // 출력결과 : "함수 foo가 실행됩니다") , a = 6
print(type(of:a)) // 출력결과 : Int
  • 지금 a 에서는 함수 foo에서 리턴값을 나온 "6"을 갖고 있다. 이건 함수를 변수에 할당하는것이 아니라, 함수의 반환값을 변수에 할당한 것이다.
let b = foo	// 출력 결과 없음
print(type(of: b)) // (Int) -> Int 함수가 변수에 할당됨. 
let c = b(5) // 출력결과 : "함수 foo가 실행됩니다") , c = 6
print(type(of: c)) // 출력결과 : Int
  • 위 코드처럼 foo 함수 자체를 b 에 할당했다. 그리고 b의 타입을 확인해보면 (Int) -> Int 증 foo 의 형태를 그대로 갖고 있다. c는 역시나 b, 즉 foo의 반환값 Int 타입을 갖고 있다.
  • 그러면 만약에 같은 이름을 갖은 함수가 있다면? Overloading

//1번 foo
func foo(base: Int) -> Int {
    return base + 1
}

//2번 foo
func foo(base: Int) -> String {
    return "\(base + 1)"
}

//3번 foo
func foo(base: Int, increase: Int) -> Int {
    return base + increase
}

let a = foo // 오류, 어떤 foo 함수인지 모르니깐!
  • 이런식으로 오류가 뜬다, 그러면 우리는 어떻게 해야할까? 두가지 방법이 있다.
  • 첫째는 타입 어노테이션을 통해 입력받을 함수의 타입을 지정해주는것이다.
  • 두번째는 함수의 식별값을 통해 입력값을 정확한 함수를 지정하는것이다.
// 타입 어노테이션
let a : (Int) -> Int = foo // 1번 foo
let b : (Int) -> String = foo // 2번 foo
let c : (Int,Int) -> Int = foo // 3번 foo

//함수식별값을 통해 함수 지정
let d = foo(base:) // 에러 Overloading으로 인한 충돌
let e = foo(base:) // 에러 Overloading으로 인한 충돌
let f = foo(base:increase:) // 3번 foo
  • 만약 반환값이 없다면? 아니면 인자값이 없다면?
func foo(){
    print (" 1 ")
}

let a : ()->() = foo // 정상 작동
let b : ()-> Void = foo // 정상 작동
let c : Void -> () = foo // 오류 Swift 4. 버전 이상부터는 해당 문법 오류

2. 함수를 다른 함수의 인자로 전달할 수 있어야 한다.

  • 이건 말 그대로 함수에서 인자로 함수를 전달할 수 있어야하는 특성이다. 아래 코드를 보자.
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
  • 복잡하게 보이지만 하나하나 보면 어렵지 않다. incr함수는 인자+1을 리턴하는 것이고, broker는 base 인자를 받고, 함수 (Int)->Int를 인자로 받고 있고, Int를 리턴한다. 즉 broker(base: 3, function: incr) 가 호출되면, broker함수에서 incr(base) 즉 incr(3)이 호출되고, incr(3)은 4를 리턴하고, 그 리턴값이 broker에 전달되고 다시 리턴하여 최종적으로 4(INT)를 리턴한다.

3.함수를 다른 함수의 반환값을 사용할 수 있어야 한다.

  • 이것도 위 코드에서 똑같이 적용할 수 있다
func incr(param: Int) -> Int {
    return param + 1
}

func broker(base: Int) -> Int {
    return incr(base)
}

broker(base: 3) // 4
  • 참 쉽죠잉?

함수의 중첩

  • 함수안에 다른 함수를 만들수 있는것인데, 굳이? 라는 생각이 처음에 들었지만, 은닉성 때문이라고 한다. 함수안에 함수가 있다면, 밖에 함수는 외부 함수, 안에 있는 함수는 내부 함수라고 하는데, 내부 함수는 외부함수를 통해야지만 접근, 호출할 수 있다. 바로 내부함수로 접근할 수가 없는 것이다. 예시를 통해 한번 보자.
func outer(_ base: Int) -> (Int) -> String {
    func inner(inc: Int) -> String {
        return "\(inc)를 반환합니다"
    }
    return inner
}

let fn1 = outer(3) // outer()가 실행되고, 그 결과로 inner가 대입됩니다
let fn2 = fn1(30) // inner(inc: 30)과 동일합니다
  • 여기서 깊게 생각해야할 점은, 함수의 생명주기 (Life Cycle)이다. 우리가 함수를 하나 부르면, 해당 함수가 더 실행이 되면, 해당 함수는 메모리에서 소멸되기 때문에, 만약 외부함수가 소멸되면 당연히 내부함수도 소멸되야하는거 아닌가? 생각할 수 있지만, let fn1 = outer(3) 때문에 내부 함수의 생명이 유지된다고 한다.
func basic(param: Int) -> (Int) -> Int {
    let value = param + 20
    
    func append(add: Int) -> Int {
        return value + add
    }
    
    return append
}

1. let result = basic(param: 10) 
2. result(10) // 40
  • 위 코드를 단순하게 단계별로 생각해보자, 1번 result에 basic(param:10)을 대입하면, basic 함수 안에서 value 는 30이 되고, append 내부 함수 리턴. 즉 현재 result 타입은 (Int)->Int
  • 그러면 result(10) 하면 에러가 떠야하는거 아닌가? value는 외부함수에 있고, 외부함수는 종료되었으니깐 없어진거 아니야? 하지만 아니다. 40이 return 된다. 이건 클로저(Closure) 때문이라고 한다.

클로저 (Closure)

  • 클로저는 Swift에서는 일회용 함수를 작성할 수 있는 구문이다. 클로저 객체는 대두분 아래 세 가지 경우 중 하나에 해당한다.
    1. 전역 함수 - 이름이 있으며, 주변 환경에서 캡처할 어떤 값도 없는 클로저
    2. 중첩 합수 - 이름이 있으며 자신을 둘러싼 함수로부터 캡처할 수 있는 클로저
    3. 클로저 표현식 - 이름이 없으며 주변 환경으로부터 값을 캡처할 수 있는 경량 문법으로 작성된 클로저

클로저 표현식

  • 클로저 표현식은 함수랑 다르게 , func 키워드과 함수명 생략한다. 그리고 아래와 같은 형식을 갖는다.
// 기본 형식
{ (매개변수) -> 반환 타입 in
	<실행할 구문> 
}

let a = { () -> Void in
	print("Hello World")
}

a() // 클로저 실행 --> 함수랑 똑같이 작동함. 

// 이것마저 생략할 수 있다.

({ () -> Void in
	print("Hello World")
})()

// 만약 매개변수가 있다면?
({ (a : Int) ->Void in
	print(a)
})(1) // 출력결과 1

클로저 경량 문법

  • 배열을 정렬할때 생각해보니깐, array.sort(by: {$0 > $1})썼던 경험이 있다. 이것도 당연히 클로저 인데 이걸 처음부터 어떻게 온건지 차근차근 확인해보자.
var array = [1,5,4,3,6,0,9]

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

array.sort(by: order) // 내림차순으로 정렬
  • 기본적으로 우리가 내림차순으로 정렬할려면 함수를 만들고, 해당함수를 sort(by:)메서드 인자로 넣는방식으로 하는데 이걸 한번 간소화 하면 아래와 같이 된다. 함수를 클로저 형태로 변환해서 굳이 함수를 안만들고 함수 안에서 클로저를 사용하는 방법이다.
array.sort(by: 
	{(s1:Int,s2:Int) -> Bool in
		if s1 > s2 {
        	return true
        }else{
        	return false
        }	
})
  • 자 그러면 이걸 더 경량화 한다면? 우리가 확인해야 하는 부분은 s1 이 s2 보다 큰지 확인 하는거니깐, 리턴값에 바로 s1>s2 하면 참이면 true 거짓이면 false를 리턴한다.
array.sort(by: 
	{(s1:Int,s2:Int) -> Bool in
		return s1 > s2
})
  • 이제는 타입 어노테이션을 제거 한다. 왜냐면 s1 > s2는 bool 리턴이라는걸 컴파일러가 알기 때문에 제거 해도 된다.
array.sort(by: {s1, s2 in return s1 > s2 })
  • 여기서 매개변수까지 제거할 수 있다. $0, $1 가 0번째 매개변수, 1번째 매개변수 뜻이니깐 그걸 써보면
array.sort(by: {return $0 > $1 })
  • 마지막으로 return까지 생략 가능하다.
array.sort(by: {$0 > $1 })
  • sort 함수 안에서는 클로저 표현식보다 더 짧게 표현할 수 있는 방법을 제공한다.
array.sort(by: >)
  • 이렇게 클로저 표현식을 쓰면 함수 + 함수 호출 을 단 한줄로 끝낼 수 있게 된다. 그렇다면 클로저를 쓴다면 성능적으로 개선이 되나? 그건 아니다. 클로저는 성능을 위한 기술이 아니라, 가독성과 유연성을 높이기 위한 기술이다. 만약에 코딩테스트를 하는데 클로저를 꼭 써야하는 상황이 아니고, 헷갈린다면 그냥 원래대로 해도 전혀 문제 없다. 다만 나중을 위해서라도 꾸준히 연습을 해보자.

트레일링 클로저 (Trailing Closure)

  • 트레일링 클로저는 함수의 마지막 인자값이 클로저 일때, 다르게 작성하는 방식을 말한다. 무조건 마지막 인자값이 클로저 일때만 가능하다는 점이 특징이다.
func divide(base: Int, success s: () -> Void) -> Int {
    defer {
        s() // 성공 함수를 실행한다
    }
    return 100 / base
}
//원래대로 라면
let test1 = divide(base: 100, success: {() in print("Success")})

// Trailing Closure 적용
let test2 = divide(base:100) { () in print("Success")}
profile
개발자 (진)

0개의 댓글