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 a
et b
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]
. Autrement dit, d
vu 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
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 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!
std::shared_ptr
n'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).Réponses:
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 :
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.
la source
De la documentation officielle de la langue Swift :
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.
la source
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
unshare
mé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 lacopy
mé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.la source
unshare()
méthode n'est pas définie.Le comportement est extrêmement similaire à la
Array.Resize
mé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
ref
paramètre à une méthode. Ainsi, l'appelArray.Resize(ref someArray, 23);
lors de l'someArray
identification d'un tableau de 20 éléments entraînera l'someArray
identification d'un nouveau tableau de 23 éléments, sans affecter le tableau d'origine. L'utilisation deref
indique 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.la source
Pour moi, cela a plus de sens si vous remplacez d'abord vos constantes par des variables:
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 dei
, il s'agit d'une opération légère. Si vous imaginez que sous le capota
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
etj
, vous devrez peut-être gérer la mémoire. Si vous imaginez qu'ile
s'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:
let
) toujours partout par défaut, et il n'y aura pas de surprise majeure.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].la source
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.la source
var
tableaux sont maintenant complètement mutables et leslet
tableaux sont complètement immuables.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.
la source
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.
la source
J'utilise .copy () pour cela.
la source
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:
Et mes résultats sont [1, 42, 3] et [1, 2, 3]
la source