@NSCopying semantic does not appear to copy in Swift initializer


(Torin Kwok) #1

Hello guys,

I wanna ask a question about the behavior of `@NSCopying` semantic in
Swift 3. Well, according to Apple's official documentation:

In Swift, the Objective-C copy property attribute translates to
@NSCopying. The type of the property must conform to the NSCopying
protocol.

However, I encountered a strange behavior when I declared a property
with the `@NSCopying` attribute:

// `Person` class inherits from `NSObject` class and conforms to `NSCopying` protocol
@NSCopying var employee: Person

and then assigned an external instance of `Person` class protocol to
this property within the designated init methods:

// Designated initializer of `Department` class
init( employee externalEmployee: Person ) {
  self.employee = externalEmployee
  super.init()

  // Assertion would fail because Swift do not actually copy the value assigned to this property         
  // even though `self.employee` has been marked as `@NSCoyping`
  // assert( self.employee !== externalEmployee )
  }

If I indeed require the deep copying behavior during the init process,
instead of making advantage of `@NSCopying` attribute, I have to
invoke the `copy()` method manually:

init( employee externalEmployee: Person ) {
  // ...
  self.employee = externalEmployee.copy() as! Person  
  // ...
  }

In fact, what really makes me confusing is that `@NSCopying` semantic
does work properly within the other parts of the class definition such
as normal instance methods, or external scope. For instance, if we're
assigning an external instance of `Person` to the `self.employee` proper
of `Department` directly through setter rather than initializer:

department.employee = johnAppleseed

then `self.employee` property and `johnAppleseed` variable will no
longer share the same underlying object now. In the other words,
`@NSCopying` attribute makes sense.

After I looked through a great deal of results given by Google, and
dicussions on StackOverflow, I finally found nothing related — the vast
majority of articles, documentations as well as issues talking about
this similar topics only focus on the basic concepts and effects of
`@NSCopying` itself but do not mentioned this strange behavior at all —
besides one radar descriping the same problem (rdar://21383959) and a
final conclusion mentioned in a guy's Gist comment: **... values set
during initialization are not cloned ...**

That is, `@NSCopying` semantic has no effect in initializers.

Then, what I want to figure out is the reason why `@NSCopying` semantic
will become effectless implicitly whithin initializers of a class, and
the special considerations behind this behavior, if any.

Thank you very much.

Best Regards,
Torin Kwok


(Jordan Rose) #2

Your observation is correct: @NSCopying currently does not affect initializers. This is because accessing a property in an initializer always does direct access to the storage rather than going through the setter. It might be reasonable to change this behavior, but it probably deserves a bit of discussion on swift-evolution; it's not 100%, for-sure a bug. (There is a Radar for this, rdar://problem/21383959 <rdar://problem/21383959>, but no bugs.swift.org <http://bugs.swift.org/> issue.)

Jordan

···

On Jan 26, 2017, at 23:30, Torin Kwok via swift-users <swift-users@swift.org> wrote:

Hello guys,

I wanna ask a question about the behavior of `@NSCopying` semantic in
Swift 3. Well, according to Apple's official documentation:

In Swift, the Objective-C copy property attribute translates to
@NSCopying. The type of the property must conform to the NSCopying
protocol.

However, I encountered a strange behavior when I declared a property
with the `@NSCopying` attribute:

// `Person` class inherits from `NSObject` class and conforms to `NSCopying` protocol
@NSCopying var employee: Person

and then assigned an external instance of `Person` class protocol to
this property within the designated init methods:

// Designated initializer of `Department` class
init( employee externalEmployee: Person ) {
 self.employee = externalEmployee
 super.init()

 // Assertion would fail because Swift do not actually copy the value assigned to this property         
 // even though `self.employee` has been marked as `@NSCoyping`
 // assert( self.employee !== externalEmployee )
 }

If I indeed require the deep copying behavior during the init process,
instead of making advantage of `@NSCopying` attribute, I have to
invoke the `copy()` method manually:

init( employee externalEmployee: Person ) {
 // ...
 self.employee = externalEmployee.copy() as! Person  
 // ...
 }

In fact, what really makes me confusing is that `@NSCopying` semantic
does work properly within the other parts of the class definition such
as normal instance methods, or external scope. For instance, if we're
assigning an external instance of `Person` to the `self.employee` proper
of `Department` directly through setter rather than initializer:

department.employee = johnAppleseed

then `self.employee` property and `johnAppleseed` variable will no
longer share the same underlying object now. In the other words,
`@NSCopying` attribute makes sense.

After I looked through a great deal of results given by Google, and
dicussions on StackOverflow, I finally found nothing related — the vast
majority of articles, documentations as well as issues talking about
this similar topics only focus on the basic concepts and effects of
`@NSCopying` itself but do not mentioned this strange behavior at all —
besides one radar descriping the same problem (rdar://21383959) and a
final conclusion mentioned in a guy's Gist comment: **... values set
during initialization are not cloned ...**

That is, `@NSCopying` semantic has no effect in initializers.

Then, what I want to figure out is the reason why `@NSCopying` semantic
will become effectless implicitly whithin initializers of a class, and
the special considerations behind this behavior, if any.

Thank you very much.

Best Regards,
Torin Kwok

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(Rod Brown) #3

Hi Torin,

Just a few notes:

I believe the @NSCopying behaviour actually uses the copy(with zone: NSZone?) method rather than the indirect copy() method. I would check that you’ve implemented that correctly. If you have, then it appears to be a bug. I would breakpoint on this method (not copy()) to check the copy is actually occurring.

I would note however that this check doesn’t necessarily work for some of Apple’s API’s though. A lot of the Foundation API’s implement copy avoidance. That is, if you copy() an NSArray, it will actually just return itself, retained, rather than a new array, because an NSArray is immutable, and so the copy is needless. NSMutableArray then overrides the method and actually does return a true copy, because it is mutable and therefore dangerous. A lot of the foundation classes that implement NSCopying do this (think of this as a poor man’s copy-on-write!), and so the !== check isn’t necessarily the correct check to ensure it is setting via NSCopying. Breakpointing or checking with mutable variant classes is a more correct check with Foundation-style APIs.

Thanks,

Rod

···

On 27 Jan 2017, at 6:30 pm, Torin Kwok via swift-users <swift-users@swift.org> wrote:

Hello guys,

I wanna ask a question about the behavior of `@NSCopying` semantic in
Swift 3. Well, according to Apple's official documentation:

In Swift, the Objective-C copy property attribute translates to
@NSCopying. The type of the property must conform to the NSCopying
protocol.

However, I encountered a strange behavior when I declared a property
with the `@NSCopying` attribute:

// `Person` class inherits from `NSObject` class and conforms to `NSCopying` protocol
@NSCopying var employee: Person

and then assigned an external instance of `Person` class protocol to
this property within the designated init methods:

// Designated initializer of `Department` class
init( employee externalEmployee: Person ) {
 self.employee = externalEmployee
 super.init()

 // Assertion would fail because Swift do not actually copy the value assigned to this property         
 // even though `self.employee` has been marked as `@NSCoyping`
 // assert( self.employee !== externalEmployee )
 }

If I indeed require the deep copying behavior during the init process,
instead of making advantage of `@NSCopying` attribute, I have to
invoke the `copy()` method manually:

init( employee externalEmployee: Person ) {
 // ...
 self.employee = externalEmployee.copy() as! Person  
 // ...
 }

In fact, what really makes me confusing is that `@NSCopying` semantic
does work properly within the other parts of the class definition such
as normal instance methods, or external scope. For instance, if we're
assigning an external instance of `Person` to the `self.employee` proper
of `Department` directly through setter rather than initializer:

department.employee = johnAppleseed

then `self.employee` property and `johnAppleseed` variable will no
longer share the same underlying object now. In the other words,
`@NSCopying` attribute makes sense.

After I looked through a great deal of results given by Google, and
dicussions on StackOverflow, I finally found nothing related — the vast
majority of articles, documentations as well as issues talking about
this similar topics only focus on the basic concepts and effects of
`@NSCopying` itself but do not mentioned this strange behavior at all —
besides one radar descriping the same problem (rdar://21383959) and a
final conclusion mentioned in a guy's Gist comment: **... values set
during initialization are not cloned ...**

That is, `@NSCopying` semantic has no effect in initializers.

Then, what I want to figure out is the reason why `@NSCopying` semantic
will become effectless implicitly whithin initializers of a class, and
the special considerations behind this behavior, if any.

Thank you very much.

Best Regards,
Torin Kwok

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(Quinn “The Eskimo!”) #4

Just by way of context, this current behaviour closely matches Objective-C conventions, where `-init` methods directly access ivars and are expected to do any necessary copies.

Share and Enjoy

···

On 27 Jan 2017, at 18:56, Jordan Rose via swift-users <swift-users@swift.org> wrote:

@NSCopying currently does not affect initializers. … it's not 100%, for-sure a bug.

--
Quinn "The Eskimo!" <http://www.apple.com/developer/>
Apple Developer Relations, Developer Technical Support, Core OS/Hardware


(Torin Kwok) #5

In Obj-C, if a property has promised that it conforms to <NSCopying>
porotocl and that it would respect copying semantic by being qualified
with `@property (copy)`, then we assign a value to `ivar` through the
setter by writting down `self.ivar = whatever` in `-init`, accessing
would not be direct but via the setter. This behaviour, in fact, doesn't
match Obj-C conventions.

Quinn The Eskimo! via swift-users writes:

···

On 27 Jan 2017, at 18:56, Jordan Rose via swift-users <swift-users@swift.org> wrote:

@NSCopying currently does not affect initializers. … it's not 100%, for-sure a bug.

Just by way of context, this current behaviour closely matches Objective-C conventions, where `-init` methods directly access ivars and are expected to do any necessary copies.

Share and Enjoy

--
Torin Kwok (郭桐)
OpenPGP/GnuPG: https://keybase.io/kwok


(Quinn “The Eskimo!”) #6

In Obj-C, if a property has promised that it conforms to <NSCopying>
porotocl and that it would respect copying semantic by being qualified
with `@property (copy)`, then we assign a value to `ivar` through the
setter by writting down `self.ivar = whatever` in `-init` …

You can do that if you choose to, but the Objective-C convention is to use direct ivar access in `-init` and `-dealloc`. This is explicitly called out in the “Access Instance Variables Directly from Initializer Methods” section of “Programming with Objective-C”, which says:

You should always access the instance variables directly from within an initialization method …

<https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/EncapsulatingData/EncapsulatingData.html#//apple_ref/doc/uid/TP40011210-CH5-SW11>

For an object with a copyable property `name`, the `-init` method would look like this:

- (instancetype)initWithName:(NSString *)name {
    self = [super init];
    if (self != nil) {
        self->_name = [name copy];
    }
    return self;
}

Share and Enjoy

···

On 30 Jan 2017, at 10:12, Torin Kwok <torin@kwok.im> wrote:
--
Quinn "The Eskimo!" <http://www.apple.com/developer/>
Apple Developer Relations, Developer Technical Support, Core OS/Hardware


(Torin Kwok) #7

Haha yes, you're right. I misunderstanded your idea, sorry. Indeed, in
Obj-C, we have the freedom to decide on whether or not assign a value to
a property by invoking setter or by accessing ivar directly and Apple's
official documentation apparently suggested that we should always access
the instance variables directly from within an initialization
method. That's reasonable, necessary and the best practice as well.

But I suppose what's the really tricky is not "whether we should invoke
setter in initializers" but the confusion made by the inconsistency of
`@NSCopying` attribute's behaviour in Swift.

I have reposted this issue on **swift-evolution** list, please see some
particular replies about that within this thread here:

- <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170123/031055.html>

- <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170123/031056.html>

Thanks!

Quinn The Eskimo! via swift-users writes:

···

On 30 Jan 2017, at 10:12, Torin Kwok <torin@kwok.im> wrote:

In Obj-C, if a property has promised that it conforms to <NSCopying>
porotocl and that it would respect copying semantic by being qualified
with `@property (copy)`, then we assign a value to `ivar` through the
setter by writting down `self.ivar = whatever` in `-init` …

You can do that if you choose to, but the Objective-C convention is to use direct ivar access in `-init` and `-dealloc`. This is explicitly called out in the “Access Instance Variables Directly from Initializer Methods” section of “Programming with Objective-C”, which says:

You should always access the instance variables directly from within an initialization method …

<https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/EncapsulatingData/EncapsulatingData.html#//apple_ref/doc/uid/TP40011210-CH5-SW11>

For an object with a copyable property `name`, the `-init` method would look like this:

- (instancetype)initWithName:(NSString *)name {
    self = [super init];
    if (self != nil) {
        self->_name = [name copy];
    }
    return self;
}

Share and Enjoy

--
Torin Kwok (郭桐)
OpenPGP/GnuPG: https://keybase.io/kwok