Pointeur et valeur sémantique pour déterminer le récepteur d'une méthode

La création de nouveaux types de données est une partie importante du travail de chaque programmeur. Dans la plupart des langues, une définition de type consiste en une description de ses champs et méthodes. Dans Golang, en plus de cela, vous devez décider quelle sémantique de destinataire pour les méthodes du nouveau type à utiliser: valeur (valeur) ou pointeur (pointeur). À première vue, cette décision peut sembler secondaire, car dans la plupart des cas, le programme fonctionnera avec n'importe quelle sémantique du destinataire. Par conséquent, de nombreuses personnes ignorent ce point et écrivent du code et ne l'ont pas compris jusqu'à la fin, ce qui est affecté par la sémantique du destinataire de la méthode. Et pour le comprendre, vous devez approfondir un peu le fonctionnement de Golang.

Prenons un petit exemple. Définissez une structure de chat avec un champ Nom et une méthode sayHello (chaîne de personnes) . Ci-après, par une méthode, je ferai référence à une fonction associée à un type particulier, un objet à une variable qui a des méthodes, et le destinataire de la méthode sera la variable indiquée entre parenthèses après le mot func dans la description de la méthode.

type cat struct { Name string } func (c *cat) sayHello(person string) { fmt.Println(fmt.Sprintf("Meow, meow, %s!", person) } 

Si nous définissons un pointeur sur cat et que nous lui demandons le champ Nom , alors, évidemment, nous obtiendrons une erreur, car le champ est appelé à partir de zéro :

 var c *cat // c=nil fmt.Println(c.Name) //panic: runtime error: invalid memory address or nil pointer dereference 

https://play.golang.org/p/L3FnRJXKqs0

Cependant, lorsque la méthode sayHello () est appelée sur la même variable, il n'y aura pas d'erreur:

 var c *cat // c=nil c.sayHello(“Human”) //Meow, meow, Human! 

https://play.golang.org/p/EMoFgKL1HEi

Pourquoi nil peut-il appeler une méthode dans cet exemple, et comment cela s'explique-t-il en termes d'architecture du langage lui-même? Cela devient possible car la méthode dans Go est du sucre syntaxique ou, en d'autres termes, un wrapper autour d'une fonction qui possède l'un des arguments de destinataire. Lorsque la méthode c.sayHello («Human») est appelée, la construction (* cat) .sayHello (c, s) ( https://play.golang.org/p/X9leJeIvxcA ) sera effectivement appelée. En appelant la méthode nil à partir de l'exemple ci-dessus, nous appelons pratiquement la fonction avec nil dans les arguments, et c'est déjà une situation tout à fait normale. Par conséquent, dans Go nil, c'est le bon destinataire pour les méthodes.

Étant donné que le récepteur de méthode est en fait un argument, les recommandations d'utilisation de la sémantique «valeur» ou «pointeur» pour le récepteur de méthode sont similaires aux recommandations pour les arguments de fonction. Ils sont à leur tour déduits de la règle de base de Go: les arguments sont toujours passés à la fonction par valeur . Cela signifie que le transfert de tout argument à la fonction se produit par sa copie: si la fonction accepte une structure en entrée, alors une copie complète de cette structure viendra à l'intérieur; s'il prend un pointeur sur un objet, alors une nouvelle variable viendra avec un pointeur sur le même objet. Cela peut être vu en comparant l'adresse de la variable avant de la transmettre à la fonction avec l'adresse de l'argument à l'intérieur de la fonction ( https://play.golang.org/p/oc2ssC_Irs8 , https://play.golang.org/p/FeQa2HUdX0a ).

Lorsque le passage de lien est utilisé:

  • Pour les grandes structures. Le pointeur occupe un seul mot machine (32, 64 bits selon le système). Par conséquent, lors de l'appel d'une méthode avec un pointeur dans le récepteur, la copie du pointeur est moins coûteuse que la copie de l'objet entier, comme ce serait le cas en passant par valeur.
  • Si la méthode appelée modifie les données de l'objet lui-même. Lorsque le destinataire est transféré par référence, la méthode peut affecter l'état de l'objet appelant en apportant indirectement des modifications. Ce qui est impossible en passant par la valeur.

Lors de l'utilisation du transfert de valeur:

  • Pour les types intégrés simples tels que les nombres, les chaînes, les booléens. Lorsque vous utilisez le pointeur, presque la même quantité de mémoire est utilisée que l'objet de ce type, et le coût de sa maintenance par le garbage collector augmente, comme cela sera décrit ci-dessous.
  • Pour les tranches, ainsi que pour d'autres types de référence: carte et canaux - cela n'a aucun sens de prendre un pointeur. Ils sont eux-mêmes déjà un pointeur.
  • Avec le multithread, le passage par valeur est sûr, contrairement au passage par référence.
  • Pour les petites structures. Dans de tels cas, la transmission par valeur est plus efficace. En effet, les données internes des méthodes sont placées dans un cadre distinct de la pile. Après avoir quitté une fonction, son cadre est effacé. Lorsque nous fouillons quelque chose le long du pointeur, nous transférons ces données de la pile vers le tas, d'où ces données peuvent être disponibles pour d'autres fonctions. L'augmentation du tas crée une charge supplémentaire pour le garbage collector, dont le fonctionnement réduit la vitesse du programme de 25% en moyenne. Lorsque vous utilisez le transfert valeur par valeur, les données restent sur la pile et aucun travail de ramasse-miettes supplémentaire n'est requis.

Lorsque vous devez penser à la sémantique du destinataire:

  • Le type de destinataire peut varier selon le sujet. Dans l'un de ses discours, Bill Kennedy a donné un bon exemple avec le type d'utilisateur décrivant l'utilisateur. Une fois passé par valeur, une copie sera créée pour l'utilisateur. Cela conduira au fait que plusieurs copies d'un même utilisateur peuvent coexister dans le programme en même temps, qui peuvent ensuite être modifiées indépendamment, ce qui ne correspond pas au domaine, car l'utilisateur réel est toujours un, et il ne peut pas être décrit à différents moments par différents ensembles les données.
  • Une autre façon sûre de déterminer le type de destinataire d'une méthode consiste à utiliser la méthode constructeur pour son type. Si le constructeur retourne une valeur / un pointeur, alors lors de la création d'une entité, il est supposé qu'ils continueront à travailler avec elle en tant que valeur / pointeur. Par conséquent, il est également préférable d'utiliser la même sémantique dans le récepteur de méthode.
  • Il existe une règle non écrite, en violation de laquelle le compilateur ne jurera pas, mais votre code ne s'améliorera certainement pas. Si l'une des méthodes de type utilise un pointeur / valeur comme récepteur, pour conserver la cohérence, les méthodes restantes doivent utiliser un pointeur / valeur. Les méthodes de type ne doivent pas avoir de hachage de récepteurs de valeur et de pointeur.

Quel est le résultat


Dans Go Value, la sémantique signifie copier une valeur; la sémantique du pointeur signifie donner accès à une valeur. Cela s'applique à la fois aux arguments des méthodes et à leurs destinataires. Pour les types intégrés, tels que les nombres, les lignes, les tranches, les cartes, les canaux et les petites structures, vous devez presque toujours utiliser le transfert basé sur la valeur. Pour les structures qui occupent une grande quantité de mémoire et les structures dont l'état peut être indirectement modifié par leurs méthodes, vous devez utiliser le transfert par référence. En outre, la sémantique du destinataire peut dépendre du domaine décrit par le type, de la sémantique renvoyée dans sa fabrique et de la sémantique du destinataire déjà utilisée dans d'autres méthodes de ce type.

Source: https://habr.com/ru/post/fr469859/


All Articles