Estructura rápida y mutante
value-type swift (5)
Hay algo que no entiendo completamente cuando se trata de cambiar los tipos de valores en Swift.
Como dice el iBook "The Swift Programming Language": de forma predeterminada, las propiedades de un tipo de valor no se pueden modificar desde sus métodos de instancia.
Y para hacer esto posible, podemos declarar métodos con la palabra clave mutating
dentro de estructuras y enumeraciones.
Lo que no está del todo claro para mí es esto: puedes cambiar una var desde fuera de una estructura, pero no puedes cambiarla desde sus propios métodos. Esto me parece contrario a la intuición, ya que en los lenguajes orientados a objetos, generalmente intentas encapsular variables para que solo puedan cambiarse desde dentro. Con las estructuras esto parece ser al revés. Para elaborar, aquí hay un fragmento de código:
struct Point {
var x = 0, y = 0
mutating func moveToX(x: Int, andY y:Int) { //Needs to be a mutating method in order to work
self.x = x
self.y = y
}
}
var p = Point(x: 1, y: 2)
p.x = 3 //Works from outside the struct!
p.moveToX(5, andY: 5)
¿Alguien sabe la razón por la cual las estructuras no pueden cambiar su contenido desde dentro de su propio contexto, mientras que los contenidos pueden cambiarse fácilmente en otro lugar?
Considera una analogía con C ++. Un método de estructura en Swift mutating
/ no mutating
es análogo a un método en C ++ que no es const
/ const
. Un método marcado const
en C ++ de manera similar no puede mutar la estructura.
Puede cambiar una var desde fuera de una estructura, pero no puede cambiarla de sus propios métodos.
En C ++, también puede "cambiar una var desde fuera de la estructura", pero solo si tiene una variable struct no const
. Si tiene una variable const
struct, no puede asignar una var, y tampoco puede llamar a un método non- const
. De manera similar, en Swift, puede cambiar una propiedad de una estructura solo si la variable struct no es una constante. Si tiene una constante de estructura, no puede asignar a una propiedad, y tampoco puede llamar a un método de mutating
.
El atributo de mutabilidad está marcado en un almacenamiento (constante o variable), no en el tipo. Puedes pensar que struct tiene dos modos: mutable e inmutable . Si asigna un valor struct a un almacenamiento inmutable (lo llamamos let
o constante en Swift) el valor se convierte en modo inmutable, y no puede cambiar ningún estado en el valor. (incluyendo llamar a cualquier método de mutación)
Si el valor se asigna a un almacenamiento mutable (lo llamamos var
o variable en Swift), puede modificar el estado de ellos, y se permite la invocación del método de mutación.
Además, las clases no tienen este modo inmutable / mutable. OMI, esto se debe a que las clases se usan generalmente para representar una entidad de referencia . Y la entidad de referencia es generalmente mutable porque es muy difícil hacer y administrar gráficos de referencia de entidades de manera inmutable con un rendimiento adecuado. Pueden agregar esta función más adelante, pero al menos no ahora.
Para los programadores de Objective-C, los conceptos mutables / inmutables son muy familiares. En Objective-C teníamos dos clases separadas para cada concepto, pero en Swift, puedes hacer esto con una estructura. Medio trabajo.
Para los programadores C / C ++, este concepto también es muy familiar. Esto es exactamente lo que hace la palabra clave const
en C / C ++.
Además, el valor inmutable se puede optimizar muy bien. En teoría, el compilador Swift (o LLVM) puede realizar una elusión de copia en valores pasados por let
, igual que C ++. Si usa la estructura inmutable sabiamente, superará a las clases refominadas.
Actualizar
Como @Joseph afirmó que esto no proporciona por qué , agrego un poco más.
Las estructuras tienen dos tipos de métodos. métodos simples y mutantes . El método simple implica inmutable (o no mutante) . Esta separación existe solo para soportar la semántica inmutable . Un objeto en modo inmutable no debe cambiar su estado en absoluto.
Entonces, los métodos inmutables deben garantizar esta inmutabilidad semántica . Lo que significa que no debe cambiar ningún valor interno. Entonces el compilador no permite ningún cambio de estado en un método inmutable. Por el contrario, los métodos de mutación son libres de modificar estados.
Y luego, puede tener una pregunta de por qué inmutable es el valor predeterminado? Eso es porque es muy difícil predecir el estado futuro de la mutación del valor, y eso generalmente se convierte en la principal fuente de dolores de cabeza e insectos. Muchas personas estuvieron de acuerdo en que la solución es evitar productos mutables, y luego inmutables por defecto estuvo en la lista de deseos durante décadas en los lenguajes de la familia C / C ++ y sus derivaciones.
Vea el estilo puramente funcional para más detalles. De todos modos, todavía necesitamos productos mutables porque los productos inmutables tienen algunas debilidades, y discutir sobre esto parece estar fuera de tema.
Espero que esto ayude.
Las estructuras Swift se pueden instanciar como constantes (vía let
) o variables (a través de var
)
Considere la estructura Array
de Swift (sí, es una estructura).
var petNames: [String] = ["Ruff", "Garfield", "Nemo"]
petNames.append("Harvey") // ["Ruff", "Garfield", "Nemo", "Harvey"]
let planetNames: [String] = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
planetNames.append("Pluto") //Error, sorry Pluto. No can do
¿Por qué el apéndice no funcionaba con los nombres de los planetas? Porque append está marcado con la palabra clave mutating
. Y como planetNames
se declaró usando let
, todos los métodos así marcados están fuera de los límites.
En su ejemplo, el compilador puede decir que está modificando la estructura asignando una o más de sus propiedades fuera de un init
. Si cambia el código un poco, verá que x
e y
no siempre son accesibles fuera de la estructura. Observe el let
en la primera línea.
let p = Point(x: 1, y: 2)
p.x = 3 //error
p.moveToX(5, andY: 5) //error
Una estructura es una agregación de campos; si una instancia de estructura particular es mutable, sus campos serán mutables; si una instancia es inmutable, sus campos serán inmutables. Por lo tanto, se debe preparar un tipo de estructura para la posibilidad de que los campos de cualquier instancia particular puedan ser mutables o inmutables.
Para que un método de estructura mute los campos de la estructura subyacente, esos campos deben ser mutables. Si se invoca un método que muta los campos de la estructura subyacente sobre una estructura inmutable, intentaría mutar los campos inmutables. Como nada bueno podría salir de eso, tal invocación debe ser prohibida.
Para lograr eso, Swift divide los métodos de estructura en dos categorías: aquellos que modifican la estructura subyacente y, por lo tanto, solo pueden invocarse en instancias de estructura mutable y aquellos que no modifican la estructura subyacente y deben ser invocables en instancias cambiables e inmutables. . El último uso es probablemente el más frecuente y, por lo tanto, es el predeterminado.
En comparación, .NET actualmente (¡todavía!) No ofrece medios para distinguir los métodos de estructura que modifican la estructura de los que no lo hacen. En cambio, al invocar un método de estructura en una instancia de estructura inmutable, el compilador realizará una copia mutable de la instancia de estructura, dejará que el método haga lo que quiera con ella y descartará la copia cuando se complete el método. Esto tiene el efecto de obligar al compilador a perder el tiempo copiando la estructura, ya sea que el método la modifique o no, aunque agregar la operación de copia casi nunca convertirá el código que sería semánticamente incorrecto en código semánticamente correcto; simplemente causará que el código que es semánticamente incorrecto de una manera (modificando un valor "inmutable") se equivoque de una manera diferente (permitiendo que el código crea que está modificando una estructura, pero descartando los intentos de cambios). Permitir que los métodos struct indiquen si modificarán la estructura subyacente puede eliminar la necesidad de una operación de copia inútil, y también asegura que el intento de uso erróneo será marcado.
Precaución: términos del profano a continuación.
Esta explicación no es rigurosamente correcta en el nivel del código más esencial. Sin embargo, ha sido revisado por un tipo que realmente trabaja en Swift y dijo que es lo suficientemente bueno como una explicación básica.
Por lo tanto, quiero intentar responder de manera simple y directa a la pregunta de "por qué".
Para ser precisos: ¿por qué tenemos que marcar las funciones de estructura como mutating
cuando podemos cambiar los parámetros de estructura sin ninguna modificación de palabras clave?
Entonces, en conjunto, tiene mucho que ver con la filosofía que mantiene Swift rápido.
Podría pensar que es como el problema de administrar direcciones físicas reales. Cuando cambie su dirección, si hay muchas personas que tienen su actual, debe notificarles a todos que se mudó. Pero si nadie tiene su dirección actual, puede simplemente mudarse a donde quiera y nadie necesita saber.
En esta situación, Swift es algo así como la oficina de correos. Si muchas personas con muchos contactos se mueven mucho, tiene una sobrecarga muy alta. Tiene que pagar una gran cantidad de personal para manejar todas esas notificaciones, y el proceso requiere mucho tiempo y esfuerzo. Es por eso que el estado ideal de Swift es que todos en su ciudad tengan la menor cantidad de contactos posible. Entonces no necesita un gran personal para manejar los cambios de dirección, y puede hacer todo lo demás más rápido y mejor.
Esta es también la razón por la cual todos los Swift están entusiasmados con los tipos de valores y los tipos de referencia. Por naturaleza, los tipos de referencia acumulan "contactos" por todas partes, y los tipos de valores generalmente no necesitan más que una pareja. Los tipos de valores son "Swift" -er.
Así que de vuelta a la imagen pequeña: structs
. Las estructuras son un gran negocio en Swift porque pueden hacer la mayoría de las cosas que los objetos pueden hacer, pero son tipos de valores.
Continuemos la analogía de la dirección física imaginando un misterStruct
que vive en someObjectVille
. La analogía se pone un poco complicada aquí, pero creo que todavía es útil.
Entonces, para modelar el cambio de una variable en una struct
, digamos que misterStruct
tiene cabello verde y recibe el orden de cambiar a cabello azul. La analogía se achica, como dije, pero más o menos lo que sucede es que en lugar de cambiar el cabello de misterStruct
, la persona mayor se muda y una nueva persona con pelo azul se mueve, y esa nueva persona comienza a llamarse misterStruct
. Nadie necesita una notificación de cambio de dirección, pero si alguien mira esa dirección, verá a un tipo con cabello azul.
Ahora modelemos lo que sucede cuando se llama a una función en una struct
. En este caso, es como misterStruct
recibe un pedido como changeYourHairBlue()
. Entonces la oficina de correos le da las instrucciones a misterStruct
"ve a cambiar tu cabello a azul y dime cuando hayas terminado".
Si está siguiendo la misma rutina que antes, si está haciendo lo que hizo cuando la variable se cambió directamente, lo que misterStruct
hará es mudarse de su propia casa y llamar a una nueva persona con cabello azul. Pero ese es el problema.
La orden fue "ve a cambiar tu cabello a azul y dime cuando hayas terminado", pero es el hombre verde el que recibió el pedido. Después de que el chico azul se mude, todavía tiene que enviarse una notificación de "trabajo completo". Pero el hombre azul no sabe nada al respecto.
[Para realmente entender esta analogía algo horrible, lo que técnicamente le sucedió a un tipo pelirrojo fue que después de mudarse, inmediatamente se suicidó. ¡Entonces no puede notificar a nadie que la tarea está completa tampoco! ]
Para evitar este problema, en casos como este solamente , Swift tiene que ir directamente a la casa en esa dirección y realmente cambiar el cabello del habitante actual . Ese es un proceso completamente diferente a solo enviar un chico nuevo.
¡Y es por eso que Swift quiere que usemos la palabra clave mutating
!
El resultado final se ve igual a cualquier cosa que tenga que referirse a la estructura: el habitante de la casa ahora tiene cabello azul. Pero los procesos para lograrlo son en realidad completamente diferentes. Parece que está haciendo lo mismo, pero está haciendo algo muy diferente. Está haciendo una cosa que Swift structs en general nunca hace.
Entonces, para ayudar un poco al pobre compilador, y no hacer que tenga que averiguar si una función muta la struct
o no, por sí misma, para cada función estructural única, se nos pide que tengamos piedad y utilicemos la palabra clave mutating
.
En esencia, para ayudar a Swift a mantenerse rápido, todos debemos hacer nuestra parte. :)
EDITAR:
Hola amigo / tío que me votó negativamente, simplemente reescribí completamente mi respuesta. Si te sienta mejor, ¿eliminarás el voto abajo?