2

I'm trying to understand why this crashes when I use self.propName notation but not when I use _ivarName notation. Happy to learn about another part of Objective-C. Thanks for the help!

// .h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface FOOMySpecialClass : NSObject

@property (nonatomic, readonly, assign) BOOL isFoo;

- (void)setIsFoo:(BOOL)isFoo;

@end

NS_ASSUME_NONNULL_END
// .m

#import "FOOMySpecialClass.h"

@interface FOOMySpecialClass()
@property (nonatomic, readwrite, assign) BOOL isFoo;
@end

@implementation FOOMySpecialClass

- (void)setIsFoo:(BOOL)isFoo {
    self.isFoo = isFoo; // <-- Crash here
}

@end

But, if I change the bool to the ivar, things work fine. I'm trying to understand why.

// .m

#import "FOOMySpecialClass.h"

@interface FOOMySpecialClass()
@property (nonatomic, readwrite, assign) BOOL isFoo;
@end

@implementation FOOMySpecialClass

- (void)setIsFoo:(BOOL)isFoo {
    _isFoo = isFoo; // <-- Change here and no crash
}

@end
// main.m

#import <Foundation/Foundation.h>
#import "FOOMySpecialClass.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FOOMySpecialClass *mySpecialClass = [[FOOMySpecialClass alloc] init];
        [mySpecialClass setIsFoo:YES];
        
        NSLog(@"%@", mySpecialClass.isFoo ? @"YES" : @"NO"); // <-- boom.
    }
    return 0;
}
h.and.h
  • 690
  • 1
  • 8
  • 26
  • 4
    Add a `NSLog(@"Calling setIsFoo:");` at the first line of the method, you'll see: infinite loop, you're calling yourself each time. – Larme Jun 16 '23 at 17:29
  • 2
    If you rename `- (void)setIsFoo:(BOOL)isFoo;` with `- (void)setIsFoo2:(BOOL)isFoo;`, and do `[mySpecialClass setIsFoo2:YES];`, there won't be a loop anymore. But `self.isFoo = someValue` is equal to `[self setIsFoo:someValue]`. – Larme Jun 16 '23 at 17:31
  • Does this answer your question? [Objective-C Property Getter/Setter crash EXC\_BAD\_ACCESS](https://stackoverflow.com/questions/28637799/objective-c-property-getter-setter-crash-exc-bad-access) – Willeke Jun 17 '23 at 08:38

2 Answers2

4

self.isFoo = isFoo; calls the same setter recursively. So you have an infinite recursion, and that crashes.

Look at the stack in Xcode and you’ll see a gazillion of nested calls to setIsFoo.

gnasher729
  • 51,477
  • 5
  • 75
  • 98
3

The important lesson here is what "dot syntax" actually means in ObjC. Originally (until around 2007) in ObjC, setting a property was always done using a method called -setX:, which would be called as [obj setThing:thing]. There was no obj.thing = thing syntax. (This existed for C structs, but that's a very different thing.)

This setThing: syntax was through convention. It was not actually part of the language, and there were no @property definitions. It was just well understood that when you wanted to expose a "writable property" called thing, the object should have methods -thing and -setThing: (plus some subtle rules about BOOL properties and the is... prefix, and by-reference get... methods that I won't dive into). This all became formalized as Key-Value Coding, but was still technically "convention," not part of the language.

In ObjC 2, the convention was made part of the language as "dot syntax." I do not exaggerate to say this change was extremely controversial. (It is still controversial in some circles, but most of us have kind of gotten over it.) With dot syntax, obj.thing is translated into [obj thing], and obj.thing = thing is translated into [obj setThing:thing]. It is purely syntactic sugar.

As a separate feature, over a couple of iterations involving ever-less @synthesize, the @property syntax was added. This let you define "properties" that would automatically define a backing ivar (with a leading _) and the required getter and setter methods to match existing conventions.

So looking at your property definition:

@property (nonatomic, readwrite, assign) BOOL isFoo;

This, provided that -isFoo and -setIsFoo: are not defined, will automatically create an ivar called _isFoo, and provide synthesized getter and setter methods that apply the requested memory management. (That's another story, leading eventually to ARC.)

But in your case you define setIsFoo:, so the setter is not generated automatically. Instead, expanding the dot syntax sugar, you've written:

- (void)setIsFoo:(BOOL)isFoo {
    [self setIsFoo:isFoo];
}

And, as you might expect, that crashes, since it's infinitely recursive. Accessesing the ivar (_isFoo) directly does not crash, and is the correct way to implement this. (Though in this specific case, since you're using the default implementation, you shouldn't write this; you should allow the system to synthesize it for you.)

Rob Napier
  • 286,113
  • 34
  • 456
  • 610