219 votes

Y a-t-il une raison pour laquelle l'assignation de tableaux Swift est inconsistante (ni une référence ni une copie profonde) ?

Je lis la documentation et je secoue constamment la tête devant certaines décisions de conception du langage. Mais ce qui m'a vraiment intrigué, c'est la manière dont les tableaux sont gérés.

Je me suis précipité au playground et j'ai essayé ces exemples. Vous pouvez les essayer aussi. Donc le premier exemple :

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

Ici a et b sont tous les deux [1, 42, 3], ce que j'accepte. Les tableaux sont référencés - OK !

Maintenant regardez cet exemple :

var c = [1, 2, 3]
var d = c
c.append(42)
c
d

c est [1, 2, 3, 42] MAIS d est [1, 2, 3]. C'est-à-dire que d a vu le changement dans le dernier exemple mais ne le voit pas dans celui-ci. La documentation dit que c'est parce que la longueur a changé.

Maintenant, que dire de celui-ci :

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f

e est [4, 5, 3], ce qui est cool. C'est agréable d'avoir un remplacement multi-index, mais f NE VOIT TOUJOURS pas le changement même si la longueur n'a pas changé.

En résumé, les références communes à un tableau voient les changements si vous changez 1 élément, mais si vous changez plusieurs éléments ou ajoutez des éléments, une copie est faite.

Cela me semble être une conception très médiocre. Ai-je raison de penser cela ? Y a-t-il une raison pour laquelle je ne vois pas pourquoi les tableaux devraient agir de cette manière ?

EDIT : Les tableaux ont changé et ont maintenant une sémantique de valeur. Beaucoup plus sain !

2 votes

Les sémantiques des tableaux Swift sont assez confuses, ils disent que c'est une valeur de type. Voici une discussion à ce sujet devforums.apple.com/message/975661#975661

95 votes

Pour mémoire, je ne pense pas que cette question devrait être fermée. Swift est un nouveau langage, il y aura donc des questions comme celle-ci pendant un moment pendant que nous apprenons tous. Je trouve cette question très intéressante et j'espère que quelqu'un aura un argument convaincant en sa faveur.

0 votes

Juste pour clarifier. La décision de conception centrale prise ici d'avoir .append "modifier" le tableau plutôt que (de manière plus judicieuse à mon avis) retourner une nouvelle séquence. Puisque les tableaux sont immuables (?) en swift cela provoque une copie implicite désagréable. Et c'est ce qui pose problème à l'OP, non pas aux tableaux en soi, mais qu'il y a quelque chose qui ne va pas avec l'implémentation de append en ayant son gâteau mutable/immuable et le mangeant également?

111voto

Lukas Points 1495

Notez que la sémantique et la syntaxe des tableaux ont été modifiées dans la version 3 de Xcode beta (article de blog), donc la question n'est plus d'actualité. La réponse suivante s'appliquait à la version beta 2 :


C'est pour des raisons de performances. En gros, ils essaient d'éviter de copier des tableaux aussi longtemps que possible (et revendiquent une performance "de type C"). Pour citer le livre de référence du langage :

Pour les tableaux, la copie n'a lieu que lorsque vous effectuez une action qui a le potentiel de modifier la longueur du tableau. Cela inclut l'ajout, l'insertion ou la suppression d'éléments, ou l'utilisation d'un indexeur en plage pour remplacer une plage d'éléments dans le tableau.

Je conviens que cela est un peu déroutant, mais au moins il y a une description claire et simple de son fonctionnement.

Cette section inclut également des informations sur la façon de s'assurer qu'un tableau est référencé de manière unique, comment forcer la copie des tableaux et comment vérifier si deux tableaux partagent un stockage commun.

61 votes

Je trouve le fait que vous ayez à la fois un bouton pour partager et un bouton pour copier une GROSSE anomalie en termes de design.

9 votes

Il s'agit d'une ingénieur. Il m'a expliqué que pour la conception de langage, ce n'est pas souhaitable et qu'ils espèrent "corriger" cela dans les prochaines mises à jour de Swift. Votez avec des radars.

2 votes

C'est juste quelque chose comme copier-sur-écriture (COW) dans la gestion de la mémoire des processus enfants de Linux, n'est-ce pas? Peut-être pouvons-nous l'appeler copier-sur-altération-de-longueur (COLA). Je vois cela comme une conception positive.

25voto

iPatel Points 15121

Depuis la documentation officielle du langage Swift:

Notez que le tableau n'est pas copié lorsque vous définissez une nouvelle valeur avec la syntaxe des crochets, car définir une seule valeur avec la syntaxe des crochets n'a pas le potentiel de changer la longueur du tableau. Cependant, si vous ajoutez un nouvel élément au tableau, vous modifiez bien la longueur du tableau. Cela incite Swift à créer une nouvelle copie du tableau au moment où vous ajoutez la nouvelle valeur. Par conséquent, a est une copie distincte et indépendante du tableau.....

Lisez toute la section Comportement d'attribution et de copie pour les tableaux dans cette documentation. Vous verrez que lorsque vous remplacez une plage d'éléments dans le tableau alors le tableau se fait une copie de lui-même pour tous les éléments.

4 votes

Merci. J'ai fait référence à ce texte vaguement dans ma question. Mais j'ai montré un exemple où le changement d'une plage de sous-scripts n'a pas changé la longueur et cela a quand même été copié. Donc si vous ne voulez pas de copie, vous devez le changer un élément à la fois.

21voto

Pascal Points 8222

Le comportement a changé avec Xcode 6 beta 3. Les tableaux ne sont plus des types de référence et ont un mécanisme de copie-sur-écriture, ce qui signifie que dès que vous modifiez le contenu d'un tableau à partir d'une variable ou d'une autre, le tableau sera copié et seule la copie sera modifiée.


Ancienne réponse :

Comme d'autres l'ont souligné, Swift essaie d'éviter de copier les tableaux si possible, y compris lors du changement de valeurs pour des index uniques.

Si vous voulez être sûr qu'une variable de tableau (!) est unique, c'est-à-dire non partagée avec une autre variable, vous pouvez appeler la méthode unshare. Cela copie le tableau sauf s'il a déjà une seule référence. Bien sûr, vous pouvez également appeler la méthode copy, qui fera toujours une copie, mais unshare est préférée pour s'assurer qu'aucune autre variable ne retient le même tableau.

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]

0 votes

Hmm, pour moi, la méthode unshare() est indéfinie.

1 votes

@Hlung Il a été supprimé dans la version bêta 3, j'ai mis à jour ma réponse.

12voto

supercat Points 25534

Le comportement est extrêmement similaire à la méthode Array.Resize en .NET. Pour comprendre ce qui se passe, il peut être utile de regarder l'histoire du jeton . en C, C++, Java, C# et Swift.

En C, une structure n'est rien de plus qu'une agrégation de variables. Appliquer le . à une variable de type structure permettra d'accéder à une variable stockée dans la structure. Les pointeurs vers des objets ne détiennent pas des agrégations de variables, mais les identifient. Si on a un pointeur qui identifie une structure, l'opérateur -> peut être utilisé pour accéder à une variable stockée dans la structure identifiée par le pointeur.

En C++, les structures et les classes non seulement agrègent des variables, mais peuvent également leur attacher un code. Utiliser le . pour invoquer une méthode sur une variable demandera à cette méthode d'agir sur le contenu de la variable elle-même ; utiliser -> sur une variable qui identifie un objet demandera à cette méthode d'agir sur l'objet identifié par la variable.

En Java, tous les types de variables personnalisés identifient simplement des objets, et invoquer une méthode sur une variable permettra à la méthode de savoir quel objet est identifié par la variable. Les variables ne peuvent pas contenir de type de données composite directement, et il n'existe aucun moyen pour une méthode d'accéder à une variable sur laquelle elle est invoquée. Ces restrictions, bien que limitantes sémantiquement, simplifient grandement l'exécution, facilitent la validation du bytecode ; de telles simplifications ont réduit la surcharge de ressources de Java à un moment où le marché était sensible à de tels problèmes, et ont ainsi contribué à son gain de popularité sur le marché. Cela signifie également qu'il n'y avait pas besoin d'un jeton équivalent au . utilisé en C ou en C++. Bien que Java aurait pu utiliser -> de la même manière que C et C++, les créateurs ont opté pour le caractère unique . puisqu'il n'était pas nécessaire pour un autre but.

En C# et dans d'autres langages .NET, les variables peuvent soit identifier des objets, soit contenir des types de données composites directement. Lorsqu'il est utilisé sur une variable d'un type de données composite, . agit sur le contenu de la variable ; lorsqu'il est utilisé sur une variable de type référence, . agit sur l'objet identifié par elle. Pour certains types d'opérations, la distinction sémantique n'est pas particulièrement importante, mais pour d'autres, elle l'est. Les situations les plus problématiques sont celles où une méthode du type de données composite qui modifierait la variable sur laquelle elle est invoquée est invoquée sur une variable en lecture seule. Si une tentative est faite pour invoquer une méthode sur une valeur ou une variable en lecture seule, les compilateurs copieront généralement la variable, laisseront la méthode agir sur celle-ci, et la jetteront. Cela est généralement sûr avec les méthodes qui ne font que lire la variable, mais pas sûr avec les méthodes qui écrivent dessus. Malheureusement, .does n'a pas encore de moyen d'indiquer quelles méthodes peuvent être utilisées en toute sécurité avec une telle substitution et lesquelles ne le peuvent pas.

En Swift, les méthodes sur les agrégats peuvent indiquer expressément si elles modifieront la variable sur laquelle elles sont invoquées, et le compilateur interdira l'utilisation de méthodes mutantes sur des variables en lecture seule (au lieu de les muter des copies temporaires de la variable qui seront ensuite jetées). En raison de cette distinction, utiliser le jeton . pour appeler des méthodes qui modifient les variables sur lesquelles elles sont invoquées est beaucoup plus sûr en Swift qu'en .NET. Malheureusement, le fait que le même jeton . soit utilisé à cette fin que pour agir sur un objet externe identifié par une variable signifie que la possibilité de confusion persiste.

S'il avait une machine à remonter le temps et revenait à la création de C# et/ou de Swift, on pourrait éviter rétroactivement une grande partie de la confusion entourant de tels problèmes en ayant les langages utiliser les jetons . et -> d'une manière beaucoup plus proche de l'utilisation en C++. Les méthodes des agrégats et des types de référence pourraient utiliser . pour agir sur la variable sur laquelle elles ont été invoquées, et -> pour agir sur une valeur (pour les composites) ou la chose identifiée par celle-ci (pour les types de référence). Cependant, aucun de ces langages n'est conçu de cette manière.

En C#, la pratique normale pour qu'une méthode modifie une variable sur laquelle elle est invoquée est de passer la variable en tant que paramètre ref à une méthode. Ainsi, appeler Array.Resize(ref someArray, 23); lorsque someArray identifie un tableau de 20 éléments fera en sorte que someArray identifie un nouveau tableau de 23 éléments, sans affecter le tableau original. L'utilisation de ref indique clairement que la méthode devrait être censée modifier la variable sur laquelle elle est invoquée. Dans de nombreux cas, il est avantageux de pouvoir modifier des variables sans avoir à utiliser de méthodes statiques ; Swift aborde ce moyen en utilisant la syntaxe .. L'inconvénient est qu'il perd la clarté quant aux méthodes qui agissent sur des variables et aux méthodes qui agissent sur des valeurs.

5voto

Jukka Suomela Points 2932

Pour moi, cela a plus de sens si vous remplacez d'abord vos constantes par des variables:

a[i] = 42            // (1)
e[i..j] = [4, 5]     // (2)

La première ligne n'a jamais besoin de modifier la taille de a. En particulier, elle n'a jamais besoin d'allouer de mémoire. Peu importe la valeur de i, il s'agit d'une opération légère. Si vous imaginez que sous le capot, a est un pointeur, il peut être un pointeur constant.

La seconde ligne peut être beaucoup plus compliquée. En fonction des valeurs de i et j, vous pouvez avoir besoin de gérer la mémoire. Si vous imaginez que e est un pointeur qui pointe vers le contenu du tableau, vous ne pouvez plus supposer que c'est un pointeur constant ; vous pouvez avoir besoin d'allouer un nouveau bloc de mémoire, de copier les données du vieux bloc mémoire vers le nouveau bloc mémoire et de changer le pointeur.

Il semble que les concepteurs du langage ont essayé de rendre (1) aussi légère que possible. Comme (2) peut impliquer une copie de toute façon, ils ont opté pour la solution selon laquelle elle agit toujours comme si vous aviez fait une copie.

C'est compliqué, mais je suis content qu'ils ne l'aient pas rendu encore plus compliqué avec par exemple des cas spéciaux tels que "si dans (2) i et j sont des constantes de compilation et que le compilateur peut déduire que la taille de e ne va pas changer, alors nous ne copions pas".


Enfin, basé sur ma compréhension des principes de conception du langage Swift, je pense que les règles générales sont les suivantes:

  • Utilisez des constantes (let) partout et toujours par défaut, et il n'y aura pas de grandes surprises.
  • Utilisez des variables (var) seulement si c'est absolument nécessaire, et soyez très prudent dans ces cas, car il y aura des surprises [ici: des copies implicites étranges de tableaux dans certaines situations mais pas toutes].

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X