web-dev-qa-db-fra.com

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

Je suis en train de lire la documentation et je hoche constamment la tête face aux décisions de conception de la langue. Mais la chose qui m'a vraiment intriguée est la façon dont les tableaux sont manipulés.

Je me suis précipité vers le terrain de jeu et ai essayé ces 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 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 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 diriez-vous 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 STILL ne voit pas le changement même si sa longueur n'a pas changé.

Donc, pour résumer, les références communes à un tableau voient les modifications si vous modifiez un élément, mais si vous modifiez plusieurs éléments ou ajoutez des éléments, une copie est réalisée.

Cela me semble 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 la sorte?

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

216
Cthutu

Notez que la sémantique et la syntaxe des tableaux 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'appliquait à la bêta 2:


C'est pour des raisons de performance. En gros, ils essaient d'éviter de copier des tableaux aussi longtemps qu'ils le peuvent (et revendiquent des "performances similaires à celles du C"). Pour citer la langue livre :

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 à plage pour remplacer une plage d'éléments dans le tableau.

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

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

109
Lukas

d'après la documentation officielle de la Swift langue :

Notez que le tableau n’est pas copié lorsque vous définissez une nouvelle valeur avec une syntaxe d’indice, car la définition d’une valeur unique avec une syntaxe d’indice n’a pas le potentiel de changer la longueur du tableau. Cependant, si vous ajoutez un nouvel élément au tableau, vous modifiez la longueur du tableau . Ceci invite Swift à créer une nouvelle copie du tableau au point 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 du comportement d'assignation et de copie pour les tableaux de cette documentation. Vous constaterez que lorsque vous remplacez une plage d'éléments dans le tableau, celui-ci en prend une copie pour tous les éléments.

25
iPatel

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 vous modifiez le contenu d'un tableau de l'une ou l'autre variable, le tableau sera copié et seule la copie sera changée.


Ancienne réponse:

Comme d'autres l'ont fait remarquer, Swift tente d'éviter de copier des tableaux, si possible, y compris when modification des valeurs pour les index simples à la fois.

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. Ceci copie le tableau à moins qu’il n’ait déjà qu’une référence. Bien sûr, vous pouvez également appeler la méthode copy, qui en 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]
20
Pascal

Le comportement est extrêmement similaire à la méthode Array.Resize 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 du . À une variable de type structure permet d'accéder à une variable stockée dans la structure. Les pointeurs sur les objets ne font pas en attente des agrégations de variables, mais les identifient. Si vous avez 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 va, sur une variable, demander à 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ées identifient simplement les objets et l'appel d'une méthode à une variable indiquera à la méthode quel objet est identifié par la variable. Les variables ne peuvent contenir directement aucun type de type de données composite, et il n'existe aucun moyen par lequel une méthode peut accéder à une variable sur laquelle elle est appelée. Ces restrictions, bien que limitantes sur le plan sémantique, simplifient grandement l’exécution et facilitent la validation du code intermédiaire; De telles simplifications ont permis de réduire les frais généraux de ressources de Java à une époque où le marché était sensible à de tels problèmes, ce qui l'a aidé à conquérir le marché. Cela signifiait également qu'aucun jeton n'était nécessaire. équivalent au . 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 opté pour -character . puisqu'il n'était pas nécessaire à d'autres fins.

En C # et dans d'autres langages .NET, les variables peuvent identifier des objets ou 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 conten de la variable; lorsqu'il est utilisé sur une variable de type référence, . agit sur l'objet identifié par lui. 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 la méthode d'un type de données composite, qui modifierait la variable sur laquelle il est appelé, 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 cette variable et la rejeteront. Cela 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, .do 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 des variables en lecture seule (au lieu de leur demander de faire des copies temporaires de la variable, qui seront ensuite mutées). se débarrasser). En raison de cette distinction, l'utilisation du jeton . Pour appeler des méthodes modifiant 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 . est utilisé à cette fin, car pour agir sur un objet externe identifié par une variable, la possibilité de confusion subsiste.

Si vous aviez une machine à remonter le temps et reveniez à la création de C # et/ou Swift, vous pourriez éviter rétroactivement une grande partie de la confusion qui entoure de tels problèmes en faisant en sorte que les langues utilisent les jetons . Et -> Dans un fichier. la mode beaucoup plus proche de l'utilisation C++. Les méthodes des agrégats et des types de référence peuvent 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 ainsi identifiée (pour les types de référence). Aucune langue n'est conçue de cette façon, cependant.

En C #, la méthode habituelle pour modifier une variable sur laquelle elle est invoquée consiste à transmettre la variable en tant que paramètre ref à une méthode. Ainsi, appeler Array.Resize(ref someArray, 23); lorsque someArray identifiera un tableau de 20 éléments obligera someArray à identifier un nouveau tableau de 23 éléments, sans affecter le tableau d'origine. L'utilisation de ref indique clairement que la méthode devrait 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 adresses qui signifie en utilisant la syntaxe .). L’inconvénient est qu’il perd la clarté quant aux méthodes qui agissent sur les variables et celles qui agissent aux valeurs.

12
supercat

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’est jamais nécessaire d’allouer de la 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, a est un pointeur, il peut s'agir d'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 allouer 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 aient essayé de garder (1) le moins de poids possible. Comme (2) peut impliquer de toute façon une copie, ils ont eu recours à la solution selon laquelle cela agit toujours comme si vous faisiez une copie.

C’est compliqué, mais je suis heureux qu’ils n’aient pas encore compliqué les choses, par exemple avec 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 changera pas, nous ne copions pas".


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

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

Ce que j’ai trouvé, c’est: Le tableau sera une copie mutable du fichier référencé si et seulement si l’opération a le potentiel de changer la longueur du tableau . Dans votre dernier exemple, f[0..2] indexant plusieurs opérations, l'opération peut potentiellement changer de longueur (il est possible que les doublons ne soient pas autorisés), de sorte qu'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
5
Kumar KL

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 dynamique. Cette mémoire contient un nombre de références suivi des données du tableau. Ainsi, vous pouvez facilement modifier une valeur dans le tableau sans copier le tableau entier 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 pointant vers le tableau d'origine. Vous les laissez donc seules.

Bien entendu, il ne serait pas difficile de faire 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 dynamique. Le conteneur contient exactement deux éléments: un nombre de références et un pointeur sur les données du tableau actuel. 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 sur les données du tableau, vous pouvez donc facilement le redimensionner et toutes les variables verront la modification.

4

Beaucoup de Swift précurseurs 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 valeur complète ( Lien Apple Developer pour ceux qui ont un compte ). Nous devrons au moins attendre la prochaine version bêta pour voir ce que cela signifie exactement.

4
Gael

J'utilise .copy () pour cela.

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

Est-ce que quelque chose a changé dans le comportement des tableaux dans les versions ultérieures 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]

0
jreft56