cocoa - تم طرح استثناء في NSOrderedSet الموصلات




core-data xcode4 (17)

في تطبيق Lion ، لدي نموذج البيانات هذا:

يتم ترتيب Item subitems للعلاقة داخل Item .

Xcode 4.1 (build 4B110) قام بإنشاء الملف Item.h و Item.m و SubItem.h و SubItem.h .

هنا هو المحتوى (autogenerated) من Item.h :

#import <Foundation/Foundation.h>

#import <CoreData/CoreData.h>

@class SubItem;

@interface Item : NSManagedObject {
@private
}

@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSOrderedSet *subitems;
@end

@interface Item (CoreDataGeneratedAccessors)

- (void)insertObject:(SubItem *)value inSubitemsAtIndex:(NSUInteger)idx;
- (void)removeObjectFromSubitemsAtIndex:(NSUInteger)idx;
- (void)insertSubitems:(NSArray *)value atIndexes:(NSIndexSet *)indexes;
- (void)removeSubitemsAtIndexes:(NSIndexSet *)indexes;
- (void)replaceObjectInSubitemsAtIndex:(NSUInteger)idx withObject:(SubItem *)value;
- (void)replaceSubitemsAtIndexes:(NSIndexSet *)indexes withSubitems:(NSArray *)values;
- (void)addSubitemsObject:(SubItem *)value;
- (void)removeSubitemsObject:(SubItem *)value;
- (void)addSubitems:(NSOrderedSet *)values;
- (void)removeSubitems:(NSOrderedSet *)values;

@end

وهنا هو المحتوى (autogenerated) من Item.m :

#import "Item.h"
#import "SubItem.h"

@implementation Item

@dynamic name;
@dynamic subitems;

@end

كما ترى ، يعرض Item الصنف طريقة تُدعى addSubitemsObject: لسوء الحظ ، عند محاولة استخدامه بهذه الطريقة:

Item *item = [NSEntityDescription insertNewObjectForEntityForName:@"Item" inManagedObjectContext:self.managedObjectContext];
item.name = @"FirstItem";

SubItem *subItem = [NSEntityDescription insertNewObjectForEntityForName:@"SubItem" inManagedObjectContext:self.managedObjectContext];

[item addSubitemsObject:subItem];

يظهر هذا الخطأ:

2011-09-12 10:28:45.236 Test[2002:707] *** -[NSSet intersectsSet:]: set argument is not an NSSet

هل بإمكانك مساعدتي؟

تحديث:

بعد 1787 يومًا فقط من تقرير الأخطاء ، واليوم (1 آب 2016) كتبت لي Apple هذا: "الرجاء التحقق من هذه المشكلة باستخدام أحدث إصدار من نظام التشغيل iOS 10 وتحديث تقرير الأخطاء على bugreport.apple.com مع نتائجك." . دعونا نأمل أن هذا هو الوقت المناسب :)


أوافق على احتمال وجود خطأ هنا. لقد قمت بتعديل تطبيق إضافة كائن> setter لإلحاق بشكل صحيح بـ NSMutableOrderedSet.

- (void)addSubitemsObject:(SubItem *)value {
     NSMutableOrderedSet* tempSet = [NSMutableOrderedSet orderedSetWithOrderedSet:self.subitems];
     [tempSet addObject:value];
     self.subitems = tempSet;
}

سيؤدي تعيين المجموعة إلى self.subitems إلى ضمان إرسال إشعارات Will / DidChangeValue>.

Leelll ، هل أنت متأكد من أنه بعد هذا الإعداد المخصص لقيم NSMutableOrderedSet المخزنة في هذه المجموعة سيتم حفظها إلى قاعدة البيانات بشكل صحيح بواسطة CoreData؟ لم أتحقق من ذلك ، ولكن يبدو أن CoreData لا تعرف شيئًا عن NSOrderedSet وتتوقع NSSet كعلاقة علاقة كثيرة.


أعتقد أن الجميع يفتقدون المشكلة الحقيقية. ليس في أساليب accessor ولكن بدلاً من ذلك في حقيقة أن NSOrderedSet ليس فئة فرعية من NSSet . لذلك عندما يتم استدعاء -interSectsSet: مع مجموعة مرتبة كوسيطة يفشل.

NSOrderedSet* setA = [NSOrderedSet orderedSetWithObjects:@"A",@"B",@"C",nil];
NSSet* setB = [NSSet setWithObjects:@"C",@"D", nil];

 [setB intersectsSet:setA];

fails with *** -[NSSet intersectsSet:]: set argument is not an NSSet

Looks like the fix is to change the implementation of the set operators so they handle the types transparently. No reason why a -intersectsSet: should work with either an ordered or unordered set.

The exception happens in the change notification. Presumably in the code that handles the inverse relationship. Since it only happens if I set an inverse relationship.

The following did the trick for me

@implementation MF_NSOrderedSetFixes

+ (void) fixSetMethods
{
    NSArray* classes = [NSArray arrayWithObjects:@"NSSet", @"NSMutableSet", @"NSOrderedSet", @"NSMutableOrderedSet",nil];

    [classes enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        NSString* name = obj;
        Class aClass = objc_lookUpClass([name UTF8String]);
        [MF_NSOrderedSetFixes fixMethodWithSetArgument:@selector(intersectsSet:) forClass:aClass];
        [MF_NSOrderedSetFixes fixMethodWithSetArgument:@selector(isSubsetOfSet:) forClass:aClass];
    }];
}

typedef BOOL (*BoolNSetIMP)(id _s,SEL sel, NSSet*);

/*
    Works for all methods of type - (BOOL) method:(NSSet*) aSet
*/
+ (void) fixMethodWithSetArgument:(SEL) aSel forClass:(Class) aClass 
{
    /* Check that class actually implements method first */
    /* can't use get_classInstanceMethod() since it checks superclass */
    unsigned int count,i;
    Method method = NULL;
    Method* methods = class_copyMethodList(aClass, &count);
    if(methods) {
        for(i=0;i<count;i++) {
            if(method_getName(methods[i])==aSel) {
                method = methods[i];
            }
        }
        free(methods);
    }
    if(!method) {
        return;
    }

   // Get old implementation
   BoolNSetIMP originalImp  = (BoolNSetIMP) method_getImplementation(method);
   IMP newImp = imp_implementationWithBlock(^BOOL(NSSet *_s, NSSet *otherSet) {
        if([otherSet isKindOfClass:[NSOrderedSet class]]) {
            otherSet = [(NSOrderedSet*)otherSet set];
        }
        // Call original implementation
        return originalImp(_s,aSel,otherSet);
    });
    method_setImplementation(method, newImp);
}
@end

أنا شخصياً قد استبدلت للتو مكالمات إلى الأساليب التي تم إنشاؤها CoreData مع المكالمات المباشرة إلى الأسلوب كما هو موضح في حل آخر بواسطة Stepep:

NSMutableOrderedSet* tempSet = [self mutableOrderedSetValueForKey:@"subitems"];
      [tempSet addObject:value];
[tempSet addObject:value];

يؤدي ذلك إلى إزالة الحاجة إلى الفئات التي قد تتعارض لاحقًا مع حل من Apple إلى الشفرة التي تم إنشاؤها عند إصلاح الخطأ.

هذا إضافة إلى كونها الطريقة الرسمية للقيام بذلك!


أوافق على أنه قد يكون هناك خلل هنا. لقد قمت بتعديل تطبيق إضافة كائن إضافة إلحاق بشكل صحيح إلى NSMutableOrderedSet.

- (void)addSubitemsObject:(SubItem *)value {
    NSMutableOrderedSet* tempSet = [NSMutableOrderedSet orderedSetWithOrderedSet:self.subitems];
    [tempSet addObject:value];
    self.subitems = tempSet;
}

سيؤدي تعيين المجموعة إلى self.subitems إلى ضمان إرسال إعلامات Will / DidChangeValue.


بدلاً من عمل نسخة أقترح استخدام أداة accessor في NSObject للوصول إلى NSMutableOrderedSet للعلاقات.

- (void)addSubitemsObject:(SubItem *)value {
      NSMutableOrderedSet* tempSet = [self mutableOrderedSetValueForKey:@"subitems"];
      [tempSet addObject:value];
 }

على سبيل المثال ، تشير ملاحظات إصدار البيانات الأساسية لنظام التشغيل iOS v5.0 إلى هذا.

في اختبار قصير عملت في طلبي.


حدثت هذه المشكلة لي أثناء ترحيل مشروع من Objective-C إلى Swift 2 باستخدام XCode 7 . استخدم هذا المشروع للعمل ، ولسبب وجيه: كنت أستخدم MOGenerator الذي كان يحتوي على طرق بديلة لإصلاح هذا الخطأ. ولكن ليست كل الطرق تتطلب بديلاً.

لذلك هنا الحل الكامل مع فئة سبيل المثال ، بالاعتماد على الموصلات الافتراضية قدر الإمكان.

لنفترض أن لدينا قائمة تحتوي على عناصر مرتبة

أول فوز سريع إذا كان لديك علاقة واحد إلى عدة ، الأسهل هو أن تفعل فقط:

item.list = list

بدلا من

list.addItemsObject(item)

الآن ، إذا لم يكن هذا خيارًا ، فإليك ما يمكنك فعله:

// Extension created from your DataModel by selecting it and
// clicking on "Editor > Create NSManagedObject subclass…"

extension List {
  @NSManaged var items: NSOrderedSet?
}

class List

  // Those two methods work out of the box for free, relying on
  // Core Data's KVC accessors, you just have to declare them
  // See release note 17583057 https://developer.apple.com/library/prerelease/tvos/releasenotes/DeveloperTools/RN-Xcode/Chapters/xc7_release_notes.html
  @NSManaged func removeItemsObject(item: Item)
  @NSManaged func removeItems(items: NSOrderedSet)

  // The following two methods usually work too, but not for NSOrderedSet
  // @NSManaged func addItemsObject(item: Item)
  // @NSManaged func addItems(items: NSOrderedSet)

  // So we'll replace them with theses

  // A mutable computed property
  var itemsSet: NSMutableOrderedSet {
    willAccessValueForKey("items")
    let result = mutableOrderedSetValueForKey("items")
    didAccessValueForKey("items")
    return result
  }

  func addItemsObject(value: Item) {
    itemsSet.addObject(value)
  }

  func addItems(value: NSOrderedSet) {
    itemsSet.unionOrderedSet(value)
  }
end

بالطبع ، إذا كنت تستخدم Objective-C ، يمكنك القيام بنفس الشيء بالضبط حيث أن هذا هو المكان الذي حصلت على الفكرة في المقام الأول :)


لقد أخطأت في حل هذه المشكلة وحللتها باستخدام تطبيق أبسط بكثير من التطبيقات الأخرى الموضحة هنا. أنا ببساطة الاستفادة من الأساليب المتاحة على NSManagedObject للتعامل مع العلاقات عند عدم استخدام الفئات الفرعية.

مثال تنفيذ لإدراج كيان في علاقة NSOrderedSet سيبدو كالتالي:

- (void)addAddress:(Address *)address
{
    if ([self.addresses containsObject:address]) {
        return;
    }
    // Use NSManagedObject's methods for inserting an object
    [[self mutableOrderedSetValueForKey:@"addresses"] addObject:address];
}

هذا يعمل تماما ، وهو ما كنت تستخدم قبل الانتقال إلى الفئات الفرعية NSManagedObject .


لقد تتبعت الخطأ. يحدث في willChangeValueForKey:withSetMutation:usingObjects:

تقوم هذه المكالمة ببدء سلسلة من الإشعارات التي قد يكون من الصعب تتبعها ، وبالطبع قد يكون للتغييرات في أحد المستجيبين آثار على أخرى ، والتي أظن أن أبل لم تفعل شيئًا.

ومع ذلك ، فإنه على ما يرام في مجموعة والعمليات فقط تعيين على OrderedSet التي عطل. وهذا يعني أن هناك أربع طرق فقط تحتاج إلى تغيير. لذلك ، كل ما فعلته هو تحويل عمليات Set إلى عمليات Array المكافئ لها. هذه تعمل بشكل مثالي والحد الأدنى (ولكنها ضرورية) النفقات العامة.

على المستوى الحرج ، يعاني هذا الحل من عيب خطير. إذا كنت تضيف كائنات وأحد الكائنات موجود بالفعل ، فإنه إما لم تتم إضافته أو نقله إلى الجزء الخلفي من القائمة المرتبة (لا أعرف أيهما). في كلتا الحالتين ، فإن الفهرس المرتقب المتوقع للكائن بحلول الوقت الذي نصل فيه إلى didChange يختلف عن ما كان متوقعًا. هذا قد يكسر بعض تطبيقات الأشخاص ، لكنه لا يؤثر على الأعمال المتعلقة بالألغام ، حيث إنني أقوم بإضافة أشياء جديدة فقط أو أؤكد مواقعهم النهائية قبل إضافتها.

- (void)addChildrenObject:(BAFinancialItem *)value {
    if ([self.children containsObject:value]) {
        return;
    }
    NSIndexSet * indexSet = [NSIndexSet indexSetWithIndex:self.children.count];
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexSet forKey:ChildrenKey];
    [[self primitiveValueForKey:ChildrenKey] addObject:value];
    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexSet forKey:ChildrenKey];
}

- (void)removeChildrenObject:(BAFinancialItem *)value {
    if (![self.children containsObject:value]) {
        return;
    }
    NSIndexSet * indexSet = [NSIndexSet indexSetWithIndex:[self.children indexOfObject:value]];
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexSet forKey:ChildrenKey];
    [[self primitiveValueForKey:ChildrenKey] removeObject:value];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexSet forKey:ChildrenKey];
}

- (void)addChildren:(NSOrderedSet *)values {
    if ([values isSubsetOfOrderedSet:self.children]) {
        return;
    }
    NSIndexSet * indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.children.count, values.count)];
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexSet forKey:ChildrenKey];
    [[self primitiveValueForKey:ChildrenKey] unionOrderedSet:values];
    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexSet forKey:ChildrenKey];
}

- (void)removeChildren:(NSOrderedSet *)values {
    if (![self.children intersectsOrderedSet:values]) {
        return;
    }
    NSIndexSet * indexSet = [self.children indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
        return [values containsObject:obj];
    }];
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexSet forKey:ChildrenKey];
    [[self primitiveValueForKey:ChildrenKey] minusOrderedSet:values];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexSet forKey:ChildrenKey];
}

بالطبع ، هناك حل أسهل. وهي كالاتي؛

- (void)addChildrenObject:(BAFinancialItem *)value {
    if ([self.children containsObject:value]) {
        return;
    }
    [self insertObject:value inChildrenAtIndex:self.children.count];
}

- (void)removeChildrenObject:(BAFinancialItem *)value {
    if (![self.children containsObject:value]) {
        return;
    }
    [self removeObjectFromChildrenAtIndex:[self.children indexOfObject:value]];
}

- (void)addChildren:(NSOrderedSet *)values {
    if ([values isSubsetOfOrderedSet:self.children]) {
        return;
    }
    [self insertChildren:values atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(self.children.count, values.count)]];
}

- (void)removeChildren:(NSOrderedSet *)values {
    if (![self.children intersectsOrderedSet:values]) {
        return;
    }
    [self removeChildrenAtIndexes:[self.children indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
        return [values containsObject:obj];
    }]];
}

لقد واجهت نفس المشكلة ، ولكن فقط عندما حاولت تجربة شيء مختلف عما كنت أفعله. لا يمكنني رؤية رمز البند الفرعي ، ولكن سأفترض أنه يحتوي على رابط عكسي للعنصر. دعنا نسمي هذا الرابط المكرر ، "parentItem" ، فإن الحل الأسهل هو:

Item *item = [NSEntityDescription insertNewObjectForEntityForName:@"Item" inManagedObjectContext:self.managedObjectContext];
item.name = @"FirstItem";

SubItem *subItem = [NSEntityDescription insertNewObjectForEntityForName:@"SubItem" inManagedObjectContext:self.managedObjectContext];

//[item addSubitemsObject:subItem];
subItem.parentItem = item;

التأثير هو أنه يستخدم كود التفاح الخاص به وهو بسيط ونظيف. بالإضافة إلى ذلك ، تتم إضافة المجموعة تلقائيًا ، ويتم تحديث جميع المراقبين. ليس هناك أى مشكلة.


نعم ، هذا بالتأكيد خطأ في البيانات الأساسية. كتبت في وقت سابق على أساس ObjC-Runtime الإصلاح ، ولكن في الوقت الذي برزت أنه سيتم إصلاحه قريبا. على أي حال ، لا يوجد مثل هذا الحظ ، لذلك أنا نشرت على GitHub كما KCOrderedAccessorFix . اعمل على حل المشكلة في جميع الكيانات:

[managedObjectModel kc_generateOrderedSetAccessors];

كيان واحد على وجه الخصوص:

[managedObjectModel kc_generateOrderedSetAccessorsForEntity:entity];

أو فقط لعلاقة واحدة:

[managedObjectModel kc_generateOrderedSetAccessorsForRelationship:relationship];

يقول Apple Docs To Many Relations : يجب عليك الوصول إلى مجموعة الوكيل القابلة للتغيير أو مجموعة مرتبة باستخدام

NSMutableOrderedSet * set = [managedObject mutableOrderedSetValueForKey:@"toManyRelation"];

سيؤدي تعديل هذه المجموعة إلى إضافة أو إزالة العلاقات إلى الكائن المُدار. الوصول إلى مجموعة الأوامر القابلة للتغيير التي تم إعدادها باستخدام الملحق سواءً كان مع [] أو. الترميز خطأ وسيفشل.


I found a fix for this bug that works for me. I just replace this:

[item addSubitemsObject:subItem];

with this:

item.subitemsObject = subItem;

I found using the method by LeeIII worked, but on profiling found it was drastically slow. It took 15 seconds to parse 1000 items. Commenting out the code to add the relationship turned 15 seconds into 2 seconds.

My workaround (which is faster but much more ugly) involves creating a temporary mutable array then copying into the ordered set when all the parsing is done. (this is only a performance win if you are going to add many relationships).

@property (nonatomic, retain) NSMutableArray* tempItems;
 ....
@synthesize tempItems = _tempItems;
 ....

- (void) addItemsObject:(KDItem *)value 
{
    if (!_tempItems) {
        self.tempItems = [NSMutableArray arrayWithCapacity:500];
    }
    [_tempItems addObject:value];
}

// Call this when you have added all the relationships
- (void) commitRelationships 
{
    if (_tempItems) {
        self.items = [NSOrderedSet orderedSetWithArray:self.tempItems];
        self.tempItems = nil;
    }
}

I hope this help someone else!


I have the same situation with an item called "signals" instead of "subitems". The solution with tempset works in my testing. Further, I had a problem with the removeSignals: method. This override seems to work:

- (void)removeSignals:(NSOrderedSet *)values {
    NSMutableOrderedSet* tempset = [NSMutableOrderedSet orderedSetWithOrderedSet:self.signals];
    for (Signal* aSignal in values) {
        [tempset removeObject:aSignal];
    }
    self.signals = tempset;
}

If there is a better way to do this, please let me know. My values input is never more than 10 -20 items so performance isn't much of a concern - nonetheless please point out anything relevant.

شكر،

Damien


I solved this problem by set the inverse to No Inverse, I don't know why, Maybe there is Apple Bug.


Robert,

I agree your answer will work for this, but keep in mind that there is an automatically created method for adding a whole set of values to a relationship already. Apple's Documentation ( as seen here under the "To-many Relationships" section or here under the "Custom To-Many Relationship Accessor Methods" section) implements them this way:

- (void)addEmployees:(NSSet *)value
{
[self willChangeValueForKey:@"employees"
      withSetMutation:NSKeyValueUnionSetMutation
      usingObjects:value];
[[self primitiveEmployees] unionSet:value];
[self didChangeValueForKey:@"employees"
      withSetMutation:NSKeyValueUnionSetMutation
      usingObjects:value];
}

- (void)removeEmployees:(NSSet *)value
{
[self willChangeValueForKey:@"employees"
      withSetMutation:NSKeyValueMinusSetMutation
      usingObjects:value];
[[self primitiveEmployees] minusSet:value];
[self didChangeValueForKey:@"employees"
      withSetMutation:NSKeyValueMinusSetMutation
      usingObjects:value];
}

يمكنك بسهولة تجميع مجموعة العلاقات الخاصة بك خارج البيانات الأساسية ثم إضافتها كلها مرة واحدة باستخدام هذه الطريقة. قد يكون قبيحًا أقل من الطريقة التي اقترحتها ؛)






xcode4