호댕의 iOS 개발

[Swift] for VS forEach 본문

Software Engineering/Swift

[Swift] for VS forEach

호르댕댕댕 2022. 1. 13. 18:16

개발을 처음 접하다보면 거의 제일 먼저 만나는 것이 반복문이다. 

 

Swift에서도 While, repeat-while, for, forEach가 반복문으로 존재한다. 

 

오늘은 이런 반복문 중에서 for-inforEach의 차이에 대해 살펴 보고자 한다. 

 

성능상 차이 

일단 둘의 성능 차이는 거의 없다고 볼 수 있다.

실제로 Swift Github에 들어가서 확인해보면 forEach의 내부 구현이 for-in으로 구현되어 있다는 것을 볼 수 있다. 

@_inlineable
  public func forEach(
    _ body: (Element) throws -> Void
  ) rethrows {
    for element in self {
      try body(element)
    }
  }

즉 for-in과 forEach가 성능상으로 유의미한 차이는 없다는 것을 알 수 잇다.

고차함수를 공부하면서 forEach를 사용해서 그런지 forEach가 막연하게 성능이나 이런 것들이 좋지 않을까 생각했었는데 전혀 아니었다. 

 

그렇다면 이 둘은 어떤 차이가 있을까? 

 

용법, 가독성의 차이

일단 forEach의 공식 문서를 보면 아래의 2가지와 같은 큰 차이점이 있다고 하고 있다. 

  1. You cannot use a break or continue statement to exit the current call of the body closure or skip subsequent calls.
  2. Using the return statement in the body closure will exit only from the current call to body, not from any outer scope, and won’t skip subsequent calls.

즉, for-in의 경우 return을 하게 되면 아예 해당 코드 블럭이 종료가 되지만 forEach의 경우 return을 하게 되면 현재 바디에서 호출한 코드블럭만 중단되고 다시 반복문이 돌게 된다. 

 

따라서 어떻게 보면 forEach를 쓰면 코드를 중간에 중단할 수 없이 끝까지 실행되는 것을 보장하게 되는 것이다. 

 

코드를 보며 어떻게 실행되는지 좀 더 자세하게 살펴보자. 

 

let numbers: [String?] = ["1", "2", nil, "3"]

numbers.forEach { number in
    print("\(String(describing: number)) start")
    guard number != nil else {
        print("\(String(describing: number)) in guard")
        return
    }
    print("\(String(describing: number)) end")
    print("----------")
}

위 코드를 보면 numbers의 element인 number가 nil이게 되면 guard else 문의 코드 블럭에 있는 코드가 실행이 되게 될 것이다. 

이때 return이 있더라도 코드블럭 전체의 실행이 종료되는 것이 아니라 현재 호출했던 하나의 반복만 끝나고 다시 반복문이 실행되게 된다. 

즉 number가 nil일때는 guard-else 문 내에 있는 코드가 실행되고 밑에 있는 코드는 실행되지 않은 것을 확인할 수 있지만 반복문의 실행 자체가 종료되진 않았다. 

 

그럼 for문을 사용했을 때는 어떨까?

let numbers: [String?] = ["1", "2", nil, "3"]

for number in numbers{
    print("\(String(describing: number)) start")
    guard number != nil else {
        print("\(String(describing: number)) in guard")
        return
    }
    print("\(String(describing: number)) end")
    print("----------")
}

로직 자체는 동일한 코드를 for문으로 사용했다. 

이때는 nil이 나온 시점에 for문의 코드 블럭 실행 자체가 종료되게 된다. 

 

만약 for문을 forEach처럼 실행시켜주고 싶다면 return을 continue로 바꿔주면 되긴 한다. 

let numbers: [String?] = ["1", "2", nil, "3"]

for number in numbers{
    print("\(String(describing: number)) start")
    guard number != nil else {
        print("\(String(describing: number)) in guard")
        continue
    }
    print("\(String(describing: number)) end")
    print("----------")
}

 

약간 내용이 다른 쪽으로 가긴 했는데 위 실험을 하다 생긴 궁금증이라 정리해본다.

그럼 break와 return을 사용했을 때의 차이는 뭘까?

 

func forInTest() {
    for number in numbers{
        print("\(String(describing: number)) start")
        guard number != nil else {
            print("\(String(describing: number)) in guard")
            return // break로 바꾸면?
        }
        print("\(String(describing: number)) end")
        print("----------")
    }
    print("after break")
}

만약 이렇게 return을 중간에 사용한다면 nil이 나올 때 함수의 실행 자체가 종료된다. 

하지만 위 코드를 break로 바꾸게 되면 어떨까?

현재의 for문의 코드 블럭 자체는 종료되지만 함수 전체는 종료되지 않아 마지막에 작성한 print("after break")는 실행되게 된다. 

 

 

그럼 다시 본론으로 돌아가보자. 

for문과 forEach문의 차이는 무엇일까?

for 문 forEach문
1. 중간에 return이 있다면 해당 함수 자체가 종료된다. 
2. continue나 break를 사용할 수 있다. 
중간에 return이 있더라도 해당 반복만 종료되고 반복이 끝까지 된다.
2. continue, break를 사용할 수 없다.  

만약 forEach에서 continue나 break를 사용하면 다음과 같은 에러를 만나게 된다.

즉 공식문서의 설명처럼 

Calls the given closure on each element in the sequence in the same order as a for-in loop.

for-in loop처럼 동작하긴 하나 loop는 아닌 것으로 확인할 수 있다. 

 

 

가독성의 경우 사람마다 기준이 다를 수 있겠지만 나의 경우 아직은 for문이 더 가독성이 좋다고 판단된다. 

따라서, 대부분의 경우 for문을 사용하는 것이 더 나을 수 있겠다는 결론을 내렸다. 

Comments