سؤال

أقوم ببعض برمجة Objective-C التي تتضمن تحليل NSXmlDocument وملء خصائص الكائن من النتيجة.

بدا الإصدار الأول مثل هذا:

if([elementName compare:@"companyName"] == 0) 
  [character setCorporationName:currentElementText]; 
else if([elementName compare:@"corporationID"] == 0) 
  [character setCorporationID:currentElementText]; 
else if([elementName compare:@"name"] == 0) 
  ...

لكني لا أحب if-else-if-else نمط ينتج هذا.أنظر إلى switch بيان أرى أنني لا أستطيع التعامل إلا ints, chars الخ وليس الأشياء..فهل هناك نمط تنفيذ أفضل لست على علم به؟

راجع للشغل لقد توصلت بالفعل إلى حل أفضل لتعيين خصائص الكائن، ولكن أريد أن أعرف على وجه التحديد عن if-else ضد switch النمط في Objective-C

هل كانت مفيدة؟

المحلول

أتمنى أن تسامحوني جميعًا على الخروج عن المألوف هنا، ولكن أود أن أتناول السؤال الأكثر عمومية وهو تحليل مستندات XML في Cocoa دون الحاجة إلى عبارات if-else.يقوم السؤال كما هو مذكور في الأصل بتعيين نص العنصر الحالي لمتغير مثيل لكائن الحرف.كما أشار جماه، يمكن حل هذه المشكلة باستخدام ترميز القيمة الرئيسية.ومع ذلك، في مستند XML الأكثر تعقيدًا، قد لا يكون هذا ممكنًا.خذ بعين الاعتبار على سبيل المثال ما يلي.

<xmlroot>
    <corporationID>
        <stockSymbol>EXAM</stockSymbol>
        <uuid>31337</uuid>
    </corporationID>
    <companyName>Example Inc.</companyName>
</xmlroot>

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

NSXMLElement* root = [xmlDocument rootElement];

// Assuming that we only have one of each element.
[character setCorperationName:[[[root elementsForName:@"companyName"] objectAtIndex:0] stringValue]];

NSXMLElement* corperationId = [root elementsForName:@"corporationID"];
[character setCorperationStockSymbol:[[[corperationId elementsForName:@"stockSymbol"] objectAtIndex:0] stringValue]];
[character setCorperationUUID:[[[corperationId elementsForName:@"uuid"] objectAtIndex:0] stringValue]];

يستخدم التالي NSXMLNode الأكثر عمومية، ويمشي عبر الشجرة، ويستخدم مباشرة بنية if-else.

// The first line is the same as the last example, because NSXMLElement inherits from NSXMLNode
NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
    if([[aNode name] isEqualToString:@"companyName"]){
        [character setCorperationName:[aNode stringValue]];
    }else if([[aNode name] isEqualToString:@"corporationID"]){
        NSXMLNode* correctParent = aNode;
        while((aNode = [aNode nextNode]) == nil && [aNode parent != correctParent){
            if([[aNode name] isEqualToString:@"stockSymbol"]){
                [character setCorperationStockSymbol:[aNode stringValue]];
            }else if([[aNode name] isEqualToString:@"uuid"]){
                [character setCorperationUUID:[aNode stringValue]];
            }
        }
    }
}

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

- (NSNode*)parse_companyName:(NSNode*)aNode
{
    [character setCorperationName:[aNode stringValue]];
    return aNode;
}

- (NSNode*)parse_corporationID:(NSNode*)aNode
{
    NSXMLNode* correctParent = aNode;
    while((aNode = [aNode nextNode]) == nil && [aNode parent != correctParent){
        [self invokeMethodForNode:aNode prefix:@"parse_corporationID_"];
    }
    return [aNode previousNode];
}

- (NSNode*)parse_corporationID_stockSymbol:(NSNode*)aNode
{
    [character setCorperationStockSymbol:[aNode stringValue]];
    return aNode;
}

- (NSNode*)parse_corporationID_uuid:(NSNode*)aNode
{
    [character setCorperationUUID:[aNode stringValue]];
    return aNode;
}

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

- (NSNode*)invokeMethodForNode:(NSNode*)aNode prefix:(NSString*)aPrefix
{
    NSNode* ret = nil;
    NSString* methodName = [NSString stringWithFormat:@"%@%@:", prefix, [aNode name]];
    SEL selector = NSSelectorFromString(methodName);
    if([self respondsToSelector:selector])
        ret = [self performSelector:selector withObject:aNode];
    return ret;
}

الآن، بدلًا من عبارة if-else الأكبر (التي تفرق بين اسم الشركة ومعرف الشركة)، يمكننا ببساطة كتابة سطر واحد من التعليمات البرمجية

NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
    aNode = [self invokeMethodForNode:aNode prefix:@"parse_"];
}

الآن أعتذر إذا أخطأت في أي من هذا، لقد مر وقت طويل منذ أن كتبت أي شيء باستخدام NSXMLDocument، لقد تأخر الوقت في الليل ولم أختبر هذا الرمز فعليًا.لذلك إذا رأيت أي خطأ، يرجى ترك تعليق أو تعديل هذه الإجابة.

ومع ذلك، أعتقد أنني أوضحت للتو كيف يمكن استخدام المحددات ذات الأسماء الصحيحة في Cocoa للتخلص تمامًا من عبارات if-else في مثل هذه الحالات.هناك عدد قليل من مسكتك وحالات الزاوية.محدد الأداء:تأخذ عائلة الأساليب فقط 0 أو 1 أو 2 من وسائط الوسيطات وأنواع الإرجاع هي كائنات، لذلك إذا كانت أنواع الوسائط ونوع الإرجاع ليست كائنات، أو إذا كان هناك أكثر من وسيطتين، فسيتعين عليك استخدام NSInvocation لاستدعائه.يجب عليك التأكد من أن أسماء الطرق التي تنشئها لن تستدعي طرقًا أخرى، خاصة إذا كان هدف الاستدعاء هو كائن آخر، ولن يعمل نظام تسمية الطريقة المحدد هذا على العناصر التي تحتوي على أحرف غير أبجدية رقمية.يمكنك التغلب على ذلك عن طريق الهروب من أسماء عناصر XML في أسماء الطرق الخاصة بك بطريقة أو بأخرى، أو عن طريق إنشاء NSDictionary باستخدام أسماء الطرق كمفاتيح والمحددات كقيم.يمكن أن يستهلك هذا الذاكرة بشكل كبير وينتهي به الأمر إلى استغراق وقت أطول.يعد إرسال PerformanceSelector كما وصفته سريعًا جدًا.بالنسبة لعبارات if-else الكبيرة جدًا، قد تكون هذه الطريقة أسرع من عبارة if-else.

نصائح أخرى

يجب عليك الاستفادة من ترميز القيمة الأساسية:

[character setValue:currentElementText forKey:elementName];

إذا كانت البيانات غير موثوقة، فقد ترغب في التحقق من صلاحية المفتاح:

if (![validKeysCollection containsObject:elementName])
    // Exception or error

إذا كنت تريد استخدام أقل قدر ممكن من التعليمات البرمجية، وتمت تسمية جميع أسماء العناصر والمحددات الخاصة بك بحيث إذا كان elementName هو @"foo" فإن setter هو setFoo:، يمكنك القيام بشيء مثل:

SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", [elementName capitalizedString]]);

[character performSelector:selector withObject:currentElementText];

أو ربما حتى:

[character setValue:currentElementText forKey:elementName]; // KVC-style

على الرغم من أن هذه ستكون بالطبع أبطأ قليلاً من استخدام مجموعة من عبارات if.

[يحرر:الخيار الثاني سبق أن ذكره أحدهم؛أُووبس!]

هل أجرؤ على اقتراح استخدام الماكرو؟

#define TEST( _name, _method ) \
  if ([elementName isEqualToString:@ _name] ) \
    [character _method:currentElementText]; else
#define ENDTEST { /* empty */ }

TEST( "companyName",      setCorporationName )
TEST( "setCorporationID", setCorporationID   )
TEST( "name",             setName            )
:
:
ENDTEST

إحدى الطرق التي قمت بها باستخدام NSStrings هي استخدام NSDictionary والتعدادات.قد لا يكون الأكثر أناقة، لكنني أعتقد أنه يجعل الكود أكثر قابلية للقراءة.يتم استخراج الكود الكاذب التالي من أحد مشاريعي:

typedef enum { UNKNOWNRESIDUE, DEOXYADENINE, DEOXYCYTOSINE, DEOXYGUANINE, DEOXYTHYMINE } SLSResidueType;

static NSDictionary *pdbResidueLookupTable;
...

if (pdbResidueLookupTable == nil)
{
    pdbResidueLookupTable = [[NSDictionary alloc] initWithObjectsAndKeys:
                          [NSNumber numberWithInteger:DEOXYADENINE], @"DA", 
                          [NSNumber numberWithInteger:DEOXYCYTOSINE], @"DC",
                          [NSNumber numberWithInteger:DEOXYGUANINE], @"DG",
                          [NSNumber numberWithInteger:DEOXYTHYMINE], @"DT",
                          nil]; 
}

SLSResidueType residueIdentifier = [[pdbResidueLookupTable objectForKey:residueType] intValue];
switch (residueIdentifier)
{
    case DEOXYADENINE: do something; break;
    case DEOXYCYTOSINE: do something; break;
    case DEOXYGUANINE: do something; break;
    case DEOXYTHYMINE: do something; break;
}

ال if-else التنفيذ الذي لديك هو الطريقة الصحيحة للقيام بذلك، منذ ذلك الحين switch لن تعمل مع الأشياء.وبصرف النظر عن صعوبة القراءة قليلاً (وهو أمر شخصي)، فلا يوجد جانب سلبي حقيقي في الاستخدام if-else التصريحات بهذه الطريقة.

على الرغم من أنه لا توجد بالضرورة طريقة أفضل للقيام بشيء كهذا للاستخدام مرة واحدة، فلماذا تستخدم "مقارنة" عندما يمكنك استخدام "isEqualToString"؟يبدو أن هذا أكثر أداءً نظرًا لأن المقارنة ستتوقف عند أول حرف غير مطابق، بدلاً من المرور عبر الأمر برمته لحساب نتيجة مقارنة صالحة (على الرغم من التفكير في الأمر، قد تكون المقارنة واضحة في نفس النقطة) - على الرغم من أنه سيبدو أكثر نظافة قليلاً لأن هذه المكالمة تُرجع BOOL.

if([elementName isEqualToString:@"companyName"] ) 
  [character setCorporationName:currentElementText]; 
else if([elementName isEqualToString:@"corporationID"] ) 
  [character setCorporationID:currentElementText]; 
else if([elementName isEqualToString:@"name"] ) 

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

بدلاً من ذلك، قم بتحليل نص كل جملة if/else-if في طريقتها الخاصة، وكلها في نفس الفئة.قم بتسمية الرسائل التي تستدعيها بطريقة مماثلة.الآن قم بإنشاء NSArray يحتوي على محددات تلك الرسائل (التي تم الحصول عليها باستخدام @selector()).قم بإجبار السلسلة التي كنت تختبرها في الشروط الشرطية إلى محدد باستخدام NSSelectorFromString() (قد تحتاج إلى سلسلة كلمات أو نقطتين إضافيتين إليها أولاً اعتمادًا على كيفية تسمية تلك الرسائل، وما إذا كانت تأخذ وسيطات أم لا).الآن قم بإجراء المحدد ذاتيًا باستخدام بيرفورمسيليكتور:.

هذا الأسلوب له الجانب السلبي لأنه يمكن أن يزدحم الفصل بالعديد من الرسائل الجديدة، ولكن ربما يكون من الأفضل أن يزدحم فصل دراسي واحد بدلاً من التسلسل الهرمي للفصل بأكمله بفئات فرعية جديدة.

نشر هذا كرد على إجابة Wevah أعلاه - كنت سأقوم بالتحرير، لكن ليس لدي سمعة عالية بما فيه الكفاية حتى الآن:

ولسوء الحظ، فإن الطريقة الأولى تفصل بين الحقول التي تحتوي على أكثر من كلمة واحدة - مثل xPosition.ستقوم CapitalizedString بتحويل ذلك إلى Xposition، والذي عند دمجه مع التنسيق يمنحك setXposition:.بالتأكيد ليس ما هو مطلوب هنا.إليك ما أستخدمه في الكود الخاص بي:

NSString *capName = [elementName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[elementName substringToIndex:1] uppercaseString]];
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", capName]);

ليست جميلة مثل الطريقة الأولى، لكنها تعمل.

لقد توصلت إلى حل يستخدم الكتل لإنشاء بنية تشبه التبديل للكائنات.وهاهو يذهب:

BOOL switch_object(id aObject, ...)
{
    va_list args;
    va_start(args, aObject);

    id value = nil;
    BOOL matchFound = NO;

    while ( (value = va_arg(args,id)) )
    {
        void (^block)(void) = va_arg(args,id);
        if ( [aObject isEqual:value] )
        {
            block();
            matchFound = YES;
            break;
        }
    }

    va_end(args);
    return matchFound;
}

كما ترون، هذه دالة C قديمة مع قائمة وسائط متغيرة.أقوم بتمرير الكائن المراد اختباره في الوسيطة الأولى، متبوعًا بأزواج case_value-case_block.(تذكر أن كتل Objective-C هي مجرد كائنات.) while تستمر الحلقة في استخراج هذه الأزواج حتى تتم مطابقة قيمة الكائن أو لا توجد حالات متبقية (انظر الملاحظات أدناه).

الاستخدام:

NSString* str = @"stuff";
switch_object(str,
              @"blah", ^{
                  NSLog(@"blah");
              },
              @"foobar", ^{
                  NSLog(@"foobar");
              },
              @"stuff", ^{
                  NSLog(@"stuff");
              },
              @"poing", ^{
                  NSLog(@"poing");
              },
              nil);   // <-- sentinel

// will print "stuff"

ملحوظات:

  • هذا هو التقريب الأول دون أي التحقق من الأخطاء
  • حقيقة أن معالجات الحالة عبارة عن كتل، تتطلب عناية إضافية عندما يتعلق الأمر بالرؤية والنطاق وإدارة الذاكرة للمتغيرات المشار إليها من الداخل
  • إذا نسيت الحارس هلكت :P
  • يمكنك استخدام قيمة الإرجاع المنطقية لتشغيل حالة "افتراضية" عندما لا تتم مطابقة أي من الحالات

إن إعادة الهيكلة الأكثر شيوعًا المقترحة لإزالة عبارات if-else أو Switch هي إدخال تعدد الأشكال (انظر http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html).يعد التخلص من هذه الشروط الشرطية أمرًا بالغ الأهمية عند تكرارها.في حالة تحليل XML مثل العينة الخاصة بك، فإنك تقوم أساسًا بنقل البيانات إلى بنية أكثر طبيعية بحيث لا تضطر إلى تكرار الشرط في مكان آخر.في هذه الحالة، من المحتمل أن تكون عبارة if-else أو Switch جيدة بما فيه الكفاية.

في هذه الحالة، لست متأكدًا مما إذا كان بإمكانك إعادة هيكلة الفصل بسهولة لإدخال تعدد الأشكال كما يقترح برادلي، نظرًا لأنه فئة أصلية من الكاكاو.بدلاً من ذلك، فإن طريقة Objective-C للقيام بذلك هي استخدام فئة فئة لإضافة فئة elementNameCode طريقة NSSting:

   typedef enum { 
       companyName = 0,
       companyID,  
       ...,
       Unknown
    } ElementCode;

    @interface NSString (ElementNameCodeAdditions)
    - (ElementCode)elementNameCode; 
    @end

    @implementation NSString (ElementNameCodeAdditions)
    - (ElementCode)elementNameCode {
        if([self compare:@"companyName"]==0) {
            return companyName;
        } else if([self compare:@"companyID"]==0) {
            return companyID;
        } ... {

        }

        return Unknown;
    }
    @end

في التعليمات البرمجية الخاصة بك، يمكنك الآن استخدام مفتاح التشغيل [elementName elementNameCode] (واحصل على تحذيرات المترجم المرتبطة إذا نسيت اختبار أحد أعضاء التعداد وما إلى ذلك).

وكما يشير برادلي، قد لا يكون هذا الأمر يستحق العناء إذا تم استخدام المنطق في مكان واحد فقط.

ما قمنا به في مشاريعنا حيث نحتاج إلى القيام بهذا النوع من الأشياء مرارًا وتكرارًا، هو إعداد CFDictionary ثابت لرسم خرائط للسلاسل/الكائنات للتحقق من قيمة عددية بسيطة.إنه يؤدي إلى رمز يشبه هذا:

static CFDictionaryRef  map = NULL;
int count = 3;
const void *keys[count] = { @"key1", @"key2", @"key3" };
const void *values[count] = { (uintptr_t)1, (uintptr_t)2, (uintptr_t)3 };

if (map == NULL)
    map = CFDictionaryCreate(NULL,keys,values,count,&kCFTypeDictionaryKeyCallBacks,NULL);


switch((uintptr_t)CFDictionaryGetValue(map,[node name]))
{
    case 1:
        // do something
        break;
    case 2:
        // do something else
        break;
    case 3:
        // this other thing too
        break;
}

إذا كنت تستهدف Leopard فقط، فيمكنك استخدام NSMapTable بدلاً من CFDictionary.

على غرار Lvsti، أستخدم الكتل لإجراء نمط تبديل على الكائنات.

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

NSObject+Functional.h

#import <Foundation/Foundation.h>
typedef id(^FilterBlock)(id element, NSUInteger idx, BOOL *stop);

@interface NSObject (Functional)
-(id)processByPerformingFilterBlocks:(NSArray *)filterBlocks;
@end

NSObject+Functional.m

@implementation NSObject (Functional)
-(id)processByPerformingFilterBlocks:(NSArray *)filterBlocks
{
    __block id blockSelf = self;
    [filterBlocks enumerateObjectsUsingBlock:^( id (^block)(id,NSUInteger idx, BOOL*) , NSUInteger idx, BOOL *stop) {
        blockSelf = block(blockSelf, idx, stop);
    }];

    return blockSelf;
}
@end

الآن يمكننا الإعداد n FilterBlocks لاختبار الحالات المختلفة.

FilterBlock caseYES = ^id(id element, NSUInteger idx, BOOL *breakAfter){ 
    if ([element isEqualToString:@"YES"]) { 
        NSLog(@"You did it");  
        *breakAfter = YES;
    } 
    return element;
};

FilterBlock caseNO  = ^id(id element, NSUInteger idx, BOOL *breakAfter){ 
    if ([element isEqualToString:@"NO"] ) { 
        NSLog(@"Nope");
        *breakAfter = YES;
    }
    return element;
};

نلصق الآن تلك الكتلة التي نريد اختبارها كسلسلة مرشح في مصفوفة:

NSArray *filters = @[caseYES, caseNO];

ويمكن تنفيذها على كائن

id obj1 = @"YES";
id obj2 = @"NO";
[obj1 processByPerformingFilterBlocks:filters];
[obj2 processByPerformingFilterBlocks:filters];

يمكن استخدام هذا الأسلوب للتبديل وأيضًا لأي تطبيق سلسلة مرشح (مشروط)، حيث يمكن للكتل تحرير العنصر وتمريره.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top