Y-a-t-il une raison pour laquelle l’affectation d’un tableau Swift est incohérente (ni une référence, ni une copie profonde)?

Je lis la documentation et je suis constamment en train de secouer la tête lors de certaines décisions de conception de la langue. Mais ce qui m’a vraiment insortinggué, c’est la manière dont les tableaux sont gérés.

Je me suis précipité vers la cour de récréation et je les ai essayées. 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 les deux [1, 42, 3] , que je peux accepter. Les tableaux sont référencés – OK!

Maintenant, voyez 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 j’ai 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, qu’en est-il 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 bien d’avoir un remplacement multi-index, mais f ne voit pas le changement même si la longueur n’a pas changé.

Donc, pour résumer, les références courantes à un tableau sont modifiées si vous modifiez un élément, mais si vous modifiez plusieurs éléments ou ajoutez des éléments, une copie est effectuée.

Cela me semble un très mauvais design. Est-ce que j’ai raison de penser cela? Y a-t-il une raison pour laquelle je ne vois pas pourquoi les tableaux devraient agir comme ça?

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

Notez que la sémantique et la syntaxe du tableau ont été modifiées dans la version Xcode beta 3 ( article de blog ), la question ne s’applique donc plus. La réponse suivante s’applique à la bêta 2:


C’est pour des raisons de performance. Essentiellement, ils essaient d’éviter de copier les tableaux aussi longtemps qu’ils le peuvent (et revendiquent des “performances de type C”). Pour citer le livre de langue:

Pour les tableaux, la copie n’a lieu que lorsque vous effectuez une action susceptible de modifier la longueur du tableau. Cela inclut l’ajout, l’insertion ou la suppression d’éléments, ou l’utilisation d’un indice à distance pour remplacer une plage d’éléments du tableau.

Je suis d’accord que c’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 manière dont un tableau est référencé de manière unique, comment forcer la copie de tableaux et comment vérifier si deux baies partagent le stockage.

De la documentation officielle de la langue Swift :

Notez que le tableau n’est pas copié lorsque vous définissez une nouvelle valeur avec la syntaxe de l’indice, car la définition d’une valeur unique avec une syntaxe en indice ne permet pas de modifier la longueur du tableau. Cependant, si vous ajoutez un nouvel élément au tableau, vous modifiez la longueur du tableau . Cela invite Swift à créer une nouvelle copie du tableau au moment où vous ajoutez la nouvelle valeur. Désormais, a est une copie indépendante distincte du tableau …..

Lisez la section Assignation et copie du comportement pour les tableaux dans cette documentation. Vous constaterez que lorsque vous remplacez une plage d’éléments dans le tableau, le tableau prend une copie de lui-même pour tous les éléments.

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 de l’une ou l’autre variable, seul le tableau une copie sera modifiée.


Vieille réponse:

Comme d’autres l’ont souligné, Swift essaie d’ éviter de copier les tableaux si possible, y compris lors de la modification des valeurs d’index simples à la fois.

Si vous voulez être sûr qu’une variable de tableau (!) Est unique, c’est-à-dire qu’elle n’est pas partagée avec une autre variable, vous pouvez appeler la méthode unshare . Cela copie le tableau sauf s’il ne contient déjà qu’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érable pour vous assurer qu’aucune autre variable ne conserve 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] 

Le comportement est extrêmement similaire à la méthode Array.Resize dans .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 d’autre qu’une agrégation de variables. Appliquer le . à une variable de type structure accédera à une variable stockée dans la structure. Les pointeurs vers les objects ne contiennent pas d’agrégation de variables, mais les identifient . Si l’on dispose d’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 les variables, mais peuvent également y attacher du code. En utilisant . invoquer une méthode sur une variable demande à cette méthode d’agir sur le contenu de la variable elle-même ; utiliser -> sur une variable qui identifie un object demandera à cette méthode d’agir sur l’object identifié par la variable.

En Java, tous les types de variables personnalisés identifient simplement des objects, et invoquer une méthode sur une variable indiquera à la méthode quel object est identifié par la variable. Les variables ne peuvent contenir aucun type de données composites directement, et aucune méthode ne permet d’accéder à une variable sur laquelle elle est appelée. Ces ressortingctions, bien que limitant sémantiquement, simplifient grandement le runtime et facilitent la validation du bytecode; de telles simplifications réduisaient la charge de ressources de Java à un moment où le marché était sensible à de tels problèmes, ce qui lui a permis de gagner du terrain sur le marché. Ils ont également signifié qu’il n’y avait pas besoin d’un jeton équivalent à la . utilisé en C ou C ++. Bien que Java ait pu utiliser -> de la même manière que C et C ++, les créateurs ont choisi d’utiliser un seul caractère . car il n’était pas nécessaire à d’autres fins.

En C # et dans d’autres langages .NET, les variables peuvent identifier des objects ou détenir directement des types de données composites. 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’object identifié par celui-ci. 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 dans lesquelles une méthode de type de données composite qui modifierait la variable sur laquelle elle est appelée est appelé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 copient généralement la variable, laissent la méthode agir sur elle et ignorent la variable. Ceci est généralement sûr avec les méthodes qui ne lisent que la variable, mais pas avec les méthodes qui y écrivent. 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.

Dans Swift, les méthodes sur les agrégats peuvent indiquer expressément si elles modifieront la variable sur laquelle elles sont appelées, et le compilateur interdira l’utilisation de méthodes de mutation sur les variables en lecture seule (plutôt que de les faire muter des copies temporaires de la variable se jeter). En raison de cette distinction, en utilisant le . Les méthodes token to call qui modifient les variables sur lesquelles elles sont appelées sont beaucoup plus sûres dans Swift que dans .NET. Malheureusement, le même fait . Le jeton est utilisé à cette fin pour agir sur un object externe identifié par une variable, ce qui laisse la possibilité de confusion.

Si vous aviez une machine à remonter le temps et que vous reveniez à la création de C # et / ou de Swift, vous pourriez éviter rétroactivement une grande partie de la confusion entourant ces problèmes en faisant utiliser les langages . et -> jetons d’une manière beaucoup plus proche de l’utilisation du C ++. Les méthodes des agrégats et des types de référence pourraient être utilisées . agir sur la variable sur laquelle ils ont été invoqués, et -> agir sur une valeur (pour les composites) ou sur la chose identifiée (pour les types de référence). Aucune langue n’est conçue de cette manière, cependant.

En C #, la méthode normale pour modifier une variable sur laquelle elle est appelée consiste à transmettre la variable en tant que paramètre ref à une méthode. Array.Resize(ref someArray, 23); ainsi Array.Resize(ref someArray, 23); lorsque someArray identifie un tableau de 20 éléments, someArray identifiera un nouveau tableau de 23 éléments, sans affecter le tableau d’origine. L’utilisation de ref montre clairement que la méthode doit modifier la variable sur laquelle elle est appelée. Dans de nombreux cas, il est avantageux de pouvoir modifier les variables sans avoir à utiliser de méthodes statiques; Swift adresse cela signifie en utilisant . syntaxe. L’inconvénient est qu’il perd la clarté quant aux méthodes qui agissent sur les variables et quelles méthodes agissent sur les valeurs.

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 changer la taille de a . En particulier, il n’a jamais besoin de faire d’allocation de mémoire. Indépendamment de 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 deuxième ligne peut être beaucoup plus compliquée. Selon les valeurs de i et j , vous devrez peut-être 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 qu’il s’agit d’un pointeur constant; Vous devrez peut-être atsortingbuer un nouveau bloc de mémoire, copier les données de l’ancien bloc de mémoire dans le nouveau bloc de mémoire et modifier le pointeur.

Il semble que les concepteurs de langage ont essayé de garder (1) aussi léger que possible. Comme (2) peut impliquer la copie de toute façon, ils ont eu recours à la solution qui agit toujours comme si vous en faisiez une copie.

C’est compliqué, mais je suis heureux qu’ils n’aient pas compliqué les choses avec, par exemple, des cas particuliers tels que “si dans (2) i et j sont des constantes de compilation et que le compilateur peut en déduire que la taille de e ne va pas pour changer, alors nous ne copions pas “ .


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

  • Utilisez les constantes ( let ) toujours par défaut, et il n’y aura pas de surprise majeure.
  • Utilisez les variables ( var ) uniquement si cela est absolument nécessaire, et soyez prudent dans ces cas, car il y aura des sursockets [ici: des copies implicites étranges de tableaux dans certaines situations mais pas toutes].

Ce que j’ai trouvé c’est: le tableau sera une copie mutable du référencé si et seulement si l’opération a le potentiel de changer la longueur du tableau . Dans votre dernier exemple, l’indexation f[0..2] avec plusieurs, l’opération a le potentiel de changer de longueur (il se peut que les doublons ne soient pas autorisés).

 var e = [1, 2, 3] var f = e e[0..2] = [4, 5] e // 4,5,3 f // 1,2,3 var e1 = [1, 2, 3] var f1 = e1 e1[0] = 4 e1[1] = 5 e1 // - 4,5,3 f1 // - 4,5,3 

Les chaînes et les tableaux de Delphi avaient exactement la même “fonctionnalité”. Lorsque vous avez examiné la mise en œuvre, cela avait du sens.

Chaque variable est un pointeur sur la mémoire dynamic. Cette mémoire contient un compte de référence suivi des données du tableau. Ainsi, vous pouvez facilement modifier une valeur dans le tableau sans copier l’intégralité du tableau ni modifier les pointeurs. Si vous souhaitez redimensionner le tableau, vous devez allouer plus de mémoire. Dans ce cas, la variable actuelle pointe vers la mémoire nouvellement allouée. Mais vous ne pouvez pas facilement retrouver toutes les autres variables qui pointent vers le tableau d’origine, vous les laissez donc seules.

Bien sûr, il ne serait pas difficile de procéder à une mise en œuvre plus cohérente. Si vous souhaitez que toutes les variables voient un redimensionnement, procédez comme suit: Chaque variable est un pointeur sur un conteneur stocké dans la mémoire dynamic. Le conteneur contient exactement deux choses, un compte de référence et un pointeur sur les données du tableau. Les données du tableau sont stockées dans un bloc distinct de mémoire dynamic. Maintenant, il n’y a qu’un seul pointeur sur les données du tableau, vous pouvez donc facilement le redimensionner et toutes les variables verront le changement.

Beaucoup d’adopteurs de Swift se sont plaints de cette sémantique de tableau sujette aux erreurs et Chris Lattner a écrit que la sémantique du tableau avait été révisée pour fournir une sémantique complète ( lien développeur Apple pour ceux qui ont un compte ). Nous devrons attendre au moins la prochaine bêta pour voir ce que cela signifie exactement.

J’utilise .copy () pour cela.

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