¿Hay alguna razón por la cual la asignación de matriz Swift es inconsistente (ni una referencia ni una copia profunda)?

216

Estoy leyendo la documentación y constantemente estoy sacudiendo la cabeza ante algunas de las decisiones de diseño del lenguaje. Pero lo que realmente me dejó perplejo es cómo se manejan las matrices.

Me apresuré al patio y probé estos. Puedes probarlos también. Entonces el primer ejemplo:

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

Aquí ay bestán los dos [1, 42, 3], que puedo aceptar. Se hace referencia a las matrices: ¡OK!

Ahora vea este ejemplo:

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

ces [1, 2, 3, 42]PERO des [1, 2, 3]. Es decir, dvio el cambio en el último ejemplo pero no lo ve en este. La documentación dice que es porque la longitud cambió.

Ahora, ¿qué tal este:

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

eEs [4, 5, 3]genial. Es bueno tener un reemplazo de índice múltiple, pero fTODAVÍA no ve el cambio a pesar de que la longitud no ha cambiado.

Para resumir, las referencias comunes a una matriz ven cambios si cambia 1 elemento, pero si cambia varios elementos o agrega elementos, se realiza una copia.

Esto me parece un diseño muy pobre. ¿Estoy en lo cierto al pensar esto? ¿Hay alguna razón por la que no veo por qué las matrices deberían actuar así?

EDITAR : Las matrices han cambiado y ahora tienen una semántica de valor. ¡Mucho más cuerdo!

Cthutu
fuente
95
Para el registro, no creo que esta pregunta deba cerrarse. Swift es un nuevo idioma, por lo que habrá preguntas como esta por un tiempo mientras todos aprendemos. Encuentro esta pregunta muy interesante y espero que alguien tenga un caso convincente sobre la defensa.
Joel Berger
44
@Joel Fine, pregúntale a los programadores, Stack Overflow es para problemas de programación específicos no elegidos.
bjb568
21
@ bjb568: Sin embargo, no es una opinión. Esta pregunta debe responder con hechos. Si algún desarrollador de Swift viene y responde "Lo hicimos así para X, Y y Z", entonces eso es un hecho directo. Es posible que no esté de acuerdo con X, Y y Z, pero si se tomó una decisión para X, Y y Z, entonces eso es solo un hecho histórico del diseño del lenguaje. Al igual que cuando pregunté por qué std::shared_ptrno tiene una versión no atómica, hubo una respuesta basada en hechos, no en opiniones (el hecho es que el comité lo consideró pero no lo quiso por varias razones).
Cornstalks
77
@JasonMArcher: solo el último párrafo se basa en la opinión (que sí, tal vez debería eliminarse). El título real de la pregunta (que considero la pregunta real en sí misma) responde con hechos. No es una razón los arreglos fueron diseñados para trabajar su forma de trabajar.
Cornstalks
77
Sí, como dijo API-Beast, esto generalmente se llama "Diseño de lenguaje copiado a la mitad".
R. Martinho Fernandes

Respuestas:

109

Tenga en cuenta que la semántica y la sintaxis de la matriz se cambiaron en la versión Xcode beta 3 ( publicación de blog ), por lo que la pregunta ya no se aplica. La siguiente respuesta se aplica a beta 2:


Es por razones de rendimiento. Básicamente, intentan evitar copiar matrices todo el tiempo que pueden (y reclaman "rendimiento tipo C"). Para citar el libro de idiomas :

Para las matrices, la copia solo se lleva a cabo cuando realiza una acción que tiene el potencial de modificar la longitud de la matriz. Esto incluye agregar, insertar o eliminar elementos, o usar un subíndice a distancia para reemplazar un rango de elementos en la matriz.

Estoy de acuerdo en que esto es un poco confuso, pero al menos hay una descripción clara y simple de cómo funciona.

Esa sección también incluye información sobre cómo asegurarse de que una matriz tenga una referencia única, cómo forzar la copia de matrices y cómo verificar si dos matrices comparten almacenamiento.

Lukas
fuente
61
Encuentro el hecho de que tanto anular el uso compartido como copiar una GRAN bandera roja en el diseño.
Cthutu
99
Esto es correcto. Un ingeniero me describió que para el diseño del lenguaje esto no es deseable, y es algo que esperan "arreglar" en las próximas actualizaciones de Swift. Vota con radares.
Erik Kerber
2
Es algo así como copiar-en-escribir (COW) en la gestión de memoria de procesos secundarios de Linux, ¿verdad? Quizás podamos llamarlo alteración de copia en longitud (COLA). Veo esto como un diseño positivo.
justhalf
3
@justhalf Puedo predecir un montón de novatos confundidos que vienen a SO y preguntar por qué sus matrices fueron / no fueron compartidas (solo de una manera menos clara).
John Dvorak
11
@justhalf: COW es una pesimización en el mundo moderno de todos modos, y en segundo lugar, COW es una técnica de solo implementación, y estas cosas de COLA conducen a compartir y compartir de manera totalmente aleatoria.
Cachorro
25

De la documentación oficial del lenguaje Swift :

Tenga en cuenta que la matriz no se copia cuando establece un nuevo valor con sintaxis de subíndice, porque establecer un valor único con sintaxis de subíndice no tiene el potencial de cambiar la longitud de la matriz. Sin embargo, si agrega un nuevo elemento a la matriz, modifica la longitud de la matriz . Esto hace que Swift cree una nueva copia de la matriz en el punto en que agrega el nuevo valor. De ahora en adelante, a es una copia separada e independiente de la matriz .....

Lea la sección completa Asignación y comportamiento de copia para matrices en esta documentación. Encontrará que cuando reemplaza un rango de elementos en la matriz, la matriz toma una copia de sí misma para todos los elementos.

iPatel
fuente
44
Gracias. Me referí a ese texto vagamente en mi pregunta. Pero mostré un ejemplo donde cambiar un rango de subíndice no cambió la longitud y todavía se copió. Entonces, si no desea una copia, debe cambiarla un elemento a la vez.
Cthutu
21

El comportamiento ha cambiado con Xcode 6 beta 3. Las matrices ya no son tipos de referencia y tienen un mecanismo de copia en escritura , lo que significa que tan pronto como cambie el contenido de una matriz de una u otra variable, la matriz se copiará y solo el Se cambiará una copia.


Vieja respuesta:

Como otros han señalado, Swift intenta evitar copiar matrices si es posible, incluso cuando cambiar los valores de índices individuales a la vez.

Si desea asegurarse de que una variable de matriz (!) Es única, es decir, no se comparte con otra variable, puede llamar al unsharemétodo. Esto copia la matriz a menos que ya solo tenga una referencia. Por supuesto, también puede llamar al copymétodo, que siempre hará una copia, pero se prefiere no compartir para asegurarse de que ninguna otra variable se aferre a la misma matriz.

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]
Pascal
fuente
hmm, para mí, ese unshare()método no está definido.
Hlung
1
@Hlung Se ha eliminado en beta 3, he actualizado mi respuesta.
Pascal
12

El comportamiento es extremadamente similar al Array.Resizemétodo en .NET. Para comprender lo que está sucediendo, puede ser útil mirar el historial del .token en C, C ++, Java, C # y Swift.

En C, una estructura no es más que una agregación de variables. Aplicando el .a una variable de tipo de estructura accederá a una variable almacenada dentro de la estructura. Los punteros a los objetos no contienen agregaciones de variables, sino que las identifican . Si uno tiene un puntero que identifica una estructura, el ->operador puede usarse para acceder a una variable almacenada dentro de la estructura identificada por el puntero.

En C ++, las estructuras y clases no solo agregan variables, sino que también pueden adjuntarles código. El uso .para invocar un método le pedirá a ese método que actúe sobre el contenido de la variable en sí ; El uso ->de una variable que identifica un objeto le pedirá a ese método que actúe sobre el objeto identificado por la variable.

En Java, todos los tipos de variables personalizadas simplemente identifican objetos, e invocar un método sobre una variable le dirá al método qué objeto identifica la variable. Las variables no pueden contener ningún tipo de tipo de datos compuesto directamente, ni hay ningún medio por el cual un método pueda acceder a una variable sobre la que se invoca. Estas restricciones, aunque semánticamente limitantes, simplifican enormemente el tiempo de ejecución y facilitan la validación del código de bytes; tales simplificaciones redujeron la sobrecarga de recursos de Java en un momento en que el mercado era sensible a tales problemas y, por lo tanto, lo ayudaron a ganar tracción en el mercado. También significaban que no había necesidad de un token equivalente al .utilizado en C o C ++. Aunque Java podría haber usado ->de la misma manera que C y C ++, los creadores optaron por usar un solo carácter. ya que no era necesario para ningún otro propósito.

En C # y otros lenguajes .NET, las variables pueden identificar objetos o contener tipos de datos compuestos directamente. Cuando se usa en una variable de un tipo de datos compuesto, .actúa sobre el contenido de la variable; cuando se usa en una variable de tipo de referencia, .actúa sobre el objeto identificadopor esto. Para algunos tipos de operaciones, la distinción semántica no es particularmente importante, pero para otros sí lo es. Las situaciones más problemáticas son aquellas en las que un método de tipo de datos compuesto que modificaría la variable sobre la que se invoca, se invoca en una variable de solo lectura. Si se intenta invocar un método en un valor o variable de solo lectura, los compiladores generalmente copiarán la variable, dejarán que el método actúe sobre eso y descartarán la variable. Esto generalmente es seguro con métodos que solo leen la variable, pero no es seguro con métodos que le escriben. Desafortunadamente, .does aún no tiene ningún medio para indicar qué métodos se pueden usar de manera segura con dicha sustitución y cuáles no.

En Swift, los métodos en agregados pueden indicar expresamente si modificarán la variable sobre la que se invocan, y el compilador prohibirá el uso de métodos de mutación en variables de solo lectura (en lugar de hacer que muten copias temporales de la variable que luego descartarse). Debido a esta distinción, usar el. token para llamar a métodos que modifican las variables sobre las que se invocan es mucho más seguro en Swift que en .NET. Desafortunadamente, el hecho de que el mismo .token se use para ese propósito como para actuar sobre un objeto externo identificado por una variable significa que existe la posibilidad de confusión.

Si tuviera una máquina del tiempo y volviera a la creación de C # y / o Swift, uno podría evitar retroactivamente gran parte de la confusión que rodea a estos problemas haciendo que los lenguajes usen los tokens .y ->de una manera mucho más cercana al uso de C ++. Los métodos tanto de los agregados como de los tipos de referencia podrían usarse .para actuar sobre la variable sobre la que fueron invocados, y ->para actuar sobre una valor (para compuestos) o la cosa identificada por ellos (para tipos de referencia). Sin embargo, ninguno de los dos idiomas está diseñado de esa manera.

En C #, la práctica normal de un método para modificar una variable sobre la que se invoca es pasar la variable como refparámetro a un método. Por lo tanto, llamar Array.Resize(ref someArray, 23);cuando someArrayidentifica una matriz de 20 elementos hará someArrayque se identifique una nueva matriz de 23 elementos, sin afectar la matriz original. El uso de refdeja en claro que se debe esperar que el método modifique la variable sobre la que se invoca. En muchos casos, es ventajoso poder modificar variables sin tener que usar métodos estáticos; Direcciones rápidas que significa mediante el uso de .sintaxis. La desventaja es que pierde claridad sobre qué métodos actúan sobre las variables y qué métodos actúan sobre los valores.

Super gato
fuente
5

Para mí, esto tiene más sentido si primero reemplaza sus constantes con variables:

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

La primera línea nunca necesita cambiar el tamaño de a. En particular, nunca necesita hacer ninguna asignación de memoria. Independientemente del valor de i, esta es una operación ligera. Si te imaginas eso debajo del capóa hay un puntero, puede ser un puntero constante.

La segunda línea puede ser mucho más complicada. Dependiendo de los valores de iy j, es posible que deba administrar la memoria. Si te imaginas esoe es un puntero que apunta al contenido de la matriz, ya no puede suponer que es un puntero constante; Es posible que deba asignar un nuevo bloque de memoria, copiar datos del antiguo bloque de memoria al nuevo bloque de memoria y cambiar el puntero.

Parece que los diseñadores de idiomas han tratado de mantener (1) lo más ligero posible. Como (2) puede implicar copiar de todos modos, han recurrido a la solución de que siempre actúa como si hicieras una copia.

Esto es complicado, pero estoy feliz de que no lo hayan complicado aún más, por ejemplo, con casos especiales como "si en (2) i y j son constantes de tiempo de compilación y el compilador puede inferir que el tamaño de e no va para cambiar, entonces no copiamos " .


Finalmente, según mi comprensión de los principios de diseño del lenguaje Swift, creo que las reglas generales son estas:

  • Utilice constantes ( let) siempre en todas partes de forma predeterminada, y no habrá sorpresas importantes.
  • Usar variables (var ) solo si es absolutamente necesario, y tenga cuidado en esos casos, ya que habrá sorpresas [aquí: copias implícitas extrañas de matrices en algunas situaciones, pero no en todas].
Jukka Suomela
fuente
5

Lo que he encontrado es: La matriz será una copia mutable de la referenciada si y solo si la operación tiene el potencial de cambiar la longitud de la matriz . En su último ejemplo, f[0..2]indexando con muchos, la operación tiene el potencial de cambiar su longitud (puede ser que no se permitan duplicados), por lo que se está copiando.

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
fuente
8
"tratado como la longitud ha cambiado" Puedo entender que se copiaría si la longitud se cambia, pero en combinación con la cita anterior, creo que esta es una "característica" realmente preocupante y que creo que muchas personas se equivocarán
Joel Berger
25
El hecho de que un idioma sea nuevo no significa que esté bien que contenga contradicciones internas evidentes.
Carreras de ligereza en órbita
Esto se ha "solucionado" en beta 3, las varmatrices ahora son completamente mutables y las letmatrices son completamente inmutables.
Pascal
4

Las cadenas y matrices de Delphi tenían exactamente la misma "característica". Cuando miraste la implementación, tenía sentido.

Cada variable es un puntero a la memoria dinámica. Esa memoria contiene un recuento de referencia seguido de los datos en la matriz. Por lo tanto, puede cambiar fácilmente un valor en la matriz sin copiar toda la matriz o cambiar los punteros. Si desea cambiar el tamaño de la matriz, debe asignar más memoria. En ese caso, la variable actual apuntará a la memoria recién asignada. Pero no puede rastrear fácilmente todas las otras variables que apuntaban a la matriz original, por lo que las deja en paz.

Por supuesto, no sería difícil hacer una implementación más consistente. Si desea que todas las variables vean un cambio de tamaño, haga lo siguiente: cada variable es un puntero a un contenedor almacenado en la memoria dinámica. El contenedor contiene exactamente dos cosas, un recuento de referencia y un puntero a los datos de la matriz real. Los datos de la matriz se almacenan en un bloque separado de memoria dinámica. Ahora solo hay un puntero a los datos de la matriz, por lo que puede cambiar el tamaño fácilmente y todas las variables verán el cambio.

Ideas comerciales Philip
fuente
4

Muchos de los primeros usuarios de Swift se han quejado de esta semántica de matriz propensa a errores y Chris Lattner ha escrito que la semántica de matriz ha sido revisada para proporcionar una semántica de valor total ( enlace de desarrollador de Apple para aquellos que tienen una cuenta ). Tendremos que esperar al menos la próxima versión beta para ver qué significa esto exactamente.

gaélico
fuente
1
El nuevo comportamiento de Array ahora está disponible a partir del SDK incluido con iOS 8 / Xcode 6 Beta 3.
smileyborg
0

Yo uso .copy () para esto.

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 
Preetham
fuente
1
Obtengo el "Valor del tipo '[Int]' no tiene miembro 'copia'" cuando ejecuto su código
jreft56
0

¿Algo cambió en el comportamiento de las matrices en versiones posteriores de Swift? Acabo de ejecutar su ejemplo:

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

Y mis resultados son [1, 42, 3] y [1, 2, 3]

jreft56
fuente