Constantes dans Objective-C

1002

Je suis en train de développer une application Cocoa et j'utilise des NSStrings constants comme moyen de stocker des noms de clés pour mes préférences.

Je comprends que c'est une bonne idée car elle permet de changer facilement les clés si nécessaire.
De plus, c'est toute la notion de «séparer vos données de votre logique».

Quoi qu'il en soit, existe-t-il un bon moyen de définir ces constantes une fois pour l'ensemble de l'application?

Je suis sûr qu'il existe un moyen simple et intelligent, mais pour l'instant mes cours redéfinissent ceux qu'ils utilisent.

Allyn
la source
7
La POO consiste à regrouper vos données avec votre logique. Ce que vous proposez n'est qu'une bonne pratique de programmation, c'est-à-dire rendre votre programme facile à modifier.
Raffi Khatchadourian

Réponses:

1287

Vous devez créer un fichier d'en-tête comme

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

(vous pouvez utiliser à la externplace de FOUNDATION_EXPORTsi votre code ne sera pas utilisé dans des environnements mixtes C / C ++ ou sur d'autres plates-formes)

Vous pouvez inclure ce fichier dans chaque fichier qui utilise les constantes ou dans l'en-tête précompilé du projet.

Vous définissez ces constantes dans un fichier .m comme

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

Constants.m doit être ajouté à la cible de votre application / framework afin qu'il soit lié au produit final.

L'avantage d'utiliser des constantes de chaîne au lieu de constantes #define'd est que vous pouvez tester l'égalité en utilisant la comparaison de pointeurs ( stringInstance == MyFirstConstant) qui est beaucoup plus rapide que la comparaison de chaînes ( [stringInstance isEqualToString:MyFirstConstant]) (et plus facile à lire, IMO).

Barry Wark
la source
67
Pour une constante entière, serait-ce: extern int const MyFirstConstant = 1;
Dan Morgan
180
Dans l'ensemble, excellente réponse, avec une mise en garde flagrante: vous ne voulez PAS tester l'égalité des chaînes avec l'opérateur == dans Objective-C, car il teste l'adresse mémoire. Utilisez toujours -isEqualToString: pour cela. Vous pouvez facilement obtenir une instance différente en comparant MyFirstConstant et [NSString stringWithFormat: MyFirstConstant]. Ne faites aucune hypothèse sur l'instance d'une chaîne que vous avez, même avec des littéraux. (Dans tous les cas, #define est une "directive de préprocesseur", et est substitué avant la compilation, de toute façon le compilateur voit un littéral de chaîne à la fin.)
Quinn Taylor
74
Dans ce cas, il est OK d'utiliser == pour tester l'égalité avec la constante, si elle est vraiment utilisée comme symbole de constante (c'est-à-dire que le symbole MyFirstConstant au lieu d'une chaîne contenant @ "MyFirstConstant" est utilisé). Un entier pourrait être utilisé à la place d'une chaîne dans ce cas (c'est vraiment ce que vous faites - utiliser le pointeur comme un entier) mais l'utilisation d'une chaîne constante facilite le débogage car la valeur de la constante a une signification lisible par l'homme .
Barry Wark
17
+1 pour "Constants.m doit être ajouté à la cible de votre application / framework afin qu'il soit lié au produit final." J'ai sauvé ma raison. @amok, faites "Get info" sur Constants.m et choisissez l'onglet "Targets". Assurez-vous qu'il est vérifié pour la ou les cibles pertinentes.
PEZ
73
@Barry: Dans Cocoa, j'ai vu un certain nombre de classes qui définissent leurs NSStringpropriétés avec copyau lieu de retain. En tant que tels, ils pourraient (et devraient) contenir une instance différente de votre NSString*constante, et la comparaison directe des adresses mémoire échouerait. En outre, je suppose que toute implémentation raisonnablement optimale de -isEqualToString:vérifierait l'égalité du pointeur avant d'entrer dans le vif du sujet de la comparaison des caractères.
Ben Mosher
280

Manière la plus simple:

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

Meilleure façon:

// Prefs.h
extern NSString * const PREFS_MY_CONSTANT;

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

L'un des avantages de la seconde est que la modification de la valeur d'une constante n'entraîne pas une reconstruction de l'ensemble de votre programme.

Andrew Grant
la source
12
Je pensais que tu n'étais pas censé changer la valeur des constantes.
ruipacheco
71
Andrew se réfère à la modification de la valeur de la constante lors du codage, et non pendant l'exécution de l'application.
Randall
7
Y a-t-il une valeur ajoutée à faire extern NSString const * const MyConstant, c'est-à-dire en faire un pointeur constant vers un objet constant plutôt qu'un simple pointeur constant?
Hari Karam Singh
4
Que se passe-t-il, si j'utilise cette déclaration dans le fichier d'en-tête, statique NSString * const kNSStringConst = @ "const value"; Quelle est la différence entre ne pas déclarer et init séparément dans les fichiers .h et .m?
karim
4
@Dogweather - Un endroit où seul le compilateur connaît la réponse. IE, si vous vouliez inclure dans un menu à propos du compilateur qui était utilisé pour compiler une build d'une application, vous pourriez le placer là puisque le code compilé n'aurait autrement aucune façon de le savoir. Je ne peux pas penser à beaucoup d'autres endroits. Les macros ne devraient certainement pas être utilisées dans de nombreux endroits. Et si j'avais #define MY_CONST 5 et ailleurs #define MY_CONST_2 25. Le résultat est que vous pourriez très bien vous retrouver avec une erreur de compilation quand il essaiera de compiler 5_2. N'utilisez pas #define pour les constantes. Utilisez const pour les constantes.
ArtOfWarfare
190

Il y a aussi une chose à mentionner. Si vous avez besoin d'une constante non globale, vous devez utiliser un staticmot-clé.

Exemple

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

En raison du staticmot clé, cette constante n'est pas visible en dehors du fichier.


Correction mineure par @QuinnTaylor : les variables statiques sont visibles dans une unité de compilation . Habituellement, il s'agit d'un seul fichier .m (comme dans cet exemple), mais il peut vous mordre si vous le déclarez dans un en-tête qui est inclus ailleurs, car vous obtiendrez des erreurs de l'éditeur de liens après la compilation

kompozer
la source
41
Correction mineure: les variables statiques sont visibles dans une unité de compilation . Habituellement, il s'agit d'un seul fichier .m (comme dans cet exemple), mais il peut vous mordre si vous le déclarez dans un en-tête qui est inclus ailleurs, car vous obtiendrez des erreurs de l'éditeur de liens après la compilation.
Quinn Taylor
Si je n'utilise pas le mot-clé statique, kNSStringConst sera-t-il disponible tout au long du projet?
Danyal Aytekin
2
Ok, juste vérifié ... Xcode ne fournit pas d'auto-complétion dans d'autres fichiers si vous laissez statique, mais j'ai essayé de mettre le même nom à deux endroits différents et j'ai reproduit les erreurs de l'éditeur de liens de Quinn.
Danyal Aytekin
1
statique dans un fichier d'en-tête ne pose pas de problèmes de l'éditeur de liens. Cependant, chaque unité de compilation incluant le fichier d'en-tête obtiendra sa propre variable statique, donc vous en obtiendrez 100 si vous incluez l'en-tête de 100 fichiers .m.
gnasher729
@kompozer Dans quelle partie du fichier .m placez-vous cela?
Basil Bourque
117

La réponse acceptée (et correcte) indique que "vous pouvez inclure ce fichier [Constants.h] ... dans l'en-tête précompilé du projet".

En tant que novice, j'ai eu du mal à le faire sans autre explication - voici comment: dans votre fichier YourAppNameHere-Prefix.pch (c'est le nom par défaut pour l'en-tête précompilé dans Xcode), importez votre Constants.h à l'intérieur du #ifdef __OBJC__bloc .

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

Notez également que les fichiers Constants.h et Constants.m ne doivent contenir absolument rien d'autre, sauf ce qui est décrit dans la réponse acceptée. (Aucune interface ou implémentation).

Victor Van Hee
la source
J'ai fait cela, mais certains fichiers génèrent une erreur lors de la compilation xcode et build et toujours des problèmes ... des idées?
J3RM
50

J'utilise généralement la manière affichée par Barry Wark et Rahul Gupta.

Cependant, je n'aime pas répéter les mêmes mots dans les fichiers .h et .m. Notez que dans l'exemple suivant, la ligne est presque identique dans les deux fichiers:

// file.h
extern NSString* const MyConst;

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

Par conséquent, ce que j'aime faire est d'utiliser des machines de préprocesseur C. Permettez-moi de vous expliquer à travers l'exemple.

J'ai un fichier d'en-tête qui définit 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

Dans ma paire .h / .m où je veux définir la constante, je fais ce qui suit:

// 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, j'ai toutes les informations sur les constantes dans le fichier .h uniquement.

Krizz
la source
Hmm, il y a une petite mise en garde cependant, vous ne pouvez pas utiliser cette technique comme celle-ci si le fichier d'en-tête est importé dans l'en-tête précompilé, car il ne chargera pas le fichier .h dans le fichier .m car il a déjà été compilé. Il y a un moyen cependant - voir ma réponse (puisque je ne peux pas mettre de bon code dans les commentaires.
Scott Little
Je ne peux pas faire fonctionner ça. Si je mets #define SYNTHESIZE_CONSTS avant #import "monfichier.h", cela fait NSString * ... dans les fichiers .h et .m (vérifié à l'aide de la vue assistant et du préprocesseur). Il génère des erreurs de redéfinition. Si je le mets après #import "monfichier.h", il fait NSString externe * ... dans les deux fichiers. Ensuite, il génère des erreurs "Symbole non défini".
arsenius
28

J'ai moi-même un en-tête dédié à la déclaration des NSStrings constants utilisés pour les préférences comme ceci:

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

Puis en les déclarant dans le fichier .m joint:

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";

Cette approche m'a bien servi.

Modifier: notez que cela fonctionne mieux si les chaînes sont utilisées dans plusieurs fichiers. Si un seul fichier l'utilise, vous pouvez simplement le faire #define kNSStringConstant @"Constant NSString"dans le fichier .m qui utilise la chaîne.

MaddTheSane
la source
25

Une légère modification de la suggestion de @Krizz, pour qu'elle fonctionne correctement si le fichier d'en-tête des constantes doit être inclus dans le PCH, ce qui est plutôt normal. Étant donné que l'original est importé dans le PCH, il ne le rechargera pas dans le .mfichier et donc vous n'obtiendrez aucun symbole et l'éditeur de liens est mécontent.

Cependant, la modification suivante lui permet de fonctionner. C'est un peu compliqué, mais ça marche.

Vous aurez besoin de 3 fichiers, .hfichier qui a les définitions constantes, le .hfichier et le .mfichier, je vais utiliser ConstantList.h, Constants.het Constants.m, respectivement. le contenu de Constants.hest simplement:

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

et le Constants.mfichier ressemble à:

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

Enfin, le ConstantList.hfichier contient les déclarations réelles et c'est tout:

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

Quelques points à noter:

  1. J'ai dû redéfinir la macro dans le .mfichier après l'avoir #undef ing pour que la macro soit utilisée.

  2. J'ai également dû utiliser #includeau lieu de #importpour que cela fonctionne correctement et éviter que le compilateur ne voie les valeurs précédemment précompilées.

  3. Cela nécessitera une recompilation de votre PCH (et probablement de l'ensemble du projet) chaque fois que des valeurs sont modifiées, ce qui n'est pas le cas si elles sont séparées (et dupliquées) comme d'habitude.

J'espère que cela est utile pour quelqu'un.

Scott Little
la source
1
L'utilisation de #include a résolu ce mal de tête pour moi.
Ramsel
Cela a-t-il une perte de performances / mémoire par rapport à la réponse acceptée?
Gyfis
En réponse à la performance par rapport à la réponse acceptée, il n'y en a pas. C'est effectivement exactement la même chose du point de vue du compilateur. Vous vous retrouvez avec les mêmes déclarations. Ils seraient EXACTEMENT les mêmes si vous remplaciez ce qui externprécède par le FOUNDATION_EXPORT.
Scott Little
14
// Prefs.h
extern NSString * const RAHUL;

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

Comme l'a dit Abizer, vous pouvez le mettre dans le fichier PCH. Une autre façon qui n'est pas si sale est de créer un fichier d'inclusion pour toutes vos clés, puis de l'inclure dans le fichier dans lequel vous utilisez les clés ou de l'inclure dans le PCH. Avec eux dans leur propre fichier include, cela vous donne au moins un endroit pour rechercher et définir toutes ces constantes.

Grant Limberg
la source
11

Si vous voulez quelque chose comme des constantes globales; un moyen rapide et sale est de mettre les déclarations constantes dans le pchfichier.

Abizern
la source
7
Modifier le .pch n'est généralement pas la meilleure idée. Vous devrez trouver un endroit pour définir réellement la variable, presque toujours un fichier .m, il est donc plus logique de la déclarer dans le fichier .h correspondant. La réponse acceptée de la création d'une paire Constants.h / m est bonne si vous en avez besoin sur l'ensemble du projet. Je place généralement les constantes le plus bas possible dans la hiérarchie, en fonction de l'endroit où elles seront utilisées.
Quinn Taylor
8

Essayez d'utiliser une méthode de classe:

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

Je l'utilise parfois.

groumpf
la source
6
Une méthode de classe n'est pas une constante. Il a un coût au moment de l'exécution et peut ne pas toujours renvoyer le même objet (il le fera si vous l'implémentez de cette façon, mais vous ne l'avez pas nécessairement implémenté de cette façon), ce qui signifie que vous devez utiliser isEqualToString:pour la comparaison, qui est un coût supplémentaire au moment de l'exécution. Lorsque vous voulez des constantes, créez des constantes.
Peter Hosey
2
@Peter Hosey, bien que vos commentaires soient exacts, nous considérons que les performances sont atteintes une fois par LOC ou plus dans des langues de "niveau supérieur" comme Ruby sans que cela ne vous inquiète. Je ne dis pas que vous n'avez pas raison, mais plutôt simplement commenter la façon dont les normes sont différentes dans différents "mondes".
Dan Rosenstark
1
Vrai sur Ruby. La plupart des codes de performance que les gens utilisent ne sont pas nécessaires pour l'application typique.
Peter DeWeese
8

Si vous aimez la constante d'espace de noms, vous pouvez tirer parti de struct, Friday Q&A 2011-08-19: Constantes et fonctions d'espaces de noms

// 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
la source
1
Une bonne chose! Mais sous ARC, vous devrez préfixer toutes les variables dans la déclaration de structure avec un __unsafe_unretainedqualificateur pour le faire fonctionner.
Cemen
7

J'utilise une classe singleton, afin de pouvoir me moquer de la classe et modifier les constantes si nécessaire pour les tests. La classe des constantes ressemble à ceci:

#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

Et il est utilisé comme ceci (notez l'utilisation d'un raccourci pour les constantes c - il enregistre la frappe à [[Constants alloc] init]chaque fois):

#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
la source
1

Si vous voulez appeler quelque chose comme ça à NSString.newLine;partir de l'objectif c et que vous voulez que ce soit une constante statique, vous pouvez créer quelque chose comme ça dans swift:

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

Et vous avez une belle définition constante lisible, et disponible à partir d'un type de votre choix tout en étant limité au contexte du type.

Renetik
la source