Gibt es einen Grund dafür, dass die Swift-Array-Zuweisung inkonsistent ist (weder eine Referenz noch eine tiefe Kopie)?

216

Ich lese die Dokumentation und schüttle ständig den Kopf bei einigen Designentscheidungen der Sprache. Aber das, was mich wirklich verwirrt hat, ist der Umgang mit Arrays.

Ich eilte zum Spielplatz und probierte diese aus. Sie können sie auch versuchen. Also das erste Beispiel:

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

Hier aund bbeides [1, 42, 3], was ich akzeptieren kann. Arrays werden referenziert - OK!

Sehen Sie sich nun dieses Beispiel an:

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

cist [1, 2, 3, 42]ABER dist [1, 2, 3]. Das heißt, dich habe die Änderung im letzten Beispiel gesehen, aber in diesem nicht. Die Dokumentation besagt, dass sich die Länge geändert hat.

Wie wäre es mit diesem:

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

eist [4, 5, 3], was cool ist. Es ist schön, einen Multi-Index-Ersatz zu haben, aber fSTILL sieht die Änderung nicht, obwohl sich die Länge nicht geändert hat.

Zusammenfassend lässt sich sagen, dass bei allgemeinen Verweisen auf ein Array Änderungen angezeigt werden, wenn Sie 1 Element ändern. Wenn Sie jedoch mehrere Elemente ändern oder Elemente anhängen, wird eine Kopie erstellt.

Dies scheint mir ein sehr schlechtes Design zu sein. Habe ich recht damit? Gibt es einen Grund, warum ich nicht verstehe, warum Arrays so handeln sollten?

BEARBEITEN : Arrays haben sich geändert und haben jetzt eine Wertsemantik. Viel vernünftiger!

Cthutu
quelle
95
Ich denke nicht, dass diese Frage geschlossen werden sollte. Swift ist eine neue Sprache, daher wird es für eine Weile Fragen wie diese geben, wenn wir alle lernen. Ich finde diese Frage sehr interessant und hoffe, dass jemand einen überzeugenden Fall zur Verteidigung hat.
Joel Berger
4
@Joel Gut, fragen Sie es bei Programmierern, Stack Overflow ist für bestimmte unbedachte Programmierprobleme.
bjb568
21
@ bjb568: Es ist jedoch keine Meinung. Diese Frage sollte mit Fakten beantwortet werden können. Wenn ein Swift-Entwickler kommt und antwortet: "Wir haben es für X, Y und Z so gemacht", dann ist das eine klare Tatsache. Sie sind vielleicht nicht mit X, Y und Z einverstanden, aber wenn eine Entscheidung für X, Y und Z getroffen wurde, ist dies nur eine historische Tatsache des Sprachdesigns. Ähnlich wie bei der Frage, warum std::shared_ptres keine nichtatomare Version gibt , gab es eine Antwort, die auf Fakten und nicht auf Meinungen beruhte (Tatsache ist, dass der Ausschuss dies in Betracht gezogen hat, es aber aus verschiedenen Gründen nicht wollte).
Cornstalks
7
@JasonMArcher: Nur der allerletzte Absatz basiert auf einer Meinung (die ja vielleicht entfernt werden sollte). Der eigentliche Titel der Frage (den ich als eigentliche Frage nehme) ist mit Fakten zu beantworten. Es gibt einen Grund, warum die Arrays so konzipiert wurden, dass sie so funktionieren, wie sie funktionieren.
Cornstalks
7
Ja, wie API-Beast sagte, wird dies normalerweise als "Copy-on-Half-Assed-Language-Design" bezeichnet.
R. Martinho Fernandes

Antworten:

109

Beachten Sie, dass die Semantik und Syntax des Arrays in der Xcode Beta 3-Version ( Blog-Beitrag ) geändert wurde , sodass die Frage nicht mehr gilt. Die folgende Antwort galt für Beta 2:


Es ist aus Leistungsgründen. Grundsätzlich versuchen sie, das Kopieren von Arrays so lange wie möglich zu vermeiden (und behaupten "C-ähnliche Leistung"). Um es mit dem Sprachbuch :

Bei Arrays erfolgt das Kopieren nur, wenn Sie eine Aktion ausführen, mit der die Länge des Arrays geändert werden kann. Dies umfasst das Anhängen, Einfügen oder Entfernen von Elementen oder die Verwendung eines Bereichsindex zum Ersetzen eines Bereichs von Elementen im Array.

Ich stimme zu, dass dies etwas verwirrend ist, aber es gibt zumindest eine klare und einfache Beschreibung, wie es funktioniert.

Dieser Abschnitt enthält auch Informationen dazu, wie Sie sicherstellen können, dass auf ein Array eindeutig verwiesen wird, wie Sie das Kopieren von Arrays erzwingen und wie Sie überprüfen, ob zwei Arrays gemeinsam genutzt werden.

Lukas
quelle
61
Ich finde die Tatsache, dass Sie sowohl eine große rote Fahne im Design freigeben als auch kopieren.
Cthutu
9
Das ist richtig. Ein Ingenieur hat mir beschrieben, dass dies für das Sprachdesign nicht wünschenswert ist und dass er hofft, es in kommenden Updates für Swift "reparieren" zu können. Stimmen Sie mit Radar ab.
Erik Kerber
2
Es ist nur so etwas wie Copy-on-Write (COW) in der Speicherverwaltung für untergeordnete Linux-Prozesse, oder? Vielleicht können wir es Copy-on-Length-Alteration (COLA) nennen. Ich sehe das als positives Design.
Justhalf
3
@justhalf Ich kann eine Menge verwirrter Neulinge vorhersagen, die zu SO kommen und fragen, warum ihre Arrays geteilt wurden / wurden (nur auf weniger klare Weise).
John Dvorak
11
@justhalf: COW ist in der modernen Welt sowieso eine Pessimisierung, und zweitens ist COW eine reine Implementierungstechnik, und dieses COLA-Zeug führt zu völlig zufälligem Teilen und Aufheben der Freigabe.
Welpe
25

Aus der offiziellen Dokumentation der Swift-Sprache :

Beachten Sie, dass das Array nicht kopiert wird, wenn Sie einen neuen Wert mit Indexsyntax festlegen, da durch das Festlegen eines einzelnen Werts mit Indexsyntax die Länge des Arrays nicht geändert werden kann. Wenn Sie jedoch ein neues Element an das Array anhängen, ändern Sie die Länge des Arrays . Dadurch wird Swift aufgefordert, an der Stelle, an der Sie den neuen Wert anhängen , eine neue Kopie des Arrays zu erstellen . Von nun an ist a eine separate, unabhängige Kopie des Arrays .....

Lesen Sie den gesamten Abschnitt Zuweisungs- und Kopierverhalten für Arrays in dieser Dokumentation. Sie werden feststellen, dass das Array beim Ersetzen einer Reihe von Elementen im Array für alle Elemente eine Kopie von sich selbst erstellt.

iPatel
quelle
4
Vielen Dank. Ich habe in meiner Frage vage auf diesen Text Bezug genommen. Aber ich habe ein Beispiel gezeigt, bei dem das Ändern eines Indexbereichs die Länge nicht geändert hat und es trotzdem kopiert wurde. Wenn Sie also keine Kopie möchten, müssen Sie sie einzeln ändern.
Cthutu
21

Das Verhalten hat sich mit Xcode 6 Beta 3 geändert. Arrays sind keine Referenztypen mehr und verfügen über einen Copy-on-Write- Mechanismus. Sobald Sie den Inhalt eines Arrays von der einen oder anderen Variablen ändern, wird das Array kopiert und nur das Eine Kopie wird geändert.


Alte Antwort:

Wie andere bereits betont haben, versucht Swift, das Kopieren von Arrays nach Möglichkeit zu vermeiden , auch wenn Werte für einzelne Indizes gleichzeitig geändert werden.

Wenn Sie sicherstellen möchten, dass eine Array-Variable (!) Eindeutig ist, dh nicht mit einer anderen Variablen geteilt wird, können Sie die unshareMethode aufrufen . Dadurch wird das Array kopiert, sofern es nicht bereits nur eine Referenz enthält. Natürlich können Sie auch die copyMethode aufrufen , die immer eine Kopie erstellt. Das Freigeben wird jedoch bevorzugt, um sicherzustellen, dass keine andere Variable dasselbe Array beibehält .

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]
Pascal
quelle
hmm, für mich ist diese unshare()Methode undefiniert.
Hlung
1
@Hlung Es wurde in Beta 3 entfernt, ich habe meine Antwort aktualisiert.
Pascal
12

Das Verhalten ist der Array.ResizeMethode in .NET sehr ähnlich . Um zu verstehen, was los ist, kann es hilfreich sein, den Verlauf des .Tokens in C, C ++, Java, C # und Swift zu betrachten.

In C ist eine Struktur nichts anderes als eine Aggregation von Variablen. Durch Anwenden von .auf eine Variable vom Strukturtyp wird auf eine in der Struktur gespeicherte Variable zugegriffen. Zeiger auf Objekte nicht halten Aggregationen von Variablen, aber identifizieren sie. Wenn man einen Zeiger hat, der eine Struktur identifiziert, kann der ->Operator verwendet werden, um auf eine Variable zuzugreifen, die in der durch den Zeiger identifizierten Struktur gespeichert ist.

In C ++ aggregieren Strukturen und Klassen nicht nur Variablen, sondern können ihnen auch Code hinzufügen. Wenn Sie .eine Methode aufrufen, wird diese Methode für eine Variable aufgefordert, auf den Inhalt der Variablen selbst zu reagieren. Wenn Sie ->eine Variable verwenden, die ein Objekt identifiziert, wird diese Methode aufgefordert, auf das durch die Variable identifizierte Objekt zu reagieren.

In Java identifizieren alle benutzerdefinierten Variablentypen einfach Objekte, und das Aufrufen einer Methode für eine Variable teilt der Methode mit, welches Objekt von der Variablen identifiziert wird. Variablen können weder einen zusammengesetzten Datentyp direkt enthalten, noch gibt es Mittel, mit denen eine Methode auf eine Variable zugreifen kann, für die sie aufgerufen wird. Diese Einschränkungen sind zwar semantisch einschränkend, vereinfachen jedoch die Laufzeit erheblich und erleichtern die Bytecode-Validierung. Solche Vereinfachungen reduzierten den Ressourcenaufwand von Java zu einer Zeit, als der Markt für solche Probleme sensibel war, und trugen so dazu bei, auf dem Markt Fuß zu fassen. Sie bedeuteten auch, dass kein Token erforderlich war, das dem .in C oder C ++ verwendeten entspricht. Obwohl Java ->genauso wie C und C ++ hätte verwendet werden können, entschieden sich die Ersteller für die Verwendung von Einzelzeichen. da es für keinen anderen Zweck benötigt wurde.

In C # und anderen .NET-Sprachen können Variablen entweder Objekte identifizieren oder zusammengesetzte Datentypen direkt enthalten. Bei Verwendung für eine Variable eines zusammengesetzten Datentyps wird .auf den Inhalt der Variablen reagiert. Wenn es für eine Variable vom Referenztyp verwendet wird, .wirkt es auf das identifizierte Objektvon ihm. Für einige Arten von Operationen ist die semantische Unterscheidung nicht besonders wichtig, für andere jedoch. Die problematischsten Situationen sind solche, in denen die Methode eines zusammengesetzten Datentyps, die die Variable ändern würde, für die er aufgerufen wird, für eine schreibgeschützte Variable aufgerufen wird. Wenn versucht wird, eine Methode für einen schreibgeschützten Wert oder eine schreibgeschützte Variable aufzurufen, kopieren Compiler die Variable im Allgemeinen, lassen die Methode darauf reagieren und verwerfen die Variable. Dies ist im Allgemeinen sicher bei Methoden, die nur die Variable lesen, aber nicht sicher bei Methoden, die darauf schreiben. Leider hat .does noch keine Möglichkeit anzugeben, welche Methoden mit einer solchen Substitution sicher verwendet werden können und welche nicht.

In Swift können Methoden für Aggregate ausdrücklich angeben, ob sie die Variable ändern, für die sie aufgerufen werden, und der Compiler verbietet die Verwendung von Mutationsmethoden für schreibgeschützte Variablen (anstatt temporäre Kopien der Variablen zu mutieren, die dann mutieren weggeworfen werden). Aufgrund dieser Unterscheidung ist die Verwendung des .Tokens zum Aufrufen von Methoden, die die Variablen ändern, für die sie aufgerufen werden, in Swift viel sicherer als in .NET. Leider bedeutet die Tatsache, dass .zu diesem Zweck dasselbe Token verwendet wird, um auf ein externes Objekt einzuwirken, das durch eine Variable identifiziert wird, dass die Möglichkeit einer Verwirrung bestehen bleibt.

Wenn man eine Zeitmaschine hätte und zur Erstellung von C # und / oder Swift zurückkehren würde, könnte man rückwirkend einen Großteil der Verwirrung vermeiden, die mit solchen Problemen verbunden ist, indem Sprachen die Token .und und ->Token auf eine Weise verwenden, die der C ++ - Verwendung viel näher kommt. Methoden sowohl von Aggregaten als auch von Referenztypen könnten verwendet werden ., um auf die Variable zu wirken, auf die sie aufgerufen wurden, und ->um auf einen Wert (für Verbundwerkstoffe) oder das dadurch identifizierte Objekt (für Referenztypen) zu wirken. Keine der beiden Sprachen ist jedoch so gestaltet.

In C # besteht die übliche Praxis für eine Methode zum Ändern einer Variablen, für die sie aufgerufen wird, darin, die Variable als refParameter an eine Methode zu übergeben. Array.Resize(ref someArray, 23);Wenn Sie also someArrayein Array mit 20 Elementen aufrufen someArray, wird ein neues Array mit 23 Elementen identifiziert, ohne dass dies Auswirkungen auf das ursprüngliche Array hat. Die Verwendung von refmacht deutlich, dass von der Methode erwartet werden sollte, dass sie die Variable ändert, für die sie aufgerufen wird. In vielen Fällen ist es vorteilhaft, Variablen ändern zu können, ohne statische Methoden verwenden zu müssen. Schnelle Adressen, dh .Syntax. Der Nachteil ist, dass es an Klarheit verliert, welche Methoden auf Variablen und welche Methoden auf Werte einwirken.

Superkatze
quelle
5

Für mich ist dies sinnvoller, wenn Sie zuerst Ihre Konstanten durch Variablen ersetzen:

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

Die erste Zeile muss niemals die Größe von ändern a. Insbesondere muss niemals eine Speicherzuweisung vorgenommen werden. Unabhängig vom Wert von iist dies eine leichte Operation. Wenn Sie sich vorstellen, dass sich unter der Haube aein Zeiger befindet, kann dies ein konstanter Zeiger sein.

Die zweite Zeile kann viel komplizierter sein. Abhängig von den Werten von iund jmüssen Sie möglicherweise eine Speicherverwaltung durchführen. Wenn Sie sich vorstellen, dass dies eein Zeiger ist, der auf den Inhalt des Arrays zeigt, können Sie nicht mehr davon ausgehen, dass es sich um einen konstanten Zeiger handelt. Möglicherweise müssen Sie einen neuen Speicherblock zuweisen, Daten aus dem alten Speicherblock in den neuen Speicherblock kopieren und den Zeiger ändern.

Es scheint, dass die Sprachdesigner versucht haben, (1) so leicht wie möglich zu halten. Da (2) ohnehin das Kopieren beinhalten kann, haben sie auf die Lösung zurückgegriffen, dass es sich immer so verhält, als ob Sie eine Kopie gemacht hätten.

Dies ist kompliziert, aber ich bin froh, dass sie es nicht noch komplizierter gemacht haben, z. B. mit Sonderfällen wie "Wenn in (2) i und j Konstanten zur Kompilierungszeit sind und der Compiler daraus schließen kann, dass die Größe von e nicht stimmt." zu ändern, dann kopieren wir nicht " .


Basierend auf meinem Verständnis der Gestaltungsprinzipien der Swift-Sprache denke ich, dass die allgemeinen Regeln folgende sind:

  • Verwenden Sie constants ( let) standardmäßig immer überall, und es gibt keine größeren Überraschungen.
  • Verwenden Sie variables ( var) nur, wenn dies unbedingt erforderlich ist, und variieren Sie in diesen Fällen vorsichtig, da es zu Überraschungen kommt [hier: seltsame implizite Kopien von Arrays in einigen, aber nicht allen Situationen].
Jukka Suomela
quelle
5

Was ich gefunden habe, ist: Das Array ist genau dann eine veränderbare Kopie des referenzierten Arrays, wenn die Operation das Potenzial hat, die Länge des Arrays zu ändern . In Ihrem letzten Beispiel, bei dem f[0..2]mit vielen indiziert wird, kann der Vorgang seine Länge ändern (möglicherweise sind Duplikate nicht zulässig), sodass er kopiert wird.

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
Kumar KL
quelle
8
"behandelt, wenn sich die Länge geändert hat" Ich kann verstehen, dass es kopiert wird, wenn die Länge geändert wird, aber in Kombination mit dem obigen Zitat denke ich, dass dies ein wirklich besorgniserregendes "Feature" ist und eines, von dem ich denke, dass viele Leute falsch liegen werden
Joel Berger
25
Nur weil eine Sprache neu ist, heißt das nicht, dass es in Ordnung ist, eklatante interne Widersprüche zu enthalten.
Leichtigkeitsrennen im Orbit
Dies wurde in Beta 3 "behoben", varArrays sind jetzt vollständig veränderbar und letArrays sind vollständig unveränderlich.
Pascal
4

Delphis Strings und Arrays hatten genau das gleiche "Merkmal". Wenn Sie sich die Implementierung ansehen, ist dies sinnvoll.

Jede Variable ist ein Zeiger auf den dynamischen Speicher. Dieser Speicher enthält einen Referenzzähler, gefolgt von den Daten im Array. So können Sie einfach einen Wert im Array ändern, ohne das gesamte Array zu kopieren oder Zeiger zu ändern. Wenn Sie die Größe des Arrays ändern möchten, müssen Sie mehr Speicher zuweisen. In diesem Fall zeigt die aktuelle Variable auf den neu zugewiesenen Speicher. Sie können jedoch nicht alle anderen Variablen aufspüren, die auf das ursprüngliche Array verweisen, sodass Sie sie in Ruhe lassen.

Natürlich wäre es nicht schwer, eine konsistentere Implementierung vorzunehmen. Wenn für alle Variablen eine Größenänderung angezeigt werden soll, gehen Sie folgendermaßen vor: Jede Variable ist ein Zeiger auf einen Container, der im dynamischen Speicher gespeichert ist. Der Container enthält genau zwei Dinge, einen Referenzzähler und einen Zeiger auf die tatsächlichen Array-Daten. Die Array-Daten werden in einem separaten Block des dynamischen Speichers gespeichert. Jetzt gibt es nur noch einen Zeiger auf die Array-Daten, sodass Sie die Größe leicht ändern können und alle Variablen die Änderung sehen.

Handelsideen Philip
quelle
4

Viele Swift-Early-Adopters haben sich über diese fehleranfällige Array-Semantik beschwert, und Chris Lattner hat geschrieben, dass die Array-Semantik überarbeitet wurde, um eine vollständige Semantik bereitzustellen ( Apple Developer-Link für diejenigen, die ein Konto haben ). Wir müssen mindestens auf die nächste Beta warten, um zu sehen, was dies genau bedeutet.

Gael
quelle
1
Das neue Array-Verhalten ist jetzt ab dem SDK verfügbar, das in iOS 8 / Xcode 6 Beta 3 enthalten ist.
smileyborg
0

Ich benutze dafür .copy ().

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 
Preetham
quelle
1
Ich erhalte "Wert vom Typ '[Int]' hat kein Mitglied 'Kopie'", wenn ich Ihren Code
ausführe
0

Hat sich in späteren Swift-Versionen etwas am Arrays-Verhalten geändert? Ich führe nur Ihr Beispiel aus:

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

Und meine Ergebnisse sind [1, 42, 3] und [1, 2, 3]

jreft56
quelle