objc_setAssociatedObject vs swift

interestingly objc_setAssociatedObject / objc_getAssociatedObject work not just on NSObject based types (notwithstanding its name) but on any swift class. even more surprisingly it works on some swift value types like structs or ints or strings but doesn't work on enums.

is it a bug or a feature that it works on some value types? shall it's parameter be typed "AnyObject" or even "NSObject" (if it's not supposed to work on non-obj-c-objects)?

it's actually cool and useful that it works on any swift class object. but i have hard time understanding what it possibly means to associate some arbitrary object with the value types, be it a number 1 or 1.0 or a string "hey" or some custom struct type value.

import Foundation

class NSObjectBasedClass: NSObject {}
class OtherClass {}
class Struct {}
enum Enum { case one, two }

let nsObject1 = NSObjectBasedClass(), nsObject2 = NSObjectBasedClass()
let otherObject1 = OtherClass(), otherObject2 = OtherClass()
let struct1 = Struct(), struct2 = Struct()
let enum1: Enum = .one, enum2: Enum = .two
let int1 = 1, int2 = 2
let double1: Double = 1, double2: Double = 2
let string1 = "hey", string2 = "dude"

private var keyA: Int = 0
private var keyB: Int = 0

func test(_ object1: Any, _ value1A: String, _ value1B: String, _ object2: Any, _ value2A: String, _ value2B: String) {
    assert(objc_getAssociatedObject(object1, &keyA) == nil)
    assert(objc_getAssociatedObject(object1, &keyB) == nil)
    assert(objc_getAssociatedObject(object2, &keyA) == nil)
    assert(objc_getAssociatedObject(object2, &keyB) == nil)
    
    objc_setAssociatedObject(object1, &keyA, value1A, .OBJC_ASSOCIATION_RETAIN)
    objc_setAssociatedObject(object1, &keyB, value1B, .OBJC_ASSOCIATION_RETAIN)
    objc_setAssociatedObject(object2, &keyA, value2A, .OBJC_ASSOCIATION_RETAIN)
    objc_setAssociatedObject(object2, &keyB, value2B, .OBJC_ASSOCIATION_RETAIN)
    
    assert(objc_getAssociatedObject(object1, &keyA) as? String == value1A)
    assert(objc_getAssociatedObject(object1, &keyB) as? String == value1B)
    assert(objc_getAssociatedObject(object2, &keyA) as? String == value2A)
    assert(objc_getAssociatedObject(object2, &keyB) as? String == value2B)
}

func testAll() {
    test(nsObject1, "ns-hello1", "ns-world1", nsObject2, "ns-hello2", "ns-world2")
    test(otherObject1, "other-hello1", "other-world1", otherObject2, "other-hello2", "other-world2")
    test(struct1, "struct-hello1", "struct-world1", struct2, "struct-hello2", "struct-world2")
    test(int1, "int-hello1", "int-world1", int2, "int-hello2", "int-world2")
    test(double1, "double-hello1", "double-world1", double2, "double-hello2", "double-world2")
    test(string1, "string-hello1", "string-world1", string2, "string-hello2", "string-world2")
    test(enum1, "enum-hello1", "enum-world1", enum2, "enum-hello2", "enum-world2") // fails
    print("done")
}

testAll()
1 Like

I would be careful with set associated object. With time I believe that for pure swift objects it will be deprecated (my personal opinion). Of course for NSObject subclasses it will always work. We are already tightening its semantics by banning associated objects on actor classes (try it, the runtime will abort). The reason for this is that:

  1. Associated objects is not a cross platform thing.
  2. Associated objects harm the ability of the optimizer to optimize by making it impossible to reason about the side effects of a deinit.
7 Likes

And here’s another caveat to add to the list posted by Michael_Gottesman:

  • Associated objects don’t play well with tagged pointer types.

Consider this code:

private var key = malloc(1)!

func test(_ stringToAssociateWith: String) {
    let a = NSString("Hello Cruel World!")
    let s1 = NSString(string: stringToAssociateWith)
    let s2 = NSString(string: stringToAssociateWith)
    objc_setAssociatedObject(s1, key, a, .OBJC_ASSOCIATION_RETAIN)
    print("associating with '\(stringToAssociateWith)':")
    print(objc_getAssociatedObject(s1, key))
    print(objc_getAssociatedObject(s2, key))
}

test("short")
print("--")
test("this is a long string, one that's way too long for a tagged pointer")

which prints:

associating with 'short':
Optional(Hello Cruel World!)
Optional(Hello Cruel World!)
--
associating with 'this is a long string, one that's way too long for a tagged pointer':
Optional(Hello Cruel World!)
nil

even more surprisingly it works on some swift value types

I believe that's the result of auto boxing.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

5 Likes

in regards to this particular aspect, in my testing associated objects work on iOS/tvOS/watchOS/macOS, so there is at least some crossplatform-ability here.

i would be glad not using associated objects, but using them helps to avoid subclassing which is undesirable in some cases. if worst comes to worst, and associated objects stop working, and i still need that functionality at the time - i'd probably have to introduce my own global dictionary of a kind with weakly referenced objects as keys and associated objects as values.

i turn to associated objects every now and then, on an as needed basis. the latest use case was: i needed to associate pieces of data with instances of UIImageView. there are other tricks i could have done (e.g. embedding my custom invisible child view under UIImageView and having associated data in that subview) but messing with the view hierarchy could cause severe undesirable effects down the road, so regretfully i had to use associated objects once again, despite of potential set of gotchas it introduces. if anyone knows a better way - please give me a shout. even something that works on NSView or NSObject objects still helps.

yep, i thought something like this is at play.

is it worth changing objc_get/setAssociatedObject "object" parameter type from "Any" to "AnyObject" to avoid foot shooting?

public func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: Any?, _ policy: objc_AssociationPolicy)
public func objc_getAssociatedObject(_ object: Any, _ key: UnsafeRawPointer) -> Any?

When I said cross platform I meant it doesn't work on Linux/Windows.

Do I understand correctly that autoboxing produces new instance every time? And trying to read associated object from the value type will not work, because writing and reading happen on different instances?

Do I understand correctly that autoboxing produces new instance every
time?

Yes. Consider this:

struct S {
    var i: Int
}
let s = S(i: 42)
AutoBoxTest.test(with: s)
AutoBoxTest.test(with: s)

where AutoBoxTest is Objective-C class like so:

@implementation AutoBoxTest

+ (void)testWithObject:(id)obj {
    NSLog(@"%p", obj);
}

@end

This prints:

2021-03-03 09:35:30.579017+0000 xxst[48048:2886302] 0x109b47550
2021-03-03 09:35:30.579463+0000 xxst[48048:2886302] 0x10050d060

Honestly, I can’t see how else it could work.


And trying to read associated object from the value type will not
work, because writing and reading happen on different instances?

Yes. If you want reference semantics, use a class. If you’re starting with a value type, do your own boxing.


is it worth changing objc_get/setAssociatedObject object parameter
type from Any to AnyObject to avoid foot shooting?

Personally I think that associated objects are a foot gun no matter the syntax (-: However, this isn’t my call and you should absolutely make your case in a bug report. Please post your bug number, just for the record.


i turn to associated objects every now and then, on an as needed
basis. the latest use case was: i needed to associate pieces of data
with instances of UIImageView.

And you didn’t use a subclass because?

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

you mean the objc_setAssociatedObject in particular or a general idea of associating external values to objects? i don't see a better way if subclassing is prohibited. (with NSImageView there is potentially cell->representedObject route, but that might be already used for something and i don't see it with UI/WK counterparts).

i played with idea of creating my own version of associated objects (a global dictionary with "object holders", where objects are weakly referenced within holders, the dictionary key is a string made out of the object address, and a special protection against a newly created object that was placed at the same memory location as the object that was just deallocated) and it seems to work fine, so will keep this option as a backup plan.

yep, FB9027014

as a library author i want my library feature to work on existing UIImageViews / NSImageViews / WKInterfaceImages - the latter doesn't even support subclassing to begin with - and in all cases it would be a nightmare for users to change their existing storyboards / xibs / sources to a different type. besides users can already use (or will use in the future) ImageView subclasses for some good reasons, and those subclasses could be in external libraries as well (or in the system itself).

1 Like