A herdanza permítenos herdar propiedades e métodos dunha clase a outra.
- Ao herdar podemos usar os métodos e propiedades sen necesidade de volver a crealos na nova clase (é unha das vantaxes de herdar, que podemos usar métodos e propiedades xa existentes da clase que herdamos).
- Ao usar herdanza o que buscamos é encapsular o noso código en tipos ben definidos para reutilizar código. Desta maneira o noso código queda máis limpo e fácil de entender.
Definicións #
Hai uns termos que usamos ao traballar con herdanza:
- Superclase, é a clase da que van herdar outras clases. Tamén se lle adoita chamar clase abstracta, clase base ou clase pai.
- Subclase, é a clase que herdou dunha superclase. Tamén se lle coñece como clase derivada ou clase filla.
Ao derivar clases, creamos o que a miúdo se coñece como xerarquía de clases.
En Swift, unha subclase só pódese derivar dunha soa clase principal directa. Este é un concepto coñecido como herdanza única
Superclase ou clase base #
Partimos da seguinte clase:
class Animal {
var nome: String
var idade: Int
init(nome: String, idade: Int) {
self.nome = nome
self.idade = idade
}
func son() {
print("Sonido do Animal")
}
}
Esta clase é moi abstracta, é dicir, non estamos a tratar con ningún animal en concreto. Imaxina que necesitas crear unha clase Can
máis específica:
class Can {
var nome: String
var idade: Int
init(nome: String, idade: Int) {
self.nome = nome
self.idade = idade
}
func son() {
print("Sonido do Animal")
}
}
Can
ten exactamente as mesmas propiedades e método que a clase Animal
.
Así, podemos crear unha subclase de Animal
e chamala Can
, que herdará as súas propiedades e método. Desta maneira, Can
queda simplificada:
class Animal {
var nome: String
var idade: Int
init(nome: String, idade: Int) {
self.nome = nome
self.idade = idade
}
func son() {
print("Sonido do Animal")
}
}
class Can: Animal {
}
Agora que temos o tipo Can
herdando de Animal
, imos crear unha instancia de Can
:
class Animal {
var nome: String
var idade: Int
init(nome: String, idade: Int) {
self.nome = nome
self.idade = idade
}
func son() {
print("Sonido do Animal")
}
}
class Can: Animal {
}
let can = Can()
// RESULTADO 👇
// erro: missing arguments for parameters 'nome', 'idade' in call
Obtemos un erro, debido a que na subclase Can
temos que ter en conta e inicializar as propiedades da superclase. É por iso que para crear unha instancia de Can
debemos usar un inicializador da superclase (xa que debemos dar un valor a todas as súas propiedades):
var can = Can(nome: "Boby", idade: 2)
print("\(can.nome)")
// RESULTADO 👇
// Boby
Ampliar a funcionalidade dunha subclase #
Dentro da clase Can
podemos engadir máis métodos ou propiedades que non existen na superclase.
Engadir propiedades #
Se queremos engadir unha propiedade na clase Can
, vemos que o seguinte código da un erro de compilación:
class Can: Animal {
var patas: Int
}
// RESULTADO 👇
// Error stored property 'patas' without initial value prevents synthesized initializers
Para solventar o erro, na clase Can
temos que crear un inicializador para dar un valor inicial á propiedade patas
:
class Can: Animal {
var patas: Int
init(patas: Int) {
self.patas = patas
}
}
Agora imos obter outro erro xa que debemos chamar ao init
de Animal
para poder dar un valor e iniciar ás propiedades de Animal
(a superclase de Can
). Iso o facemos utilizando super.init
, que se encarga de chamar ao inicializador da superclase cos valores iniciais que se asignasen ás súas propiedades:
class Can: Animal {
var patas: Int
init(patas: Int, nome: String, idade: Int) {
self.patas = patas
super.init(nome: nome, idade: idade)
}
}
var can = Can(patas: 4, nome: "Boby", idade: 2)
print("\(can.nome)")
Engadir métodos #
Neste caso, creamos o método comer()
:
class Animal {
var nome: String
var idade: Int
init(nome: String, idade: Int) {
self.nome = nome
self.idade = idade
}
func son() {
print("Sonido do Animal")
}
}
class Can: Animal {
var patas: Int
init(patas: Int, nome: String, idade: Int) {
self.patas = patas
super.init(nome: nome, idade: idade)
}
func comer() {
print("Comer carne")
}
}
var can = Can(patas: 4, nome: "Boby", idade: 2)
can.comer()
// RESULTADO 👇
// Comer carne
Exemplo #
Neste exemplo creamos unha clase chamada ContaBancaria
deseñada para ter un número de conta bancaria e o saldo actual correspondente:
class ContaBancaria {
var numConta: Int = 0
var saldoConta: Float = 0.0
init(numero: Int,saldo: Float) {
numConta = numero
saldoConta = saldo
}
func visualizarSaldo(){
print("O número de conta é: \(numConta)")
print("Saldo actual: \(saldoConta)")
}
}
Supoñamos que ademais da clase ContaBancaria
tamén necesitamos unha clase para usar contas de aforro. Unha conta de aforros (ao igual que unha conta bancaria) tamén terá que ter un número de conta, un saldo actual e métodos para acceder a eses datos.
- Para iso, creamos unha nova clase que sexa unha subclase da clase
ContaBancaria
. - A nova clase herdará todas as características da clase
ContaBancaria
, podéndose ampliar para agregar a funcionalidade adicional requirida para unha conta de aforros.
Creamos unha subclase de ContaBancaria
que chamaremos ContaAforro
simplemente especificando ContaBancaria
como a clase da que herdamos:
class ContaBancaria {
var numConta: Int = 0
var saldoConta: Float = 0.0
init(numero: Int,saldo: Float) {
numConta = numero
saldoConta = saldo
}
func visualizarSaldo(){
print("O número de conta é: \(numConta)")
print("Saldo actual: \(saldoConta)")
}
}
class ContaAforro:ContaBancaria {
}
var aforro = ContaAforro(numero: 12345, saldo: 0.0)
aforro.visualizarSaldo() //Imprime: Saldo actual: 0.0
Tede en conta que, aínda que aínda non engadimos ningunha variable ou método, a clase herdou todos os métodos e propiedades da clase principal ContaBancaria
.
- Polo tanto, podemos crear unha instancia da clase
ContaAforro
e establecer variables e métodos de chamada exactamente da mesma maneira que o fixemos coa claseContaBancaria
nos exemplos anteriores (liñas 20 e 21
). - Dito isto, realmente non logramos nada a menos que tomemos medidas para ampliar a clase
ContaAforro
.
A continuación:
- Agora necesitamos ampliar a subclase (
ContaAforro
) para que teña as características que necesitamos para que sexa útil para almacenar a información da conta de aforros. - Para facer isto, simplemente engadimos as propiedades e métodos que proporcionan a nova funcionalidade, do mesmo xeito que o fariamos para calquera outra clase que desexemos crear:
class ContaBancaria {
var numConta: Int = 0
var saldoConta: Float = 0.0
init(numero: Int,saldo: Float) {
numConta = numero
saldoConta = saldo
}
func visualizarSaldo(){
print("O número de conta é: \(numConta)")
print("Saldo actual: \(saldoConta)")
}
}
class ContaAforro:ContaBancaria {
var tipoInteres: Float = 0.0
func calcularInteres() -> Float {
return tipoInteres * saldoConta
}
}
var aforro = ContaAforro(numero: 12345, saldo: 1000.0)
aforro.tipoInteres = 0.10
aforro.visualizarSaldo() //Imprime: Saldo actual: 0.0
print("Intereses: \(aforro.calcularInteres())")
override – Sobreescritura de métodos herdados #
Cando se utiliza a herdanza, pode ser que necesitemos sobreescribir algún método herdado da clase principal:
- Cando, por exemplo, na subclase teñamos que modificar un método herdado da clase principal para proporcionar a funcionalidade que precisa dita subclase.
- Tamén é posible que herdes un método que non faga o que necesites. Para iso, anulamos o método herdado e escribimos unha nova versión do método na subclase.
Hai dúas regras que deben obedecerse ao sobreescribir un método:
- Mesmo número e tipo de parámetros
- Mesmo tipo de retorno
Así, no seguinte exemplo, na clase ContaBancaria
temos un método chamado visualizarSaldo()
que amosa o número de conta bancaria e o saldo actual. Na subclase ContaAforro
é posible que tamén queiramos visualizar a taxa de interese actual asignada á conta. Para lograr isto, simplemente sobreescribimos o método visualizarSaldo()
na subclase ContaAforro
, empregando o prefixo override
:
class ContaBancaria {
var numConta: Int = 0
var saldoConta: Float = 0.0
init(numero: Int,saldo: Float) {
numConta = numero
saldoConta = saldo
}
func visualizarSaldo(){
print("O número de conta é: \(numConta)")
print("Saldo actual: \(saldoConta)")
}
}
class ContaAforro:ContaBancaria {
var tipoInteres: Float = 0.0
func calcularInteres() -> Float {
return tipoInteres * saldoConta
}
override func visualizarSaldo(){
print("O número de conta é: \(numConta)")
print("Saldo actual: \(saldoConta)")
print("A taxa de interés é: \(tipoInteres)")
}
}
var aforro = ContaAforro(numero: 12345, saldo: 1000.0)
aforro.tipoInteres = 0.10
aforro.visualizarSaldo()
//Imprime:
//O número de conta é: 12345
//Saldo actual: 1000.0
//A taxa de interés é: 0.1
super #
Tamén é posible facer unha chamada ao método anulado na superclase desde dentro dunha subclase. Dentro da subclase (ContaAforro
), poderiamos chamar ao método visualizarBalance()
da superclase (ContaBancaria
) para mostrar o número de conta e o saldo, antes de que se amose a taxa de interese, eliminando así unha maior duplicación de código:
class ContaBancaria {
var numConta: Int = 0
var saldoConta: Float = 0.0
init(numero: Int,saldo: Float) {
numConta = numero
saldoConta = saldo
}
func visualizarSaldo(){
print("O número de conta é: \(numConta)")
print("Saldo actual: \(saldoConta)")
}
}
class ContaAforro:ContaBancaria {
var tipoInteres: Float = 0.0
func calcularInteres() -> Float {
return tipoInteres * saldoConta
}
override func visualizarSaldo(){
super.visualizarSaldo()
print("A taxa de interés é: \(tipoInteres)")
}
}
var aforro = ContaAforro(numero: 12345, saldo: 1000.0)
aforro.tipoInteres = 0.10
aforro.visualizarSaldo()
//Imprime:
//O número de conta é: 12345
//Saldo actual: 1000.0
//A taxa de interés é: 0.1
Inicializar Subclases #
Como é frecuente cometer erros, vou a incidir na inicialización das subclases con outro exemplo, neste caso baseado na clase ContaAforro
.
A clase ContaAforro
herda o método de inicialización init
da clase principal ContaBancaria
.
A clase ContaAforro
necesita o seu propio init
para garantir que a propiedade tipoInteres
se inicialice:
class ContaBancaria {
var numConta: Int = 0
var saldoConta: Float = 0.0
init(numero: Int,saldo: Float) {
numConta = numero
saldoConta = saldo
}
func visualizarSaldo(){
print("O número de conta é: \(numConta)")
print("Saldo actual: \(saldoConta)")
}
}
class ContaAforro:ContaBancaria {
var tipoInteres: Float = 0.0
init(numero: Int,saldo: Float,tipo: Float) {
super.init(numero: numero, saldo: saldo)
tipoInteres = tipo
}
func calcularInteres() -> Float {
return tipoInteres * saldoConta
}
override func visualizarSaldo(){
print("A taxa de interés é: \(tipoInteres)")
super.visualizarSaldo()
}
}
var aforro = ContaAforro(numero: 12345, saldo: 1000.0, tipo: 0.10)
aforro.visualizarSaldo()
//Imprime:
//O número de conta é: 12345
//Saldo actual: 1000.0
//A taxa de interés é: 0.1
Para evitar posibles problemas de inicialización, o método init
da superclase (ContaBancaria
) sempre debe chamarse despois de que se completen as tarefas de inicialización para a subclase (ContaAforro
).
extension – Extender unha clase #
As extensións pódense utilizar para engadir características como métodos, inicializadores, propiedades calculadas e subíndices a unha clase existente sen necesidade de crear e facer referencia a unha subclase.
Isto é particularmente poderoso cando se utilizan extensións para agregar funcionalidade ás clases integradas da linguaxe Swift e os frameworks SDK de iOS.
Supoñamos que necesitamos agregar un par de propiedades calculadas adicionais á clase Double
:
extension Double {
var cadrado: Double{
return self * self
}
var cubo: Double{
return self * self * self
}
}
var numero: Double = 2.0
print("O cadrado de \(numero) é \(numero.cadrado)")
print("O cubo de \(numero) é \(numero.cubo)")
//Imprime:
//O cadrado de 2.0 é 4.0
//O cubo de 2.0 é 8.0
As extensións proporcionan unha forma rápida de ampliar a funcionalidade dunha clase sen necesidade de usar subclases :
- Con todo, as subclases aínda teñen algunhas vantaxes sobre as extensións
- Non é posible, por exemplo, anular a funcionalidade existente dunha clase usando unha extensión e as extensións non poden conter propiedades almacenadas.
Evitar herdar dunha clase #
En ocasións queremos restrinxir que se poida herdar dunha clase. Para facelo ésó temos que usar a keyword final
diante de nosa clase
No seguinte exemplo, imos crear a clase Animal e imos indicar que ningunha outra clase pode herdar dela:
final class Animal {
var nome: String
var idade: Int
init(nome: String, idade: Int) {
self.nome = nome
self.idade = idade
}
func son() {
print("Sonido do Animal")
}
}
Se agora creamos unha nova clase chamada Can
e tentamos herdar Animal
, recibimos un erro do compilador:
class Animal {
var nome: String
var idade: Int
init(nome: String, idade: Int) {
self.nome = nome
self.idade = idade
}
func son() {
print("Sonido do Animal")
}
}
class Can: Animal {
}
/ RESULTADO 👇
// Error: inheritance from a final class 'Animal'