Comptage automatique des références dans les compilateurs mobiles Delphi

De RAD Studio
Aller à : navigation, rechercher

Remonter à Considérations Delphi pour les applications multi-périphériques


Les utilisateurs de Delphi sont depuis longtemps familiers à la notion de comptage automatique de références (Automatic Reference Counting, ARC). Dans le passé, les compilateurs de bureau Delphi (DCC32, DCC64, DCCOSX) supportaient ARC pour les interfaces (introduit dans Delphi 3), les tableaux dynamiques et les chaînes (AnsiString a été introduit dans Delphi 2). A présent, les compilateurs mobiles Delphi introduisent le comptage automatique des références pour les classes. En conséquence, la notion de référence faible a également été introduite afin de gérer les "cycles", avec une autre fonctionnalité : la Surcharge des opérateurs pour les classes.

Pour obtenir une description détaillée du comptage automatique de références, voir Release Notes for ARC (Apple Computers) (EN).

Comptage automatique des références

Le comptage automatique de références est une façon de gérer la durée de vie d'un objet sans libérer l'objet quand il n'est plus nécessaire. Un bon exemple est une variable locale qui référence un objet qui sort de la portée. Ensuite, l'objet est automatiquement détruit. Comme vu ci-dessus, Delphi prend déjà en charge le comptage de références pour les chaînes et les objets référencés par le biais de variables de type interface. Toutefois, ARC dans les compilateurs mobiles Delphi a une certaine souplesse permettant de résoudre des problèmes tels que les références circulaires qui sont difficiles à gérer avec les variables de type interface.

Une autre notion introduite dans les nouveaux compilateurs mobiles Delphi est la référence faible. Celle-ci permet de résoudre des problèmes de références circulaires.

Le schéma de gestion de la mémoire est le même pour les apps compilées pour le périphérique iOS et le simulateur iOS :

  • DCCIOSARM (le compilateur Delphi pour le périphérique iOS 32 bits) dispose par défaut du comptage de références automatique.
  • DCCIOSARM (le compilateur Delphi pour le périphérique iOS 64 bits) dispose par défaut du comptage de références automatique.
  • DCCIOS32.EXE (le compilateur Delphi pour le simulateur iOS) active également ARC, bien que DCCIOS32 soit basé sur l'architecture du compilateur traditionnel.

Remarque : La seule zone dans laquelle ces nouvelles fonctionnalités apparaissent dans le code source de RAD Studio est à l'intérieur des blocs conditionnels {$AUTOREFCOUNT} au sein de la RTL (par exemple, dans System.pas).

Changements du style de codage

Puisque les nouveaux compilateurs mobiles supportent le comptage automatique de références, le code peut être simplifié considérablement quand vous avez besoin de faire référence à des objets temporaires au sein d'une méthode. La gestion de la mémoire est complètement ignorée :

class procedure TMySimpleClass.CreateOnly;
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  MyObj.DoSomething;
end;

Dans l'exemple ci-dessus, le destructeur de l'objet est appelé quand le programme atteint l'instruction end, c'est-à-dire quand la variable MyObj sort de la portée.

Il est également possible d'arrêter l'utilisation de l'objet avant la fin de la méthode. Pour ce faire, définissez la variable sur nil :

class procedure TMySimpleClass.SetNil;
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  MyObj.DoSomething (False); // True => raise
  MyObj := nil;	
  // do something else
end;

Dans ce cas, l'objet est détruit avant la fin de la méthode, exactement comme nous définissons la variable sur nil. Dans le cas où la procédure DoSomething déclenche une exception, l'instruction d'affectation nil sera ignorée. En fin de compte, quand la méthode est terminée, l'objet est toujours détruit.

Au fur et à mesure que le cycle de vie de l'objet suit le flux du programme, vous pouvez interroger le compteur de références d'un objet (seulement sur les plates-formes disposant du comptage de références) en utilisant la nouvelle propriété RefCount :

public
    property RefCount: Integer read FRefCount;

Remarque : L'interrogation du compteur de références d'un objet n'est pas recommandée et en général, ne doit pas être utilisée.

La vitesse des opérations à blocage

Les opérations d'incrémentation et de décrémentation du compteur de références d'un objet sont accomplies en utilisant des opérations thread-safe afin que le membre d'instance du compteur de références soit thread-safe. Cela signifie que la variable d'instance du compteur de références est correctement délimitée en mémoire afin de garantir que tous les threads observent immédiatement le changement et ne peuvent pas modifier une valeur obsolète.

Remarque : Le mécanisme de comptage de références automatique ne protège pas des conditions de concurrence et des blocages.

ARC et la directive {$AUTOREFCOUNT}

Vous devez envisager l'usage de la directive {$IFDEF AUTOREFCOUNT} afin d'obtenir le meilleur code pour les deux scénarios : ARC et le scénario traditionnel. AUTOREFCOUNT définit le code qui utilise le comptage de références automatique, tel que le code pour les compilateurs mobiles Delphi. C'est une directive importante, différente de {$IFDEF NEXTGEN} (la directive qui définit les nouvelles fonctionnalités de langage des compilateurs mobiles). AUTOREFCOUNT pourrait être utile à l'avenir, dans le cas où ARC est implémenté au-dessus des compilateurs de bureau Delphi.

Les méthodes Free et DisposeOf sous ARC

Pour les classes, les développeurs Delphi sont habitués à un modèle de codage différent, basé sur l'appel de la méthode Free, protégé par un bloc try-finally.

Par exemple :

class procedure TMySimpleClass.TryFinally;
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  try
    MyObj.DoSomething;
  finally
    MyObj.Free;
  end;
end;

Avec les compilateurs de bureau Delphi, Free est une méthode de TObject qui vérifie si la référence en cours n'est pas nil, et dans ce cas, appelle le destructeur Destroy qui retire l'objet de la mémoire après l'exécution du code de destructeur adéquat.

Dans le nouveau compilateur mobile Delphi, l'appel de Free est remplacé par l'affectation de la variable à nil. Dans le cas où il s'agissait de la dernière référence à l'objet, il est toujours retiré de la mémoire après l'appel de son destructeur. Pour les autres références, rien ne se passe (sauf une décrémentation du compteur de références).

FreeAndNil (MyObj);

De la même façon, un appel comme celui ci-dessus définit l'objet sur nil et déclenche de nouveau la destruction de l'objet seulement si aucune autre variable n'y fait référence. Dans la plupart des cas, c'est correct, étant donné que vous ne voulez pas détruire les objets utilisés dans une autre partie d'un programme. D'autre part, il existe des scénarios où vous devez exécuter immédiatement le code du destructeur, indépendamment de la présence d'autres références en suspens à l'objet. Pour permettre aux développeurs de forcer l'exécution du destructeur (sans libérer l'objet réel de la mémoire), le nouveau compilateur introduit un modèle dispose :

MyObject.DisposeOf;

Cet appel exécute avec vigueur le code du destructeur, même avec des références en suspens existantes. A ce stade, l'objet est placé dans un état spécial, de telle sorte que le destructeur n'est pas appelé de nouveau dans le cas d'autres opérations de destruction ou quand le compteur de références atteint la valeur zéro et que la mémoire est réellement libérée. Cet état disposed (ou l'état zombie) est assez significatif, et vous pouvez interroger un objet pour celui-ci en utilisant la propriété Disposed.

Comme mentionné plus tôt, les blocs try-finally classiques utilisés avec les compilateurs de bureau Delphi fonctionnent toujours correctement sous le nouveau compilateur, même s'ils ne sont pas requis. Toutefois, dans la plupart des cas, vous souhaiterez utiliser à la place le modèle dispose (à moins que vous ne vouliez recompiler le code pour les versions antérieures de Delphi).

Dans l'exemple ci-dessous, l'effet reste le même avec le compilateur classique, puisque DisposeOf appelle Free. A la place, sur ARC, le code exécute le destructeur quand c'est attendu (au même moment que le compilateur classique, mais la mémoire est gérée par le mécanisme ARC).

var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  try
    MyObj.DoSomething;
  finally
    MyObj.DisposeOf;
  end;
end;

Remarque : Le stockage de l'indicateur Disposed est un bit dans le champ FRefCount, prenant en compte qu'un objet a une limite de 230 références.

Méthodes de construction et de destruction de TObject sous ARC

C'est un résumé des méthodes de la classe TObject relatives à la création et à la destruction.

Une des façons de voir la différence entre Free et DisposeOf consiste à considérer l'intention. Lors de l'utilisation de Free, l'intention est que l'utilisateur nécessite que cette référence particulière soit détachée de l'instance. Elle n'implique aucune sorte de destruction ou de désallocation. En revanche, DisposeOf est la manière pour le développeur de préciser explicitement l'instance nécessaire pour "se libérer lui-même". Elle n'implique jamais nécessairement des désallocations, DisposeOf effectue un "pré-nettoyage" explicite de l'instance, qui dépend alors de la sémantique du compteur de références normal pour désallouer éventuellement l'instance.

Références faibles

Un autre concept important pour ARC est le rôle des références faibles que vous pouvez créer en les identifiant avec l'attribut [weak]. Supposons que deux objets s'auto-référencent en utilisant un champ, et qu'une variable externe fait référence au premier. Le compteur de références du premier objet sera 2 (la variable externe, et le deuxième objet), tandis que le compteur de références du deuxième objet est 1. A présent, comme la variable externe sort de la portée, le compteur de références des deux objets reste à 1, et ils restent en mémoire indéfiniment.

Pour résoudre ce type de situation, et de nombreux scénarios similaires, utilisez une référence faible pour rompre les références circulaires quand la dernière référence externe sort de la portée.

Une référence faible est une référence à un objet qui n'incrémente pas son compteur de références. Pour déclarer une référence faible, utilisez l'attribut [weak], supporté par les compilateurs mobiles Delphi.

Etant donné le scénario précédent, si la référence depuis le deuxième objet de retour vers le premier est faible, les deux objets sont détruits quand la variable externe sort de la portée. Voici un exemple de cette situation :

type
  TMyComplexClass = class;

  TMySimpleClass = class
  private
    [Weak] FOwnedBy: TMyComplexClass;
  public
    constructor Create();
    destructor Destroy (); override;
    procedure DoSomething(bRaise: Boolean = False);
  end;

  TMyComplexClass = class
  private
    fSimple: TMySimpleClass;
  public
    constructor Create();
    destructor Destroy (); override;
    class procedure CreateOnly;
  end;

Dans l'extrait de code suivant, le constructeur de la classe complexe crée un objet de l'autre classe :

constructor TMyComplexClass.Create;
begin
  inherited Create;
  FSimple := TMySimpleClass.Create;
  FSimple.FOwnedBy := self;
end;

N'oubliez pas que le champ FOwnedBy est une référence faible ; il n'augmente donc pas le compteur de références de l'objet auquel il se réfère, dans ce cas l'objet en cours (self).

Etant donné cette structure de classe, nous pouvons écrire :

class procedure TMyComplexClass.CreateOnly;
var
  MyComplex: TMyComplexClass;
begin
  MyComplex := TMyComplexClass.Create;
  MyComplex.fSimple.DoSomething;
end;

Cela ne provoque aucune perte de mémoire, tant que la référence faible est utilisée correctement.

Un autre exemple de l'utilisation des références faibles est cet extrait de code de la RTL Delphi ; il fait partie de la déclaration de la classe TComponent :

type
  TComponent = class(TPersistent, IInterface,
    IInterfaceComponentReference)
  private
    [Weak] FOwner: TComponent;

Si vous utilisez l'attribut weak dans du code compilé par l'un des compilateurs de bureau Delphi, l'attribut est ignoré. Avec DCC32, vous devez vous assurer que vous ajoutez le code approprié dans le destructeur d'un objet "propriétaire" pour libérer également l'objet "possédé". L'appel de Free est autorisé, bien que l'effet soit différent dans les compilateurs mobiles Delphi. Le comportement est correct dans la plupart des circonstances.

Remarque : Quand une instance a sa mémoire libérée, toutes les références [weak] actives sont définies sur nil. Comme une référence strong (normal), une variable [weak] peut seulement être nil ou référencer une instance valide. C'est la raison principale pour laquelle une référence weak doit être assignée à une référence strong avant d'être testée à nil et déréférencée. L'affectation à une référence strong ne permet pas la libération prématurée de l'instance.

var
  O: TComponent;// a strong reference
begin
  O := FOwner;  // assigning a weak reference to a strong reference
  if O <> nil then
    O.<method>;// safe to dereference
end;

Remarque : Vous pouvez seulement passer une variable [Weak] à un paramètre var ou out qui est également marqué comme [Weak]. Vous ne pouvez pas passer une référence strong régulière à un paramètre [Weak] var ou out.

L'attribut Unsafe

Pour définir l'attribut unsafe sur une fonction, utilisez cette syntaxe :

[Result: Unsafe] function ReturnUnsafe: TObject;

Avertissement : [Unsafe] peut également être appliqué aux variables (membres) et aux paramètres. Il doit être seulement utilisé en dehors de l'unité System dans de très rares situations. Il est considéré comme dangereux et son utilisation n'est pas recommandée car aucun code associé au comptage de références n'est généré.

Remarque : Vous pouvez seulement passer une variable [Unsafe] à un paramètre var ou out qui est également marqué en tant que [Unsafe]. Vous ne pouvez pas passer une référence strong régulière à un paramètre [Unsafe] var ou out.

Surcharge des opérateurs pour les classes

Un très intéressant effet de bord de l'utilisation du gestionnaire de mémoire ARC est que le compilateur peut gérer la durée de vie des objets temporaires renvoyés par les fonctions. Un cas spécifique est celui des objets temporaires renvoyés par les opérateurs. En fait, une nouvelle fonctionnalité du nouveau compilateur Delphi est la capacité à définir des opérateurs pour les classes, en utilisant une syntaxe et un modèle identiques à ceux ayant été disponibles pour les enregistrements depuis Delphi 2006.

Remarque : L'exemple de code suivant fonctionne avec les compilateurs mobiles Delphi (iOS), mais ne peut pas être compilé par les compilateurs de bureau Delphi.

En tant qu'exemple, considérez la simple classe suivante :

type
  TNumber = class
  private
    FValue: Integer;
    procedure SetValue(const Value: Integer);
  public
    property Value: Integer read FValue write SetValue;
    constructor Create(Value: Integer);
    function ToString: string; override;
    class operator Add (a, b: TNumber): TNumber;
    class operator Implicit (n: TNumber): Integer;
    class operator Implicit(n: Integer): TNumber;
  end;

constructor TNumber.Create(Value: Integer); begin
  inherited Create;
  Self.Value := Value;
end;

class operator TNumber.Add(a, b: TNumber): TNumber; begin
  Result := TNumber.Create(a.Value + b.Value); end;

class operator TNumber.Implicit (n: TNumber): Integer; begin
  Result := n.Value;
end;

class operator TNumber.Implicit (n: Integer): TNumber; begin
  Result := TNumber.Create(n);
end;

procedure TNumber.SetValue(const Value: Integer); begin
  FValue := Value;
end;

function TNumber.ToString: string;
begin
  Result := IntToStr(Value);
end;

procedure Test;
var
  a, b, c: TNumber;
begin
  a := 10;
  b := 20;
  c := a + b;
  Writeln(c.ToString);
end;

Voir aussi

Exemple de code