C'è una ragione per cui l'assegnazione dell'array Swift è incoerente (né un riferimento né una copia approfondita)?


216

Sto leggendo la documentazione e scuoto costantemente la testa per alcune delle decisioni progettuali del linguaggio. Ma la cosa che mi ha davvero lasciato perplesso è come vengono gestite le matrici.

Mi sono precipitato nel parco giochi e ho provato questi. Puoi provarli anche tu. Quindi il primo esempio:

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

Qui ae bsono entrambi [1, 42, 3], che posso accettare. Le matrici sono referenziate - OK!

Ora vedi questo esempio:

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

cè [1, 2, 3, 42]MA dè [1, 2, 3]. Cioè, dho visto il cambiamento nell'ultimo esempio ma non lo vede in questo. La documentazione dice che è perché la lunghezza è cambiata.

Ora, che ne dici di questo:

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

eè [4, 5, 3], che è bello. È bello avere una sostituzione multi-indice, ma fSTILL non vede il cambiamento anche se la lunghezza non è cambiata.

Quindi, per riassumere, i riferimenti comuni a un array vedono le modifiche se si modifica 1 elemento, ma se si modificano più elementi o si aggiungono elementi, viene creata una copia.

Questo mi sembra un design molto scarso. Ho ragione nel pensare questo? C'è un motivo per cui non vedo perché gli array debbano agire in questo modo?

EDIT : le matrici sono cambiate e ora hanno una semantica di valore. Molto più sano!


95
Per la cronaca, non penso che questa domanda dovrebbe essere chiusa. Swift è una nuova lingua, quindi ci saranno domande come questa per un po 'mentre impariamo tutti. Trovo questa domanda molto interessante e spero che qualcuno abbia un caso convincente sulla difesa.
Joel Berger,

4
@Joel Bene, chiedilo ai programmatori, StackTranslate.it è per problemi di programmazione specifici non inventati.
bjb568,

21
@ bjb568: non è opinione, però. Questa domanda dovrebbe essere rispondente ai fatti. Se qualche sviluppatore di Swift arriva e risponde "Lo abbiamo fatto in quel modo per X, Y e Z", allora è un dato di fatto. Potresti non essere d'accordo con X, Y e Z, ma se è stata presa una decisione per X, Y e Z, questo è solo un fatto storico del design del linguaggio. Proprio come quando ho chiesto perché std::shared_ptrnon avesse una versione non atomica, c'era una risposta basata sui fatti, non sull'opinione (il fatto è che il comitato l'ha presa in considerazione ma non l'ha voluta per vari motivi).
Cornstalks,

7
@JasonMArcher: solo l'ultimo paragrafo è basato sull'opinione (che sì, forse dovrebbe essere rimosso). Il titolo effettivo della domanda (che prendo come domanda vera e propria) è rispondente ai fatti. V'è una ragione per le matrici sono stati progettati per lavorare il loro modo di lavorare.
Cornstalks,

7
Sì, come diceva API-Beast, questo di solito si chiama "Copia-su-metà-Assed-Language-Design".
R. Martinho Fernandes,

Risposte:


109

Si noti che la semantica e la sintassi dell'array sono state modificate nella versione Xcode beta 3 ( post di blog ), quindi la domanda non si applica più. La seguente risposta si applica alla beta 2:


È per motivi di prestazioni. Fondamentalmente, cercano di evitare di copiare array il più a lungo possibile (e rivendicano "prestazioni simil-C"). Per citare il libro di lingua :

Per gli array, la copia ha luogo solo quando si esegue un'azione che ha il potenziale per modificare la lunghezza dell'array. Ciò include l'aggiunta, l'inserimento o la rimozione di elementi o l'utilizzo di un indice a distanza per sostituire un intervallo di elementi nell'array.

Sono d'accordo che questo sia un po 'confuso, ma almeno c'è una descrizione chiara e semplice di come funziona.

Questa sezione include anche informazioni su come assicurarsi che un array abbia un riferimento univoco, come forzare la copia degli array e come verificare se due array condividono l'archiviazione.


61
Trovo il fatto che tu abbia sia annullare la condivisione sia copiare una GRANDE bandiera rossa nel design.
Cthutu,

9
Questo è corretto. Un ingegnere mi ha descritto che per la progettazione del linguaggio questo non è desiderabile ed è qualcosa che sperano di "risolvere" nei prossimi aggiornamenti di Swift. Vota con i radar.
Erik Kerber,

2
È solo qualcosa come il copy-on-write (COW) nella gestione della memoria del processo figlio Linux, giusto? Forse possiamo chiamarlo copia-su-lunghezza-alterazione (COLA). Vedo questo come un design positivo.
solo l'

3
@justhalf Posso prevedere un gruppo di neofiti confusi che vengono a SO e chiedono perché i loro array sono stati / non sono stati condivisi (solo in modo meno chiaro).
John Dvorak,

11
@justhalf: COW è comunque una pessimizzazione nel mondo moderno, e in secondo luogo, COW è una tecnica di sola implementazione, e questa roba della COLA porta alla condivisione e alla condivisione totalmente casuali.
Puppy,

25

Dalla documentazione ufficiale della lingua Swift :

Si noti che l'array non viene copiato quando si imposta un nuovo valore con la sintassi del pedice, poiché l'impostazione di un singolo valore con la sintassi del pedice non ha il potenziale per modificare la lunghezza della matrice. Tuttavia, se si aggiunge un nuovo elemento all'array, si modifica la lunghezza dell'array . Ciò richiede a Swift di creare una nuova copia dell'array nel punto in cui si aggiunge il nuovo valore. D'ora in poi, a è una copia separata e indipendente dell'array .....

Leggi l'intera sezione Assegnazione e copia del comportamento degli array in questa documentazione. Scoprirai che quando sostituisci un intervallo di elementi nell'array, l'array ne prende una copia per tutti gli elementi.


4
Grazie. Ho fatto riferimento a quel testo vagamente nella mia domanda. Ma ho mostrato un esempio in cui la modifica di un intervallo di sottoscrizioni non ha modificato la lunghezza e ha comunque copiato. Quindi, se non vuoi una copia, devi cambiarla un elemento alla volta.
Cthutu,

21

Il comportamento è cambiato con Xcode 6 beta 3. Gli array non sono più tipi di riferimento e hanno un meccanismo di copia su scrittura , il che significa che non appena si cambia il contenuto di un array da una o dall'altra variabile, l'array verrà copiato e solo il una copia verrà cambiata.


Vecchia risposta:

Come altri hanno sottolineato, Swift cerca di evitare di copiare array se possibile, anche quando si modificano i valori per singoli indici alla volta.

Se vuoi essere sicuro che una variabile di matrice (!) Sia unica, cioè non condivisa con un'altra variabile, puoi chiamare il unsharemetodo. Questo copia l'array a meno che non abbia già un solo riferimento. Ovviamente puoi anche chiamare il copymetodo, che farà sempre una copia, ma si preferisce annullare la condivisione per assicurarsi che nessun'altra variabile rimanga sullo stesso array.

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

hmm, per me, quel unshare()metodo non è definito.
Hlung,

1
@Hlung È stato rimosso in beta 3, ho aggiornato la mia risposta.
Pascal,

12

Il comportamento è estremamente simile al Array.Resizemetodo in .NET. Per capire cosa sta succedendo, può essere utile esaminare la cronologia del .token in C, C ++, Java, C # e Swift.

In C, una struttura non è altro che un'aggregazione di variabili. Applicando .a una variabile di tipo struttura si accederà a una variabile memorizzata all'interno della struttura. I puntatori agli oggetti non contengono aggregazioni di variabili, ma le identificano . Se uno ha un puntatore che identifica una struttura, l' ->operatore può essere utilizzato per accedere a una variabile memorizzata all'interno della struttura identificata dal puntatore.

In C ++, le strutture e le classi non solo aggregano le variabili, ma possono anche associare il codice ad esse. L'uso .di invocare un metodo richiederà a una variabile quel metodo di agire sul contenuto della variabile stessa ; l'utilizzo ->su una variabile che identifica un oggetto chiederà a quel metodo di agire sull'oggetto identificato dalla variabile.

In Java, tutti i tipi di variabili personalizzati identificano semplicemente gli oggetti e invocando un metodo su una variabile dirà al metodo quale oggetto è identificato dalla variabile. Le variabili non possono contenere direttamente alcun tipo di tipo di dati composito, né esiste alcun modo per cui un metodo può accedere a una variabile su cui viene invocato. Queste restrizioni, anche se semanticamente limitanti, semplificano notevolmente l'autonomia e facilitano la convalida del bytecode; tali semplificazioni hanno ridotto il sovraccarico di risorse di Java in un momento in cui il mercato era sensibile a tali problemi, e quindi l'hanno aiutato a guadagnare trazione sul mercato. Significavano anche che non era necessario un token equivalente a quello .utilizzato in C o C ++. Sebbene Java avrebbe potuto usare ->allo stesso modo di C e C ++, i creatori hanno optato per l'uso di un carattere singolo. poiché non era necessario per nessun altro scopo.

In C # e altri linguaggi .NET, le variabili possono identificare oggetti o contenere direttamente tipi di dati compositi. Se utilizzato su una variabile di un tipo di dati composito, .agisce sul contenuto della variabile; se utilizzato su una variabile di tipo di riferimento, .agisce sull'oggetto identificatoda esso. Per alcuni tipi di operazioni, la distinzione semantica non è particolarmente importante, ma per altri lo è. Le situazioni più problematiche sono quelle in cui il metodo di un tipo di dati composito che modifica la variabile su cui viene invocato viene invocato su una variabile di sola lettura. Se si tenta di invocare un metodo su un valore o una variabile di sola lettura, i compilatori generalmente copiano la variabile, lasciano agire il metodo su di essa e scartano la variabile. Questo è generalmente sicuro con i metodi che leggono solo la variabile, ma non con i metodi che vi scrivono. Sfortunatamente, .does non ha ancora alcun mezzo per indicare quali metodi possono essere tranquillamente utilizzati con tale sostituzione e quali no.

In Swift, i metodi sugli aggregati possono indicare espressamente se modificheranno la variabile su cui vengono invocati e il compilatore vieterà l'uso di metodi mutanti su variabili di sola lettura (piuttosto che farli mutare copie temporanee della variabile che poi scartato). A causa di questa distinzione, l'utilizzo del .token per chiamare metodi che modificano le variabili su cui vengono invocate è molto più sicuro in Swift che in .NET. Sfortunatamente, il fatto che lo stesso .token venga utilizzato a tale scopo per agire su un oggetto esterno identificato da una variabile significa che rimane la possibilità di confusione.

Se avesse una macchina del tempo e tornasse alla creazione di C # e / o Swift, si potrebbe evitare retroattivamente gran parte della confusione che circonda tali problemi facendo in modo che le lingue utilizzino i token .e ->in un modo molto più vicino all'utilizzo del C ++. I metodi di entrambi gli aggregati e i tipi di riferimento potrebbero utilizzare .per agire sulla variabile su cui sono stati invocati e ->per agire su un valore (per i composti) o sulla cosa identificata in tal modo (per i tipi di riferimento). Nessuna delle due lingue è progettata in questo modo, tuttavia.

In C #, la pratica normale per un metodo di modificare una variabile su cui viene invocata è passare la variabile come refparametro a un metodo. Pertanto, chiamare Array.Resize(ref someArray, 23);quando someArrayidentifica un array di 20 elementi farà someArrayidentificare un nuovo array di 23 elementi, senza influire sull'array originale. L'uso di refchiarisce che il metodo dovrebbe prevedere la modifica della variabile su cui è invocato. In molti casi, è vantaggioso poter modificare le variabili senza utilizzare metodi statici; Indirizzi rapidi che significa utilizzando la .sintassi. Lo svantaggio è che perde il chiarimento su quali metodi agiscono sulle variabili e quali metodi agiscono sui valori.


5

Per me questo ha più senso se prima sostituisci le tue costanti con variabili:

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

La prima riga non deve mai cambiare la dimensione di a. In particolare, non è mai necessario eseguire alcuna allocazione di memoria. Indipendentemente dal valore di i, questa è un'operazione leggera. Se immagini che sotto il cofano ci asia un puntatore, può essere un puntatore costante.

La seconda riga potrebbe essere molto più complicata. A seconda dei valori di ie j, potrebbe essere necessario eseguire la gestione della memoria. Se immagini che esia un puntatore che punta al contenuto dell'array, non puoi più supporre che sia un puntatore costante; potrebbe essere necessario allocare un nuovo blocco di memoria, copiare i dati dal vecchio blocco di memoria al nuovo blocco di memoria e modificare il puntatore.

Sembra che i progettisti del linguaggio abbiano cercato di mantenere (1) il più leggero possibile. Poiché (2) può comportare comunque la copia, hanno fatto ricorso alla soluzione che agisce sempre come se si eseguisse una copia.

Questo è complicato, ma sono felice che non lo abbiano reso ancora più complicato, ad esempio in casi speciali come "if in (2) i e j sono costanti di tempo di compilazione e il compilatore può dedurre che la dimensione di e non sta andando per cambiare, quindi non copiamo " .


Infine, in base alla mia comprensione dei principi di progettazione del linguaggio Swift, penso che le regole generali siano queste:

  • Usa costanti (let ) sempre ovunque di default, e non ci saranno grandi sorprese.
  • Usa le variabili ( var) solo se è assolutamente necessario, e fai molta attenzione in quei casi, poiché ci saranno sorprese [qui: strane copie implicite di array in alcune ma non in tutte le situazioni].

5

Quello che ho trovato è: l'array sarà una copia mutabile di quella referenziata se e solo se l'operazione ha il potenziale per cambiare la lunghezza dell'array . Nel tuo ultimo esempio, f[0..2]indicizzando con molti, l'operazione ha il potenziale per cambiarne la lunghezza (potrebbe essere che i duplicati non siano consentiti), quindi viene copiato.

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
"trattato come la lunghezza è cambiata" Posso capire che verrebbe copiato se la lunghezza fosse cambiata, ma in combinazione con la citazione sopra, penso che questa sia una "caratteristica" davvero preoccupante e che penso che molte persone sbaglieranno
Joel Berger,

25
Solo perché una lingua è nuova, non significa che contenga contraddizioni interne palese.
Corse di leggerezza in orbita,

Questo è stato "risolto" in beta 3, gli vararray sono ora completamente mutabili e gli letarray sono completamente immutabili.
Pascal,

4

Le stringhe e le matrici di Delphi avevano esattamente la stessa "caratteristica". Quando hai visto l'implementazione, ha avuto senso.

Ogni variabile è un puntatore alla memoria dinamica. Tale memoria contiene un conteggio dei riferimenti seguito dai dati nell'array. Quindi puoi facilmente cambiare un valore nell'array senza copiare l'intero array o cambiare alcun puntatore. Se si desidera ridimensionare l'array, è necessario allocare più memoria. In tal caso, la variabile corrente punterà alla memoria appena allocata. Ma non puoi facilmente rintracciare tutte le altre variabili che puntavano alla matrice originale, quindi le lasci sole.

Naturalmente, non sarebbe difficile realizzare un'implementazione più coerente. Se si desidera che tutte le variabili visualizzino un ridimensionamento, procedere come segue: ogni variabile è un puntatore a un contenitore archiviato nella memoria dinamica. Il contenitore contiene esattamente due elementi, un conteggio di riferimento e un puntatore ai dati effettivi dell'array. I dati dell'array sono archiviati in un blocco separato di memoria dinamica. Ora c'è solo un puntatore ai dati dell'array, quindi puoi ridimensionarlo facilmente e tutte le variabili vedranno il cambiamento.


4

Molti dei primi utenti di Swift si sono lamentati di questa semantica di array soggetta a errori e Chris Lattner ha scritto che la semantica di array era stata rivista per fornire semantica a valore pieno ( link Apple Developer per coloro che hanno un account ). Dovremo aspettare almeno per la prossima beta per vedere cosa significa esattamente.


1
Il nuovo comportamento dell'array è ora disponibile a partire
dall'SDK

0

Per questo uso .copy ().

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

1
Ottengo "Il valore del tipo '[Int]' non ha alcun membro 'copia'" quando
eseguo

0

Qualcosa è cambiato nel comportamento degli array nelle versioni successive di Swift? Ho appena eseguito il tuo esempio:

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

E i miei risultati sono [1, 42, 3] e [1, 2, 3]

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.