Diferença entre classes e structs

TL;DR: Objetos (instâncias de classes) são sempre armazenadas por referência, portanto a declaração var b = a não cria uma cópia — ambas as variáveis se referem à mesma instância.  Já com structs, cada variável se refere a uma única instância, portanto var b = a cria uma nova instância (uma cópia independente) de a.

Afinal, qual é a diferença entre Class e Struct em Swift?

Vamos ilustrar essa diferença a partir de duas declarações diferentes: Movie, que é uma struct, e Music, que é uma classe:

struct Movie {
    var title: String
    var year: Int

    // Structs não requerem inicializadores, por isso não criei um ‘init(...)’
}

class Music {
    var title: String
    var artist: String
    var year: Int

    init(title: String, artist: String, year: Int) {
        self.title = title
        self.artist = artist
        self.year = year
    }
}

Classes: Um objeto, várias referências

A grande diferença é que uma variável do tipo Music (que é uma classe) armazena apenas uma referência para uma música. Assim, quando essa variável é copiada — para uma outra variável ou para um argumento de uma função, por exemplo — estamos apenas criando novas referências para o mesmo objeto.  Em outras palavras, podemos pensar que, em princípio, um objeto nunca é copiado: podemos ter várias variáveis que são sinônimos para a mesma instância.

Para ilustrar esse fato, vamos criar uma instância dessa classe:

var happyMusic = Music(title: "happy", artist: "Pharel Wiliams", year: 2013)

O comando acima realiza duas operações em sequência:

  1. Cria uma instância da classe Music;
  2. Cria uma referência para essa instância, chamada happyMusic.

A grande sacada ocorre quando criamos novas variáveis para esse objeto. Algo como:

var myFavoriteMusic = happyMusic

Esse comando não cria um novo objeto: as duas variáveis são sinônimos para o mesmo objeto.  Assim, quando fazemos uma alteração em uma dessas variáveis, essa alteração é observada quando acessamos o objeto a partir de qualquer outra referência:

var myFavoriteMusic = happyMusic
myFavoriteMusic.artist = "Pharrell Williams"

print("O nome do artista foi corrigido para \(happyMusic.artist).")
// saída: O nome do artista foi corrigido para Pharrell Williams.

O mesmo efeito é percebido quando passamos um objeto para uma função:

func capitalizeMusicTitle(_ music: Music) {
    music.title = music.title.capitalized
}

capitalizeMusicTitle(happyMusic)
print("O título da música é \(happyMusic.title).")
// saída: O título da música é Happy.

Em resumo, todas as variáveis que usei nos exemplos fazem referência ao mesmo objeto.  Em nenhum momento esse objeto foi copiado na memória: apenas uma instância existe do início ao fim.

Structs: Cada variável é uma instância independente

No caso de structs, o comportamento é totalmente diferente, e isso pode confundir bastante o programador iniciante.  Cada variável é uma instância independente, mesmo que tenha sido criada por atribuição.  Vamos ilustrar isso a seguir:

var sadMovie = Movie(title: "Grave of the Fireflies", year: 1888)
var myFavoriteJapaneseAnime = sadMovie
myFavoriteJapaneseAnime.year = 1988

print("O ano de produção correto é \(sadMovie.year) ou \(myFavoriteJapaneseAnime.year)?")
// saída: O ano de produção correto é 1888 ou 1988?

Como se pode ver, as duas variáveis armazenam instâncias independentes entre si: ao fazer alterações na segunda instância, elas não se propagam para a primeira.

Cuidado com as sutilezas!

Mesmo depois de alguma experiência em Swift, certas situações podem causar confusão.  Talvez uma dessas situações mais comuns é quando precisamos varrer os elementos de uma array de structs para fazer algumas modificações.  Digamos que precisamos corrigir o ano de produção de alguns filmes cujo ano de produção foi registrado com dois dígitos:

var some1998Movies = [
    Movie(title: "A Bug's Life", year: 98),
    Movie(title: "Armageddon", year: 1998),
    Movie(title: "Deep Impact", year: 1998),
    Movie(title: "Doctor Dolittle", year: 98),
    Movie(title: "Godzilla", year: 98),
    Movie(title: "Patch Adams", year: 1998),
    Movie(title: "Rush Hour", year: 1998),
    Movie(title: "Saving Private Ryan", year: 98),
    Movie(title: "The Waterboy", year: 1998),
    Movie(title: "There's Something About Mary", year: 98),
]

for var movie in some1998Movies {
    if movie.year < 100 {
        movie.year += 1900
    }
    print("\(movie.title) (\(movie.year))")
}
// Saída:
// Armageddon (1998)
// Deep Impact (1998)
// Doctor Dolittle (1998)
// Godzilla (1998)
// Patch Adams (1998)
// Rush Hour (1998)
// Saving Private Ryan (1998)
// The Waterboy (1998)
// There's Something About Mary (1998)

Como se pode ver, aparentemente os elementos da array foram todos corrigidos, pois os “print”s indicam que todos os anos estão com quatro dígitos.  Mas então, por que o seguinte comando ainda mostra o ano “98”???

print("Ano do primeiro filme: \(some1998Movies[0].year)")
// Saída: Ano do primeiro filme: 98

(Sugestão: Tente descobrir o erro no código antes de ler a resposta abaixo.)

Vamos lá: Lembre-se de que a atribuição de uma instância de struct cria uma cópia, ou seja, uma nova instância com os mesmos dados.  Isso não é diferente no caso de um for.  A variável de iteração var movie recebe uma cópia de cada elemento a cada iteração, e essa cópia é independente daquela que está na array! Portanto, alterações na variável movie não ocorrem nos elementos armazenados na array.

Talvez seja mais fácil enxergar isso sabendo que o bloco for acima é igual ao seguinte:

for index in 0..<some1998Movies.count {
    var movie = some1998Movies[index] // Veja a cópia ocorrendo aqui!
    if movie.year < 100 {
        movie.year += 1900
    }
    print("\(movie.title) (\(movie.year))")
}

E qual é a solução para esse caso?

Bom, basta fazer as alterações diretamente na array, ao invés de na variável de iteração. O código fica mais rebuscado, mas funciona:

for index in 0..<some1998Movies.count {
    let movie = some1998Movies[index] // Não adianta ‘var’, pois as alterações não ocorrem na array
    if movie.year < 100 {
        some1998Movies[index].year += 1900 // Operação diretamente na array
    }
    print("\(movie.title) (\(some1998Movies[index].year))") // Não use ‘movie.year’, pois essa cópia agora está desatualizada
}

Dica: Podemos usar o método Array.enumerated() para tornar o código um pouco mais “limpo”:

for (index, movie) in some1998Movies.enumerated() {
    if movie.year &lt; 100 {
        some1998Movies[index].year += 1900  // Operação diretamente na array
    }
    print("\(movie.title) (\(some1998Movies[index].year))")  // Não use ‘movie.year’, pois essa cópia agora está desatualizada
}

Ainda assim, não vamos nos livrar do problema original, que é a necessidade de fazer alterações diretamente na array.

É claro que esse problema não acontece com classes, pois nesse caso a variável de iteração armazena referências para os mesmos objetos que estão na array. (Para ser mais correto: uma array de objetos é na verdade uma array de referências a objetos.)

O que escolho: classes ou structs?

Em geral, structs são para blocos simples de informação que não sofrem alterações com o tempo. Claro que isto não é uma regra, mas é importante manter uma luz vermelha acesa sempre que uma instância que é usada em mais de um lugar precisa ser modificada aqui e ali: como vimos, em structs essas alterações ocorrem apenas naquela cópia.

Portanto, no caso do tipo Movie que usei nesta página, talvez fosse muito melhor que tivesse sido criado como uma classe — isso evitaria o código rebuscado que vimos nos dois últimos exemplos.

Para mais discussões, sugiro uma olhada na documentação da Apple que fala sobre classes e structs1 e neste vídeo da WWDC’15 que também fala sobre o assunto2.

  1. The Swift Programming Language: Structures and Classes. ↩︎
  2. WWDC 2015 talk: Protocol Oriented Programming in Swift. ↩︎

1 Comment

  1. Conteúdo muito rico e muito legal de se aprender pela linguagem simples do autor. Sempre que fico com dúvida venho aqui para lembrar alguns pontos importantes de aplicação.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *


Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.