Capítulo 13 — Estados mais seguros com enum class
Série: Do zero ao jogo comercial com C++ e SFML
Quando números deixam de representar bem o significado do nosso jogo, podemos criar um tipo próprio para os estados válidos
No capítulo anterior aprendemos a utilizar switch quando uma mesma informação pode corresponder a várias opções discretas conhecidas.
Nosso exemplo final representava as estações do ano desta maneira:
const int SPRING = 1;
const int SUMMER = 2;
const int AUTUMN = 3;
const int WINTER = 4;
int currentSeason = SUMMER;
Depois:
switch (currentSeason)
{
case SPRING:
std::cout << "Spring" << std::endl;
break;
case SUMMER:
std::cout << "Summer" << std::endl;
break;
case AUTUMN:
std::cout << "Autumn" << std::endl;
break;
case WINTER:
std::cout << "Winter" << std::endl;
break;
default:
std::cout << "Unknown season." << std::endl;
break;
}
Comparado aos números soltos:
case 1:
case 2:
case 3:
case 4:
as constantes melhoraram bastante a leitura.
Agora enxergamos:
SPRING
SUMMER
AUTUMN
WINTER
em vez de:
1
2
3
4
Mas ainda existe um problema importante.
Nosso estado continua declarado assim:
int currentSeason = SUMMER;
Para C++, currentSeason ainda é simplesmente:
int
Isso significa que podemos escrever:
currentSeason = 999;
E do ponto de vista do tipo:
int recebe int
não existe nenhuma incompatibilidade.
Só que, no nosso domínio:
999
não é uma estação.
Nosso código permite representar um estado que não deveria fazer sentido.
Hoje vamos melhorar isso.
Em vez de dizer:
currentSeasoné um número inteiro.
queremos dizer:
currentSeasoné uma estação.
Para isso conheceremos:
enum class
1. O problema não está no switch
É importante localizar corretamente o problema.
Nosso switch:
switch (currentSeason)
{
case SPRING:
break;
case SUMMER:
break;
case AUTUMN:
break;
case WINTER:
break;
}
não é o verdadeiro problema.
A limitação está aqui:
int currentSeason;
Estamos utilizando um tipo extremamente genérico para representar um conceito muito específico.
Um int pode representar valores como:
-500
0
1
42
999
100000
Mas nosso conceito de estação possui apenas quatro possibilidades:
Spring
Summer
Autumn
Winter
Existe uma diferença entre:
o que o tipo permite
e:
o que o domínio considera válido
Queremos aproximar essas duas coisas.
2. Nomes melhores ajudaram, mas não resolveram tudo
No capítulo anterior fizemos:
const int SPRING = 1;
const int SUMMER = 2;
const int AUTUMN = 3;
const int WINTER = 4;
Isso melhorou:
int currentSeason = SUMMER;
em comparação com:
int currentSeason = 2;
Agora conseguimos ler:
currentSeason recebe Summer
em vez de tentar lembrar que:
2 significa Summer
Mas ainda podemos escrever:
currentSeason = 800;
A variável continua sendo um int.
As constantes melhoraram os nomes dos valores.
Ainda não criaram um tipo específico para o conceito.
3. Queremos representar um conjunto fechado de possibilidades
Nosso conceito possui:
Season
│
├── Spring
├── Summer
├── Autumn
└── Winter
Não queremos qualquer número.
Queremos uma dessas opções nomeadas.
Esse tipo de problema aparece constantemente em jogos.
Por exemplo:
Direction
├── North
├── South
├── East
└── West
Ou:
GameState
├── Menu
├── Playing
├── Paused
└── GameOver
Ou:
NpcState
├── Idle
├── Walking
├── Working
└── Sleeping
Quando temos um conjunto finito de valores nomeados, uma enumeração pode ser uma ótima representação.
4. Nossa primeira enum class
Podemos declarar:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
Leia isso conceitualmente como:
crie um tipo chamado
Seasonque possui estas opções nomeadas.
Agora temos um novo tipo:
Season
Ele não é simplesmente:
int
para o código que utiliza essa enumeração.
5. Nossa convenção de nomes
Seguiremos nossas convenções de código.
O tipo:
Season
utiliza:
PascalCase
Os valores:
Spring
Summer
Autumn
Winter
também utilizarão nomes claros em inglês e PascalCase.
Portanto:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
em vez de:
enum class season
{
SPRING,
SUMMER,
AUTUMN,
WINTER
};
Aqui não estamos tratando os enumeradores como nossas constantes nomeadas anteriores.
Estamos definindo valores pertencentes ao tipo Season.
6. Declarando uma variável do novo tipo
Antes:
int currentSeason = SUMMER;
Agora:
Season currentSeason = Season::Summer;
Podemos decompor:
Season currentSeason = Season::Summer;
│ │ │
│ │ └── valor da enumeração
│ │
│ └── nome da variável
│
└── tipo
Isso comunica muito mais.
7. Leia o código como uma frase
Season currentSeason = Season::Summer;
podemos interpretar como:
currentSeasoné umaSeasone atualmente possui o valorSummer.
Compare com:
int currentSeason = 2;
Qual deles comunica melhor o domínio?
A diferença é grande.
8. O :: apareceu novamente
Já conhecemos:
std::cout
e:
std::string
Agora temos:
Season::Summer
O operador:
::
é o operador de resolução de escopo.
Neste contexto:
Season::Summer
indica que:
Summer
é um valor pertencente ao escopo do tipo:
Season
Da mesma maneira:
Season::Winter
deixa explícito que estamos falando do Winter de Season.
9. Por que isso é melhor que nomes soltos?
Imagine que futuramente também tenhamos:
enum class EventType
{
Spring,
Summer
};
Agora podemos ter:
Season::Spring
e:
EventType::Spring
Os nomes ficam qualificados por seus tipos.
Isso reduz colisões e deixa o código mais explícito.
Essa é uma das vantagens de enum class.
10. O problema do valor 999
Voltemos à nossa limitação.
Antes:
int currentSeason = SUMMER;
currentSeason = 999;
Isso era perfeitamente compatível com o tipo.
Agora:
Season currentSeason = Season::Summer;
currentSeason = 999;
não é uma atribuição válida.
Temos:
Season
de um lado e:
int
do outro.
O compilador agora consegue nos ajudar a proteger a intenção do código.
11. Faça o erro propositalmente
Experimente:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
int main()
{
Season currentSeason = Season::Summer;
currentSeason = 999;
return 0;
}
Faça Build.
Observe o diagnóstico.
Não precisamos decorar a mensagem exata.
Pergunte:
Qual é o tipo de currentSeason?
Qual é o tipo de 999?
Existe conversão implícita permitida aqui?
O que nossa declaração está tentando proteger?
Depois restaure:
currentSeason = Season::Winter;
12. Tornando estados inválidos mais difíceis de representar
Esse é um princípio importante de Engenharia de Software.
Antes:
int currentSeason;
permitia naturalmente:
currentSeason = -10;
currentSeason = 123;
currentSeason = 999;
Agora:
Season currentSeason;
faz com que o uso normal da variável trabalhe com valores como:
Season::Spring
Season::Summer
Season::Autumn
Season::Winter
O sistema de tipos começou a representar melhor nossa regra.
13. Mas enum class não torna qualquer estado inválido literalmente impossível
Precisamos ser tecnicamente precisos.
É possível utilizar conversões explícitas para criar um valor de enumeração correspondente a um valor subjacente que não tenha um enumerador nomeado, por exemplo:
Season currentSeason = static_cast<Season>(999);
Ainda não estudamos:
static_cast
e não precisamos dele agora.
O ponto importante é:
enum classimpede que inteiros arbitrários sejam atribuídos implicitamente como fazíamos antes.
Portanto, ela torna usos inválidos muito mais difíceis de acontecer acidentalmente.
Não devemos vender a ideia incorreta de que um enum class torna matematicamente impossível qualquer valor não enumerado em todas as circunstâncias.
14. enum class e switch trabalham muito bem juntos
Nosso switch anterior utilizava:
int currentSeason
Agora podemos escrever:
switch (currentSeason)
{
case Season::Spring:
std::cout << "Spring" << std::endl;
break;
case Season::Summer:
std::cout << "Summer" << std::endl;
break;
case Season::Autumn:
std::cout << "Autumn" << std::endl;
break;
case Season::Winter:
std::cout << "Winter" << std::endl;
break;
}
Agora cada case comunica exatamente qual estado representa.
15. Nossa implementação completa inicial
#include <iostream>
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
int main()
{
Season currentSeason = Season::Summer;
switch (currentSeason)
{
case Season::Spring:
std::cout << "Spring" << std::endl;
break;
case Season::Summer:
std::cout << "Summer" << std::endl;
break;
case Season::Autumn:
std::cout << "Autumn" << std::endl;
break;
case Season::Winter:
std::cout << "Winter" << std::endl;
break;
}
return 0;
}
Resultado:
Summer
Nosso código ficou mais próximo da linguagem do problema.
16. Antes e depois
Antes
const int SPRING = 1;
const int SUMMER = 2;
const int AUTUMN = 3;
const int WINTER = 4;
int currentSeason = SUMMER;
Depois
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
Season currentSeason = Season::Summer;
Compare mentalmente:
int
com:
Season
O segundo tipo comunica uma informação que o primeiro não conseguia comunicar.
17. Não precisamos escolher números manualmente
Observe nossa enumeração:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
Não escrevemos:
1
2
3
4
O compilador associa valores subjacentes aos enumeradores.
Para nosso código atual, não precisamos nos preocupar com quais números específicos estão por trás deles.
Nossa intenção está nos nomes.
18. Podemos definir valores explícitos?
Sim.
Seria possível escrever:
enum class Season
{
Spring = 1,
Summer = 2,
Autumn = 3,
Winter = 4
};
Mas precisamos perguntar:
nosso programa realmente depende desses números?
Neste momento, não.
Então preferiremos:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
Menos detalhes desnecessários.
19. Quando valores explícitos podem fazer sentido?
No futuro poderemos encontrar situações como:
protocolo externo
formato persistido
compatibilidade de arquivo
integração com API
formato binário
serialização versionada
em que valores numéricos explícitos tenham importância.
Aí definiremos esses valores conscientemente.
Não faremos isso antecipadamente.
20. enum class cria um tipo forte e separado
Imagine:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
enum class Direction
{
North,
South,
East,
West
};
Agora temos dois tipos diferentes:
Season
Direction
Então isto não representa uma atribuição válida:
Season currentSeason = Season::Summer;
Direction playerDirection = Direction::North;
currentSeason = playerDirection;
Mesmo que internamente enumerações possuam representações integrais, os tipos são distintos no código.
Isso é exatamente o tipo de proteção que queremos.
21. O compilador começa a reconhecer erros conceituais
Imagine uma função futura que receba:
Season
e alguém tente passar:
Direction
São conceitos diferentes.
Um sistema de tipos mais expressivo ajuda o compilador a detectar certas classes de erro.
Estamos começando a perceber uma ideia poderosa:
quanto melhor representamos nossos conceitos nos tipos, mais ajuda o compilador pode nos oferecer.
22. enum class não substitui bool
Suponha:
bool isAlive = true;
Não precisamos criar:
enum class AliveState
{
Alive,
Dead
};
apenas porque aprendemos enumerações.
Se uma condição realmente possui natureza booleana:
true
false
bool pode continuar sendo a melhor representação.
De novo:
nova ferramenta
≠
usar em todo lugar
23. Quando enum class começa a fazer sentido?
Pergunte:
Existe um conjunto conhecido e limitado de estados?
Os estados possuem nomes significativos?
Um bool seria insuficiente?
Usar números ou strings esconderia o domínio?
Queremos impedir misturas acidentais entre conceitos diferentes?
Se as respostas apontarem nessa direção, enum class pode ser uma excelente opção.
24. std::string também não seria necessariamente melhor
Poderíamos escrever:
std::string currentSeason = "Summer";
Parece legível.
Mas também podemos escrever:
currentSeason = "Summmer";
com três letras m.
Ou:
currentSeason = "banana";
A linguagem não sabe que essas strings são inválidas para o conceito de estação.
Com:
Season currentSeason = Season::Summer;
trabalhamos com um conjunto específico de valores nomeados pelo tipo.
25. Comparando as três representações
Número genérico
int currentSeason = 2;
Problemas:
2 significa o quê?
Qualquer int pode ser atribuído.
Texto
std::string currentSeason = "Summer";
Melhor para leitura, mas:
qualquer texto pode ser atribuído
erros de digitação são possíveis
comparações dependem de strings
enum class
Season currentSeason = Season::Summer;
Comunica:
tipo específico
+
valores nomeados
+
menos conversões implícitas
+
melhor intenção
Para nosso problema atual, essa representação é muito mais adequada.
26. O switch agora se torna mais expressivo
Compare:
switch (currentSeason)
{
case 1:
break;
case 2:
break;
}
com:
switch (currentSeason)
{
case Season::Spring:
break;
case Season::Summer:
break;
}
A segunda versão praticamente documenta a própria regra.
Não precisamos procurar:
qual número significa Summer?
O significado está diretamente no código.
27. Precisamos de default?
No capítulo anterior utilizamos:
default:
para tratar números inesperados.
Agora nosso switch trabalha com:
Season
e listamos:
Spring
Summer
Autumn
Winter
Uma possibilidade seria:
switch (currentSeason)
{
case Season::Spring:
break;
case Season::Summer:
break;
case Season::Autumn:
break;
case Season::Winter:
break;
default:
break;
}
Mas existe um detalhe interessante.
28. Em enums, default nem sempre é automaticamente a melhor escolha
Imagine que futuramente adicionemos:
enum class Season
{
Spring,
Summer,
Autumn,
Winter,
StormSeason
};
Mas esqueçamos de alterar o switch.
Dependendo do compilador e das configurações de warning, um switch sobre uma enumeração sem default pode permitir que o compilador nos avise sobre um enumerador não tratado.
Se sempre tivermos:
default:
esse tipo de diagnóstico pode ficar menos útil em determinadas configurações.
Portanto, para enumerações fechadas controladas por nós, não adotaremos a regra:
sempre coloque
default.
A decisão dependerá do contexto.
29. Para nosso exercício, vamos tratar todos os valores nomeados
Podemos manter:
switch (currentSeason)
{
case Season::Spring:
std::cout << "Spring" << std::endl;
break;
case Season::Summer:
std::cout << "Summer" << std::endl;
break;
case Season::Autumn:
std::cout << "Autumn" << std::endl;
break;
case Season::Winter:
std::cout << "Winter" << std::endl;
break;
}
Temos quatro enumeradores.
Temos quatro case.
A intenção é cobrir explicitamente todos os estados conhecidos.
30. Teste todos os estados
Comece:
Season currentSeason = Season::Spring;
Esperado:
Spring
Depois:
Season currentSeason = Season::Summer;
Esperado:
Summer
Depois:
Season currentSeason = Season::Autumn;
Esperado:
Autumn
Finalmente:
Season currentSeason = Season::Winter;
Esperado:
Winter
Uma enumeração com quatro estados nos dá quatro cenários naturais de validação.
31. Um exemplo um pouco mais contextual
Podemos aproveitar nosso futuro farming game sem construir o sistema de estações de verdade.
#include <iostream>
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
int main()
{
Season currentSeason = Season::Autumn;
std::cout << "=== CURRENT SEASON ===" << std::endl;
switch (currentSeason)
{
case Season::Spring:
std::cout << "Spring" << std::endl;
std::cout << "Flowers begin to bloom." << std::endl;
break;
case Season::Summer:
std::cout << "Summer" << std::endl;
std::cout << "Days are longer and warmer." << std::endl;
break;
case Season::Autumn:
std::cout << "Autumn" << std::endl;
std::cout << "Leaves begin to fall." << std::endl;
break;
case Season::Winter:
std::cout << "Winter" << std::endl;
std::cout << "Cold weather reaches the farm." << std::endl;
break;
}
return 0;
}
Resultado:
=== CURRENT SEASON ===
Autumn
Leaves begin to fall.
32. Isso ainda não é um sistema de estações
Precisamos manter nossa disciplina arquitetural.
Ainda não temos:
SeasonSystem
Calendar
DayCycle
Weather
CropGrowth
WorldSimulation
Temos apenas:
um tipo
+
uma variável
+
um switch
O exemplo contextualiza o conceito.
Não representa a arquitetura final do jogo.
33. Outro exemplo: direção
Podemos criar:
enum class Direction
{
North,
South,
East,
West
};
Depois:
Direction playerDirection = Direction::East;
Isso comunica muito melhor que:
int playerDirection = 3;
Ou:
std::string playerDirection = "East";
Novamente temos um conjunto fechado de estados nomeados.
34. Outro exemplo: estado do jogo
Futuramente poderemos chegar a algo como:
enum class GameState
{
MainMenu,
Playing,
Paused,
GameOver
};
Isso pode ser muito útil.
Mas não criaremos agora:
GameStateManager
SceneManager
StateMachine
Só porque já conseguimos imaginar esses nomes.
O tipo nasce quando o problema realmente existir.
35. Cuidado com enums gigantes
Também não queremos fazer:
enum class Everything
{
Player,
Farm,
Sword,
Spring,
Pause,
Enemy,
Coin,
Tree
};
Uma enumeração deve representar um conceito coerente.
Por exemplo:
Season
contém estações.
Direction
contém direções.
GameState
contém estados do jogo.
Boa modelagem continua importando.
36. enum class não elimina a necessidade de regras
Considere:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
Agora nosso tipo representa bem as estações disponíveis.
Mas ele não sabe automaticamente:
qual estação vem depois de Spring?
quantos dias possui Summer?
quais crops crescem em Autumn?
quanto tempo dura Winter?
A enumeração representa os estados possíveis.
As regras envolvendo esses estados virão depois.
Não coloque comportamento inexistente dentro do conceito apenas porque agora temos nomes melhores.
37. E o tamanho em memória?
Talvez você encontre afirmações como:
enum class ocupa 4 bytes
Isso pode acontecer em ambientes comuns, mas não devemos transformar isso em uma regra universal.
Enumerações possuem um underlying type, ou tipo subjacente.
Quando não especificamos um tipo subjacente para uma enum class, C++ utiliza int como tipo subjacente padrão.
Ainda assim, nossa decisão de utilizar enumeração aqui não está baseada em economia de memória.
Está baseada em:
modelagem
clareza
type safety
intenção
38. Podemos controlar o tipo subjacente
C++ permite algo como:
enum class Season : unsigned char
{
Spring,
Summer,
Autumn,
Winter
};
Mas não temos nenhuma necessidade real disso neste momento.
Escolher um tipo subjacente menor apenas porque sabemos que quatro estados cabem nele seria otimização e complexidade sem benefício demonstrado.
Então manteremos:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
Simples e claro.
39. Não precisamos pensar em memória antes do problema existir
Nosso programa possui uma variável:
Season currentSeason;
Não temos milhões de entidades contendo essa informação.
Não medimos nenhum problema.
Portanto:
não existe problema de memória demonstrado
↓
não existe otimização necessária
Quando desempenho e layout de dados importarem, mediremos.
40. Microdesafio — direção
Crie:
enum class Direction
{
North,
South,
East,
West
};
Depois:
Direction playerDirection = Direction::North;
Utilize:
switch (playerDirection)
para imprimir:
Moving North.
Moving South.
Moving East.
Moving West.
Teste todos os valores.
41. Uma possível solução
#include <iostream>
enum class Direction
{
North,
South,
East,
West
};
int main()
{
Direction playerDirection = Direction::East;
switch (playerDirection)
{
case Direction::North:
std::cout << "Moving North." << std::endl;
break;
case Direction::South:
std::cout << "Moving South." << std::endl;
break;
case Direction::East:
std::cout << "Moving East." << std::endl;
break;
case Direction::West:
std::cout << "Moving West." << std::endl;
break;
}
return 0;
}
Resultado:
Moving East.
42. Microdesafio — misturando tipos errados
Crie também:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
enum class Direction
{
North,
South,
East,
West
};
Depois:
Season currentSeason = Season::Summer;
Direction playerDirection = Direction::North;
Agora tente:
currentSeason = playerDirection;
Faça Build.
Observe como o compilador rejeita a mistura.
Isso demonstra concretamente que:
Season
e:
Direction
são tipos diferentes.
43. Microdesafio — tente usar um inteiro
Com:
Season currentSeason = Season::Spring;
tente:
currentSeason = 3;
Compile.
Depois corrija:
currentSeason = Season::Autumn;
Compare a intenção das duas formas.
44. Não faça casts apenas para derrotar o sistema de tipos
Se o compilador rejeitar:
currentSeason = 3;
não pense imediatamente:
vou descobrir como forçar a conversão.
A pergunta primeiro deve ser:
por que estou tentando colocar um inteiro dentro de
Season?
Talvez o problema esteja no modelo ou na origem dos dados.
Conversões explícitas têm usos legítimos.
Mas não devem ser utilizadas apenas para silenciar o compilador.
45. Código final do capítulo
Nossa evolução principal ficará assim:
#include <iostream>
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
int main()
{
Season currentSeason = Season::Summer;
std::cout << "=== CURRENT SEASON ===" << std::endl;
switch (currentSeason)
{
case Season::Spring:
std::cout << "Spring" << std::endl;
std::cout << "Flowers begin to bloom." << std::endl;
break;
case Season::Summer:
std::cout << "Summer" << std::endl;
std::cout << "Days are longer and warmer." << std::endl;
break;
case Season::Autumn:
std::cout << "Autumn" << std::endl;
std::cout << "Leaves begin to fall." << std::endl;
break;
case Season::Winter:
std::cout << "Winter" << std::endl;
std::cout << "Cold weather reaches the farm." << std::endl;
break;
}
return 0;
}
Resultado:
=== CURRENT SEASON ===
Summer
Days are longer and warmer.
46. Comparando a evolução completa
Começamos com:
int currentSeason = 2;
Depois:
const int SUMMER = 2;
int currentSeason = SUMMER;
Agora:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
Season currentSeason = Season::Summer;
Nossa evolução foi:
NÚMERO
↓
NÚMERO COM NOME
↓
TIPO COM ESTADOS NOMEADOS
Essa é uma evolução real de modelagem.
47. O código ficou maior ou menor?
Se contarmos caracteres, talvez a enumeração tenha adicionado algumas linhas.
Mas Engenharia de Software não mede qualidade apenas pela menor quantidade de texto.
Agora o compilador sabe diferenciar:
Season
de:
Direction
E o leitor vê:
Season::Summer
em vez de:
2
Nosso código ficou mais expressivo.
48. Quando não usar enum class
Não crie uma enumeração apenas porque uma variável possui alguns valores atuais.
Por exemplo:
int playerHealth = 100;
Não faria sentido criar:
enum class Health
{
Zero,
One,
Two,
Three
};
Health é naturalmente um valor numérico no nosso modelo atual.
Da mesma maneira:
float playerSpeed = 4.5f;
não precisa virar uma enumeração.
Use enum class quando o problema realmente envolver um conjunto discreto e nomeado de possibilidades.
49. Nossa arquitetura ainda continua mínima
Apesar de termos criado um novo tipo, nossa estrutura continua:
CppGameEngine/
└── main.cpp
A declaração:
enum class Season
pode continuar dentro de nosso arquivo atual para este estágio didático.
Não precisamos criar:
Enums/
Domain/
GameStates/
Season/
por causa de uma enumeração.
Quando o projeto crescer e os tipos começarem a ser reutilizados em diferentes arquivos, reorganizaremos a estrutura conscientemente.
50. Validando a implementação
Faça:
Ctrl + S
Depois:
Ctrl + Shift + B
Confirme:
Build succeeded
Execute:
Ctrl + F5
Teste:
Season currentSeason = Season::Spring;
Depois:
Season currentSeason = Season::Summer;
Depois:
Season currentSeason = Season::Autumn;
Finalmente:
Season currentSeason = Season::Winter;
Todos os estados devem produzir o comportamento correspondente.
51. Testes negativos
Também queremos confirmar aquilo que não deve funcionar.
Tente:
currentSeason = 999;
Esperado:
erro de compilação
Tente criar:
Direction playerDirection = Direction::North;
e depois:
currentSeason = playerDirection;
Esperado:
erro de compilação
Esses testes negativos demonstram uma vantagem importante do novo modelo.
O compilador está impedindo determinadas combinações incorretas.
52. Checklist de compreensão
Antes de avançar, tente responder:
Qual problema existia em
int currentSeason?Por que constantes nomeadas melhoraram, mas não resolveram completamente o problema?
O que é uma enumeração?
O que
enum class Seasoncria?Por que escrevemos
Season::Summer?O que significa
::nesse contexto?Por que
SeasoneDirectionsão tipos distintos?Por que
currentSeason = 999;não funciona mais implicitamente?enum classtorna literalmente impossível qualquer valor não enumerado?Por que não utilizamos números explícitos para cada estação?
Por que não especificamos um tipo subjacente menor?
Quando
enum classé uma representação adequada?Quando
bool,int,floatou outro tipo simples continua sendo melhor?Por que
switchcombina tão bem com enumerações?
Se você consegue responder essas perguntas sem apenas repetir a sintaxe, o objetivo do capítulo foi atingido.
53. Git
Depois de validar:
git status
Adicione:
git add CppGameEngine/main.cpp
Ajuste o caminho caso sua estrutura local seja diferente.
Commit sugerido:
git commit -m "refactor: replace season integers with enum class"
Observe que usamos:
refactor
em vez de:
feat
porque a principal mudança deste capítulo é melhorar a representação do conceito já existente sem introduzir uma funcionalidade de negócio completamente nova.
Nosso comportamento de estação continua essencialmente o mesmo.
A modelagem ficou melhor.
54. Atualizando o CHANGELOG
Em [Unreleased]:
### Changed
- Replaced integer-based season values with a strongly typed `Season` enum class.
- Updated season selection to use named `Season` values.
- Updated season `switch` cases to use scoped enumerators.
### Refactored
- Removed integer constants previously used to represent seasons.
- Improved type safety and semantic clarity for season state.
Depois:
git add CHANGELOG.md
git commit -m "docs: update changelog with season enum refactor"
O ROADMAP técnico continua sem necessidade de alteração neste ponto.
55. O que aprendemos?
No capítulo anterior tínhamos:
const int SPRING = 1;
const int SUMMER = 2;
const int AUTUMN = 3;
const int WINTER = 4;
int currentSeason = SUMMER;
Hoje evoluímos para:
enum class Season
{
Spring,
Summer,
Autumn,
Winter
};
Season currentSeason = Season::Summer;
Aprendemos que:
uma enumeração representa um conjunto de valores nomeados;
enum classcria um tipo específico e com escopo;Season::Summerdeixa explícito a qual tipo aquele valor pertence;SeasoneDirectionnão são intercambiáveis;inteiros arbitrários não são atribuídos implicitamente a uma
enum class;o sistema de tipos pode impedir determinadas classes de erro;
enum classcombina muito bem comswitch;valores explícitos para enumeradores só devem ser definidos quando houver motivo;
otimizar o tipo subjacente sem necessidade seria prematuro;
defaultem umswitchsobre enumeração merece uma decisão consciente;enumerações representam estados possíveis, não todas as regras relacionadas a esses estados;
tipos mais expressivos aproximam o código do domínio.
Conclusão
Nosso programa começou representando uma estação assim:
int currentSeason = 2;
Isso funcionava.
Mas exigia conhecimento externo:
2 significa Summer.
Depois melhoramos:
const int SUMMER = 2;
int currentSeason = SUMMER;
Agora o código carregava um nome.
Mas o tipo ainda dizia:
qualquer inteiro
Finalmente chegamos a:
Season currentSeason = Season::Summer;
Agora nome e tipo trabalham juntos.
Podemos visualizar nossa evolução:
2
↓
SUMMER
↓
Season::Summer
Cada etapa reduziu uma parte da ambiguidade.
Essa é uma ideia que aparecerá muitas vezes durante nossa jornada:
um bom tipo não serve apenas para armazenar dados; ele também comunica quais dados fazem sentido naquele contexto.
E isso é muito maior que enumerações.
Conforme avançarmos em C++, veremos o sistema de tipos sendo utilizado para representar cada vez melhor as regras e conceitos do nosso jogo.
Mas agora existe outro problema.
Nosso programa já sabe tomar decisões.
Consegue escolher entre múltiplas opções.
Possui estados nomeados.
Só que ainda executa suas instruções essencialmente uma vez.
Imagine que quiséssemos algo como:
mostrar Day 1
mostrar Day 2
mostrar Day 3
mostrar Day 4
mostrar Day 5
Poderíamos escrever:
std::cout << "Day 1" << std::endl;
std::cout << "Day 2" << std::endl;
std::cout << "Day 3" << std::endl;
std::cout << "Day 4" << std::endl;
std::cout << "Day 5" << std::endl;
Mas e se quisermos repetir uma ação:
10 vezes?
100 vezes?
enquanto uma condição permanecer verdadeira?
Copiar e colar instruções claramente não será uma boa solução.
Precisamos ensinar nosso programa a repetir comportamento.
Próximo capítulo
Capítulo 14 — Loops com while
Série: Do zero ao jogo comercial com C++ e SFML
No próximo artigo começaremos com um problema extremamente simples:
Day 1
Day 2
Day 3
Day 4
Day 5
e perceberemos que repetir manualmente:
std::cout
std::cout
std::cout
std::cout
std::cout
não escala.
Queremos chegar a uma ideia como:
ENQUANTO uma condição for verdadeira
↓
repita uma ação
Será o momento de conhecer:
while
Vamos entender:
por que loops existem;
condição de repetição;
contador;
atualização do estado;
quando o loop termina;
o que acontece quando a condição já começa falsa;
por que esquecer de atualizar a condição pode gerar um loop infinito;
como investigar um programa que parece ter travado;
como decisões e repetições começam a trabalhar juntas;
e como esse conceito começará a preparar o terreno para algo central em jogos: repetir atualizações continuamente.
Ainda não construiremos nosso Game Loop real.
Primeiro precisamos entender o que significa repetir uma instrução de forma controlada.
Nosso programa já sabe escolher.
Agora ele aprenderá a repetir.
Comentários
Ainda não há comentários. Sê o primeiro a comentar!