Swift 공부하면서 새롭게 알게된 내용 정리

sorted VS. sort

“자바스크립트 같은 스크립트 언어를 기존에 써왔던 사람이라면 Swift라는 언어는 비교적 쉽게 배울수있는 것 같다.” 라고 생각했다가 뒤통수 맞는 것들이 몇가지 있는데 그중에 하나가 바로 sorted 함수와 sort 함수다. 둘의 차이는 정렬을 해서 반환값이 있냐? 없냐의 차이!

mutating func sort(isOrderedBefore: (T, T) -> Bool)
func sorted(isOrderedBefore: (T, T) -> Bool) -> [T]

함수 시그니처만 봐도 sort는 반환값이 없다. -_-;;

참고, Javascript 에서 sorted 함수는 당연히 없고, sort 함수는 본래 배열을 정렬시키고 정렬된 값을 slice해서 새로운 값으로 반환한다. 즉, Swift의 sort 와 sorted 두가지 역할을 모두 수행한다.

극강의 타입 추론 능력!

Swift 의 또 다른 매력이라고 하면 타입 추론을 빼놓을수없다. 얼마나 똑똑한 언어인지 개발자의 귀차니즘을 많이 줄여준다(?) 하지만 단점은 이게 뭔소린가하는 느낌을 줄정도다. 하지만 타입 추론 방식을 좀 이해하고 나니 지금은 꾀나 익숙해졌다.

var arr = [6,2,1,2,3,3]
arr.sort(<)   <--- 이게 뭐얌?             --- 1번
println(arr)   // "[1, 2, 2, 3, 3, 6]"

arr.sort(<) 이게 도대체 뭔소리야 하는 사람도 있겠지만 결과는 오름차순 정렬이다. sort 함수는 인자로 오퍼레이터를 받는건가? 라고 생각할수도 있겠지만 사실 위 코드는 아래와 같이 풀어 쓸수도 있다.

var arr = [6,2,1,2,3,3]
arr.sort{$0 < $1} <--- 이건 또 뭐얌?      --- 2번
println(arr)  

당연히 2번은 1번과 같은 코드다. 2번에서 $0과 $1이라는 변수는 Swift에서 인자의 타입이 무엇이 정확하게 추론이 가능할때 타입 이름을 특별히 지정하지 않아도 쓸수있는 키워드 정도로 이해하면 된다. 물론 인자가 3개라면 $2도 쓸수있다. 그렇다면 이 함수의 원형은 어떻길래 추론이 가능한걸까? 추론 되기전 상태로 좀 더 거슬러 올라가보자.

var arr = [6,2,1,2,3,3]
arr.sort{                              --- 3번
   (t1, t2) -> Bool in
   t1 < t2  
}
println(arr)  

3번에서 sort 다음에 {} 중괄호는 일단 넘어가자. 괄호안에 in 이라는 키워드는 이 함수가 클로저 함수임을 나타낸다. 즉 in 앞에 내용이 이 함수의 시그니처다. 그러니까 인자 2개를 받아서 Bool형으로 반환한다는 이야기!! 뚜둥~!! 그런데 또 여기에 return 문이 없다. Swift 에서 함수의 마지막 문장은 return 문이 없어도 그 문장의 결과값이 return 문으로 반환된다. 그러니까 4번 처럼 된다는 얘기다.

var arr = [6,2,1,2,3,3]
arr.sort{                              --- 4번
   (t1, t2) -> Bool in

   return t1 < t2  
}
println(arr)  

그런데 여기서 또 의문이 생겼다. 도대체 t1과 t2는 어떤 타입이길래 서로 비교가 가능한걸까? 이걸 추론해 내는 것이 Swift 컴파일러의 능력인것이다!! arr 변수는 숫자형 배열이기때문에 바로 위에 문장을 기반으로 sort 내부에서 수행되는 t1과 t2가 Int형임을 알수있는 것이다. 그러니까 5번처럼 인식한다는 얘기!

var arr = [6,2,1,2,3,3]
arr.sort{                              --- 5번
   (t1:Int, t2:Int) -> Bool in

   return t1 < t2  
}
println(arr)  

맨위에 sort 함수 시그니처를 보면 sort 함수는 같은 제네릭타입 T를 인자로 받는다. 따라서 t1과 t2는 같은 타입이 된다. 만약 t2를 Int가 아닌 다른 타입으로 캐스팅 한다면 어떻게 될까?

arr.sort{                              --- 5-1 번 
   (t1:Int, t2:Float) -> Bool in
   return t1 < t2  
}

당연한 얘기지만 sort 함수의 인자 타입이 달라서 실행할수없다고 에러를 낸다. 역시 똑똑한 녀석!!

“Cannot invoke ‘sort’ with an argument list of type ‘((Int, Float) -> Bool)’

마지막으로 저 뜬금없는 sort 다음에 {} 중괄호!! 너는 도대체 뭐냐? 아래 6번 코드를 보자!

var arr = [6,2,1,2,3,3]
arr.sort() {                              --- 6번
   (t1:Int, t2:Int) -> Bool in
   return t1 < t2  
}
println(arr)  

잉? 이건 또 모야? 앞에 () 괄호를 써도 되는 거네? 맞다! {} 중괄호 앞에 괄호가 와도 된다. 6번에 괄호가 생략된 이유는 괄호 안에 들어가는 마지막 인자가 함수일 경우 Swift는 마지막 함수를 괄호 밖으로 끄집어 내서 쓸수있도록 허용한다. 정리하면 괄호안에 들어가는 인자가 여러개 일때, 중간에 함수가 들어가는 경우는 뺄수없다.

// p는 변수, f는 함수라고 가정한다. 
// 이런 함수는 f1을 끄지어 내지 못한다. 
x.method(p1, f1, p2) ---> X

//  이렇게 맨 마지막 인자가 함수면 끄집어 낼수있다. 
x.method(p1, p2, f1) ---> x.method(p1, p2) {} 

제네릭 타입의 형변환

클로저 함수를 다루다 보면 타입 추론에 의해서 생략되는 녀석들이 많아 가끔씩 헷갈릴때가 있는데 이럴때는 꼭 해당 함수에 마우스를 올리고 커맨드 키와 함께 문서를 열어보는 것이 좋다. 여튼 Swift에서 제공하는 많은 내장 함수들이 Typealias나 제네릭 같은 타입을 많이 쓰고 있어서 클로저 함수에 인자를 받아서 쓰다보면 다음과 같은 에러가 종종 발생한다.

(!) Cannot invoke ‘predicate’ with an argument list of type ‘(T)’

extension Array {
  func myFilter<T>(predicate:(T) -> Bool) -> [T] {
    var result = [T]()

    for i in self {
      if predicate(i) {      // <----- 여기가 문제!!
        result.append(i)
      }
    }
    return result
  }  
}

위에서 myFilter 함수는 predicate 라는 클로저 함수를 인자로 받아서 제네릭 타입인 T를 요소로 갖는 배열을 반환하는 함수다. 내부에서 쓰이는 self는 Array 클래스를 확장했으므로 당연히 내부에서 관리되는 배열일 것이라는 추측을 할수있다. 실제로 출력을 해봐도 배열값은 맞다. 하지만 컴파일러는 self 배열에서 꺼낸 i 타입이 사실 뭔지 모르고, predicate가 T 타입을 받아야한다는 사실만 알고 있기 때문에 “i가 뭔지 모르겠는데 T타입은 아닌거 같아. 제대로 넣어줘~!” 라는 메세지를 준다.

extension Array {
  func myFilter<T>(predicate:(T) -> Bool) -> [T] {
    var result = [T]()

    for i in self {
      if predicate(i as! T) {      // <----- 해결!!
        result.append(i as! T)     // <----- 해결!!
      }
    }
    return result
  }  
}

그래서 이럴때 as! 를 이용해 타입을 확실히 명시해주면 해결된다!