Y a-t-il une raison pour laquelle l'affectation de tableau Swift est incohérente (ni une référence ni une copie complète)?


216

Je lis la documentation et je secoue constamment la tête à certaines des décisions de conception de la langue. Mais ce qui m'a vraiment intrigué, c'est la façon dont les tableaux sont traités.

Je me suis précipité vers la cour de récréation et les ai essayés. Vous pouvez aussi les essayer. Donc, le premier exemple:

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

Ici aet bles 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

cest [1, 2, 3, 42]MAIS dest [1, 2, 3]. Autrement dit, dvu le changement dans le dernier exemple mais ne le voit pas dans celui-ci. La documentation indique que c'est parce que la longueur a changé.

Maintenant, que diriez-vous de celui-ci:

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

eest [4, 5, 3], ce qui est cool. C'est bien d'avoir un remplacement multi-index, mais fSTILL ne voit pas le changement même si la longueur n'a pas changé.

Donc, pour résumer, les références communes à un tableau voient des 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 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 comme ça?

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


95
Pour mémoire, je ne pense pas que cette question devrait être close. Swift est une nouvelle langue, il va donc y avoir des questions comme celle-ci pendant un certain temps alors que nous apprenons tous. Je trouve cette question très intéressante et j'espère que quelqu'un aura un dossier convaincant sur la défense.
Joel Berger

4
@Joel Fine, demandez-le aux programmeurs, Stack Overflow est destiné à des problèmes de programmation spécifiques non exprimés.
bjb568

21
@ bjb568: Ce n'est pas une opinion, cependant. Cette question devrait être répondue par des faits. Si un développeur Swift vient et répond "Nous l'avons fait comme ça pour X, Y et Z", alors c'est tout à fait vrai. Vous n'êtes peut-être pas d'accord avec X, Y et Z, mais si une décision a été prise pour X, Y et Z, ce n'est qu'un fait historique de la conception du langage. Tout comme lorsque j'ai demandé pourquoi il std::shared_ptrn'y avait pas de version non atomique, il y avait une réponse basée sur des faits, pas sur des opinions (le fait est que le comité l'a envisagée mais ne l'a pas voulue pour diverses raisons).
Cornstalks

7
@JasonMArcher: Seul le tout dernier paragraphe est basé sur l'opinion (qui oui, peut-être devrait être supprimée). Le titre réel de la question (que je considère comme la question elle-même) peut être répondu par des faits. Il y a une raison pour laquelle les tableaux ont été conçus pour fonctionner comme ils fonctionnent.
Cornstalks

7
Oui, comme l'a dit API-Beast, cela s'appelle généralement "Copy-on-Half-Assed-Language-Design".
R. Martinho Fernandes

Réponses:


109

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


C'est pour des raisons de performances. Fondamentalement, ils essaient d'éviter de copier des 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 dans le tableau.

Je suis d'accord que c'est un peu déroutant, mais au moins il y a une description claire et simple de la façon dont cela fonctionne.

Cette section comprend é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 le stockage.


61
Je trouve le fait que vous avez à la fois annuler le partage et copier un GRAND drapeau rouge dans la conception.
Cthutu

9
C'est correct. Un ingénieur m'a décrit que pour la conception de langage, ce n'est pas souhaitable, et c'est quelque chose qu'ils espèrent "corriger" dans les prochaines mises à jour de Swift. Votez avec des radars.
Erik Kerber

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

3
@justhalf Je peux prédire un tas de débutants confus venant sur SO et demandant pourquoi leurs tableaux étaient / n'étaient pas partagés (juste de manière moins claire).
John Dvorak

11
@justhalf: COW est de toute façon une pessimisation dans le monde moderne, et deuxièmement, COW est une technique de mise en œuvre uniquement, et ce truc COLA conduit à un partage et à un partage totalement aléatoires.
Puppy

25

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 d'indice, car la définition d'une valeur unique avec la syntaxe d'indice n'a pas le potentiel 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 séparée et indépendante du tableau .....

Lisez l'intégralité de la section Attribution et comportement de copie des 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.


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

21

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


Ancienne réponse:

Comme d'autres l'ont souligné, Swift essaie d' éviter de copier des tableaux si possible, y compris lors de la modification des valeurs d'index uniques à 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 unshareméthode. Cela copie le tableau sauf s'il n'a déjà qu'une seule référence. Bien sûr, vous pouvez également appeler la copyméthode, qui fera toujours une copie, mais le partage 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]

hmm, pour moi, cette unshare()méthode n'est pas définie.
Hlung

1
@Hlung Il a été supprimé en beta 3, j'ai mis à jour ma réponse.
Pascal

12

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

En C, une structure n'est rien de plus qu'une agrégation de variables. L'application de la .à une variable de type structure permet d'accéder à une variable stockée dans la structure. Les pointeurs vers les objets ne contiennent pas d' agrégations de variables, mais les identifient . Si l'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 agrégent non seulement les variables, mais peuvent également y attacher du code. Utiliser .pour invoquer une méthode demandera à une variable d'agir sur le contenu de la variable elle-même ; l'utilisation ->d'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ées identifient simplement les objets, et l'invocation d'une méthode sur une variable indiquera à la méthode quel objet est identifié par la variable. Les variables ne peuvent pas contenir directement tout type de type de données composites, ni aucun moyen permettant à une méthode d'accéder à une variable sur laquelle elle est invoquée. Ces restrictions, bien que sémantiquement limitantes, simplifient considérablement l'exécution et facilitent la validation du bytecode; de telles simplifications ont réduit les frais généraux de Java à un moment où le marché était sensible à de tels problèmes, et l'ont ainsi aidé à gagner du terrain sur le marché. Ils signifiaient également qu'il n'y avait pas besoin d'un jeton équivalent à celui .utilisé en C ou C ++. Bien que Java aurait 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 soit identifier des objets, soit contenir directement des types de données composites. 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 cela. 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 d'un 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 appeler 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 en conséquence et ignorent la variable. Ceci est généralement sûr avec des méthodes qui ne lisent que la variable, mais pas avec des 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 invoquées, et le compilateur interdira l'utilisation de méthodes de mutation sur des variables en lecture seule (plutôt que de les faire muter des copies temporaires de la variable qui sera ensuite se débarrasser). En raison de cette distinction, l'utilisation du .jeton pour appeler des méthodes qui modifient les variables sur lesquelles elles sont appelées est beaucoup plus sûre dans Swift que dans .NET. Malheureusement, le fait que le même .jeton soit utilisé à cette fin pour agir sur un objet externe identifié par une variable signifie qu'il existe un risque de confusion.

Si nous avions une machine à remonter le temps et que nous revenions à la création de C # et / ou Swift, on pourrait éviter rétroactivement une grande partie de la confusion entourant ces problèmes en faisant en sorte que les langages utilisent les jetons .et ->d'une manière beaucoup plus proche de l'utilisation de C ++. Les méthodes des agrégats et des types de référence pourraient être utilisées .pour agir sur la variable sur laquelle ils ont été invoqués et ->pour agir sur une valeur (pour les composites) ou sur la chose identifiée (pour les types de référence). Cependant, aucune des deux langues n'est conçue de cette façon.

En C #, la pratique normale pour une méthode de modifier une variable sur laquelle elle est invoquée est de passer la variable comme refparamètre à une méthode. Ainsi, l'appel Array.Resize(ref someArray, 23);lors de l' someArrayidentification d'un tableau de 20 éléments entraînera l' someArrayidentification d'un nouveau tableau de 23 éléments, sans affecter le tableau d'origine. L'utilisation de refindique clairement que la méthode doit ê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 des méthodes statiques; Swift résout cela en utilisant la .syntaxe. L'inconvénient est qu'il perd de préciser quelles méthodes agissent sur les variables et quelles méthodes agissent sur les valeurs.


5

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. Quelle que soit la valeur de i, il s'agit d'une opération légère. Si vous imaginez que sous le capot aest un pointeur, il peut s'agir d'un pointeur constant.

La deuxième ligne peut être beaucoup plus compliquée. Selon les valeurs de iet j, vous devrez peut-être gérer la mémoire. Si vous imaginez qu'il es'agit d'un pointeur pointant vers le contenu du tableau, vous ne pouvez plus supposer qu'il s'agit d'un pointeur constant; vous devrez peut-être allouer un nouveau bloc de mémoire, copier les données de l'ancien bloc de mémoire vers le nouveau bloc de mémoire et modifier le pointeur.

Il semble que les concepteurs de langage aient essayé de garder (1) le plus léger possible. Comme (2) peut impliquer une copie de toute façon, ils ont recouru à la solution qui consiste à toujours agir comme si vous faisiez une copie.

C'est compliqué, mais je suis heureux 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 le compilateur peut inférer que la taille de e ne va pas changer, alors nous ne copions pas " .


Enfin, d'après 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) toujours partout par défaut, et il n'y aura pas de surprise majeure.
  • N'utilisez des variables ( var) que si cela est absolument nécessaire, et soyez prudent dans ces cas, car il y aura des surprises [ici: d'étranges copies implicites de tableaux dans certaines mais pas toutes les situations].

5

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

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

8
« traité comme longueur a changé : » Je peux comprendre que ce serait copiés ssi la longueur est changé, mais en combinaison avec la citation ci - dessus, je pense que cela est une « caractéristique » vraiment inquiétant et que je pense que beaucoup de gens vont se tromper
Joel Berger

25
Ce n'est pas parce qu'une langue est nouvelle qu'elle peut contenir des contradictions internes flagrantes.
Courses de légèreté en orbite le

Cela a été "corrigé" dans la version bêta 3, les vartableaux sont maintenant complètement mutables et les lettableaux sont complètement immuables.
Pascal

4

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 vers la mémoire dynamique. Cette mémoire contient un décompte de référence suivi des données du tableau. Ainsi, vous pouvez facilement modifier une valeur dans le tableau sans copier tout le tableau ou modifier les pointeurs. Si vous souhaitez redimensionner le tableau, vous devez allouer plus de mémoire. Dans ce cas, la variable actuelle pointera vers la mémoire nouvellement allouée. Mais vous ne pouvez pas facilement retrouver toutes les autres variables qui pointaient vers le tableau d'origine, vous les laissez donc seules.

Bien sûr, il ne serait pas difficile de réaliser une implémentation plus cohérente. Si vous souhaitez que toutes les variables voient un redimensionnement, procédez comme suit: Chaque variable est un pointeur vers un conteneur stocké dans la mémoire dynamique. Le conteneur contient exactement deux choses, un compte de référence et un pointeur vers les données réelles du tableau. Les données de la matrice sont stockées dans un bloc séparé de mémoire dynamique. Maintenant, il n'y a qu'un seul pointeur vers les données du tableau, vous pouvez donc facilement le redimensionner et toutes les variables verront le changement.


4

De nombreux adopteurs précoces 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 de pleine valeur ( lien Apple Developer pour ceux qui ont un compte ). Nous devrons attendre au moins la prochaine version bêta pour voir ce que cela signifie exactement.


1
Le nouveau comportement de la baie est désormais disponible à partir du SDK inclus avec iOS 8 / Xcode 6 Beta 3.
smileyborg

0

J'utilise .copy () pour cela.

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

1
J'obtiens "La valeur de type '[Int]' n'a aucun membre 'copie'" quand
j'exécute

0

Quelque chose a-t-il changé dans le comportement des tableaux dans les versions ultérieures de Swift? Je viens d'exécuter votre exemple:

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

Et mes résultats sont [1, 42, 3] et [1, 2, 3]

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.