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í a
y b
está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
c
es [1, 2, 3, 42]
PERO d
es [1, 2, 3]
. Es decir, d
vio 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
e
Es [4, 5, 3]
genial. Es bueno tener un reemplazo de índice múltiple, pero f
TODAVÍ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!
std::shared_ptr
no 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).Respuestas:
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 :
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.
fuente
De la documentación oficial del lenguaje Swift :
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.
fuente
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
unshare
método. Esto copia la matriz a menos que ya solo tenga una referencia. Por supuesto, también puede llamar alcopy
método, que siempre hará una copia, pero se prefiere no compartir para asegurarse de que ninguna otra variable se aferre a la misma matriz.fuente
unshare()
método no está definido.El comportamiento es extremadamente similar al
Array.Resize
mé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
ref
parámetro a un método. Por lo tanto, llamarArray.Resize(ref someArray, 23);
cuandosomeArray
identifica una matriz de 20 elementos harásomeArray
que se identifique una nueva matriz de 23 elementos, sin afectar la matriz original. El uso deref
deja 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.fuente
Para mí, esto tiene más sentido si primero reemplaza sus constantes con variables:
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 dei
, 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
i
yj
, 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:
let
) siempre en todas partes de forma predeterminada, y no habrá sorpresas importantes.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].fuente
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.fuente
var
matrices ahora son completamente mutables y laslet
matrices son completamente inmutables.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.
fuente
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.
fuente
Yo uso .copy () para esto.
fuente
¿Algo cambió en el comportamiento de las matrices en versiones posteriores de Swift? Acabo de ejecutar su ejemplo:
Y mis resultados son [1, 42, 3] y [1, 2, 3]
fuente