NSSet isEqualToSet behaves apparently non-deterministically

Originator:ibrahimshaath
Number:rdar://35604025 Date Originated:November 16 2017
Status:Closed Resolved:
Product:iOS / Foundation Product Version:
Classification:Bug Reproducible:Always
 
Summary: NSSets containing NSObjects which (in terms of their implementation of isEqual) are identical do not always evaluate isEqualToSet: as true.

Steps to Reproduce:
Run the attached trivial project on an iOS simulator in Xcode 9.1; observe the console output.

Expected Results:
From the sets as built, one would expect equality to be determined as true every time through the loop.

Actual Results:
The determination of equality is inconsistent. It varies per run and fails an enormous majority of the time.

Comments

https://developer.apple.com/documentation/objectivec/nsobjectprotocol/1418795-isequal The key paragraph: If two objects are equal, they must have the same hash value. This last point is particularly important if you define isEqual(_:) in a subclass and intend to put instances of that subclass into a collection. Make sure you also define hash in your subclass.

By ibrahimshaath at Nov. 17, 2017, 1:26 a.m. (reply...)

// Sorry, first time using this tool, and I didn't realise I can't attach a code sample. To reproduce, replace the ViewController.m in a brand new project with the following:

import "ViewController.h"

pragma mark - UltraObject

@interface UltraObject : NSObject @property (nonatomic, readonly) BOOL truthiness; - (instancetype)initWithTruthiness:(BOOL)truthiness; @end

@interface UltraObject () @property (nonatomic, assign) BOOL truthiness; @end

@implementation UltraObject

  • (instancetype)initWithTruthiness:(BOOL)truthiness { if (self = [super init]) { _truthiness = truthiness; } return self; }

  • (BOOL)isEqual:(id)object { if (object == self) { return YES; } if (object == nil) { return NO; } if ([object isKindOfClass:[self class]] == NO) { return NO; } BOOL thisTruthiness = self.truthiness; BOOL thatTruthiness = ((UltraObject *)object).truthiness; return thisTruthiness == thatTruthiness; }

@end

pragma mark - ViewController

@implementation ViewController

  • (void)viewDidLoad { [super viewDidLoad];

    NSUInteger equal = 0; NSUInteger unequal = 0;

    for (NSUInteger i = 0; i <= 1000000; i++) {

    UltraObject *truthyA = [[UltraObject alloc] initWithTruthiness:YES];
    UltraObject *falsyA = [[UltraObject alloc] initWithTruthiness:NO];
    NSSet<UltraObject *> *setA = [NSSet setWithObjects:truthyA, falsyA, nil];
    
    UltraObject *truthyB = [[UltraObject alloc] initWithTruthiness:YES];
    UltraObject *falsyB = [[UltraObject alloc] initWithTruthiness:NO];
    NSSet<UltraObject *> *setB = [NSSet setWithObjects:truthyB, falsyB, nil];
    
    if ([setA isEqualToSet:setB]) {
        equal++;
    } else {
        unequal++;
    }
    

    }

    NSLog(@"Equal: %@", @(equal)); NSLog(@"Unequal: %@", @(unequal)); }

@end

By ibrahimshaath at Nov. 17, 2017, 1:10 a.m. (reply...)

Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!