arrays - for - ¿Hay alguna razón por la que la asignación de matriz Swift sea inconsistente(ni una referencia ni una copia profunda)?
string array in swift 3 (9)
Estoy leyendo la documentación y constantemente estoy sacudiendo la cabeza en algunas de las decisiones de diseño del lenguaje. Pero lo que realmente me dejó perplejo es cómo se manejan los arreglos.
Corrí al patio de recreo y probé esto. Puedes probarlos también. Así que el primer ejemplo:
var a = [1, 2, 3]
var b = a
a[1] = 42
a
b
Aquí b
son ambos [1, 42, 3]
, que puedo aceptar. Las matrices son referenciadas - ¡OK!
Ahora vea este ejemplo:
var c = [1, 2, 3]
var d = c
c.append(42)
c
d
c
es [1, 2, 3, 42]
PERO d
es [1, 2, 3]
. Es decir, vi el cambio en el último ejemplo, pero no lo veo 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
e
es [4, 5, 3]
, que es genial. Es bueno tener un reemplazo de índice múltiple, pero STILL no ve el cambio a pesar de que la longitud no ha cambiado.
Entonces, para resumir, las referencias comunes a una matriz ven cambios si cambia 1 elemento, pero si cambia múltiples elementos o agrega elementos, se hace una copia.
Esto me parece un diseño muy pobre. ¿Tengo razón al pensar esto? ¿Hay alguna razón por la que no veo por qué los arreglos deberían actuar de esta manera?
EDITAR : Las matrices han cambiado y ahora tienen valor semántico. ¡Mucho más sano!
El comportamiento es extremadamente similar al método Array.Resize
en .NET. Para entender lo que está pasando, puede ser útil mirar la historia de .
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 accederemos a una variable almacenada dentro de la estructura. Los punteros a 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 las clases no solo agregan variables, sino que también pueden adjuntar código a ellas. Utilizando .
para invocar un método, una variable le pedirá a ese método que actúe sobre el contenido de la variable en sí ; utilizando ->
en una variable que identifique 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 está identificado por la variable. Las variables no pueden contener ningún tipo de tipo de datos compuestos directamente, ni hay ningún medio por el cual un método pueda acceder a una variable sobre la cual se invoca. Estas restricciones, aunque limitadas semánticamente, simplifican enormemente el tiempo de ejecución y facilitan la validación de bytecode; 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 ayudó a ganar fuerza en el mercado. También significaron que no había necesidad de una ficha equivalente a la .
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 mantener tipos de datos compuestos directamente. Cuando se utiliza en una variable de un tipo de datos compuesto,. actúa sobre los contenidos de la variable; cuando se utiliza en una variable de tipo de referencia,. Actúa sobre el objeto identificado por él. Para algunos tipos de operaciones, la distinción semántica no es particularmente importante, pero para otros lo es. Las situaciones más problemáticas son aquellas en las que se invoca un método de tipo de datos compuesto que modificaría la variable sobre la cual 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 los métodos que solo leen la variable, pero no con los métodos que escriben en ella. Desafortunadamente, .does todavía 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 cual 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 ser descartado). Debido a esta distinción, utilizando el .
El token para llamar a los métodos que modifican las variables sobre las que se invocan es mucho más seguro en Swift que en .NET. Lamentablemente, el hecho de que lo mismo .
el token se usa para ese propósito para actuar sobre un objeto externo identificado por una variable significa que la posibilidad de confusión permanece.
Si tuviera una máquina del tiempo y volviera a la creación de C # y / o Swift, se podría evitar retroactivamente gran parte de la confusión que rodea a tales problemas haciendo que los idiomas usen el .
y ->
tokens de una manera mucho más cercana al uso de C ++. Se podrían utilizar métodos tanto de agregados como de tipos de referencia .
para actuar sobre la variable sobre la cual fueron invocados, y ->
actuar sobre un valor (para compuestos) o la cosa identificada por ello (para tipos de referencia). Sin embargo, ninguno de los idiomas está diseñado de esa manera.
En C #, la práctica normal de un método para modificar una variable sobre la cual se invoca es pasar la variable como un parámetro ref
a un método. Llamando así a Array.Resize(ref someArray, 23);
cuando someArray
identifica una matriz de 20 elementos hará que someArray
identifique una nueva matriz de 23 elementos, sin afectar la matriz original. El uso de ref
deja claro que se debe esperar que el método modifique la variable sobre la que se invoca. En muchos casos, es ventajoso poder modificar las variables sin tener que usar métodos estáticos; Direcciones rápidas que significa mediante el uso .
sintaxis. La desventaja es que pierde, aclara qué métodos actúan sobre las variables y qué métodos actúan sobre los valores.
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 la Se cambiará una copia.
Respuesta antigua:
Como han señalado otros, Swift intenta evitar copiar arreglos si es posible, incluso cuando se cambian valores para índices individuales a la vez.
Si desea estar seguro de que una variable de matriz (!) Es única, es decir, no compartida con otra variable, puede llamar al método de no unshare
. Esto copia la matriz a menos que ya tenga solo una referencia. Por supuesto, también puede llamar al método de copy
, que siempre hará una copia, pero se prefiere no compartir para asegurarse de que ninguna otra variable se mantenga en la misma matriz.
var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a // [1, 42, 3]
b // [1, 2, 3]
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 memoria dinámica. Esa memoria contiene un recuento de referencia seguido por los datos en la matriz. Por lo tanto, puede cambiar fácilmente un valor en la matriz sin copiar la matriz completa ni cambiar ningún puntero. Si desea cambiar el tamaño de la matriz, tiene que 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 apuntan a la matriz original, por lo que las deja solas.
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 esto: 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.
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 copia.
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
Muchos de los primeros en adoptar Swift se han quejado de esta semántica de matriz propensa a errores y Chris Lattner ha escrito que la semántica de matriz se ha revisado para proporcionar una semántica de valor completo ( 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.
Para mí esto tiene más sentido si primero reemplazas tus 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 tiene que hacer ninguna asignación de memoria. Independientemente del valor de i
, esta es una operación liviana. Si imagina que debajo del capó hay un puntero, puede ser un puntero constante.
La segunda línea puede ser mucho más complicada. Dependiendo de los valores de i
y j
, es posible que deba realizar la administración de la memoria. Si imagina que e
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 bloque de memoria antiguo 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 hiciera una copia.
Esto es complicado, pero estoy contento de que no lo hicieron aún más complicado con, por ejemplo, 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, basado en mi comprensión de los principios de diseño del lenguaje Swift, creo que las reglas generales son las siguientes:
- Use constantes (
let
) siempre en todas partes por defecto, y no habrá ninguna sorpresa importante. - Use las variables (
var
) solo si es absolutamente necesario, y tenga cuidado al variar en esos casos, ya que habrá sorpresas [aquí: extrañas copias implícitas de arreglos en algunas situaciones, pero no en todas]].
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 la beta 2:
Es por razones de rendimiento. Básicamente, intentan evitar copiar arreglos todo el tiempo que puedan (y reclaman "rendimiento tipo C"). Para citar el book idiomas:
Para las matrices, la copia solo tiene lugar cuando se 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 de rango 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 las matrices y cómo verificar si dos matrices comparten el almacenamiento.
Yo uso .copy () para esto.
var a = [1, 2, 3]
var b = a.copy()
a[1] = 42
De la documentación oficial del idioma swift :
Tenga en cuenta que la matriz no se copia cuando establece un nuevo valor con la sintaxis del subíndice, ya que la configuración de un solo valor con la sintaxis del 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 le indica a Swift que cree una nueva copia de la matriz en el punto en que agregue el nuevo valor. De aquí 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 reemplace un rango de elementos en la matriz, la matriz tomará una copia de sí misma para todos los elementos.