@NSCopying does not appear to copy

Originator:jose.ibanez
Number:rdar://21383959 Date Originated:June 15, 2015
Status:Open Resolved:
Product:Swift Product Version:1.2, 2
Classification:Serious Bug Reproducible:Always
 
gist: https://gist.github.com/jose-ibanez/c813f10d301c0e2b54be

Summary:
Swift properties declared with the `@NSCopying` attribute do not actually copy the value assigned to them or read from them.

Steps to Reproduce:
1. Declare a property with the `@NSCopying` attribute with a type that conforms to the `NSCopying` protocol:

    class Foo : NSObject, NSCopying { .. }
    class Test : NSObject {
      @NSCopying public var foo : Foo
    }

2. Set the property:

    let test = Test()
    let foo = Foo()
    foo.bar = "initial"
    test.foo = foo

3. Observe that `foo` and `test.foo` are different pointers.

    foo === test.foo // expected: false, actual: true

3. Change a property on foo:

    foo.bar = "changed"

4. Observe the property on Test has changed:

    print(test.foo.bar) // expected: "initial", actual: "changed"

Expected Results:
Objects assigned to or read from a property with the `@NSCopying` attribute should create a copy of the object using `copyWithZone()`

Actual Results:
The `@NSCopying` attribute appears to do nothing.

Version:
Xcode 6.3.2 / Swift 1.2
Xcode 7b1 / Swift 2

Comments

Added clarifying comment.

I've learned something with the help of the swift-lang slack org that clarifies the issue a bit, but I believe there is still a bug here.

So, the beginning of my problem was that when you assign to a property before calling super.init() it doesn't use the property, so no copying takes place, as in the following example:

class Foo : NSObject, NSCopying {
    var bar = "bar"
    func copyWithZone(zone: NSZone) -> AnyObject {
        let copy = Foo()
        copy.bar = bar
        return copy
    }
}
class Test : NSObject {
    @NSCopying private(set) var foo : Foo?
    convenience override init() {
        self.init(foo: nil)
    }
    init(foo : Foo?) {
        self.foo = foo
        super.init()
    }
}
let foo = Foo()
foo.bar = "initial"
let test = Test(foo: foo)
print(foo === test.foo) // true
foo.bar = "changed"
print(test.foo!.bar) // "changed"

Rather, foo is copied only when it's set using the property after initialization, so this works:

let foo = Foo()
foo.bar = "initial"
let test = Test()
test.foo = foo
print(foo === test.foo) // false
foo.bar = "changed"
print(test.foo.bar) // "initial"

However, the property foo does not return a copy when it is read, so it can still be mutated without being set:

let foo = Foo()
foo.bar = "initial"
let test = Test()
test.foo = foo
print(foo === test.foo) // false
foo.bar = "changed"
print(test.foo!.bar) // "initial"
let readFoo = test.foo!
readFoo === test.foo! // true
readFoo.bar = "changed"
print(test.foo!.bar) // "changed"

I can live with having to manually copy during init, but I believe that @NSCopying properties should also return a copy of the object.

By jose.ibanez at June 15, 2015, 6:02 p.m. (reply...)

Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!