Категории для NSMutableString и NSString вызывают путаницу связывания? - PullRequest
2 голосов
/ 07 августа 2009

Я расширил и NSString, и NSMutableString некоторыми удобными методами, использующими категории. Эти добавленные методы имеют одинаковое имя, но имеют разные реализации. Например, я реализовал функцию ruby ​​"strip", которая удаляет пробельные символы в конечных точках для обоих, но для NSString она возвращает новую строку, а для NSMutableString она использует "deleteCharactersInRange", чтобы удалить существующую строку и вернуть ее (например, рубиновая полоса!).

Вот типичный заголовок:

@interface NSString (Extensions)
-(NSString *)strip;
@end

и

@interface NSMutableString (Extensions)
-(void)strip;
@end

Проблема в том, что когда я объявляю NSString * s и запускаю [s strip], он пытается запустить версию NSMutableString и вызывает расширение.

NSString *s = @"   This is a simple string    ";
NSLog([s strip]);

не удается с:

Завершение работы приложения из-за отсутствия связи исключение NSInvalidArgumentException, причина: Попытка мутировать неизменный объект with deleteCharactersInRange: '

Ответы [ 3 ]

4 голосов
/ 07 августа 2009

Вы были укушены подробностями реализации: некоторые объекты NSString являются экземплярами подкласса NSMutableString, и только частный флаг контролирует, является ли объект изменчивым или нет.

Вот тестовое приложение:

#import <Foundation/Foundation.h>

int main(int argc, char **argv) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSString *str = [NSString stringWithUTF8String:"Test string"];
    NSLog(@"%@ is a kind of NSMutableString? %@", [str class], [str isKindOfClass:[NSMutableString class]] ? @"YES" : @"NO");

    [pool drain];
    return EXIT_SUCCESS;
}

Если вы скомпилируете и запустите его на Leopard (по крайней мере), вы получите такой вывод:

NSCFString is a kind of NSMutableString? YES

Как я уже сказал, объект имеет закрытый флаг, управляющий тем, является ли он изменяемым или нет. Поскольку я прошел через NSString, а не NSMutableString, этот объект не является изменяемым. Если вы попытаетесь изменить его, вот так:

NSMutableString *mstr = str;
[mstr appendString:@" is mutable!"];

вы получите (1) заслуженное предупреждение (которое можно было бы замолчать с помощью приведения, но это было бы плохой идеей) и (2) то же исключение, которое вы получили в своем собственном приложении.

Решение, которое я предлагаю, заключается в том, чтобы обернуть вашу изменяющуюся полосу в блок @try и вызвать вашу реализацию NSString (return [super strip]) в блоке @catch.

Кроме того, я бы не рекомендовал давать методу разные типы возвращаемых данных. Я бы сделал мутацию один возврат self, как retain и autorelease сделать. Тогда вы всегда можете сделать это:

NSString *unstripped = …;
NSString *stripped = [unstripped strip];

, не беспокоясь о том, является ли unstripped изменчивой строкой или нет. Фактически, этот пример дает хороший пример того, что вы должны полностью удалить мутацию strip или переименовать копирование strip в stringByStripping или что-то подобное (по аналогии с replaceOccurrencesOfString:… и stringByReplacingOccurrencesOfString:…).

1 голос
/ 04 марта 2010

Ключ к проблеме, с которой вы столкнулись, лежит в тонком замечании о полиморфизме в Objective-C. Поскольку язык не поддерживает перегрузку методов, предполагается, что имя метода уникально идентифицирует метод в данном классе. Существует неявное (но важное) предположение, что переопределенный метод имеет ту же семантику, что и метод, который он переопределяет.

В данном случае, возможно, семантика двух методов не одинакова; то есть первый метод возвращает новую строку, инициализированную «раздетой» версией содержимого получателя, тогда как второй метод непосредственно изменяет содержимое получателя. Эти две операции действительно не эквивалентны.

Я думаю, что если вы внимательно посмотрите, как Apple называет свои API-интерфейсы, особенно в Foundation, это действительно поможет пролить свет на некоторые семантические нюансы. Например, в NSString есть несколько методов для создания новой строки, содержащей измененную версию получателя, например

- (NSString *)stringByAppendingFormat:(NSString *)format ...;

Обратите внимание, что имя является существительным, где первое слово описывает возвращаемое значение, а остальная часть имени описывает аргумент. Теперь сравните это с соответствующим методом в NSMutableString для добавления непосредственно к получателю:

- (void)appendFormat:(NSString *)format ...;

В отличие от этого, этот метод является глаголом, потому что нет возвращаемого значения для описания. Поэтому из одного только имени метода ясно, что -appendFormat: действует на получателя, тогда как -stringByAppendingFormat: нет, и вместо этого возвращает новую строку.

(Кстати, в NSString уже есть метод, который выполняет хотя бы часть того, что вы хотите: -stringByTrimmingCharactersInSet:. Вы можете передать whitespaceCharacterSet в качестве аргумента для обрезания начальных и конечных пробелов.)

Так что, хотя поначалу это может показаться раздражающим, я думаю, что в долгосрочной перспективе вам действительно стоит попытаться подражать соглашениям Apple об именах. Если ничего другого, это поможет сделать ваш код более самодокументированным, особенно для других разработчиков Obj-C. Но я думаю, что это также поможет прояснить некоторые семантические тонкости Objective-C и фреймворков Apple.

Кроме того, я согласен, что внутренние детали кластеров классов могут сбивать с толку, тем более что они в основном непрозрачны для нас. Однако факт остается фактом: NSString - это кластер классов, который использует NSCFString как для изменяемых, так и для неизменяемых экземпляров. Поэтому, когда ваша вторая категория добавляет другой метод -strip, он заменяет метод -strip, добавленный первой категорией. Изменение имени одного или обоих методов устранит эту проблему.

И поскольку в NSString уже существует метод, обеспечивающий те же функции, возможно, вы могли бы просто добавить изменяемый метод. В идеале его имя будет соответствовать существующему методу, поэтому оно будет:

- (void)trimCharactersInSet:(NSCharacterSet *)set
1 голос
/ 07 августа 2009

Пример облегчит понимание проблемы:

@interface Foo : NSObject {
}
- (NSString *)method;
@end

@interface Bar : Foo {
}
- (void)method;
@end

void MyFunction(void) {
    Foo *foo = [[[Bar alloc] init] autorelease];
    NSString *string = [foo method];
}

В приведенном выше коде будет выделен экземпляр «Bar», но вызываемый объект (код в MyFunction) имеет ссылку на этот объект Bar через тип Foo, насколько известно вызывающему, foo реализует «имя» вернуть строку. Однако, поскольку foo на самом деле является экземпляром bar, он не будет возвращать строку.

В большинстве случаев вы не можете безопасно изменять тип возвращаемого значения или типы аргументов унаследованного метода. Есть несколько особых способов, которыми вы можете это сделать. Их называют ковариация и контравариантность . По сути, вы можете изменить тип возвращаемого значения унаследованного метода на более сильный тип, и вы можете изменить типы аргументов унаследованного метода на более слабый тип. Рациональное за этим заключается в том, что каждый подкласс должен удовлетворять интерфейсу своего базового класса.

Поэтому, хотя нельзя изменить тип возвращаемого значения «method» с NSString * на void, было бы законно изменить его с NSString * на NSMutableString *.

...