Objective-C Properties Problems

Objective-C 2.0 introduced properties as syntactic sugar over getter and setter a very long time back. Properties are very convenient for most of the things but there are quite a few issues that I feel always with them. Today I would like to document them.

Dot syntax

In C there idea of -> and . to access struct properties and it is simple to understand. For a given struct as:

typedef struct _Person {
  char name[1024];
  int age;
} Person;

We have . for values

Person setAge(Person p, int age)
{
  p.age = age;
  return p;
}

And we have -> for references

void setName(Person *p, const char *name)
{
  strcpy(p->name, name);
}

With Objective-C, all NSObject subclasses can only be used as reference types, like NSString *, so using the . on reference types breaks my brain every single time.

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

void setName(Person *p, NSString *name)
{
  p.name = name;
}

I believe the intention was good, but they could be picked some clearly non-ambiguous operator, like say ~>. I know it would look unnatural to C programmers, that was one of the core design principle of Objective-C, to have a syntax that is clearly distinguishable from C. That is why we have things like [ ], @interface and -(void)foo:(Bar *)bar because they don’t look anything like C.

I’ve a conspiracy theory, that the . was used for property because Apple wanted the developers to adapt Swift syntax gradually, where . is the only way to access properties.

Error prone

Let’s say we want to implement a property chaining where the only storage is NSDate and everything is just a computed getters and setters on top of it

@interface DateWrapper : NSObject
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) NSDateComponents *dateComps;
@property (nonatomic, assign) NSInteger hour;
@end
@implementation DateWrapper

- (instancetype)init
{
  self = [super init];
  if (self) {
    _date = [NSDate dateWithTimeIntervalSince1970:0];
  }
  return self;
}

- (NSInteger)hour
{
  return self.dateComps.hour;
}

- (void)setHour:(NSInteger)hour
{
  self.dateComps.hour = hour;
}

- (NSDateComponents *)dateComps
{
  return [[NSCalendar currentCalendar] components:NSCalendarUnitHour 
                                         fromDate:self.date];
}

- (void)setDateComps:(NSDateComponents *)dateComps
{
  self.date = [[NSCalendar currentCalendar] dateFromComponents:dateComps];
}

@end

One could accidentally use the interface as:

- (void)run
{
  DateWrapper *dw = [[DateWrapper alloc] init];
  NSLog(@"BEF: %ld", dw.hour); // 1
  dw.hour += 1;
  NSLog(@"AFT: %ld", dw.hour); // 1 ❌
}

These bugs could happen again because of the C like dot syntax. Because we know this works in C

CGRect frame = CGRectZero;
frame.size.width += 10; // ✅

We might apply the same principles to Objective-C properties

self.dateComps.hour += hour; // ❌

If you haven’t figured it out yet. The bug is because self.dateComps call the getter and we update the hour on a temporary object. The right way would be

NSDateComponents *tmp = self.dateComps;
tmp.hour += hour;
self.dateComps = tmp;

The bug becomes more visible by switching to getters and setters because every operation is clear in terms of where are we reading the data and where are we writing it.

@interface DateWrapper : NSObject
{
  NSDate *_date;
}
@end
@implementation DateWrapper

- (instancetype)init
{
  self = [super init];
  if (self) {
    _date = [NSDate dateWithTimeIntervalSince1970:0];
  }
  return self;
}

- (NSInteger)hour
{
  return [[self dateComps] hour]; // read
}

- (void)setHour:(NSInteger)hour
{
  NSDateComponents *comps = [self dateComps]; // read
  [comps setHour:hour]; // write
  [self setDateComps:comps]; // write
}

- (NSDateComponents *)dateComps
{
  return [[NSCalendar currentCalendar] components:NSCalendarUnitHour 
                                         fromDate:_date]; // read
}

- (void)setDateComps:(NSDateComponents *)dateComps
{
  _date = [[NSCalendar currentCalendar] dateFromComponents:dateComps]; // write
}

@end
- (void)run
{
  DateWrapper *dw = [[DateWrapper alloc] init];
  NSLog(@"BEF: %ld", [dw hour]); // 1
  [dw setHour:[dw hour] + 1];
  NSLog(@"AFT: %ld", [dw hour]); // 2
}

Chaining Setters

One limitation of property is that the setter has to return a void which could be problematic if you want to design your interface to be chainable. Take this usage for example

- (Person *)updatePerson:(Person *)person
{
  person.firstName = @"Barney";
  person.lastName = @"Gumble";
  person.age = 40;
  return person;
}

If we drop the whole property requirement, we could return instancetype from every setter which would make the setters as chainable.

- (Person *)updatePerson:(Person *)person
{
    return [[[person setFirstName:@"Barney" ]
                      setLastName:@"Gumble" ]
                           setAge:40        ];
}

Ambiguous Attributes

Properties are ambiguous when we’re writing custom getter and setters. If we’ve a C function that encodes and decodes string to int, and we wish to provide a Objective-C wrapper.

int my_encrypt(const char *str);
const char *my_decrypt(int hash);
@interface StringWrapper : NSObject
{
  int _hash;
}
@end

@implementation StringWrapper
- (void)setString:(NSString *)string
{
  _hash = my_encrypt([string cStringUsingEncoding:NSUTF8StringEncoding]);
}

- (NSString *)string
{
  return [NSString stringWithCString:my_decrypt(_hash) 
                            encoding:NSUTF8StringEncoding];
}
@end

Next if we wish to expose string as a property, what should the attributes look like? We might use copy or retain, but that would be a lie, since we know we’re not copying or retaining anything.

@property (nonatomic, copy) NSString *string;

We might simply leave it unspecified, but again unspecified does mean the default attributes atomic, readwrite, which might again cause confusion.

@property NSString *string;

With plain getter and setter, the memory storage or threading intention is never leaked outside.

@interface StringWrapper : NSObject
- (NSString *)string;
- (void)setString:(NSString *)string;
@end

Good parts: MRR

The only good use of a property that I can imagine is when we’re working on a Manual Retain Release memory model, the retain and release calls could be error prone.

Take a look at the following example with a bug:

@interface Foo : NSObject
{
  Bar *_bar;
}
@end
@implementation Foo
- (instancetype)init
{
  self = [super init];
  if (self) {
    _bar = [[Bar alloc] init];
  }
  return self;
}

- (void)dealloc
{
  [_bar release];
  [super dealloc];
}

- (Bar *)bar
{
  return _bar;
}

- (void)setBar:(Bar *)bar
{
  [_bar release]; // ❌ might also release bar
  _bar = [bar retain];
}

@end

This might introduce a bug if the bar is same as _bar in setBar:. The first release would deallocated the object immediately and the retain in the next line would have no effect. The right way to implement a setter would be to call retain before release.

- (void)setBar:(Bar *)bar
{
  [bar retain];
  [_bar release];
  _bar = bar;
}

This is one of many consideration one has to look out for when using MRR model. So with MRR it’s usually better to just use property for retain or copy attributes and let the compiler generate the correct code

@interface Foo : NSObject
@property (nonatomic, retain) Bar *bar;
@end

@implementation Foo
- (instancetype)init
{
  self = [super init];
  if (self) {
    _bar = [[Bar alloc] init];
  }
  return self;
}

- (void)dealloc
{
  [_bar release];
  [super dealloc];
}
@end

Parting Thoughts

I do like the convenience of properties as a short hand for plain getter and setter. That means no property chaining and the intention is very clear. I also like using properties when working MRR memory model. Or when implementing a readonly property. But there are these few things I don’t like about properties that sometimes I deliberately avoid using them for sake of accidental bugs.

I would file Objective-C properties under the Apple Fuckups section. It could’ve been really brilliant if they had consulted the Objective-C language designers and not C/C++, and hopefully not Swift.

I’m probably not the first one to dislike Objective-C properties, and I know I’m not the last. I’m actually a bit glad that Apple has now shifted the gears to Swift, so finally I can write Objective-C that way I always wanted to.