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 a
und b
beides [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
c
ist [1, 2, 3, 42]
ABER d
ist [1, 2, 3]
. Das heißt, d
ich 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
e
ist [4, 5, 3]
, was cool ist. Es ist schön, einen Multi-Index-Ersatz zu haben, aber f
STILL 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!
std::shared_ptr
es 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).Antworten:
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 :
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.
quelle
Aus der offiziellen Dokumentation der Swift-Sprache :
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.
quelle
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
unshare
Methode aufrufen . Dadurch wird das Array kopiert, sofern es nicht bereits nur eine Referenz enthält. Natürlich können Sie auch diecopy
Methode aufrufen , die immer eine Kopie erstellt. Das Freigeben wird jedoch bevorzugt, um sicherzustellen, dass keine andere Variable dasselbe Array beibehält .quelle
unshare()
Methode undefiniert.Das Verhalten ist der
Array.Resize
Methode 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
ref
Parameter an eine Methode zu übergeben.Array.Resize(ref someArray, 23);
Wenn Sie alsosomeArray
ein Array mit 20 Elementen aufrufensomeArray
, wird ein neues Array mit 23 Elementen identifiziert, ohne dass dies Auswirkungen auf das ursprüngliche Array hat. Die Verwendung vonref
macht 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.quelle
Für mich ist dies sinnvoller, wenn Sie zuerst Ihre Konstanten durch Variablen ersetzen:
Die erste Zeile muss niemals die Größe von ändern
a
. Insbesondere muss niemals eine Speicherzuweisung vorgenommen werden. Unabhängig vom Wert voni
ist dies eine leichte Operation. Wenn Sie sich vorstellen, dass sich unter der Haubea
ein Zeiger befindet, kann dies ein konstanter Zeiger sein.Die zweite Zeile kann viel komplizierter sein. Abhängig von den Werten von
i
undj
müssen Sie möglicherweise eine Speicherverwaltung durchführen. Wenn Sie sich vorstellen, dass diese
ein 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:
let
) standardmäßig immer überall, und es gibt keine größeren Überraschungen.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].quelle
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.quelle
var
Arrays sind jetzt vollständig veränderbar undlet
Arrays sind vollständig unveränderlich.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.
quelle
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.
quelle
Ich benutze dafür .copy ().
quelle
Hat sich in späteren Swift-Versionen etwas am Arrays-Verhalten geändert? Ich führe nur Ihr Beispiel aus:
Und meine Ergebnisse sind [1, 42, 3] und [1, 2, 3]
quelle