Directive {$j} : modifiez les constantes (constantes typées, verrouillage...)

Contenu du snippet

Enjeu: assigner une valeur à une constante, faire varier les constantes.

Voici le problème :

program Probleme;
uses Dialogs;
const MaCte = 'Salut';
begin
MaCte:='Modifiée';
ShowMessage(MaCte);
end.

Soit c'est une étourderie, soit c'est la volonté du programmeur de modifier sa constante, dans tel cas c'est interdit. Alors, comment faire ?

Nous n'utiliserons pas l'assembleur. On va simplement jouer avec la directive de compilation {$J}.

D'habitude, lorsqu'on déclare une constante, on ne spécifie pas son type, car Delphi choisit automatiquement le plus petit possible pour occuper le moins de mémoire possible. Ce peut être mis en évidence par ceci :
Val1 = 1; est déclaré en Byte
Val2 = 1.0; est déclaré en Real

Et bien que ce soit la même valeur, le comportement n'est pas le même. Démonstration :

program Exemple_N1;
const Val1 = 1;
Val2 = 1.0;
var i : integer;
begin
i:=Val1;
i:=Val2;
end.

De cette manière, il est possible de définir implicitement le type de stockage d'une constante. Ca n'a l'air de rien comme ça, mais il faut faire très attention, surtout avec les divisions. En effet, DIV s'applique aux entiers, mais "/" sert pour les flottants. On peut alors s'amuser avec INT et ROUND pour arranger les conversions.

Autre remarque: le typage des constantes est un bon moyen pour forcer le type. Par exemple, ci-dessous, nous avons deux déclarations équivalentes :
Val1 = 1.0;
Val2 : real = 1; //une déclaration a priori entière devient flottante

Modifions notre code du tout début;

program ProblemePresqueResolu;
uses Dialogs;
const MaCte : string = 'Salut';
begin
MaCte:='Modifiée';
ShowMessage(MaCte);
end.

Le résultat est magique : la constante vient de se faire modifier !!!!! Et on a touché à presque rien du tout.

Sauf qu'il y a un problème : la constante n'est plus vraiment constante. N'oublions pas que le but premier d'une constante est de ne pas varier. Pour y remédier, on va faire intervenir notre directive {$J}.

C'est une directive locale, c'est-à-dire qu'elle a une valeur par défaut déclinable individuellement dans chacune des unités du projet. De ce fait, elle s'applique uniquement au fichier PAS dans lequel elle a été déclarée. Par opposition, $APPTYPE est une variable globale, c'est-à-dire qu'elle nécessite un seul appel pour être valable de manière unique dans tout le projet, quel que soit le fichier. $J porte deux noms transparents: "$J" et "$WRITEABLECONST". Mais problème: cette directive est par défaut {$J+}, ou encore {$J ON}.

Un paradoxe est présent dans l'aide. Je cite: «mais pour les nouvelles applications, il est conseillé d'initialiser les variables et de compiler votre code en mode {$J-}». Si vous ne connaissiez pas $J, espérons que vous n'ayez jamais commis des erreurs de programmation du type "MaCte:=[...]". Le mode (+) a élevé improprement la tolérance.

Dans la suite, j'enlève les USES pour éclaicir la lecture.

Jouons avec un premier exemple.

program Exemple_N2;
{$J+}
const MaCte : string = 'Salut';
begin
ShowMessage(MaCte);
MaCte:='Modifiée';
ShowMessage(MaCte);
{$J-}
MaCte:='Non modifiable désormais';
ShowMessage(MaCte);
end;

La compilation de ce petit programme montre qu'il ne marche absolument pas : le résultat n'est pas celui escompté. Ceci dit, ça ne veut pas dire que {$J-} est inefficace. Elle est juste mal utilisée. Pour vous en convaincre, regardez les exemples suivants. $J est censée être initialisée à (+) dès le départ.

program Exemple_N3;
{$J-}
const MaCte : string = 'Salut';
begin
MaCte:='Modifiée'; //le compilateur se bloque: ERREUR !
ShowMessage(MaCte);
end;

program Exemple_N4;
const MaCte : string = 'Salut';
{$J-}
begin
MaCte:='Modifiée';
ShowMessage(MaCte); //il n'y a pas de problèmes de compilation
end;

program Exemple_N5;
const MaCte : string = 'Salut';
{$J-}
AutreCte : string = 'Je m''incruste';
begin
MaCte:='Modifiée'; //ici c'est bon...
AutreCte:='Oh?'; //...mais pas là
ShowMessage(MaCte);
end;

program Exemple_N6;
const MaCte : string = 'Salut';
{$J-}
AutreCte : string = 'Je m''incruste';
{$J+}
EncoreCte : string = 'Vais-je me faire cisailler ?';
begin
MaCte:='Modifiée';
AutreCte:='Bug...'; //le bug est seulement ici
EncoreCte:='Modifiée';
end;

Il faut savoir une chose importante: Delphi lit une seule fois le code, du haut vers le bas. La configuration de Delphi change à chaque rencontre de $J et génère des erreurs UNIQUEMENT sur les constantes qui ont été déclarées au moment où Delphi était en mode $J-. L'illustration en est faite par l'exemple N°6.

Donc, pour protéger d'un seul coup toutes les constantes, il faut verrouiller par {$J-} l'en-tête de l'unité. Mais dans ce cas, il est impossible de modifier les constantes par la suite, car compte tenu de l'exemple N°2, il ne suffit pas de basculer localement en J+ pour autoriser la modification, car celle-ci est délivrée au moment du référencement de la constante. Et comme on est en $J- dès le début, alors toutes les constantes sont verrouillées définitivement dans l'unité où elles sont déclarées.

Essayons donc avec deux unités afin de répartir séparemment les modes (+) et (-). Créeons une nouvelle unité avec "Fichier, Nouveau". On l'appelle "UnLocker.pas". Par défaut, elle est initialisée à $J+, mais pour assurer le coup et valider son mode de fonctionnement, on va faire une redondance de sécurité. On a alors le fichier suivant:

{$J+} //déverrouillée
unit UnLocker;
interface
uses Unit1;
procedure SetCteValue(Value:string); //exportation
implementation
procedure SetCteValue(Value:string);
begin
MaCte:=Value; //d'où l'utilité de USES
end;
end.

Reconcentrons nous sur notre fichier principal. On met un bouton sur la fiche et on crée un évènement OnClick. Par opposition à UnLocker, Unit1 doit être protégée. Ca nous donne:

unit Unit1;
{$J-} //unité verrouillée
interface
uses Classes, Forms, Dialogs, Controls, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
end;
const MaCte : string = 'Salut'; //positionnée avant IMPLEMENTATION
var Form1 : TForm1;
implementation
uses UnLocker; //appel au secours
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage(MaCte);
SetCteValue('Modifiée');
ShowMessage(MaCte);
MaCte:='Je tente ma chance'; //raté
ShowMessage(MaCte);
SetCteValue('Je retente d''une autre manière et j''ai gagné le pactole');
ShowMessage(MaCte);
end;
end.

Il se confirme une chose: $J est bien locale, car $J- de Unit1 n'a pas empêché UnLocker de modifier les variables protégées au sein de Unit1. On déduit alors que les constantes typées n'héritent pas en globalité de la valeur de $J de l'unité dans laquelle elles ont été déclarées. Ca nous arrange bien, car sinon on serait couillonné.

Là, on ne peut modifier qu'une constante. Faisons mieux... En mettant un paramètre à SetCteValue, il est possible de modifier une constante prédéfinie sans pour autant démultiplier les procédures. En couplant avec des constantes fixes (non variables), on identifie mieux les constantes typées à modifier. L'intérêt est triple: ça permet de ne pas faire de confusion, de mieux gérer son code et de ne pas apprendre par coeur des nombres si on ne sait plus après à quoi ils se réfèrent.

const CTE_BASE = $0; //la référence qui sera incrémentée
CTE_Val1 = CTE_BASE+1;
CTE_Val2 = CTE_BASE+2;
procedure SetCteValue(Value:string; Idx:cardinal);
begin
case Idx of
CTE_Val1: MaCteN1:=Value;
CTE_Val2: MaCteN2:=Value;
else {rien};
end;
end;

Voilà, le contrat est rempli... Nous savons modifier les constantes protégées.

Conclusion :


Vous pouvez toujours aller visiter http://altert.family.free.fr/

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.