Magia de cadenas en Swift: por qué "simplemente tomar un carácter" es un desafío
\n\nCualquier desarrollador que haya migrado a Swift desde Python, JavaScript o C++, tarde o temprano se topa con un muro. Una tarea aparentemente simple: "Dame el tercer carácter de la cadena". En Python es str[2]. En JavaScript es str[2]. En Swift... es más complicado. Y si alguna vez ha buscado la respuesta a la pregunta "get nth character of a string in Swift", sabrá que Stack Overflow está literalmente repleto de esta pregunta (miles de visitas, cientos de votos).
¿Por qué? Porque Swift trata las cadenas no como una matriz de bytes, sino como una secuencia compleja de grafemas compatible con Unicode. Y si se aborda la tarea de frente, el rendimiento puede caer y el código puede volverse ilegible.
\n\nEn este artículo no solo daremos la respuesta. Analizaremos la optimización para obtener el enésimo carácter en Swift, veremos el rendimiento de diferentes enfoques y elegiremos el mejor. ¡Allá vamos!
\n\n¿Por qué string[2] no funciona? Breve introducción a String.Index
\n\nEn Swift, String no es una colección de Character con acceso aleatorio. Es una colección con acceso a través de índices, que no son números enteros. ¿Por qué es así?
- \n
- Unicode y grafemas. Un solo "carácter" puede constar de varios escalares Unicode (por ejemplo, el emoji "🇷🇺" o "é" con acento). Si el índice fuera simplemente un Int, entonces
str[1]podría devolver la mitad de un emoji. \n - Rendimiento. Almacenar el índice como un Int para una cadena Unicode es un compromiso. Swift utiliza acceso O(1) al inicio y al final, pero para un índice arbitrario es necesario recorrer la cadena. \n
Por lo tanto, la forma estándar de obtener el enésimo carácter en Swift se ve así:
\n\nlet str = "¡Hola, Swift!"\nlet index = str.index(str.startIndex, offsetBy: 2)\nlet char = str[index] // "o"\n\n¿Funciona? Sí. ¿Es óptimo? No.
\n\nEl problema del rendimiento: qué esconde offsetBy
\n\nCada llamada a str.index(startIndex, offsetBy: n) es una operación O(n). Imagine que tiene una cadena de 10,000 caracteres y quiere obtener el carácter en el índice 9999. Swift recorrerá los 9999 caracteres para encontrar el deseado. ¿Hace esto en un bucle 100 veces? Obtiene una complejidad cuadrática.
Aquí hay un ejemplo de código lento que suelen usar los principiantes:
\n\nlet longString = "A" + String(repeating: "b", count: 10_000) + "C"\nfor i in 0..<longString.count {\n let index = longString.index(longString.startIndex, offsetBy: i)\n let char = longString[index]\n // Hacer algo con char\n}\n\nEste código funcionará muy lentamente, porque cada llamada a offsetBy comienza desde startIndex. De hecho, la complejidad es O(n²).
Optimización n.º 1: Uso de índice incremental
\n\nLa forma más sencilla de acelerar get nth character Swift es no recalcular el índice desde cero cada vez. Use index(after:) o formIndex(after:).
let longString = "A" + String(repeating: "b", count: 10_000) + "C"\nvar index = longString.startIndex\nfor _ in 0..<longString.count {\n let char = longString[index]\n // Hacer algo con char\n index = longString.index(after: index) // ¡O(1)!\n}\n\nAhora la complejidad es lineal O(n). Pero, ¿qué pasa si necesitamos acceso aleatorio por índice, no secuencial?
\n\nOptimización n.º 2: Conversión a una matriz de caracteres (con salvedades)
\n\nSi necesita acceder con frecuencia a los caracteres por un índice arbitrario (enésimo carácter), y la cadena no es gigantesca, lo más sencillo es convertirla en [Character]. Entonces el acceso será O(1).
let str = "¡Hola, Swift!"\nlet chars = Array(str)\nlet char = chars[2] // "o