Estructuras y Clases
Las estructuras y las clases son construcciones flexibles de propósito general que se convierten en los bloques de construcción del código de tu programa. Defines propiedades y métodos para agregar funcionalidades a tus estructuras y clases utilizando la misma sintaxis que utilizas para definir constantes, variables y funciones.
A diferencia de otros lenguajes de programación, Swift no requiere que crees archivos separados de interfaz e implementación para estructuras y clases personalizadas. En Swift, se define una estructura o clase en un solo archivo, y la interfaz externa de esa clase o estructura se pone automáticamente a disposición del resto de código.
Nota
Una instancia de una clase se conoce tradicionalmente como objeto. Sin embargo, las estructuras y clases de Swift están mucho más cerca en funcionalidad que en otros lenguajes, y gran parte de este capítulo describe funcionalidades que se aplica tanto a una clase como a una estructura. Debido a esto, se utiliza el término instancia al ser más general.
Comparando Estructuras y Clases
Estructuras y clases tienen muchas cosas en común en Swift. Ambas pueden:
- Definir propiedades para almacenar valores
- Definir métodos para proporcionar funcionalidad
- Definir subscripts para proporcionar acceso a sus valores utilizando la sintaxis del subscript
- Definir initializers (iniciadores) para configurar su estado inicial
- Ampliarse para expandir su funcionalidad más allá de una implementación predeterminada
- Conformar protocolos para proporcionar una funcionalidad estándar de cierto tipo
Para más información, mira Propiedades, Métodos, Subscripts, Inicialización, Extensiones, y Protocolos.
Las clases tienen capacidades adicionales que las estructuras no tienen:
- Heredar características que otras clases no tienen.
- La conversión de tipos te permite verificar e interpretar el tipo de una instancia de clase en tiempo de ejecución.
- Los desinicializadores (desinitializers) permiten que una instancia de una clase libere cualquier recurso que tenga asignado.
- El conteo de referencias permite más de una referencia a una instancia de clase.
Para más información, mira Herencia, Type Casting, Desinicialización, and Automatic Reference Counting.
Las capacidades adicionales que admiten las clases tienen el costo de una mayor complejidad. Como pauta general, utiliza estructuras porque son más fáciles de razonar y usa clases cuando sean apropiadas o necesarias. En la práctica, esto significa que la mayoría de los tipos de datos personalizados que definas serán estructuras y enumeraciones. Para una comparación más detallada, mira Elegir Entre Estructuras y Clases.
Clases y actores comparte muchas de sus características y comportamientos. Para más información sobre actores, mira Concurrencia.
Definición de sintaxis
Las estructuras y clases tienen una definición de sintaxis muy similar. Defines una estructura con la palabra struct
y una clase con la palabra class
. Ambas colocan su definición completa dentro de un par de llaves:
1struct UnaEstructura {
2 // la definición de la estructura va aquí
3}
4
5class UnaClase {
6 // la definición de la clase va aquí
7}
Nota
Cada vez que defines una nueva estructura o clase, defines un nuevo tipo en Swift.
Asigna nombres UpperCamelCase
a los tipos (como el ejemplo UnaEstructura
and UnaClase
) para que coincida con el uso de mayúsculas de los tipos estándar de Swift (como String
,Int
, y Bool
). Asigna nombres lowerCamelCase
a propiedades y métodos (como frameRate
e incrementarContador
) para diferenciarlos de los nombres de tipos.
Aquí hay un ejemplo de una definición de estructura y otra de clase:
1struct Resolucion {
2 var ancho = 0
3 var alto = 0
4}
5
6class ModoDeVideo {
7 var resolucion = Resolucion()
8 var entrelazada = false
9 var frameRate = 0.0
10 var nombre: String?
11}
El ejemplo de arriba define una estructura llamada Resolucion
, para describir una resolución de pantalla basada en píxeles. Esta estructura tiene dos propiedades almacenadas llamadas ancho
y alto
. Las propiedades almacenadas son constantes o variables que se agrupan y almacenan como parte de la estructura o clase. De estas dos propiedades se infieren que el tipo es Int
al asignarles un valor inicial de 0
.
El ejemplo de arriba también define una nueva clase llamada ModoDeVideo
, para describir un modo de vídeo específico para la visualización de vídeo. Esta clase tiene almacenada cuatro variables. La primera, resolucion
, es inicializada con una nueva instancia de la estructura Resolucion
, que infiere un tipo de propiedad de Resolución
. Para las otras tres variables, las nuevas instancias de ModoDeVideo
se inicializarán con una configuración entrelazada
en false
(que significa “video no entrelazado”), una velocidad de reproducción de fotogramas de 0.0
, y un valor String
opcional llamado nombre
. A la propiedad nombre
se le asigna automáticamente un valor predeterminado nil
, o "sin valor", porque es de un tipo opcional.
Instancias de Estructura y Clase
La definición de la estructura Resolucion
y de la clase ModoDeVideo
solo describen cómo se verá una Resolucion
o ModoDeVideo
. Por ellas mismas, no describen una resolución o un modo de vídeo específico. Para hacer eso, debes crear una instancia de la estructura o clase.
La sintaxis para crear una instancia de estructura o clases es muy similar en ambos casos:
1let algunaResolucion = Resolucion()
2let algunModoDeVideo = ModoDeVideo()
Tanto las estructuras como las clases usan sintaxis de inicializador para nuevas instancias. La sintaxis de inicialización más simple utiliza el nombre del tipo de la clase o estructura seguido de paréntesis vacíos, como Resolucion()
o ModoDeVideo()
. Esto crea una nueva instancia de la clase o estructura, con cualquier propiedad inicializada a sus valores predeterminados. La inicialización de clases y estructuras esta descrita con más detalle en Inicialización.
Acceso a Propiedades
Puede acceder a las propiedades de una instancia se utiliza sintaxis de puntos. En la sintaxis de puntos, escribes el nombre de la propiedad inmediatamente después del nombre de la instancia, separado por un punto (.
), sin espacios:
1print("El ancho de algunaResolucion es \(algunaResolucion.ancho)")
2// Imprime "El ancho de algunaResolucion es 0"
En este ejemplo, algunaResolucion.ancho
refiere a la propiedad ancho
de algunaResolucion
, y devuelve su valor inicial por defecto 0
.
Puedes profundizar en las subpropiedades, como la propiedad ancho
en la propiedad resolucion
de un ModoDeVideo
:
1print("El ancho de algunaResolucion es \(algunModoDeVideo.resolucion.ancho)")
2// Imprime "El ancho de algunaResolucion es 0"
También puede usar la sintaxis de puntos para asignar un nuevo valor a una variable:
1algunModoDeVideo.resolucion.ancho = 1280
2
3print("El ancho de algunModoDeVideo es ahora \(algunModoDeVideo.resolucion.ancho)")
4// Imprime "El ancho de algunModoDeVideo es ahora 1280"
Memberwise Initializers para los Tipos de Estructura
Todas las estructuras tienen un memberwise initializer generado automáticamente, que puedes usar para inicializar las propiedades de nuevas instancias de la estructura. Los valores iniciales para las propiedades de la nueva instancia se pueden pasar al inicializador por nombre:
1let vga = Resolucion(ancho: 640, alto: 480)
A diferencia de las estructuras, las instancias de las clases no reciben inicializador por defecto. Los inicializadores se describen con más detalle en Inicialización.
Las Estructuras y las Enumeraciones son Tipos de Valor
Un tipo de valor se copia cuando es asignado a una variable o constante, o cuando es pasado a una función.
De hecho, has estado utilizando tipos de valor extensamente a lo largo de los capítulos anteriores. Todos los tipos básicos en Swift (enteros, números de coma flotante, booleanos, cadenas, arrays y diccionarios) son tipos de valor, y en el fondo se implementan como estructuras.
Todas las estructuras y enumeraciones son tipos de valor en Swift. Esto significa que cualquier instancia de estructura y enumeración que crees, y cualquier tipo de valor que tengas como propiedades, siempre se copian cuando se utilizan en tu código.
Nota
Las colecciones definidas por la biblioteca estándar como arrays, diccionarios y las cadenas utilizan una optimización para reducir el costo de rendimiento al copiar. En lugar de hacer una copia inmediatamente, estas colecciones comparten la memoria donde se almacenan los elementos entre la instancia original y cualquier copia. Si se modifica una de las copias de la colección, los elementos se copian justo antes de la modificación. El comportamiento que se ve en tu código siempre es como si se realizara inmediatamente una copia.
Considera este ejemplo, que utiliza la estructura Resolucion
del ejemplo anterior:
1let hd = Resolucion(ancho: 1920, alto: 1080)
2var cinema = hd
Este ejemplo declara una constante llamada hd
y la establece en una instancia de Resolucion
inicializada con el ancho y el alto del video Full HD (1920 píxeles de ancho por 1080 píxeles de alto).
Luego declara una variable llamada cinema
y la establece en el valor actual de hd
. Debido a que Resolucion
es una estructura, se realiza una copia de la instancia existente, y esta nueva copia se asigna a cinema
. Aunque hd
y cinema
ahora tienen el mismo ancho
y alto
, son dos instancias completamente diferentes.
A continuación, la propiedad ancho
de cinema
se modifica para que sea el ancho del estándar 2K que se utiliza para la proyección de cine digital. (2048 píxeles de ancho y 1080 píxeles de alto):
1cinema.ancho = 2048
Verificando la propiedad ancho
de cinema
muestra que efectivamente ha cambiado para ser 2048
:
1print("cinema es ahora \(cinema.ancho) píxeles de ancho")
2// Imprime "cinema es ahora 2048 píxeles de ancho"
Sin embargo, la propiedad ancho
de la instancia hd
original todavía tiene el valor antiguo de 1920
:
1print("hd es aún \(hd.ancho) píxeles de ancho")
2// Prints "hd es aún 1920 píxeles de ancho"
Cuando a cinema
se le dió el valor actual de hd
, los valores almacenados en hd
se copiaron en la nueva instancia de cinema
. El resultado final fueron dos instancias completamente separadas que contenían los mismos valores numéricos. Sin embargo, debido a que son instancias separadas, establecer el ancho de cinema
en 2048
no afecta el ancho almacenado en hd
, como se muestra en la siguiente figura:
El mismo comportamiento se aplica a las enumeraciones:
1enum PuntoBrujula {
2 case norte, sur, este, oeste
3
4 mutating func girarAlNorte() {
5 self = .norte
6 }
7}
8
9var direccionActual = PuntoBrujula.oeste
10let direccionRecordada = direccionActual
11
12direccionActual.girarAlNorte()
13
14print("La dirección actual es \(direccionActual)")
15print("La dirección recordada es \(direccionRecordada)")
16// Prints "La dirección actual es norte"
17// Prints "La dirección recordad es oeste"
Cuando a direccionRecordada
se le asigna el valor de direccionActual
, en realidad se establece en una copia. Cambiar el valor de direccionActual
a partir de entonces no afecta a la copia del valor original que se almacenó en direccionRecordada
.
Las Clases son Tipos de Referencia
A diferencia de los tipos de valor, los tipos de referencia no se copian cuando se asignan a una variable o constante, o cuando se pasan a una función. En lugar de una copia, se utiliza una referencia a la misma instancia existente.
Aquí hay un ejemplo, usando la clase ModoDeVideo
definida anteriormente:
1let diezOchenta = ModoDeVideo()
2
3diezOchenta.resolucion = hd
4diezOchenta.entrelazado = true
5diezOchenta.nombre = "1080i"
6diezOchenta.frameRate = 25.0
Este ejemplo se declara una nueva constante llamada diezOchenta
y se configura para referirse a una nueva instancia de la clase ModoDeVideo
. Al modo de video se le asigna una copia de la resolución HD de 1920
por 1080
de antes. Está configurado para ser entrelazado, su nombre está configurado en "1080i"
y su velocidad de fotogramas está configurada en 25.0
fotogramas por segundo.
A continuación, diezOchenta
es asignada a una nueva constante llamada tambienDiezOchenta
, y su velocidad de fotogramas es modificado:
1let tambienDiezOchenta = diezOchenta
2
3tambienDiezOchenta.frameRate = 30.0
Debido a que las clases son tipos de referencia, diezOchenta
y tambienDiezOchenta
se refieren en realidad a la misma instancia de ModoDeVideo
. Es decir, son solo dos nombres diferentes para una misma instancia, como se muestra en la siguiente figura:
Verificar la propiedad frameRate
de diezOchenta
muestra correctamente la nueva velocidad de fotogramas de 30.0
desde la instancia ModoDeVideo
subyacente:
1print("La propiedad frameRate de diezOchenta ahora es \(diezOchenta.frameRate)")
2// Imprime "La propiedad frameRate de diezOchenta ahora es 30.0"
Este ejemplo muestra cómo los tipos de referencia pueden ser más difíciles de razonar. Si diezOchenta
y tambienDiezOchenta
estuvieran muy separados en el código de tu programa, podría ser difícil encontrar todas las veces en la que el modo de video ha sido cambiado. Siempre que uses diezOchenta
, también tienes que pensar en el código que usa tambienDiezOchenta
, y viceversa. Por el contrario, es más fácil razonar sobre los tipos de valor porque todo el código que interactúa con el mismo valor está muy cerca en los archivos de origen.
Ten en cuenta que diezOchenta
y tambienDiezOchenta
se declaran como constantes, en lugar de variables. Sin embargo, aún puedes cambiar diezOchenta.frameRate
y tambienDiezOchenta.frameRate
porque los valores de diezOchenta
y tambienDiezOchenta
son constantes en sí mismos y en realidad no cambian. diezOchenta
y tambienDiezOchenta
no "almacenan" la instancia de ModoDeVideo
; en cambio, ambos se refieren a una instancia de ModoDeVideo
. Lo que ha cambiado es la propiedad frameRate
de ModoDeVideo
subyacente, no los valores de las referencias constantes a ese ModoDeVideo
.
Operadores de Identidad
Debido a que las clases son tipos de referencia, es posible que varias constantes y variables se refieran a la misma instancia única de una clase en segundo plano. (No ocurre lo mismo con las estructuras y las enumeraciones, porque siempre se copian cuando se asignan a una constante o variable, o se pasan a una función).
A veces puede ser útil averiguar si dos constantes o variables se refieren exactamente a la misma instancia de una clase. Para hacer esto, Swift proporciona dos operadores de identidad:
- Idéntica a (
===
) - No idéntica a (
!==
)
Utiliza estos operadores para verificar si dos constantes o variables se refieren a la misma instancia única:
1if diezOchenta === tambienDiezOchenta {
2 print("diezOchenta y tambienDiezOchenta se refieren a la misma instancia de ModoDeVideo.")
3}
4// Imprime "diezOchenta y tambienDiezOchenta se refieren a la misma instancia de ModoDeVideo."
Ten en cuenta que idéntico a (representado por tres signos de igual, o ===
) no significa lo mismo que igual a (representado por dos signos de igual, o ==
). Idéntico a significa que dos constantes o variables de tipo de clase se refieren exactamente a la misma instancia de clase. Igual a significa que dos instancias se consideran iguales o equivalentes en valor, para algún significado apropiado de igual, definido por el diseñador de tipo.
Cuando defines tus propias estructuras y clases personalizadas, es tu responsabilidad decidir qué califica como dos instancias iguales. El proceso de definición de tus propias implementaciones de los operadores ==
y !=
se describe en Operadores de equivalencia.
Punteros
Si tiene experiencia con C, C++ u Objective-C, puede saber que estos lenguajes usan punteros para referirse a direcciones en la memoria. Una constante o variable de Swift que se refiere a una instancia de algún tipo de referencia es similar a un puntero en C, pero no es un puntero directo a una dirección en la memoria y no requiere que se escriba un asterisco (*
) para indicar que se está creando una referencia. En cambio, estas referencias se definen como cualquier otra constante o variable en Swift. La biblioteca estándar proporciona tipos de punteros y búferes que puedes usar si necesitas interactuar con los punteros directamente; consulta Administración manual de memoria.