La forma más eficiente de iterar sobre todos los caracteres en un NSString

¿Cuál es la mejor manera de iterar sobre todos los caracteres en un NSString? ¿Te gustaría recorrer la longitud de la cadena y usar el método?

[aNSString characterAtIndex:index]; 

¿O desea utilizar un búfer de caracteres basado en NSString?

Definitivamente obtendría un búfer char primero, luego iterar sobre eso.

 NSString *someString = ... unsigned int len = [someString length]; char buffer[len]; //This way: strncpy(buffer, [someString UTF8String]); //Or this way (preferred): [someString getCharacters:buffer range:NSMakeRange(0, len)]; for(int i = 0; i < len; ++i) { char current = buffer[i]; //do something with current... } 

Creo que es importante que las personas entiendan cómo lidiar con Unicode, así que terminé escribiendo una respuesta monstruosa, pero en el espíritu de tl; dr comenzaré con un fragmento que debería funcionar bien. Si desea conocer los detalles (¡lo que debería hacer!), Continúe leyendo después del fragmento.

 NSUInteger len = [str length]; unichar buffer[len+1]; [str getCharacters:buffer range:NSMakeRange(0, len)]; NSLog(@"getCharacters:range: with unichar buffer"); for(int i = 0; i < len; i++) { NSLog(@"%C", buffer[i]); } 

¿Aún conmigo? ¡Bueno!

La respuesta aceptada actualmente parece confundir bytes con caracteres / letras. Este es un problema común al encontrar unicode, especialmente desde un fondo C. Las cadenas en Objective-C se representan como caracteres unicode ( unichar ) que son mucho más grandes que los bytes y no se deben usar con las funciones de manipulación de cadenas C estándar.

( Editar : ¡Esta no es la historia completa! Para mi gran vergüenza, me olvidé por completo de dar cuenta de los caracteres compostables, donde una "letra" se compone de múltiples puntos de código Unicode. Esto te da una situación en la que puedes tener uno " carta "resolviendo múltiples unichars, que a su vez son múltiples bytes cada uno. Hoo boy. Por favor refiérase a esta gran respuesta para los detalles sobre eso.)

La respuesta adecuada a la pregunta depende de si desea iterar sobre los caracteres / letras (a diferencia del tipo char ) o los bytes de la cadena (lo que el tipo de caracteres realmente significa). Con el ánimo de limitar la confusión, usaré los términos byte y carta a partir de ahora, evitando el personaje de término posiblemente ambiguo.

Si quieres hacer lo anterior e iterar sobre las letras de la cadena, debes tratar exclusivamente con unichars (lo siento, pero ahora estamos en el futuro, ya no puedes ignorarlo). Encontrar la cantidad de letras es fácil, es la propiedad de la longitud de la cadena. Un fragmento de ejemplo es como tal (igual que el anterior):

 NSUInteger len = [str length]; unichar buffer[len+1]; [str getCharacters:buffer range:NSMakeRange(0, len)]; NSLog(@"getCharacters:range: with unichar buffer"); for(int i = 0; i < len; i++) { NSLog(@"%C", buffer[i]); } 

Si, por otro lado, desea iterar sobre los bytes en una cadena, comienza a complicarse y el resultado dependerá completamente de la encoding que elija usar. La opción predeterminada decente es UTF8, así que eso es lo que mostraré.

Al hacer esto, tiene que calcular cuántos bytes será la cadena UTF8 resultante, un paso donde es fácil equivocarse y usar la -length la cadena. Una razón principal por la que esto es muy fácil de hacer, especialmente para un desarrollador de EE. UU., Es que una cadena con letras que caen en el espectro ASCII de 7 bits tendrá la misma longitud de bytes y letras . Esto se debe a que UTF8 codifica letras ASCII de 7 bits con un solo byte, por lo que una cadena de prueba simple y un texto en inglés básico podrían funcionar perfectamente bien.

La forma correcta de hacerlo es usar el método -lengthOfBytesUsingEncoding:NSUTF8StringEncoding (u otra encoding), asigne un búfer con esa longitud, luego convierta la cadena a la misma encoding con -cStringUsingEncoding: y -cStringUsingEncoding: en ese búfer. Código de ejemplo aquí:

 NSUInteger byteLength = [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; char proper_c_buffer[byteLength+1]; strncpy(proper_c_buffer, [str cStringUsingEncoding:NSUTF8StringEncoding], byteLength); NSLog(@"strncpy with proper length"); for(int i = 0; i < byteLength; i++) { NSLog(@"%c", proper_c_buffer[i]); } 

Solo para aclarar el motivo de por qué es importante mantener las cosas en orden, mostraré un código de ejemplo que maneja esta iteración de cuatro maneras diferentes, dos incorrectas y dos correctas. Este es el código:

 #import  int main() { NSString *str = @"буква"; NSUInteger len = [str length]; // Try to store unicode letters in a char array. This will fail horribly // because getCharacters:range: takes a unichar array and will probably // overflow or do other terrible things. (the compiler will warn you here, // but warnings get ignored) char c_buffer[len+1]; [str getCharacters:c_buffer range:NSMakeRange(0, len)]; NSLog(@"getCharacters:range: with char buffer"); for(int i = 0; i < len; i++) { NSLog(@"Byte %d: %c", i, c_buffer[i]); } // Copy the UTF string into a char array, but use the amount of letters // as the buffer size, which will truncate many non-ASCII strings. strncpy(c_buffer, [str UTF8String], len); NSLog(@"strncpy with UTF8String"); for(int i = 0; i < len; i++) { NSLog(@"Byte %d: %c", i, c_buffer[i]); } // Do It Right (tm) for accessing letters by making a unichar buffer with // the proper letter length unichar buffer[len+1]; [str getCharacters:buffer range:NSMakeRange(0, len)]; NSLog(@"getCharacters:range: with unichar buffer"); for(int i = 0; i < len; i++) { NSLog(@"Letter %d: %C", i, buffer[i]); } // Do It Right (tm) for accessing bytes, by using the proper // encoding-handling methods NSUInteger byteLength = [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; char proper_c_buffer[byteLength+1]; const char *utf8_buffer = [str cStringUsingEncoding:NSUTF8StringEncoding]; // We copy here because the documentation tells us the string can disappear // under us and we should copy it. Just to be safe strncpy(proper_c_buffer, utf8_buffer, byteLength); NSLog(@"strncpy with proper length"); for(int i = 0; i < byteLength; i++) { NSLog(@"Byte %d: %c", i, proper_c_buffer[i]); } return 0; } 

La ejecución de este código dará como resultado lo siguiente (con NSLog Cruft recortada), mostrando exactamente qué tan diferentes pueden ser las representaciones de bytes y letras (las dos últimas salidas):

 getCharacters:range: with char buffer Byte 0: 1 Byte 1: Byte 2: C Byte 3: Byte 4: : strncpy with UTF8String Byte 0: Ð Byte 1: ± Byte 2: Ñ Byte 3: Byte 4: Ð getCharacters:range: with unichar buffer Letter 0: б Letter 1: у Letter 2: к Letter 3: в Letter 4: а strncpy with proper length Byte 0: Ð Byte 1: ± Byte 2: Ñ Byte 3: Byte 4: Ð Byte 5: º Byte 6: Ð Byte 7: ² Byte 8: Ð Byte 9: ° 

Ninguno. La sección “Optimizar las manipulaciones del texto” de las “Pautas de rendimiento del cocoa” en la documentación de Xcode recomienda:

Si desea iterar sobre los caracteres de una cadena, una de las cosas que no debe hacer es usar el método characterAtIndex: para recuperar cada carácter por separado. Este método no está diseñado para acceso repetido. En su lugar, considere buscar los caracteres de una vez utilizando el getCharacters:range: e iterando sobre los bytes directamente.

Si desea buscar una cadena de caracteres o subcadenas específicas, no itere por los caracteres uno por uno. En su lugar, utilice métodos de nivel superior como rangeOfString: rangeOfCharacterFromSet: o substringWithRange: que están optimizados para buscar los caracteres NSString .

Consulte esta respuesta de desbordamiento de stack en Cómo eliminar espacios en blanco del extremo derecho de NSString para ver un ejemplo de cómo dejar que rangeOfCharacterFromSet: itere sobre los caracteres de la cadena en lugar de hacerlo usted mismo.

Si bien la solución de Daniel probablemente funcionará la mayor parte del tiempo, creo que la solución depende del contexto. Por ejemplo, tengo una aplicación de ortografía y necesito repetir sobre cada carácter tal como aparece en pantalla, que puede no corresponderse con la forma en que se representa en la memoria. Esto es especialmente cierto para el texto proporcionado por el usuario.

Usando algo como esta categoría en NSString:

 - (void) dumpChars { NSMutableArray *chars = [NSMutableArray array]; NSUInteger len = [self length]; unichar buffer[len+1]; [self getCharacters: buffer range: NSMakeRange(0, len)]; for (int i=0; i 

Y alimentarlo con una palabra como mañana podría producir:

 mañana = m, a, ñ, a, n, a 

Pero podría producir igual de fácilmente:

 mañana = m, a, n, ̃, a, n, a 

El primero se producirá si el hilo está en forma unicode precompuesta y el último si está en forma descompuesta.

Podría pensar que esto podría evitarse utilizando el resultado de preSistemaStringWithCanonicalMapping o precomposedStringWithCompatibilityMapping de NSString, pero este no es necesariamente el caso, como lo advierte Apple en Technical Q & A 1225 . Por ejemplo, una cadena como e̊gâds (que hice totalmente) aún produce lo siguiente incluso después de convertir a una forma precompuesta.

  e̊gâds = e, ̊, g, â, d, s 

La solución para mí es usar enumerateSubstringsInRange de NSString pasando NSStringEnumerationByComposedCharacterSecuencias como la opción de enumeración. Reescribiendo el ejemplo anterior para que se vea así:

 - (void) dumpSequences { NSMutableArray *chars = [NSMutableArray array]; [self enumerateSubstringsInRange: NSMakeRange(0, [self length]) options: NSStringEnumerationByComposedCharacterSequences usingBlock: ^(NSString *inSubstring, NSRange inSubstringRange, NSRange inEnclosingRange, BOOL *outStop) { [chars addObject: inSubstring]; }]; NSLog(@"%@ = %@", self, [chars componentsJoinedByString: @", "]); } 

Si alimentamos esta versión e̊gâds entonces obtenemos

 e̊gâds = e̊, g, â, d, s 

como esperaba, que es lo que quiero.

La sección de documentación sobre Caracteres y Grupos de Grafemas también puede ser útil para explicar algo de esto.

Nota: Parece que algunas de las cadenas de Unicode que utilicé están tropezando SO cuando formateadas como código. Las cadenas que utilicé son mañana y e̊gâds.

Aunque técnicamente obtendría valores de NSString individuales, aquí hay un enfoque alternativo:

 NSRange range = NSMakeRange(0, 1); for (__unused int i = range.location; range.location < [starring length]; range.location++) { NSLog(@"%@", [aNSString substringWithRange:range]); } 

(El __unused int i bit es necesario para silenciar la advertencia del comstackdor).

Prueba cadena enum con bloques

Crear categoría de NSString

.h

 @interface NSString (Category) - (void)enumerateCharactersUsingBlock:(void (^)(NSString *character, NSInteger idx, bool *stop))block; @end 

.metro

 @implementation NSString (Category) - (void)enumerateCharactersUsingBlock:(void (^)(NSString *character, NSInteger idx, bool *stop))block { bool _stop = NO; for(NSInteger i = 0; i < [self length] && !_stop; i++) { NSString *character = [self substringWithRange:NSMakeRange(i, 1)]; block(character, i, &_stop); } } @end 

ejemplo

 NSString *string = @"Hello World"; [string enumerateCharactersUsingBlock:^(NSString *character, NSInteger idx, bool *stop) { NSLog(@"char %@, i: %li",character, (long)idx); }]; 

No deberías usar

 NSUInteger len = [str length]; unichar buffer[len+1]; 

deberías usar la asignación de memoria

 NSUInteger len = [str length]; unichar* buffer = (unichar*) malloc (len+1)*sizeof(unichar); 

y en el uso final

 free(buffer); 

para evitar problemas de memoria