Skip to content

Instantly share code, notes, and snippets.

@CodaFi
Last active May 14, 2020 06:37
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save CodaFi/a012ba4fb2df8b8826af2c85297f393e to your computer and use it in GitHub Desktop.
Save CodaFi/a012ba4fb2df8b8826af2c85297f393e to your computer and use it in GitHub Desktop.

Multiple Choice Questions:

Unless otherwise noted, all questions assume a working installation of clang running on a modern 64-bit Mac with Automatic Reference Counting enabled in a modern runtime.

1) Exceptions, Errors, and Handling

1.1 What is Cocoa’s policy around catching exceptions thrown by frameworks?


  • a) Exceptions thrown by frameworks are fatal and should never be caught.

There is no valid reason to ever catch an exception thrown across framework boundaries. Candidates coming from other languages where exceptions play a more prominent role, e.g. Java, may assume they are benign. Structuring code with exceptions in Objective-C is considered an extremely poor practice and can lead to general program instability.

1.2 Can NSAssert be disabled in release builds.

  • c) NSAssert can be disabled by defining a condition at build time.

Specifically, NSAssert can be disabled by defining NS_BLOCK_ASSERTIONS or passing the flag -DNS_BLOCK_ASSERTIONS=1. This is a bit of basic knowledge that can often be surprising to those used to assert() in C, which is disabled via NDEBUG. NSAssert can have other surprising effects as well, so its use is discouraged in canonical Objective-C.

1.3 What will be the outcome of compiling and then executing the following program?

@import Foundation;

@interface CFIFruitList : NSObject
@property (nonatomic, copy) NSMutableArray<NSString *> *fruitNames;
@end

@implementation CFIFruitList
- (instancetype)init {
  self = [super init];
  if (self) {
    NSMutableArray *favoriteFruits = [@[ "Apple", "Banana", "Pear", "Orange", "Peach" ] mutableCopy];
    self.fruitNames = favoriteFruits;
  }
  return self;
}

- (void)removeAName {
  if (self.fruitNames.count == 0) return;
  [self.fruitNames removeObjectAtIndex:self.fruitNames.count - 1];
}
@end

int main(int argc, char **argv) {
  CFIFruitList *fruitList = [[CFIFruitList alloc] init];
  [fruitList removeAName];
  return EXIT_SUCCESS;
}
  • c) This program will crash at runtime.

The cause of the crash is a combination of the property declaration, the initializer, and the method call. The property declaration declares fruitNames as having a setter that takes a copy of the new value. In Cocoa collections, doing so returns a collection that is immutable. Hence a message like -removeObjectAtIndex: will compile, be considered well-typed, and yet cause a crash because the receiver is, in fact, not mutable. Candidates that do not notice the property declaration or misunderstand the semantics of copy may assume that this program will run without error.

1.4 For a method with the following signature:

- (BOOL)performOperationWithError:(NSError **)error;
  • c) error is passed by writeback because it is __autoreleasing.

A "canonical invocation" looks like the following:

NSError *error = nil;
if (![object performOperationWithError:&error]) { /* handle error */ }

A candidate may assume, because the address of error is being passed, that error itself is being passed by reference and normally they would be correct. However, modern Objective-C makes assurances that any object-type double pointer always has an __autoreleasing inner pointer regardless of the declaration (To override this, one must declare the inner pointer strong or weak). Hence error is passed by writeback, not just reference.

2) Objective-C Objects

2.1 Which of the following is the the proper declaration of a convenience initializer?

  • a) + (instancetype)foo;

Traditionally, a convenience initializer existed to perform allocation and initialization in one "convenient" method call - hence it cannot be an instance method. Under MRR, a convenience initializer also added one other convenience: returning an object with a reference count of +0. Initializers are required under the language to return objects +1, hence -init-family initializers are not correct.

2.2 What occurs when messaging an object that is nil?

  • c) Nothing happens.

This is a well-known property of the Objective-C language that may be confusing to those coming from languages where interactions with null cause crashes.

2.3 In general, what occurs when sending a non-nil object a message for a selector it does not implement?

  • b) An exception is thrown.

The history of the decision to crash when invalid messages were sent to non-conforming objects is apocryphal at best. Candidates may confuse this question with the previous one, but make no mistake an object must actually implement or forward a message to be able to respond to it in a semantically meaningful way.

2.4 For a non-nil, user-defined Objective-C object, foo, what is the result of evaluating the expression *(__bridge void **)foo?

  • b) The class metadata of the object is obtained.

This question tests the candidate's knowledge of the basic layout of Objective-C classes. This answer is correct because an Objective-C object is any structure that has an isa pointer as its first member. Hence, a dereference of any object will return that isa pointer. The "user-defined" portion is important. Under the modern runtime, the definition of object has expanded to include tagged pointers which may reuse the bits for their own storage. User-defined objects cannot take advantage of a tagged pointer representation, and so the deference is a well-defined operation. Experienced candidates may also recall non-pointer isa and isa masking becoming facilities of the modern runtime. As of this writing, macOS uses "packed isa", iOS uses the full magic isa, and the simulators and Windows use plain pointer isa.

3) ARC

3.1 Does the following code cause a retain cycle? If so, why? If not, why not?

  • d) No, it does not leak because a weak reference to self is captured by the block.

Any answer to this question relies on the implementation details of NSParameterAssert as well as block capture semantics. d, however, is the most correct. NS*Assert implementations that do not have C in their names take a strong reference to self in the definition of the macro, so can implicitly cause retain cycles if used in blocks. However, in this case, we have shadowed the declaration of self with our own weakSelf property and there will be no cycle here. Avoiding NS*Assert and recognizing its fragility is an important skill in modern Objective-C.

__weak id weakSelf = self;
self.someProperty = ^ (BOOL someFlag) {
    id self = weakSelf;
    NSParameterAssert(someFlag);
    [self doSomethingElse];
};

3.2 What is the name of the rule whereby user-allocated CoreFoundation objects are the responsibility of the caller to release?

  • b) "The Create Rule"

A la CFCreate*. The Create Rule mirrors the malloc rule in C: That which one CFCreates, one must CFRelease. This is why copying functions are named CFCreateCopy* or CFCreateMutableCopy* as well.

3.3 Which of the three bridging casts (__bridge, __bridge_retained, __bridge_transfer) releases its operand after the cast is performed?

  • c) __bridge_transfer

This attribute is the most rarely used of the three but is required in cases whereby cast objects have been over-retained by a particular abstraction like a CFArrayRef and must be cast back into Objective-C land.

3.4 Which of the three bridging casts (__bridge, __bridge_retained, __bridge_transfer) retains its operand after the cast is performed?

  • b) __bridge_retained

This attribute is the second most used of the three. It must be used when taking command of an object as a C-style pointer representation. ARC creates an extra retain for safety, then hands the responsibility of further releases to the user.

3.5 Which statement about Automatic Reference Counting is most correct?

  • b) Automatic Reference Counting generally manages the memory of most types, but some manual reference balancing may be required.

ARC, while a well-developed and feature-rich piece of technology, is still just semantic analysis. The entire semantics of Objective-C cannot be cached out in any one compiler implementation, so ARC is necessarily incomplete. There are many cases where it is necessary, and even desirable, for the user to take over for ARC and handle things themselves. Some candidates may have been informed that ARC is infallible, but this is most certainly not the case.

4) Runtime Wrangling

4.1 A selector (SEL) is:

  • a) A unique runtime-relevant name for an Objective-C method.

A selector is an interned string representation of a runtime-relevant handle that can be used to pick out a particular method's IMP pointer, or implementation pointer.

4.2 Where may the Objective-C runtime allocate blocks?

  • d) Blocks may appear on both the heap and the stack.

Blocks are strange newcomers to the Objective-C language. They can change the storage semantics of variables that they capture and automatically relocate themselves from the stack to the heap if necessary. All of this is done transparently so many candidates may have never had to think of where blocks reside in memory.

4.3 How may times will +initialize be called by the runtime?

  • b) +initialize can be called many times by the runtime.

+initialize can be called many times as it takes for each class in the hierarchy to receive the message. It is a poor place to actually perform initialization in most cases, and if it must be used, the class of the object should be used as a guard to ensure the initializing code is called exactly once as is usually desired.

4.4 What is meant by a "Non-Fragile Runtime ABI"?

  • b) The ability to add instance variables to a class without recompilation of subclasses.

The Non-Fragile Runtime is precisely the reason this exam begins with a disclaimer about the modern runtime. Objective-C classes used to be subject to the "sliding iVar" problem. See here for more.

4.5 Observe the following code declared in a framework linked with and loaded by a program:

// MyFramework.h
@interface NSObject (NoExceptionMessaging)
- (void)doesNotRecognizeSelector:(SEL)aSelector;
@end

// MyFramework.m
@implementation NSObject (NoExceptionMessaging)
- (void)doesNotRecognizeSelector:(SEL)aSelector { }
@end

4.6 What is a possible behavior the program may encounter if it attempts to send an object a message with a selector it does not respond to:

  • d) All of the above.

The behavior of extensions that override methods is entirely undefined and is a source of bugs. Extensions may be loaded at any time by the runtime in any order and so which implementation of a particular method "wins" the race cannot be relied upon.

4.7 What is the Runtime Type Encoding of the signature of an instance method that takes no arguments and returns void?

  • a) "v@:"

Runtime type encodings are relevant to those that need to perform runtime-level dynamic construction of classes. Candidates may assume from the description of the selector provided that there will be no runtime encoding, however every Objective-C instance method is given at least two arguments: self and _cmd.

5) Language Constructs

5.1 In the context of methods in the init family, what is a difference, if any, between id and instancetype.

  • a) There is no difference.

Methods in the init family are expected to have a rigid structure in modern Objective-C. As part of that rigidity, id and instancetype have the same semantics when used in return position.
The additional type safety afforded by instancetype over id applies only to methods outside the init family.

5.2 Which is the most correct definition of a top-level constant of type NSString *?

  • d) const NSString *const MyConstant = @"MyConstant";

Strings that are missing const-qualification on the pointer type may be modified and are hence not actually constant. String that are missing the const-qualifier on the class type are still constant pointers, but may not be passed to some routines that expect non-constant data. Note that this behavior can be desirable - notably to avoid CV-qualifier mismatch warnings. However the most correct answer is d.

5.3 What is a difference, if any, between properties marked atomic and those marked nonatomic?

  • c) The synthesized body of a nonatomic property does not ensure integrity of the returned value, while an atomic property does ensure integrity.

This is a critical question for any candidate claiming proficiency in Objective-C. nonatomic and atomic properties are often cargo-culted into a codebase with little regard as the semantics they entail. The distinction exists so that code that wishes to access a value from multiple threads can mark that it will give a coherent response (atomic) rather than "any response at all" (nonatomic). Nowhere in the semantics of the language does it guarantee thread safety and it is a serious mistake to assume this is the case.

5.4 True or False, @synthesize is required for the proper declaration of object properties.

  • b) False

@synthesize has not been a requirement for property declarations backed by instance variables for multiple major releases. It remains in poorly-written tutorials and outdated references because it has been cargo-culted into modern Objective-C. There is still a use for @synthesize, though. If a protocol has a requirement for which the class provides a backing instance variable, @synthesize can be used to associate the storage and the requirement without the need for explicit declarations.

5.5 What is the role of NSObject in the declaration of the protocol below?

@protocol MyProtocol <NSObject, NSCopying>
- (void)doTheThing;
@end
  • b) It indicates the NSObject protocol is an ancestor of MyProtocol.

Many candidates may not know that NSObject is both a protocol and a class. Here, the compiler appropriately selects the protocol definition because a protocol may not declare a superclass requirement. For that, there is angle- bracket type syntax NSObject<MyProtocol> *.

5.6 Why might an explicit comparison to YES, as in the code below, be dangerous?

/// ... omitted for brevity
if ([@"abcdefghijklmnopqrstuvwxyz" containsString:@"abc"] == YES) { /**/ }
/// ... omitted for brevity
  • c) It may be dangerous because BOOL can take on more values than just YES and NO.

On 64-bit iOS, the representation of BOOL was corrected to be a 1-bit boolean. On 64-bit OS X, the representation of BOOL is still signed char, which means it can take on many distinct values other than just 0 or 1. It is dangerous to assume that any method will return just the constants YES or NO.

5.7 Why might an explicit comparison to nil be necessary instead of a cast?

/// ... omitted for brevity
- (void)doMagic:(Magic *)magic {
  return (BOOL)magic;
}
/// ... omitted for brevity
  • c) The explicit comparison is necessary because an implicit comparison may slice magic.

As before, on 64-bit OS X, the representation of BOOL is signed char.
Implicit pointer comparisons will always slice the pointer value to fit a signed char. Any pointer value that has low-bits set to 0 will show up as NO or false despite being non-nil. A check against nil should always be explicit wherever possible.

Free Form:

1) (Toll Free) Bridging

Briefly describe a possible implementation of a bridged CoreFoundation-to-Objective-C type. What considerations must be made to ensure the bridging is "toll-free"?

2) Auto Release Pools

Why might wrapping a tight loop in an @autoreleasepool make looping code more performant? In general, which part of the loop should be wrapped, the inside of the loop body or the entire loop itself?

3) ARC In Depth

Will ARC manage the reference counts of Objective-C objects that are members of C structures or unions? Justify your response.

4) Attributes

Why is the -CGColor property of NSColor and UIColor marked with NS_RETURNS_INNER_POINTER (__attribute((objc_returns_inner_pointer)))?

5) Blocks

Where does the &x "live", the stack or the heap? Justify your answer.

/// Class details omitted ...
- (void)foo {
  __block int x = 0;
  int *pointerToX = &x;
  int (^block)() = ^{
      x += 1;
      return x;
  };
}
/// Class details omitted ...

Challenge Questions:

Two-Step Initialization?

Given an example of a class where +alloc is required but not necessarily -init?

A Walk On The Wild Side

// 1) What type do I have?
// 2) Implement me to return a value of the proper type.  (Invoke the callback in the appropriate location.)
void (^callback(NSInteger, void (^fp)(NSInteger)))(NSInteger);
@saagarjha
Copy link

Hey, I enjoyed working through your questions but I think got different answers on some of them. What do you think?

make no mistake an object must actually implement a message to be able to respond to it in a non-program-crashing way

There are a couple of chances to handle this before a crash occurs, isn't there? For example, by implementing -[NSObject forwardingTargetForSelector:].

User-defined objects cannot take advantage of a tagged pointer representation, and so the deference is a well-defined operation.

On my Mac, I can make a user-defined class with a non-pointer isa field. If I run this:

@import Foundation;

@interface Foo : NSObject
@end

@implementation Foo
@end

int main(void) {
	Foo *foo = [Foo new];
	printf("isa: %p\n", Foo.class);
	printf("isa, but not really: %p ", *(void **)(__bridge void *)foo);
}

I get this:

isa: 0x10dd1d0d8
isa, but not really: 0x1d80010dd1d0d9

On 64-bit OS X, the representation of BOOL is still signed char, which means it can take on many distinct values other than just 0 or 1.

I believe that would be undefined behavior: https://reviews.llvm.org/D27607? The conclusion is still sound, however.

@CodaFi
Copy link
Author

CodaFi commented Feb 5, 2020 via email

@saagarjha
Copy link

Fair enough ;) Another follow up for something that isn't wrong on Apple's platforms, but you might be interested in (or might know already):

A selector is an interned string representation of a runtime-relevant handle that can be used to pick out a particular method's IMP pointer, or implementation pointer.

I don't think the runtime actually requires that SEL is the same as char *. objc4 seemingly goes out of its way to hide what it is, and on GNUstep they actually use a pointer to an aggregate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment