[Swift] Collection Type의 할당과 Copy

[updated 2014.7.9]

Xcode 6 beta 3 시기에 맞춰 Swift  Collection에 대대적 변경이 이뤄졌다. 아래는 Xcode 6 release notes 에서 발췌한 내용이다.

• Array in Swift has been completely redesigned to have full value semantics like Dictionary and
String have always had in Swift.  This resolves various mutability problems – now a ‘let’ array
is completely immutable, and a ‘var’ array is completely mutable – composes properly with
Dictionary and String, and solves other deeper problems.  Value semantics may be surprising
if you are used to NSArray or C arrays: a copy of the array now produces a full and
independent copy of all of the elements using an efficient lazy copy implementation.  This is a
major change for Array, and there are still some performance issues to be addressed.  Please
see the Swift Programming Language for more information.  (17192555)!

이제 Dictionary 와 Array 는 변수 할당시 (or 파라미터로 값 전달 시) 값이 항상 복제(copy)된다.

Collection의 아이템이 Reference Type인 경우, Reference Type이 참조하고 있는 값은 복제되지 않고 유지된다.

본 포스팅의 주제였던  Array의 특이사항들 – 값이 복제 되지 않고 전달되는 케이스 – 는 모두 없던 일이 되었다. Array 메소드인 copy(), unshare() 도 사라졌다.

Classes and Structures – The Swift Programming Language 에서도 Collection Assignment and Copy 섹션은 보이지 않는다.

복잡했던 Swift 의 Collection이 정리되었다. 이제 아래 내용들은 추억으로 남기자.

* 참고로 Swift 는 Reference Type 의 copy 를 권장하지 않는 듯 하다. class 객체를 복제할 일이 생기면, struct 이 더 적합한 것은 아니지 고민을 해보자. (관련한 Apple Developer Forum 글 링크 )

 

Swift의 Collection Type인 Dictionary 와 Array는 Structure로서 Value Type이다. (NSArray, NSDictionary 는 Class로서 Reference Type이다.)

같은 Structure Type 임에도 Dictionary와 Array 는 변수 할당시 (or 파라미터로 값 전달시) 값을 처리하는 로직에서 차이를 보인다.

Dictionary

Value Type 의 기본에 맞게 Dictionary는 새로운 변수(or 상수)에 할당이 되면 값이 복제(Copy) 된다.

Dictionary의 key or value 가 Reference Type (Class or Function) 이면, Reference Type이 참조하고 있는 값은 복제 되지 않고 유지된다. 

<del>// Reference Type Test Class</del>
<del>class TestClass{</del>
<del>    var testProperty = "objc"</del>
<del>}</del>

<del>var baseDictionay : Dictionary<Int, Any> = [1:"abc", 2:TestClass() , 3:"def"]</del>

<del>// 새로운 변수에 할당. copy 됨</del>
<del>var copiedTestDictionay = baseDictionay</del>

<del>// 1. Value Type 변경</del>
<del>baseDictionay[1] = "ghi" // value 값 변경</del>

<del>println("\(baseDictionay[1])") // print "ghi"</del>
<del>println("\(copiedTestDictionay[1])") // print "abc</del>

<del>// => 값 변경이 서로에게 독립적</del>

<del>// 2. Ref. Type 변경</del>

<del>// TestClass 객체의 프로퍼티를 변경</del>
<del>(baseDictionay[2] as TestClass).testProperty = "swift"</del>

<del>println("\((baseDictionay[2] as TestClass).testProperty)")  // print "swift"</del>
<del>println("\((copiedTestDictionay[2] as TestClass).testProperty)") // print "swift"</del>

<del>// => Ref.Type 값 변경은 서로 영향 미침</del>

 Array

Array는 Dictionary와는 다르게 변수에 값이 할당 되거나 함수의 파라미터로 전달 될때, 값이 복제되지 않고 그대로 전달(공유)된다.

Array 값의 copy 여부가 결정되는 시점은 할당(or 전달) 이 아니라, 그 후에 발생하는 값의 변경시점이다.

값의 변경이란 아이템 추가, 아이템 삭제, ranged subscript 를 이용한 값의 변경처럼 Array의 length에 영향을 미치는 동작을 의미한다.  (ranged subscript 란 [..] or […] 로 Array의 값을 변경하는 것으로 아이템 삭제가 가능)

Array 값의 Copy 처리는 Dictionay와 동일하다. (Array 아이템이 Reference Type 이면 Reference Type이 참조하는 값은 copy 되지 않고 공유된다.)

<del>var baseArray = [1,2,3,4,5]</del>
<del>var copiedArray_1 = baseArray // no copy</del>
<del>var copiedArray_2 = baseArray // no copy</del>

<del>//1. 단순 값 변경</del>
<del>baseArray[0] = 10</del>

<del>println(baseArray[0]) // print 10</del>
<del>println(copiedArray_1[0]) // print 10</del>
<del>println(copiedArray_2[0]) // print 10</del>

<del>// => copy 되지 않앗으므로, 변경 값이 모든 array에 영향</del>

<del>// 2. ranged subscript 이용한 값 변경</del>

<del>// baseArray의 값 변경 (실질 size 변경은 없지만 ranged subscript는 size 변경시킬 수 있는 능력을 소유)</del>
<del>// 결국 baseArray는 복제된 값을 가짐</del>
<del>baseArray[2..4] = [6,7,8] // copy</del>

<del>println(baseArray[3]) // print 7</del>
<del>println(copiedArray_1[3]) // print 4</del>
<del>println(copiedArray_2[3]) // print 4</del>

<del>// => copy되었으므로 다른 array에 영향 미치지 못함</del>

<del>// 남은 Array의 값을 변경</del>
<del>copiedArray_1[3] = 11</del>

<del>println(baseArray[3]) // print 7, 앞서 복제되었기에 독립적</del>
<del>println(copiedArray_1[3]) // print 11</del>
<del>println(copiedArray_2[3]) // print 11</del>
<del>// => copiedArray_1과 copiedArray_2 는 여전히 같은 값을 공유함</del>

Array 값을 변경하기 전에 명시적으로 값이 copy 되도록 할 수 있다.

unshare()와 copy() 메소드를 이용한다.

</del>
<del>// 위 코드 계속</del>

<del>copiedArray_1.unshare() // 독립선언</del>
<del>copiedArray_1[3] = 20</del>

<del>println(baseArray[3) // print 7</del>
<del>println(copiedArray_1[3]) // print 20</del>
<del>println(copiedArray_2[3]) // print 11</del>

<del>

unshare()는 return이 없는 함수다.

여러 변수가 하나의 Array를 공유하는 경우,  그 중 하나의 변수에서 unshare()를 호출하면 복제된 값을 변수가 갖는다. 만약 Array 값을 가지는 변수가 이미 하나라면 (unique 하다면) unshare() 메소드는 호출되더라도 값을 복제하지 않는다.

반면 copy()는 Array 값을 지니는 변수의 unique 여부와 상관없이 항상 값을 복제한다. 이런 행위에 맞게 copy()는 Array<T> 를 반환하도록 정의되어 있다.

<del>// 위 코드 계속</del>

<del>var copiedArray_3 = copiedArray_2.copy() // 명시적 copy</del>
<del>copiedArray_3[3] = 100</del>

<del>println(copiedArray_2[3]) // print 11</del>
<del>println(copiedArray_3[3]) // print 100</del>

궁금증

  • Array 를 이처럼 복잡하게 만들어 놓은 이유는 뭘까? Value Type 과 Reference Type이 애매하게 섞여있다.  어느 부분에 이점이 있는걸까?
  • 그럼 Dictionary 는 왜 Array 처럼 처리하지 않을까?
  • Array 나 Dictionary 가 copy 되어도 Reference Type의 값은 copy 되지 않는데, copy의 의미를 봤을 때 헷갈리지 않을까?
  • 온전히 독립적인 Collection을 만들려면 Reference Type 의 복제는 별도로 구현해야 하나?

 

* Reference

 

[Swift] Optional Type 그리고 ?

Optional의 존재는 nil 로부터 온다.

 nil 의미

Swift 에서의 nil 은 Objective-C 에서의 nil 과 다르다.

  • nil of Swift : 값이 없다  in 모든타입 (Reference or Value Type)
  • nil of Objective-C : 값이 없다 in Reference Type

즉, Swift에선 어떤 타입이든 값이 없으면  nil 이다.

Optional Type 

Optional Type 은 nil이 될 수도  있는 변수 (or 상수)를 의미하며 선언할 때 ‘?’ 를 사용한다.  ‘?’  이 없으면 Non-Optional Type 으로 nil이 될 수 없다.

// optional type.
var optionalString : String?   // nil 로 초기화

// non-optional type.
var nonOptionalString : String // nil 이 될 수 없음. 초기화 필요

WWDC 2014 에서 Safe, Power, Modern 세가지를 Swift의 특징으로 꼽았다. Optional은  Safe에 속한다.

기존 nil 로 인한 앱 크래쉬 문제를  nil 전용 변수 타입을 따로 정의해서 compile 시점에서 막아줄께 하는 것이다. (실제로 이것이 얼마나 안전한지는 더 써봐야 알겠다.)

개발자는 변수 선언할 때마다 Optional Type으로 선언할 것인지 판단해야 한다. 꼭 필요한 경우에만 Optional Type 사용을 권장한다.

Optional은 enum 으로 정의된다.

enum Optional<T>{
    case None
    case Some(T)
}

! (forced-unwrapping operator)

Optional을 이용하여 안전한 코드를 짜려면 반드시 if 문으로 값이 있는지 확인을 해야 한다. 값이 있는 경우엔 ‘!’ 를 이용하여 값이 있음을 명시적으로 알려줘야 한다.

‘!’ 는 forced-unwrapping operator 로서 애매모호한 Optional Type 값을 명확하게 들어낸다. 이를 통해 불필요한 Optional Type 추가 발생을 막는다.

let numCards = ["one" : 1 , "two" : 2, "three" : 3,
               "four" : 4 , "five": 5]

// dictionary[key] 는 optional type을 반환 (즉,myCard 는 Int?)
let myCard = numCards["two"]
if myCard { // 값을 체크

    let card = myCard! // 값이 있으므로 !로 값을 드러냄
    // card의 타입은 Int (Int? 가 아님)
   
    println("card \(card)")

}else {

    println("no card")
}

Runtime Error

Optional Type을 if 문 없이 unwrapping 하여 사용하면 Runtime Error가 발생한다. 더 이상 안전하지 않다.

let numCards = ["one" : 1 , "two" : 2, "three" : 3, "four" : 4 , "five": 5]
let myCard = numCards["nine"]    // myCard = nil 

println("\(myCard!)")      // Runtime Error 발생

 Optional Binding

결국 Optional Type을 다룰때는 항상

  1. 값 여부를 확인 하고
  2. 값이 있으면 unwrapping 하여

사용해야 한다.

그러나 매번 위처럼 단계적으로 처리하는 건 불편하다. Optional Binding은 이를 동시에 할 수 있다.

Optional Binding 포멧은 if let ~ 이다.

let numCards = ["one" : 1 , "two" : 2, "three" : 3,
               "four" : 4 , "five": 5]

if let card = numCards["two"] { // 값 체크 & unwapping 

    println("card \(card)")  // card 는 Int 타입.

}else{
    println("no card")
}

Optional Chaining

코드를 짜다보면 Optional Binding 이 중첩될 수 있다. 반복되는  if let~ 은  귀찮다.

여러 층의 Optional Binding을 한 줄에 쓰게 하는 것이 Optional Chaining이다. (Swift 개발팀에서도 Optional이 불편하긴 했나보다.)

Optional Chaining은 ‘?’ 기호를 사용한다. (Optional Type 선언에 사용하는 ‘?’ 과 동일시 하면 헷갈린다. 비슷하지만 다른것으로 보자.)

아래 예제 코드는 WWDC2014 에서 소개된 코드다.

// 중첩 Optional Binding
if let home = paul.residence {
    if let postalAddress = home.address{
        if let building = postalAddress.buildingNumber{
            if let convertedNumber = building.toInt(){
                println("building number of paul is \(convertedNumber)")
            }
         }
     }
}

// Optional Chaining
if let addressNumber = paul.residence?.address?.buildingNumber?.toInt(){

    println("building number of paul is \(addressNumber)")</pre>
}

코드를 간단히 설명하면 paul 은 class 개체로 Non-Optional Type 이고 나머지 residence, address, buildingNumber 는 Optional Type 이다.

paul 의 집 주소를 알기 위해 Optioanl Binding으로 순차적으로 값을 확인한다. 이를 Optional Chaining 을 이용하면 한 줄에 끝이다.

Optional Chaining을 철도 선로로 이해하면 쉽다. 값이 없으면 nil 행이고 값이 있으면 keep going. (WWDC2014 비디오 스크린샷을 올리기가 좀 그렇지만, 좋은 설명이라 일단 첨부한다.)

< Optional Chaining from WWDC2014 >

 

Optional 과 Enum Pattern Matching

앞서 살펴봤듯이 Optional 은 enum 타입이다.

enum Optional<T>{
    case None
    case Some(T)
}

따라서 enum을 이용한 Pattern Matching 이 가능하다.

let numberCards : Dictionary<String,Int> = [ "one" : 1, "two" : 2, "three": 3, "four" : 4,
                  "five" : 5, "six": 6, "seven" : 7]

for num in numberCards.keys{

    // Dictionary[key] 리턴은 optional type
    switch numberCards[num]{

        case .Some(let v) where v > 3 :
            println("\(v)")
        default:
          println("...")

    }

}

numberCards[num] 의 리턴값은 Optional Type 이므로 – 여기선 Int? – 위와 같이 switch, enum 을 이용하여 처리할 수 있다.

 

* Reference

%d bloggers like this: