Constantes en Objective-C

1002

Estoy desarrollando una aplicación Cocoa , y estoy usando NSStrings constantes como formas de almacenar nombres clave para mis preferencias.

Entiendo que esta es una buena idea porque permite cambiar fácilmente las claves si es necesario.
Además, es toda la noción de 'separar sus datos de su lógica'.

De todos modos, ¿hay una buena manera de definir estas constantes una vez para toda la aplicación?

Estoy seguro de que hay una manera fácil e inteligente, pero en este momento mis clases simplemente redefinen las que usan.

Allyn
fuente
77
OOP se trata de agrupar sus datos con su lógica. Lo que está proponiendo es solo una buena práctica de programación, es decir, hacer que su programa sea fácil de cambiar.
Raffi Khatchadourian

Respuestas:

1287

Debe crear un archivo de encabezado como

// Constants.h
FOUNDATION_EXPORT NSString *const MyFirstConstant;
FOUNDATION_EXPORT NSString *const MySecondConstant;
//etc.

(puede usar en externlugar de FOUNDATION_EXPORTsi su código no se usará en entornos mixtos C / C ++ o en otras plataformas)

Puede incluir este archivo en cada archivo que use las constantes o en el encabezado precompilado para el proyecto.

Define estas constantes en un archivo .m como

// Constants.m
NSString *const MyFirstConstant = @"FirstConstant";
NSString *const MySecondConstant = @"SecondConstant";

Constants.m debe agregarse al objetivo de su aplicación / marco para que esté vinculado al producto final.

La ventaja de usar constantes de cadena en lugar de constantes #define'd es que puede probar la igualdad usando la comparación de puntero ( stringInstance == MyFirstConstant), que es mucho más rápido que la comparación de cadena ( [stringInstance isEqualToString:MyFirstConstant]) (y más fácil de leer, IMO).

Barry Wark
fuente
67
Para una constante entera sería: extern int const MyFirstConstant = 1;
Dan Morgan
180
En general, una gran respuesta, con una advertencia evidente: NO desea probar la igualdad de cadena con el operador == en Objective-C, ya que prueba la dirección de memoria. Siempre use -isEqualToString: para esto. Puede obtener fácilmente una instancia diferente comparando MyFirstConstant y [NSString stringWithFormat: MyFirstConstant]. No haga suposiciones sobre qué instancia de una cadena tiene, incluso con literales. (En cualquier caso, #define es una "directiva de preprocesador", y se sustituye antes de la compilación, de cualquier forma el compilador ve un literal de cadena al final.)
Quinn Taylor
74
En este caso, está bien usar == para probar la igualdad con la constante, si realmente se usa como un símbolo constante (es decir, se usa el símbolo MyFirstConstant en lugar de una cadena que contiene @ "MyFirstConstant"). En este caso, se podría usar un número entero en lugar de una cadena (realmente, eso es lo que está haciendo, usar el puntero como un número entero), pero usar una cadena constante hace que la depuración sea un poco más fácil ya que el valor de la constante tiene un significado legible para los humanos .
Barry Wark
17
+1 para "Constantes.m debe agregarse al objetivo de su aplicación / marco para que esté vinculado al producto final". Salvó mi cordura. @amok, haga "Obtener información" en Constants.m y elija la pestaña "Objetivos". Asegúrese de que esté marcado para los objetivos relevantes.
PEZ
73
@Barry: En Cocoa, he visto varias clases que definen sus NSStringpropiedades en copylugar de retain. Como tal, podrían (y deberían) mantener una instancia diferente de su NSString*constante, y la comparación directa de direcciones de memoria fallaría. Además, presumiría que cualquier implementación razonablemente óptima de -isEqualToString:comprobaría la igualdad del puntero antes de entrar en el meollo de la comparación de caracteres.
Ben Mosher
280

La manera más fácil:

// Prefs.h
#define PREFS_MY_CONSTANT @"prefs_my_constant"

Mejor manera:

// Prefs.h
extern NSString * const PREFS_MY_CONSTANT;

// Prefs.m
NSString * const PREFS_MY_CONSTANT = @"prefs_my_constant";

Una ventaja del segundo es que cambiar el valor de una constante no causa una reconstrucción de todo su programa.

Andrew Grant
fuente
12
Pensé que no se suponía que debías cambiar el valor de las constantes.
ruipacheco
71
Andrew se refiere a cambiar el valor de la constante mientras codifica, no mientras la aplicación se está ejecutando.
Randall
77
¿Hay algún valor agregado al hacer extern NSString const * const MyConstant, es decir, convertirlo en un puntero constante a un objeto constante en lugar de solo un puntero constante?
Hari Karam Singh
44
¿Qué sucede si uso esta declaración en el archivo de encabezado, NSString estático * const kNSStringConst = @ "valor const"; ¿Cuál es la diferencia entre no declarar e iniciar por separado en archivos .h y .m?
karim
44
@Dogweather: un lugar donde solo el compilador sabe la respuesta. Es decir, si desea incluir en un menú acerca de qué compilador se utilizó para compilar una compilación de una aplicación, puede colocarlo allí, ya que de lo contrario el código compilado no tendría conocimiento. No puedo pensar en muchos otros lugares. Las macros ciertamente no deberían usarse en muchos lugares. ¿Qué pasaría si tuviera #define MY_CONST 5 y en otros lugares #define MY_CONST_2 25. El resultado es que muy bien puede terminar con un error de compilación cuando intenta compilar 5_2. No use #define para constantes. Use const para constantes.
ArtOfWarfare
190

También hay una cosa para mencionar. Si necesita una constante no global, debe usar la staticpalabra clave.

Ejemplo

// In your *.m file
static NSString * const kNSStringConst = @"const value";

Debido a la staticpalabra clave, este const no es visible fuera del archivo.


Corrección menor por @QuinnTaylor : las variables estáticas son visibles dentro de una unidad de compilación . Por lo general, este es un único archivo .m (como en este ejemplo), pero puede morderte si lo declaras en un encabezado que se incluye en otro lugar, ya que obtendrás errores de enlace después de la compilación

kompozer
fuente
41
Corrección menor: las variables estáticas son visibles dentro de una unidad de compilación . Por lo general, este es un único archivo .m (como en este ejemplo), pero puede morderte si lo declaras en un encabezado que se incluye en otro lugar, ya que obtendrás errores de enlazador después de la compilación.
Quinn Taylor
Si no uso la palabra clave estática, ¿estará kNSStringConst disponible durante todo el proyecto?
Danyal Aytekin el
2
Ok, acabo de marcar ... Xcode no proporciona autocompletado en otros archivos si dejas la estática desactivada, pero intenté poner el mismo nombre en dos lugares diferentes y reproduje los errores del enlazador de Quinn.
Danyal Aytekin el
1
La estática en un archivo de encabezado no da problemas de enlazador. Sin embargo, cada unidad de compilación, incluido el archivo de encabezado, obtendrá su propia variable estática, por lo que obtendrá 100 de ellos si incluye el encabezado de 100 archivos .m.
gnasher729
@kompozer ¿En qué parte del archivo .m coloca esto?
Basil Bourque
117

La respuesta aceptada (y correcta) dice que "puede incluir este archivo [Constants.h] ... en el encabezado precompilado para el proyecto".

Como novato, tuve dificultades para hacerlo sin más explicaciones: así es cómo: en su archivo YourAppNameHere-Prefix.pch (este es el nombre predeterminado para el encabezado precompilado en Xcode), importe sus Constantes.h dentro del #ifdef __OBJC__bloque .

#ifdef __OBJC__
  #import <UIKit/UIKit.h>
  #import <Foundation/Foundation.h>
  #import "Constants.h"
#endif

También tenga en cuenta que los archivos Constants.h y Constants.m no deben contener absolutamente nada más que lo que se describe en la respuesta aceptada. (Sin interfaz o implementación).

Victor Van Hee
fuente
Hice esto pero algunos archivos arrojan un error al compilar "Uso del identificador no declarado 'CONSTANTSNAME' Si incluyo la constante.h en el archivo que arroja el error, funciona, pero eso no es lo que quiero hacer. He limpiado, apagado xcode y build y aún problemas ... alguna idea?
J3RM
50

Generalmente estoy usando la forma publicada por Barry Wark y Rahul Gupta.

Aunque, no me gusta repetir las mismas palabras en los archivos .h y .m. Tenga en cuenta que en el siguiente ejemplo la línea es casi idéntica en ambos archivos:

// file.h
extern NSString* const MyConst;

//file.m
NSString* const MyConst = @"Lorem ipsum";

Por lo tanto, lo que me gusta hacer es utilizar alguna maquinaria de preprocesador C. Déjame explicarte a través del ejemplo.

Tengo un archivo de encabezado que define la macro STR_CONST(name, value):

// StringConsts.h
#ifdef SYNTHESIZE_CONSTS
# define STR_CONST(name, value) NSString* const name = @ value
#else
# define STR_CONST(name, value) extern NSString* const name
#endif

En mi par .h / .m donde quiero definir la constante, hago lo siguiente:

// myfile.h
#import <StringConsts.h>

STR_CONST(MyConst, "Lorem Ipsum");
STR_CONST(MyOtherConst, "Hello world");

// myfile.m
#define SYNTHESIZE_CONSTS
#import "myfile.h"

et voila, tengo toda la información sobre las constantes en el archivo .h solamente.

Krizz
fuente
Hmm, hay un poco de advertencia, sin embargo, no puede usar esta técnica como esta si el archivo de encabezado se importa al encabezado precompilado, porque no cargará el archivo .h en el archivo .m porque ya se compiló. Sin embargo, hay una manera: vea mi respuesta (ya que no puedo poner un buen código en los comentarios.
Scott Little
No puedo hacer que esto funcione. Si pongo #define SYNTHESIZE_CONSTS antes de #import "myfile.h", hace NSString * ... tanto en .h como .m (marcado con la vista auxiliar y el preprocesador). Lanza errores de redefinición. Si lo pongo después de #import "myfile.h" hace NSString * externo ... en ambos archivos. Luego arroja errores de "símbolo indefinido".
arsenius
28

Yo mismo tengo un encabezado dedicado a declarar NSStrings constantes utilizados para preferencias de esta manera:

extern NSString * const PPRememberMusicList;
extern NSString * const PPLoadMusicAtListLoad;
extern NSString * const PPAfterPlayingMusic;
extern NSString * const PPGotoStartupAfterPlaying;

Luego declarándolos en el archivo .m adjunto:

NSString * const PPRememberMusicList = @"Remember Music List";
NSString * const PPLoadMusicAtListLoad = @"Load music when loading list";
NSString * const PPAfterPlayingMusic = @"After playing music";
NSString * const PPGotoStartupAfterPlaying = @"Go to startup pos. after playing";

Este enfoque me ha servido bien.

Editar: tenga en cuenta que esto funciona mejor si las cadenas se utilizan en varios archivos. Si solo lo usa un archivo, puede hacerlo #define kNSStringConstant @"Constant NSString"en el archivo .m que usa la cadena.

MaddTheSane
fuente
25

Una ligera modificación de la sugerencia de @Krizz, para que funcione correctamente si el archivo de encabezado de constantes se va a incluir en la PCH, lo cual es bastante normal. Dado que el original se importa a la PCH, no lo volverá a cargar en el .marchivo y, por lo tanto, no obtendrá símbolos y el enlazador no está satisfecho.

Sin embargo, la siguiente modificación le permite funcionar. Es un poco complicado, pero funciona.

Necesitará 3 archivos, .harchivo que tiene las definiciones constantes, el .harchivo y el .marchivo, usaré ConstantList.h, Constants.hy Constants.m, respectivamente. los contenidos de Constants.hson simplemente:

// Constants.h
#define STR_CONST(name, value) extern NSString* const name
#include "ConstantList.h"

y el Constants.marchivo se ve así:

// Constants.m
#ifdef STR_CONST
    #undef STR_CONST
#endif
#define STR_CONST(name, value) NSString* const name = @ value
#include "ConstantList.h"

Finalmente, el ConstantList.harchivo tiene las declaraciones reales y eso es todo:

// ConstantList.h
STR_CONST(kMyConstant, "Value");

Un par de cosas a anotar:

  1. Tuve que redefinir la macro en el .marchivo después de #undef haberla usado para que se usara la macro.

  2. También tuve que usar en #includelugar de #importesto para que funcione correctamente y evitar que el compilador vea los valores previamente compilados.

  3. Esto requerirá una recompilación de su PCH (y probablemente de todo el proyecto) cada vez que se modifiquen los valores, lo que no es el caso si están separados (y duplicados) como es normal.

Espero que sea útil para alguien.

Scott Little
fuente
1
Usar #include solucionó este dolor de cabeza para mí.
Ramsel
¿Tiene esto alguna pérdida de rendimiento / memoria en comparación con la respuesta aceptada?
Gyfis
En respuesta al rendimiento en comparación con la respuesta aceptada, no hay ninguno. Efectivamente, es exactamente lo mismo desde el punto de vista del compilador. Terminas con las mismas declaraciones. Serían EXACTAMENTE iguales si reemplazaras lo externanterior con el FOUNDATION_EXPORT.
Scott Little
14
// Prefs.h
extern NSString * const RAHUL;

// Prefs.m
NSString * const RAHUL = @"rahul";
rahul gupta
fuente
12

Como dijo Abizer, puede ponerlo en el archivo PCH. Otra forma que no es tan sucia es hacer un archivo de inclusión para todas sus claves y luego incluirlo en el archivo en el que está utilizando las claves o incluirlo en la PCH. Con ellos en su propio archivo de inclusión, que al menos le brinda un lugar para buscar y definir todas estas constantes.

Grant Limberg
fuente
11

Si quieres algo como constantes globales; una forma rápida y sucia es colocar las declaraciones constantes en el pcharchivo.

Abizern
fuente
77
Editar el .pch generalmente no es la mejor idea. Tendrá que encontrar un lugar para definir realmente la variable, casi siempre un archivo .m, por lo que tiene más sentido declararlo en el archivo .h correspondiente. La respuesta aceptada de crear un par Constantes.h / m es buena si los necesita en todo el proyecto. Generalmente pongo constantes lo más abajo posible en la jerarquía, en función de dónde se utilizarán.
Quinn Taylor
8

Intenta usar un método de clase:

+(NSString*)theMainTitle
{
    return @"Hello World";
}

Lo uso a veces.

groumpf
fuente
66
Un método de clase no es una constante. Tiene un costo en tiempo de ejecución, y puede que no siempre devuelva el mismo objeto (lo hará si lo implementa de esa manera, pero no necesariamente lo ha implementado de esa manera), lo que significa que debe usarlo isEqualToString:para la comparación, que es un costo adicional en tiempo de ejecución. Cuando quieras constantes, haz constantes.
Peter Hosey
2
@ Peter Hosey, aunque sus comentarios son correctos, tomamos ese rendimiento una vez por LOC o más en idiomas de "nivel superior" como Ruby sin preocuparse por ello. No estoy diciendo que no tienes razón, sino más bien comentando cómo las normas son diferentes en diferentes "mundos".
Dan Rosenstark
1
Es cierto en Ruby. La mayoría del código de rendimiento para personas es bastante innecesario para la aplicación típica.
Peter DeWeese
8

Si le gusta la constante de espacio de nombres, puede aprovechar struct, Friday Q&A 2011-08-19: Constantes y funciones de espacios de nombres

// in the header
extern const struct MANotifyingArrayNotificationsStruct
{
    NSString *didAddObject;
    NSString *didChangeObject;
    NSString *didRemoveObject;
} MANotifyingArrayNotifications;

// in the implementation
const struct MANotifyingArrayNotificationsStruct MANotifyingArrayNotifications = {
    .didAddObject = @"didAddObject",
    .didChangeObject = @"didChangeObject",
    .didRemoveObject = @"didRemoveObject"
};
onmyway133
fuente
1
Una gran cosa! Pero bajo ARC necesitará prefijar todas las variables en la declaración de estructura con __unsafe_unretainedcalificador para que funcione.
Cemen
7

Utilizo una clase singleton, para poder burlarme de la clase y cambiar las constantes si es necesario para las pruebas. La clase de constantes se ve así:

#import <Foundation/Foundation.h>

@interface iCode_Framework : NSObject

@property (readonly, nonatomic) unsigned int iBufCapacity;
@property (readonly, nonatomic) unsigned int iPort;
@property (readonly, nonatomic) NSString * urlStr;

@end

#import "iCode_Framework.h"

static iCode_Framework * instance;

@implementation iCode_Framework

@dynamic iBufCapacity;
@dynamic iPort;
@dynamic urlStr;

- (unsigned int)iBufCapacity
{
    return 1024u;
};

- (unsigned int)iPort
{
    return 1978u;
};

- (NSString *)urlStr
{
    return @"localhost";
};

+ (void)initialize
{
    if (!instance) {
        instance = [[super allocWithZone:NULL] init];
    }
}

+ (id)allocWithZone:(NSZone * const)notUsed
{
    return instance;
}

@end

Y se usa así (tenga en cuenta el uso de una abreviatura para las constantes c: ahorra escribir [[Constants alloc] init]cada vez):

#import "iCode_FrameworkTests.h"
#import "iCode_Framework.h"

static iCode_Framework * c; // Shorthand

@implementation iCode_FrameworkTests

+ (void)initialize
{
    c  = [[iCode_Framework alloc] init]; // Used like normal class; easy to mock!
}

- (void)testSingleton
{
    STAssertNotNil(c, nil);
    STAssertEqualObjects(c, [iCode_Framework alloc], nil);
    STAssertEquals(c.iBufCapacity, 1024u, nil);
}

@end
Howard Lovatt
fuente
1

Si desea llamar a algo como esto NSString.newLine;desde el objetivo c, y desea que sea constante estática, puede crear algo como esto rápidamente:

public extension NSString {
    @objc public static let newLine = "\n"
}

Y tiene una definición constante agradable y legible, y está disponible desde el tipo de su elección, mientras que el estilo está limitado al contexto de tipo.

Renetik
fuente