Aprenda Programação: Registros (Structs ou Records)
Créditos para a imagem: Imagem criada pelo autor usando o programa Spectacle.
Pré-Requisitos
Na introdução sobre ambientes de desenvolvimento, indiquei Python, Lua e JavaScript como boas escolhas de linguagens de programação para iniciantes. Posteriormente, comentei sobre GDScript como opção para pessoas que tenham interesse em programar jogos digitais ou simulações. Para as atividades de introdução a programação, você precisará de, no mínimo, um ambiente de desenvolvimento configurado em uma das linguagens anteriores.
Caso queira experimentar programação sem configurar um ambiente, você pode usar um dos editores online que criei:
Contudo, eles não possuem todos os recursos dos interpretadores para as linguagens. Assim, cedo ou tarde, você precisará configurar um ambiente de desenvolvimento. Caso precise configurar um, confira os recursos a seguir.
Assim, se você tem um Ambiente Integrado de Desenvolvimento (em inglês, Integrated Development Environment ou IDE) ou a combinação de editor de texto com interpretador, você está pronto para começar. Os exemplos assumem que você saiba executar código em sua linguagem escolhida, como apresentado nas páginas de configuração.
Caso queira usar outra linguagem, a introdução provê links para configuração de ambientes para as linguagens C, C++, Java, LISP, Prolog e SQL (com SQLite). Em muitas linguagens, basta seguir os modelos da seção de experimentação para adaptar sintaxe, comandos e funções dos trechos de código. C e C++ são exceções, por requerem o uso de ponteiros para acesso à memória.
Abstração e Decomposição de Dados
Uma coleção (como um vetor ou um dicionário) permite armazenar valores em uma única variável. Algumas linguagens (sobretudo linguagens de alto nível) permitem armazenar valores de diferentes tipos em uma mesma variável de coleção (por exemplo, em um vetor). Outras linguagens (especialmente linguagens de nível mais baixo, como C e C++) admitem armazenar um único tipo de dados em uma variável de coleção.
Algo similar ocorre com funções (ou subrotinas). Algumas linguagens (como Lua e Python) permitem o retorno de múltiplos valores; outras (como JavaScript e GDScript) impõe que uma função possa retornar um único valor.
No caso de subrotinas, uma forma de retornar vários valores consiste no uso de coleções. No caso de estruturas de dados, pode-se usar técnicas como vetores paralelos como forma de distribuir diversos valores para uma mesma entidade.
Todavia, existe uma forma alternativa para evitar o problema, que é aplicável tanto para o caso de subrotinas quanto para coleções. Registros (structures, structs ou records) permitem agrupar múltiplos dados para a composição de um novo tipo composto de dados. Uma variável do tipo de um registro combina várias outras variáveis em uma só.
Assim como subrotinas permitem realizar decomposição funcional, registros fornecem uma forma de exercer uma decomposição de dados de um programa (ela também possa ser pensada como uma composição de dados, dependendo do sentido escolhido).
Isso permite pensar nos dados de um programa em um nível mais alto, expandindo o conceito de abstração de dados.
Por exemplo, ao invés de pensar em dados de uma pessoa como nome, idade e gênero, pode-se definir um tipo de dados Pessoa
que seja composto por nome, idade e gênero.
Toda variável criada usando o tipo Pessoa
terá os dados pertinentes para uma pessoa no problema ou domínio considerado.
Registros (Structs ou Records) e Classes
Muitas linguagens de programação fornecem recursos para que uma programadora ou um programador possa criar seus próprios tipos de dados. Os recursos, contudo, podem variar.
Linguagens imperativas (paradigma procedural ou imperativo) costumam permitir a definição de registros ou estruturas (structures, structs ou records). Em geral, registros permitem combinar dados de tipos pré-existentes em um novo tipo. Os dados de um registro são comumente chamados de atributos (attributes) ou campos (fields). Os valores de atributos ou campos definem o estado do registro. Essa é a abordagem mais simples e básica para a criação de tipos compostos.
Em linguagens que permitam armazenar referências para subrotinas em variáveis, um registro pode conter dados e subrotinas. Em linguagens que não permitem, um registro pode ter apenas dados. Para diferenciar ambas, pode-se usar o termo Plain Old Data (POD) ou Passive Data Structure (PDS) para se referir a registros que contenham apenas dados. A expressão plain old pode ser entendida como "bom e velho"; ou seja, os bons e velhos dados -- simples, confiáveis e sem extravagâncias. Em outras palavras, sem surpresas: dados puros, não misturados com código.
Linguagens orientadas a objeto (paradigma da Programação Orientada a Objetos (POO) ou Object-Oriented Programming (OOP)) permitem criar classes. Uma classe permitem combinar atributos com subrotinas para processá-los, chamadas métodos. Uma classe tende a ser contrário de um POD, porque ela permite misturar dados e código em uma única estrutura, chamada objeto. Um objeto é uma instância de uma classe.
Classes podem ser muito mais poderosas, sofisticadas e versáteis que PODs (doravante chamados registros). Em classes, pode-se usar técnicas como data hiding (que engloba o conceito de encapsulamento), herança, polimorfismo e delegação para definir processamentos complexos. Por exemplo, é possível permitir ou restringir o acesso a dados dependendo de como a classe é definida; em potencial, é possível restringir alterações de dados apenas usando métodos específicos. Assim, em classes, os dados podem ser meros detalhes. A manipulação de um objeto pode ser feita por vias exclusivas de métodos. Em outras palavras, pode-se abstrair os dados e operá-los usando-se interfaces. Em boas implementações usando OOP, pode-se manter-se uma mesma interface e modificar-se a definição de dados. O programa continuará a funcionar como se nunca mudara.
Em registros, existem apenas dados como atributos. Dados são variáveis que podem ser alteradas em qualquer lugar. Pode-se criar subrotinas para manipulá-los, mas nada impede modificações diretas. Atributos em registros são, portanto, modificáveis em qualquer local em que a variável seja acessível. Os atributos são reunidos apenas em um registro para conveniência de uso e acesso. Trata-se de um agrupamento ou composição, simples e sem surpresas.
Em pseudocódigo, um registro pode ser tão simples como a seguinte representação:
registro nome_do_registro
inicio
atributo1: tipo1
atributo2: tipo2
// ...
atributoN: tipoN
fim
variavel variavel_registro: nome_do_registro
variavel_registro.atributo1 = ...
variavel_registro.atributo2 = ...
// ...
variavel_registro.atributoN = ...
O tipo de um atributo pode ser qualquer tipo definido anteriormente. Isso significa que ele pode ser um tipo primitivo, um tipo composto (como os estudados anteriormente), e, em muitas linguagens de programação, de um tipo de registro declarado anteriormente.
Em geral, registros podem ser entendidos como um tipo de dados composto por outros tipos de dados.
Linguagens de programação fornecerão um operador (tipicamente um ponto como .
, no qual o nome da variável precede o ponto, e o nome do atributo sucede o ponto) para acesso a cada atributo definido.
Para o exemplo de um tipo Pessoa
, o pseudocódigo ficaria:
registro Pessoa
inicio
nome: caracteres
idade: inteiro
genero: caracteres
fim
variavel franco: Pessoa
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
variavel voce: Pessoa
voce.nome = "???"
voce.idade = 4321
voce.genero = "???"
Pessoa
poderia ter atributos para pronomes, para uma solução mais inclusiva.
Neste tópico, segue-se a convenção de que todo nome de tipo definido por registro será iniciado por uma letra maiúscula. Essa é uma convenção usual em muitas linguagens de programação e permite distingüir rapidamente o nome de um tipo do um identificador (como o nome uma variável ou subrotina).
OOP e Mau Uso de Objetos
Registros são mais simples; simplicidade é conveniente para o aprendizado inicial. Portanto, mesmo em linguagens que permitam programação orientada o objetos (como Python, JavaScript e GDScript), este tópico usará classes como se fossem registros. A escolha requer maturidade para uso e pode não ser exatamente recomendável para iniciantes. De fato, pessoas acostumadas a OOP podem discordar da abordagem ou sentirem-se incomodadas pelo (mau) uso de objetos como registros; as reações seriam justas e aceitáveis. Contudo, elas restringiriam o potencial para aprendizado.
Como habitualmente, minha opinião é que linguagens de programação são ferramentas. Paradigmas de programação também são ferramentas. Paradigmas são como lentes que auxiliam entender, modelar e resolver problemas. Eles não são soluções, embora possam influenciar como ocorrerá a construção de uma solução. As lentes destacam certos detalhes em detrimento de outros. Computação sempre possui contrapartidas.
A orientação a objetos não é um paradigma superior a todos os outros. Como qualquer outro paradigma, OOP tem benefícios e limitações. Entender registros como dados poderá permitir, posteriormente, usar classes e objetos com maior simplicidade e elegância, ao invés de explorar excessivamente idiomas e padrões de OOP mesmo quando eles não sejam realmente adequados ou necessários.
Assim, a abordagem adotada neste tópico não é um bom exemplo de OOP, principalmente nos exemplos iniciais. A escolha é intencional, pois OOP poderá ser detalhada no futuro com profundidade adequada. Ao invés de orientação a objetos, os paradigmas explorados neste tópico são imperativo e procedural.
Neste momento, o foco são os dados armazenados em tipos compostos. Em qualquer paradigma de programação, é fundamental entender o fluxos de dados de um programa. Compreender um problema e modelá-los em dados faz parte de toda solução. Planejar e conceber o fluxo de dados do programa é importante; para boas arquiteturas de software, entender o fluxo contribui para a criação de boas abstrações. Por outro lado, começar a criar abstrações antes de entender o problema pode levar a problemas de arquitetura a médio ou longo prazo. Criar classes e tipos de dados indiscriminadamente também, especialmente caso eles sejam forçadas para partes ou situações em que são inadequadas ou desnecessárias.
Dados não requerem OOP; para processá-los, pode-se usar todos os recursos explorados anteriormente. A abordagem escolhida permitirá explicar como se criar um modelo simples de orientação a objetos, com alguns recursos básicos. Para isso, bastará explorar conceitos já discutidos. Da simplicidade para a complexidade, em passos incrementais.
Linguagens de Programação Textual: JavaScript, Python, Lua e GDScript
JavaScript, Python e GDScript permitem programação orientada a objetos. Lua não possui um conceito para registros nem para classes, embora seja possível implementar muitos recursos de OOP por meio de tabelas.
class Pessoa {
constructor() {
this.nome = ""
this.idade = 0
this.genero = ""
}
}
// Também é possível fazer:
// class Pessoa {
// nome = ""
// idade = 0
// genero = ""
//}
let franco = new Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
// O nome poderia ser nova_pessoa() ou new_pessoa() ou Pessoa() para ficar
// mais próximo de outras linguagens de programação.
function crie_pessoa() {
// JavaScript Object.
return {
"nome": "",
"idade": 0,
"genero": ""
}
// Ou:
// return {
// nome: "",
// idade: 0,
// genero: ""
// }
}
let franco = crie_pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
class Pessoa:
def __init__(self):
self.nome = ""
self.idade = 0
self.genero = ""
franco = Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
-- O nome poderia ser nova_pessoa() ou new_pessoa() ou Pessoa() para ficar
-- mais próximo de outras linguagens de programação.
function crie_pessoa()
return {
nome = "",
idade = 0,
genero = ""
}
end
local franco = crie_pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
extends Node
# Inner class.
class Pessoa:
# Caso se omita um valor inicial, ele será null.
var nome = ""
var idade = 0
var genero = ""
# Um método _init() é opcional, como construtor.
func _ready():
var franco = Pessoa.new()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
extends Node
# Arquivo como classe.
# Nome do arquivo é o nome da classe.
# Também é possível escolher um nome personalizado para a classe usando:
# class_name Pessoa
# Caso se omita um valor inicial, ele será null.
var nome
var idade
var genero
func _init():
nome = ""
idade = 0
genero = ""
func _ready():
nome = "Franco"
idade = 1234
genero = "Masculino"
var franco = self
printt(self, franco)
printt(nome, self.nome, franco.nome)
printt(idade, self.idade, franco.idade)
printt(genero, self.genero, franco.genero)
Documentação para criação de registros ou classes:
- JavaScript: documentação;
- Python: documentação;
- Lua: não possui comandos para criação de registros ou classes. Contudo, é possível criar tabelas (documentação) para uso como registros ou classes simples;
- GDScript:
- Arquivo como classe: documentação;
- Classe interna (inner class): documentação.
JavaScript, Python e GDScript permitem a definição de classes.
Algo que se pode notar é a existência de uma nova palavra reservada para referenciar a própria instância do objeto manipulado pela classe.
Ela chama-se this
em JavaScript, e self
em Python e GDScript (Lua também propõe self
como convenção para a própria variável, usada quando se chama uma subrotina usando o operador :
; mais detalhes quando apropriado).
Em linguagens como GDScript, em casos em que não exista ambigüidades para se determinar a variável (por exemplo, não exista um parâmetro de subrotina não tenha o mesmo nome de um atributo), é possível omitir o uso the self
.
Caso contrário, o uso é obrigatório para se referenciar o atributo da classe ao invés da outra variável.
Em JavaScript, a introdução de classes é relativamente recente (ECMAScript 2015).
Antes de 2015, objetos em JavaScript eram criados como JavaScript Objects, previamente apresentados em Coleções como uma das alternativas de uso de dicionários.
Atualmente, pode-se escolher usar classes nativas (definidas como class
) ou JavaScript Objects.
Em GDScript, todo arquivo de código-fonte define uma classe. Em outras palavras, todos os programas escritos até agora na linguagem definiam classes. Variáveis declaradas fora de uma subrotina são atributos da classe (ao invés de variáveis globais). Todas as subrotinas declaradas em um arquivo na linguagem são métodos. Também é possível a criação de classes internas (inner classes), que podem ser usadas como registros. Uma classe interna é uma classe aninhada, ou seja, uma classe definida dentro de outra classe.
Nos códigos que definem classes, um método especial chamado construtor realiza a alocação e inicialização de um objeto (uma variável) do tipo da classes.
Um construtor é chamado automaticamente toda vez que um objeto da classe é construído, para fazer a inicialização com os valores definidos.
Isso permite que objetos em POO sempre tenham um estado inicial válido e conhecido (ao invés de aleatório, como pode ocorrer com variáveis de tipos primitivos).
Em JavaScript, o construtor chama-se constructor()
e é opcional.
Em Python, ele chama-se __init__()
, usando dois underscores antes e depois da palavra, e é obrigatório.
Atributos em Python devem ser declarados na definição de __init()__
(caso contrário, eles serão compartilhados entre todas as instâncias da classe, algo conhecido como atributo estático).
Em GDScript, ele chama-se _init()
e é opcional.
Além disso, em códigos com classe, a linguagem pode exigir o uso de uma palavra reservada para a criação de um objeto.
Em JavaScript, ela é new()
.
Em Python, deve-se usar parênteses após o nome da classe.
Em GDScript, deve-se usar .new()
após o nome da classe (interna).
Em Lua, registros podem ser criados como tabelas.
A forma mais simples é declarar uma tabela com algumas chaves do tipo cadeia de caracteres.
Para conveniência, pode-se definir uma função para criar um registro vazio.
Isso foi feito em crie_pessoa()
(tanto em Lua, quanto para JavaScript Object).
Embora opcional, a construção simplifica a criação de vários registros com os mesmos atributos.
Ao invés de duplicar código, basta chamar a subrotina.
Isso é prático porque facilita a alteração de todas as variáveis do tipo quando se alterar a definição do registro (embora referências às variáveis antigas ainda precisem ser corrigidas manualmente).
Em Python, Lua e GDScript, escrever uma variável do tipo de um registro com print()
escreverá o endereço da referência.
Em JavaScript, console.log()
escreverá os valores armazenados no objeto.
Passagem por Referência ou Passagem por Valor?
Para o uso de registros (ou classes como registros), os parágrafos anteriores são quase suficientes para começar a usá-los em programas. Um importante detalhe remanescente é saber que existem linguagens de programação que passam registros ou objetos para subrotinas por valor (por exemplo, C e C++), enquanto outras passam por referência (caso de JavaScript, Python, Lua e GDScript). Como JavaScript, Python, Lua e GDScript passam registros e objetos por referência, deve-se atentar que mudanças de valores de um parâmetro em uma subrotina afetarão a variável original passada como parâmetro, e serão persistentes após o término da chamada.
class Pessoa {
constructor() {
this.nome = ""
this.idade = 0
this.genero = ""
}
}
function inicialize_pessoa(pessoa) {
pessoa.nome = "Franco"
pessoa.idade = 1234
pessoa.genero = "Masculino"
}
let franco = new Pessoa()
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
inicialize_pessoa(franco)
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
function crie_pessoa() {
// JavaScript Object.
return {
"nome": "",
"idade": 0,
"genero": ""
}
}
function inicialize_pessoa(pessoa) {
pessoa.nome = "Franco"
pessoa.idade = 1234
pessoa.genero = "Masculino"
}
let franco = crie_pessoa()
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
inicialize_pessoa(franco)
console.log(franco)
console.log(franco.nome)
console.log(franco.idade)
console.log(franco.genero)
class Pessoa:
def __init__(self):
self.nome = ""
self.idade = 0
self.genero = ""
def inicialize_pessoa(pessoa):
pessoa.nome = "Franco"
pessoa.idade = 1234
pessoa.genero = "Masculino"
franco = Pessoa()
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
inicialize_pessoa(franco)
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
function crie_pessoa()
return {
nome = "",
idade = 0,
genero = ""
}
end
function inicialize_pessoa(pessoa)
pessoa.nome = "Franco"
pessoa.idade = 1234
pessoa.genero = "Masculino"
end
local franco = crie_pessoa()
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
inicialize_pessoa(franco)
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
extends Node
class Pessoa:
var nome
var idade
var genero
func inicialize_pessoa(pessoa):
pessoa.nome = "Franco"
pessoa.idade = 1234
pessoa.genero = "Masculino"
func _ready():
var franco = Pessoa.new()
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
inicialize_pessoa(franco)
print(franco)
print(franco.nome)
print(franco.idade)
print(franco.genero)
extends Node
var nome
var idade
var genero
func _init():
nome = ""
idade = 0
genero = ""
func inicialize_pessoa():
nome = "Franco"
idade = 1234
genero = "Masculino"
func inicialize_pessoa_com_parametro(pessoa):
pessoa.nome = "Franco Garcia"
pessoa.idade = 4321
pessoa.genero = "MASCULINO"
func _ready():
print(self)
printt(nome, self.nome)
printt(idade, self.idade)
printt(genero, self.genero)
# A chamada alterará o próprio objeto.
# No caso, a alteração de valores é um efeito colateral da chamada.
inicialize_pessoa()
print(self)
printt(nome, self.nome)
printt(idade, self.idade)
printt(genero, self.genero)
# O código anterior é equivalente ao uso de self como parâmetro.
inicialize_pessoa_com_parametro(self)
print(self)
printt(nome, self.nome)
printt(idade, self.idade)
printt(genero, self.genero)
O termo inicialize
é usado como initialize
, comumente abreviado como init
(ou setup
) em programação.
Um segundo detalhe importante refere-se à comparação em linguagens que tratam objetos como referências.
Comparações: Igualdade e Diferença
Como variáveis que armazenam os valores em JavaScript, Python, Lua e GDScript armazenam referências, é importante tomar cuidado com comparações de igualdade e diferença. Elas devem ser realizadas membro a membro, ao invés de usar o operador de cada linguagem. Caso um atributo seja uma referência, ele também deverá ser comparado adequadamente.
class Pessoa {
constructor() {
this.nome = ""
this.idade = 0
this.genero = ""
}
}
function pessoas_iguais(pessoa1, pessoa2) {
return ((pessoa1.nome === pessoa2.nome) &&
(pessoa1.idade === pessoa2.idade) &&
(pessoa1.genero === pessoa2.genero))
}
function pessoas_diferentes(pessoa1, pessoa2) {
return (!pessoas_iguais(pessoa1, pessoa2))
}
let pessoa1 = new Pessoa()
let pessoa2 = new Pessoa()
console.log(pessoa1 === pessoa2, pessoas_iguais(pessoa1, pessoa2))
console.log(pessoa1 !== pessoa2, pessoas_diferentes(pessoa1, pessoa2))
function crie_pessoa() {
// JavaScript Object.
return {
"nome": "",
"idade": 0,
"genero": ""
}
}
function pessoas_iguais(pessoa1, pessoa2) {
return ((pessoa1.nome === pessoa2.nome) &&
(pessoa1.idade === pessoa2.idade) &&
(pessoa1.genero === pessoa2.genero))
}
function pessoas_diferentes(pessoa1, pessoa2) {
return (!pessoas_iguais(pessoa1, pessoa2))
}
let pessoa1 = crie_pessoa()
let pessoa2 = crie_pessoa()
console.log(pessoa1 === pessoa2, pessoas_iguais(pessoa1, pessoa2))
console.log(pessoa1 !== pessoa2, pessoas_diferentes(pessoa1, pessoa2))
class Pessoa:
def __init__(self):
self.nome = ""
self.idade = 0
self.genero = ""
def pessoas_iguais(pessoa1, pessoa2):
return ((pessoa1.nome == pessoa2.nome) and
(pessoa1.idade == pessoa2.idade) and
(pessoa1.genero == pessoa2.genero))
def pessoas_diferentes(pessoa1, pessoa2):
return (not pessoas_iguais(pessoa1, pessoa2))
pessoa1 = Pessoa()
pessoa2 = Pessoa()
print(pessoa1 == pessoa2, pessoas_iguais(pessoa1, pessoa2))
print(pessoa1 != pessoa2, pessoas_diferentes(pessoa1, pessoa2))
function crie_pessoa()
return {
nome = "",
idade = 0,
genero = ""
}
end
function pessoas_iguais(pessoa1, pessoa2)
return ((pessoa1.nome == pessoa2.nome) and
(pessoa1.idade == pessoa2.idade) and
(pessoa1.genero == pessoa2.genero))
end
function pessoas_diferentes(pessoa1, pessoa2)
return (not pessoas_iguais(pessoa1, pessoa2))
end
local pessoa1 = crie_pessoa()
local pessoa2 = crie_pessoa()
print(pessoa1 == pessoa2, pessoas_iguais(pessoa1, pessoa2))
print(pessoa1 ~= pessoa2, pessoas_diferentes(pessoa1, pessoa2))
extends Node
class Pessoa:
var nome
var idade
var genero
func pessoas_iguais(pessoa1, pessoa2):
return ((pessoa1.nome == pessoa2.nome) and
(pessoa1.idade == pessoa2.idade) and
(pessoa1.genero == pessoa2.genero))
func pessoas_diferentes(pessoa1, pessoa2):
return (not pessoas_iguais(pessoa1, pessoa2))
func _ready():
var pessoa1 = Pessoa.new()
var pessoa2 = Pessoa.new()
printt(pessoa1 == pessoa2, pessoas_iguais(pessoa1, pessoa2))
printt(pessoa1 != pessoa2, pessoas_diferentes(pessoa1, pessoa2))
extends Node
class_name Pessoa
var nome
var idade
var genero
func _init():
nome = ""
idade = 0
genero = ""
func pessoas_iguais(pessoa):
return ((self.nome == pessoa.nome) and
(self.idade == pessoa.idade) and
(self.genero == pessoa.genero))
func pessoas_diferentes(pessoa):
return (not pessoas_iguais(pessoa))
func _ready():
var pessoa2 = get_script().new()
printt(self == pessoa2, pessoas_iguais(pessoa2))
printt(self != pessoa2, pessoas_diferentes(pessoa2))
# Não é possível usar class_name para este caso.
# var pessoa3 = Pessoa.new()
# printt(self == pessoa3, pessoas_iguais(pessoa3))
# printt(self != pessoa3, pessoas_diferentes(pessoa3))
A versão potencialmente mais confusa é a em GDScript utilizando o próprio arquivo como classe.
O método get_script()
documentação permite referenciar o próprio arquivo de código-fonte, usando para instanciar uma nova variável.
Em todos os casos anteriores, a comparação usando operadores retorna o valor incorreto, pois compara endereços.
As duas variáveis devem ser iguais, pois ambas foram inicializadas com os mesmos valores (cadeias de caracteres vazias para nome
e genero
e 0
para idade
).
A comparação usando as subrotinas retorna o valor correto.
Como os operadores de igualdade e diferença são o reverso um do outro, pode-se definir um dos dois.
Em seguida, pode-se definir o segundo como a negação do primeiro.
Para ordenação de valores em vetores de registros, também pode ser interessante definir subrotinas que informem se um valor é menor que outro (para ordem crescente), ou maior que outro (para ordem decrescente).
Por exemplo, um critério para ordenação de uma variável do tipo Pessoa
poderia ser ordem alfabética para nome.
Outro critério poderia ser idade ou gênero.
Também é possível definir prioridades para a ordenação (por exemplo, primeiro por nome, depois por idade, depois por gênero).
Para isso, basta realizar a próxima comparação com o critério imediatamente menos importante caso os valores comparados anteriormente sejam iguais.
Recursos Mais Avançados
Esta seção apresenta alguns recursos mais avançados para uso com registros e/ou classes. Eles são mais complexos, mas fornecem conveniências e praticidades para atividades de programação. Além disso, nem todo recurso estará disponível em toda linguagem de programação. Caso as subseções pareçam muito complexas, pode-se avançar para a seção Técnicas Usando Registros.
Sobrecarga de Operadores
Linguagens com recursos para sobrecarga de operadores permitir redefinir o comportamento de operadores para registros, potencialmente tornando o código mais legível. A sobrecarga de operadores foi mencionada previamente (por exemplo, em Operações Relacionais e Comparações). Neste momento, a técnica pode ser incorporada para outros operadores, como aritméticos, lógicos e relacionais.
Das linguagens consideradas para exemplos, Python e Lua permitem sobrecarregar operadores aritméticos, lógicos e relacionais.
O exemplo a seguir sobrecarrega operador de igualdade e do operador de diferença para o registro Pessoa
.
class Pessoa:
def __init__(self):
self.nome = ""
self.idade = 0
self.genero = ""
# É importante notar a indentação.
# A subrotina (método) está definida dentro da classe, no mesmo nível de
# __init__().
def __eq__(self, pessoa):
if (type(self) != type(pessoa)):
return false
return ((self.nome == pessoa.nome) and
(self.idade == pessoa.idade) and
(self.genero == pessoa.genero))
def __nq__(self, pessoa):
return (not self == pessoa)
pessoa1 = Pessoa()
pessoa2 = Pessoa()
print(pessoa1 == pessoa2)
print(pessoa1 != pessoa2)
function pessoas_iguais(pessoa1, pessoa2)
if (type(pessoa1) ~= type(pessoa2)) then
return nil
end
return ((pessoa1.nome == pessoa2.nome) and
(pessoa1.idade == pessoa2.idade) and
(pessoa1.genero == pessoa2.genero))
end
function crie_pessoa()
local dados = {
nome = "",
idade = 0,
genero = ""
}
local metatable = {
__eq = pessoas_iguais
}
local pessoa = setmetatable(dados, metatable)
return pessoa
end
local pessoa1 = crie_pessoa()
local pessoa2 = crie_pessoa()
print(pessoa1 == pessoa2)
print(pessoa1 ~= pessoa2)
Python permite sobrecarregar todos os operadores descritos na documentação. No caso dos operadores de igualdade e diferença, caso se defina apenas o operador de igualdade, Python utiliza a negação dele automaticamente para criar o operador de diferença (ou vice-versa).
Lua utiliza metatable
para sobrecarga de operadores.
O conceito já foi comentado previamente (por exemplo, em Coleções).
Este é um bom momento para explicá-lo.
Lua usa metatable
(meta-tabela) como uma forma de permitir redefinir o comportamento de operadores (e algumas operações) da linguagem.
Para isso, deve-se definir uma tabela com chaves e funções para as operações desejadas, e associá-las a outra tabela (a que receberá as operações) usando setmetatable()
(documentação).
Operadores e operações disponíveis variam de acordo com a versão da linguagem. Versões mais recentes possuem mais operadores. Por exemplo, documentação para versão 5.1 e documentação para versão 5.4. No caso de igualdade e diferença, Lua requer apenas a implementação do operador igualdade. O operador de diferença é criado automaticamente como a negação do operador de igualdade. Além disso, versões anteriores à 5.3 podem exigir uma mesma referência para a função. Por exemplo, o código a seguir funciona corretamente na versão atual (5.4) de Lua, mas não funciona até a versão 5.2. Até a versão 5.2, como a função anônima definida teria endereços diferentes, o operador sobrecarregado não seria chamado.
-- Requer Lua 5.3 ou mais recente.
function crie_pessoa()
local dados = {
nome = "",
idade = 0,
genero = ""
}
local metatable = {
__eq = function(pessoa1, pessoa2)
if (type(pessoa1) ~= type(pessoa2)) then
return nil
end
return ((pessoa1.nome == pessoa2.nome) and
(pessoa1.idade == pessoa2.idade) and
(pessoa1.genero == pessoa2.genero))
end
}
local pessoa = setmetatable(dados, metatable)
-- Os endereços de __eq serão diferentes.
-- print(metatable, getmetatable(pessoa), getmetatable(pessoa).__eq)
return pessoa
end
local pessoa1 = crie_pessoa()
local pessoa2 = crie_pessoa()
print(pessoa1 == pessoa2)
print(pessoa1 ~= pessoa2)
A linha comentada em ambos os exemplos de Lua permite obter os endereços da função __eq()
criada.
Uma alternativa possível é declarar a metatable
de forma global ou local para o arquivo e usá-la para todas as instâncias criadas (como memória compartilhada).
Como todas as variáveis criadas em crie_pessoa()
terão a mesma metatable
chamada metatable_pessoa
, os endereços definidos serão os mesmos.
Assim, o código a seguir é válido em versões anteriores à Lua 5.3 (e também em mais recentes).
Uma segunda vantagem da abordagem é que se economiza memória.
local metatable_pessoa = {
__eq = function(pessoa1, pessoa2)
if (type(pessoa1) ~= type(pessoa2)) then
return nil
end
return ((pessoa1.nome == pessoa2.nome) and
(pessoa1.idade == pessoa2.idade) and
(pessoa1.genero == pessoa2.genero))
end
}
function crie_pessoa()
local dados = {
nome = "",
idade = 0,
genero = ""
}
local pessoa = setmetatable(dados, metatable_pessoa)
return pessoa
end
local pessoa1 = crie_pessoa()
local pessoa2 = crie_pessoa()
print(pessoa1 == pessoa2)
print(pessoa1 ~= pessoa2)
Embora nem toda linguagem de programação forneça recursos para sobrecarga de operadores, eles podem ser úteis.
Em particular, eles tendem a tornar a leitura de código mais simples.
Por exemplo, ao invés de somar duas matrizes com some_matrizes(x, y)
, poder-se-ia sobrecarregar o operador +
para permitir escrever x + y
como soma de matrizes.
A vantagem ficaria mais clara ao se considerar x + y + z
, que seria escrito como some_matrizes(some_matrix(x, y), z)
sem sobrecarga de operadores.
A primeira versão é mais imediata (embora possa ocultar o custo computacional caso não se lembre que se trata de uma operação personalizada ao invés de uma soma convencional).
Além disso, deve-se escolher operadores adequados.
Por exemplo, sobrecarregar o operador /
para realizar a soma das matrizes seria confuso (embora possível).
Polimorfismo
OOP define um conceito chamado polimorfismo, também comentado previamente (por exemplo, em Subrotinas (Funções e Procedimentos)). Com classes e herança em OOP, é possível fazer com que classes filhas (ou classes derivadas) possam redefinir métodos das respectivas classes pai (ou superclasse).
Como o objetivo atual não é introduzir detalhadamente OOP, os códigos a seguir são exemplos rápidos de como usar polimorfismo com tipos compostos para redefinir o operador para conversão de dados de um registro (classe) para cadeias de caracteres.
class Pessoa {
constructor() {
this.nome = ""
this.idade = 0
this.genero = ""
}
toString() {
let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"
return resultado
}
}
let franco = new Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
console.log(franco)
console.log("" + franco)
let texto = "Olá, " + franco + "!"
console.log(texto)
function Pessoa() {
this.nome = ""
this.idade = 0
this.genero = ""
}
Pessoa.prototype.toString = function() {
let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"
return resultado
}
let franco = new Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
console.log(franco)
console.log("" + franco)
let texto = "Olá, " + franco + "!"
console.log(texto)
class Pessoa:
def __init__(self):
self.nome = ""
self.idade = 0
self.genero = ""
def __str__(self):
resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"
return resultado
def __eq__(self, pessoa):
if (type(self) != type(pessoa)):
return false
return ((self.nome == pessoa.nome) and
(self.idade == pessoa.idade) and
(self.genero == pessoa.genero))
def __nq__(self, pessoa):
return (not self == pessoa)
franco = Pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
print(franco)
texto = "Olá, " + str(franco) + "!"
print(texto)
function pessoas_iguais(pessoa1, pessoa2)
if (type(pessoa1) ~= type(pessoa2)) then
return nil
end
return ((pessoa1.nome == pessoa2.nome) and
(pessoa1.idade == pessoa2.idade) and
(pessoa1.genero == pessoa2.genero))
end
function pessoa_para_string(pessoa)
local resultado = pessoa.nome .. " (" .. pessoa.idade .. " anos, gênero " .. pessoa.genero .. ")"
return resultado
end
function concatene(x, y)
local resultado = tostring(x) .. tostring(y)
return resultado
end
local metatable_pessoa = {
__eq = pessoas_iguais,
__tostring = pessoa_para_string,
__concat = concatene
}
function crie_pessoa()
local dados = {
nome = "",
idade = 0,
genero = ""
}
local pessoa = setmetatable(dados, metatable_pessoa)
return pessoa
end
local franco = crie_pessoa()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
print(franco)
print(tostring(franco))
print("" .. franco)
texto = "Olá, " .. franco .. "!"
print(texto)
extends Node
class Pessoa:
var nome
var idade
var genero
func _to_string():
var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"
return resultado
func _ready():
var franco = Pessoa.new()
franco.nome = "Franco"
franco.idade = 1234
franco.genero = "Masculino"
print(franco)
var texto = "Olá, " + str(franco) + "!"
print(texto)
extends Node
var nome
var idade
var genero
func _init():
nome = ""
idade = 0
genero = ""
func _ready():
nome = "Franco"
idade = 1234
genero = "Masculino"
var franco = self
printt(self, franco)
var texto = "Olá, " + str(franco) + "!"
print(texto)
texto = "Olá, " + str(self) + "!"
print(texto)
func _to_string():
var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"
return resultado
Em JavaScript, deve-se implementar o método toString()
(documentação) para converter automaticamente um classe para cadeia de caracteres.
Em Python, deve-se implementar __str__()
(documentação).
Em Lua, deve-se definir __tostring
e __concat
para a metatable
(__tostring
: documentação; __concat
: documentação para versão 5.1 e documentação para versão 5.4).
Em GDScript, deve-se implementar _to_string()
(documentação).
O uso de polimorfismo para implementar a subrotina para conversão de registro (ou objeto) para cadeia de caracteres facilita conversões implícitas (ou explícitas).
Uma segunda vantagem é comumente permitir a escrita dos conteúdos de um objeto em texto usando o comando ou subrotina padrão para escrita, como print()
.
Construtores
Um construtor é uma subrotina que aloca memória e inicializa valores iniciais de um objeto em OOP.
Para registros, um construtor pode ser uma simples função.
A função crie_pessoa()
, por exemplo, inicializa os atributos do registro com valores considerados zeros (zero para números, Falso
para valores lógicos, cadeia de caracteres vazia para texto).
Com alguns parâmetros, ela poderia inicializar a variável recém-criada com valores fornecidos pela chamada.
class Pessoa {
constructor(nome = "", idade = 0, genero = "") {
this.nome = nome
this.idade = idade
this.genero = genero
}
toString() {
let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"
return resultado
}
}
let franco = new Pessoa("Franco", 1234, "Masculino")
console.log(franco)
console.log("" + franco)
let texto = "Olá, " + franco + "!"
console.log(texto)
function Pessoa(nome = "", idade = 0, genero = "") {
this.nome = nome
this.idade = idade
this.genero = genero
}
Pessoa.prototype.toString = function() {
let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"
return resultado
}
let franco = new Pessoa("Franco", 1234, "Masculino")
console.log(franco)
console.log("" + franco)
let texto = "Olá, " + franco + "!"
console.log(texto)
class Pessoa:
def __init__(self, nome = "", idade = 0, genero = ""):
self.nome = nome
self.idade = idade
self.genero = genero
def __str__(self):
resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"
return resultado
def __eq__(self, pessoa):
if (type(self) != type(pessoa)):
return false
return ((self.nome == pessoa.nome) and
(self.idade == pessoa.idade) and
(self.genero == pessoa.genero))
def __nq__(self, pessoa):
return (not self == pessoa)
franco = Pessoa("Franco", 1234, "Masculino")
print(franco)
texto = "Olá, " + str(franco) + "!"
print(texto)
function pessoas_iguais(pessoa1, pessoa2)
if (type(pessoa1) ~= type(pessoa2)) then
return nil
end
return ((pessoa1.nome == pessoa2.nome) and
(pessoa1.idade == pessoa2.idade) and
(pessoa1.genero == pessoa2.genero))
end
function pessoa_para_string(pessoa)
local resultado = pessoa.nome .. " (" .. pessoa.idade .. " anos, gênero " .. pessoa.genero .. ")"
return resultado
end
function concatene(x, y)
local resultado = tostring(x) .. tostring(y)
return resultado
end
local metatable_pessoa = {
__eq = pessoas_iguais,
__tostring = pessoa_para_string,
__concat = concatene
}
function crie_pessoa(nome, idade, genero)
local dados = {
nome = nome or "",
idade = idade or 0,
genero = genero or ""
}
local pessoa = setmetatable(dados, metatable_pessoa)
return pessoa
end
local franco = crie_pessoa("Franco", 1234, "Masculino")
print(franco)
print(tostring(franco))
print("" .. franco)
texto = "Olá, " .. franco .. "!"
print(texto)
extends Node
class Pessoa:
var nome
var idade
var genero
func _init(nome = "", idade = 0, genero = ""):
self.nome = nome
self.idade = idade
self.genero = genero
func _to_string():
var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"
return resultado
func _ready():
var franco = Pessoa.new("Franco", 1234, "Masculino")
print(franco)
var texto = "Olá, " + str(franco) + "!"
print(texto)
extends Node
var nome
var idade
var genero
func _init(nome = "", idade = 0, genero = ""):
self.nome = nome
self.idade = idade
self.genero = genero
func _ready():
nome = "Franco"
idade = 1234
genero = "Masculino"
var franco = self
printt(self, franco)
var texto = "Olá, " + str(franco) + "!"
print(texto)
texto = "Olá, " + str(self) + "!"
print(texto)
func _to_string():
var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"
return resultado
Em muitas linguagens de programação, o nome de um parâmetro do construtor não precisa ser igual ao nome do atributo. Eles podem ser diferentes; além disso, os parâmetros da assinatura podem ser diferentes dos atributos declarados. Contudo, é bastante comum usar o mesmo nome para parâmetro e variável, quando se trata de inicialização direta. Aliás, quando todos os parâmetros são atribuídos diretamente aos atributos sem nenhum processamento (como validação), deve-se considerar se o uso de um simples registro seria melhor que a adoção de uma classe.
De qualquer forma, o uso de um mesmo nome gera ambigüidades, pois existem duas variáveis diferentes em um mesmo escopo com nomes idênticos.
Nos exemplos anteriores, o uso de this
ou self
remove ambigüidades em casos assim.
O uso de self
ou this
referencia explicitamente o valor armazenado na classe ou no registro, ao invés da variável local ou do parâmetro (que é referenciada sem o uso da palavra reservada).
Ademais, o construtor pode ter código adicional para validação de dados (ao invés de simples atribuições de valores), caso necessário. Além disso, em linguagens de programação que permitam a definição de valores padrão para parâmetros, eles podem ser usados para criar campos opcionais em construtores.
Por exemplo, em JavaScript:
new Pessoa()
cria uma variável com nome""
, idade0
e gênero""
.new Pessoa("Franco")
cria uma variável com nome"Franco"
, idade0
e gênero""
.new Pessoa("Franco", 1234)
cria uma variável com nome"Franco"
, idade1234
e gênero""
.new Pessoa("Franco", 1234, "Masculino")
cria uma variável com nome"Franco"
, idade1234
e gênero"Masculino"
.
É importante notar que não se pode omitir parâmetros intermediários.
Por exemplo, new Pessoa("Franco", "Masculino")
atribuiria "Masculino"
à idade
.
Como JavaScript não faz verificação de tipos, a atribuição seria válida.
Uma forma de evitar o problema seria usar asserções, como será comentado em uma subseção.
Atenção.
Em Python, a inicialização de um parâmetro padrão não pode ser um tipo de referência; todo tipo de referência deve ser inicializado para cada objeto.
Caso contrário, os dados podem ser compartilhados.
Assim, exceto caso o valor seja de um tipo primitivo, é melhor usar None
como valor padrão, para, então, inicializar o valor desejado (por exemplo, um vetor, dicionário ou objeto) no construtor.
Subrotinas como Construtores
Pode ser interessante observar que, caso se alterasse o nome de crie_pessoa()
para Pessoa()
, o resultado em Lua tornar-se-ia bastante similar a construtores em linguagens de programação com suporte para classes.
-- Restante da implementação...
function Pessoa(nome, idade, genero)
local dados = {
nome = nome or "",
idade = idade or 0,
genero = genero or ""
}
local pessoa = setmetatable(dados, metatable_pessoa)
return pessoa
end
local franco = Pessoa("Franco", 1234, "Masculino")
O resultado é uma subrotina para criar um registro de forma similar a Python e JavaScript.
Outra possibilidade seria incorporar uma subrotina para construção (por exemplo, new()
) em uma tabela chamada Pessoa
.
-- Restante da implementação...
local Pessoa = {
new = function(nome, idade, genero)
local dados = {
nome = nome or "",
idade = idade or 0,
genero = genero or "",
}
local pessoa = setmetatable(dados, metatable_pessoa)
return pessoa
end
}
local franco = Pessoa.new("Franco", 1234, "Masculino")
No segundo caso, o resultado é semelhante à instanciação de objetos feita em GDScript: Pessoa.new()
.
Assim, conhecendo-se os fundamentos e técnicas de programação, pode-se usar recursos existentes em uma linguagem para incorporar algumas funcionalidades presentes em outras. De certa forma, é como criar sua própria linguagem personalizada (ou dialeto personalizado) usando a linguagem original como matéria-prima. Embora o resultado nem sempre seja idiomático, incorporar recursos que permitam a você usar uma linguagem com mais eficiência é algo conveniente e poderoso.
Como de costume, linguagens de programação são ferramentas. Em potencial, elas são ferramentas que podem criar novas ferramentas. Meta-ferramentas, por assim dizer. Adapte-as conforme suas necessidades para que elas possam atendê-la ou atendê-lo cada vez melhor.
Parâmetros Nomeados
Embora não seja possível da forma convencional (usando parâmetros posicionais), existem formas de omitir parâmetros intermediários. A primeira delas é mais restrita, embora a melhor alternativa. Algumas linguagens de programação (como Python) fornecem um recurso chamado parâmetro nomeado (named parameter). Por exemplo, em Python:
Pessoa()
cria uma variável com nome""
, idade0
e gênero""
.Pessoa("Franco")
cria uma variável com nome"Franco"
, idade0
e gênero""
.Pessoa("Franco", 1234)
cria uma variável com nome"Franco"
, idade1234
e gênero""
.Pessoa("Franco", 1234, "Masculino")
cria uma variável com nome"Franco"
, idade1234
e gênero"Masculino"
.Pessoa(nome = "Franco")
cria uma variável com nome"Franco"
, idade0
e gênero""
.Pessoa(nome = "Franco", idade = 1234)
cria uma variável com nome"Franco"
, idade1234
e gênero""
.Pessoa(nome = "Franco", genero = "Masculino")
cria uma variável com nome"Franco"
, idade0
e gênero"Masculino"
.
Com parâmetros nomeados, a ordem e a quantidade de parâmetros não importa.
class Pessoa:
def __init__(self, nome = "", idade = 0, genero = ""):
self.nome = nome
self.idade = idade
self.genero = genero
def __str__(self):
resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"
return resultado
def __eq__(self, pessoa):
if (type(self) != type(pessoa)):
return false
return ((self.nome == pessoa.nome) and
(self.idade == pessoa.idade) and
(self.genero == pessoa.genero))
def __nq__(self, pessoa):
return (not self == pessoa)
print(Pessoa())
print(Pessoa("Franco"))
print(Pessoa("Franco", 1234))
print(Pessoa("Franco", 1234, "Masculino"))
print(Pessoa(nome = "Franco"))
# Exemplos com parâmetros fora de ordem.
print(Pessoa(idade = 1234, nome = "Franco"))
print(Pessoa(genero = "Masculino", idade = 1234))
print(Pessoa(genero = "Masculino", nome = "Franco", idade = 1234))
Todos as chamadas do construtor Pessoa()
no exemplo anterior são válidas e inicializam um novo objeto com os valores escolhidos para cada parâmetro nomeado.
Em linguagens sem parâmetros nomeados, é possível passar um dicionário para simular a técnica. Como a ordem de chaves em um dicionário é irrelevante, a passagem pode ser feita em qualquer ordem. Da mesma forma, caso não se defina a chave, pode-se usar um valor padrão para inicializar o valor ignorado.
Em particular, a técnica é comum em JavaScript e possui um recurso para facilitar o uso, chamado de atribuição via desestruturação (destructuring assignment; documentação). A atribuição via desestruturação permite fazer:
let {x, y} = {"x": 1, "y": 2}
console.log(x)
console.log(y)
Ao invés de:
let d = {"x": 1, "y": 2}
let x = d["x"]
let y = d["y"]
console.log(x)
console.log(y)
Assim, a versão em JavaScript será mais legível que a definida para Lua e GDScript. Lua e GDScript precisam usar a versão usual de acesso a valores em dicionários. Como Python permite o uso de parâmetros nomeados, omitiu-se uma implementação para a linguagem.
class Pessoa {
constructor({nome, idade, genero} = {nome: "", idade: 0, genero: ""}) {
// Se nome for undefined ou null, inicializa com o valor padrão.
this.nome = (nome) ? nome : ""
this.idade = (idade) ? idade : 0
this.genero = (genero) ? genero : ""
}
toString() {
let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"
return resultado
}
}
let franco = new Pessoa({
nome: "Franco",
idade: 1234,
genero: "Masculino"
})
console.log(franco)
console.log("" + franco)
let texto = "Olá, " + franco + "!"
console.log(texto)
function Pessoa({nome, idade, genero} = {nome: "", idade: 0, genero: ""}) {
// Se nome for undefined ou null, inicializa com o valor padrão.
this.nome = (nome) ? nome : ""
this.idade = (idade) ? idade : 0
this.genero = (genero) ? genero : ""
}
Pessoa.prototype.toString = function() {
let resultado = this.nome + " (" + this.idade + " anos, gênero " + this.genero + ")"
return resultado
}
let franco = new Pessoa({
nome: "Franco",
idade: 1234,
genero: "Masculino"
})
console.log(franco)
console.log("" + franco)
let texto = "Olá, " + franco + "!"
console.log(texto)
function pessoas_iguais(pessoa1, pessoa2)
if (type(pessoa1) ~= type(pessoa2)) then
return nil
end
return ((pessoa1.nome == pessoa2.nome) and
(pessoa1.idade == pessoa2.idade) and
(pessoa1.genero == pessoa2.genero))
end
function pessoa_para_string(pessoa)
local resultado = pessoa.nome .. " (" .. pessoa.idade .. " anos, gênero " .. pessoa.genero .. ")"
return resultado
end
function concatene(x, y)
local resultado = tostring(x) .. tostring(y)
return resultado
end
local metatable_pessoa = {
__eq = pessoas_iguais,
__tostring = pessoa_para_string,
__concat = concatene
}
function crie_pessoa(dados)
dados = dados or {}
local dados_pessoa = {
nome = dados.nome or "",
idade = dados.idade or 0,
genero = dados.genero or ""
}
local pessoa = setmetatable(dados_pessoa, metatable_pessoa)
return pessoa
end
local franco = crie_pessoa({
nome = "Franco",
idade = 1234,
genero = "Masculino"
})
print(franco)
print(tostring(franco))
print("" .. franco)
texto = "Olá, " .. franco .. "!"
print(texto)
extends Node
class Pessoa:
var nome
var idade
var genero
func _init(dados = {}):
self.nome = dados.get("nome", "")
self.idade = dados.get("idade", 0)
self.genero = dados.get("genero", "")
func _to_string():
var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"
return resultado
func _ready():
var franco = Pessoa.new({
nome = "Franco",
idade = 1234,
genero = "Masculino"
})
print(franco)
var texto = "Olá, " + str(franco) + "!"
print(texto)
extends Node
var nome
var idade
var genero
func _init(dados = {}):
self.nome = dados.get("nome", "")
self.idade = dados.get("idade", 0)
self.genero = dados.get("genero", "")
func _ready():
_init({
nome = "Franco",
idade = 1234,
genero = "Masculino"
})
var franco = self
printt(self, franco)
var texto = "Olá, " + str(franco) + "!"
print(texto)
texto = "Olá, " + str(self) + "!"
print(texto)
func _to_string():
var resultado = self.nome + " (" + str(self.idade) + " anos, gênero " + self.genero + ")"
return resultado
Em GDScript usando o arquivo como classe, o exemplo fica um pouco estranho.
Uma alternativa melhor seria exportar variáveis para edição visual no editor, usando a palavra reservada export
antes de declaração de cada variável (documentação; a versão em desenvolvimento também possui um tutorial com novos recursos que aparecerão na versão 4 de Godot Engine).
Como ainda não se detalhou o uso do editor, a informação serve como curiosidade neste momento.
Exceto em JavaScript, que permite a inicialização usando atribuição via desestruturação, eu particularmente não recomendaria o uso da técnica. A razão é que ela oculta os parâmetros esperados da definição da subrotina, exigindo documentação ou o uso de um valor padrão como exemplo.
Métodos
Subrotinas podem ser funções, procedimentos ou métodos. Funções e procedimentos são subrotinas independentes. Em OOP, métodos são funções ou procedimentos atrelados a classes, que podem acessar e modificar o estado (os atributos) da classe.
Uma forma de pensar um método é como se fosse uma função que recebe uma variável do tipo da classe como primeiro parâmetro.
Esse parâmetro pode ser chamado, por exemplo, de this
ou self
.
Em algumas linguagens de programação, isso é exatamente o que ocorre.
const IDADE_MINIMA_MAIORIDADE_CIVIL = 18
class Pessoa {
constructor(nome = "", idade = 0, genero = "") {
this.nome = nome
this.idade = idade
this.genero = genero
}
possui_maioridade_civil() {
return (this.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
}
}
let franco = new Pessoa("Franco", 1234, "Masculino")
console.log(franco.possui_maioridade_civil())
const IDADE_MINIMA_MAIORIDADE_CIVIL = 18
function possui_maioridade_civil(pessoa) {
if (!pessoa.idade) {
return undefined
}
return (pessoa.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
}
function crie_pessoa(nome = "", idade = 0, genero = "") {
return {
"nome": nome,
"idade": idade,
"genero": genero,
"possui_maioridade_civil": possui_maioridade_civil
}
}
let franco = crie_pessoa("Franco", 1234, "Masculino")
console.log(franco.possui_maioridade_civil(franco))
from typing import Final
IDADE_MINIMA_MAIORIDADE_CIVIL: Final = 18
class Pessoa:
def __init__(self, nome = "", idade = 0, genero = ""):
self.nome = nome
self.idade = idade
self.genero = genero
def possui_maioridade_civil(self):
return (self.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
franco = Pessoa("Franco", 1234, "Masculino")
print(franco.possui_maioridade_civil())
local IDADE_MINIMA_MAIORIDADE_CIVIL <const> = 18
function possui_maioridade_civil(pessoa)
if (pessoa.idade == nil) then
return false
end
return (pessoa.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
end
function crie_pessoa(nome, idade, genero)
local pessoa = {
nome = nome or "",
idade = idade or 0,
genero = genero or "",
-- Chave Referência para função
possui_maioridade_civil = possui_maioridade_civil
}
return pessoa
end
local franco = crie_pessoa("Franco", 1234, "Masculino")
print(franco.possui_maioridade_civil(franco))
-- O operador : passa a própria variável como primeiro parâmetro para o método chamado.
print(franco:possui_maioridade_civil())
extends Node
const IDADE_MINIMA_MAIORIDADE_CIVIL = 18
class Pessoa:
var nome
var idade
var genero
func _init(nome = "", idade = 0, genero = ""):
self.nome = nome
self.idade = idade
self.genero = genero
func possui_maioridade_civil():
# Ou return (self.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
return (idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
func _ready():
var franco = Pessoa.new("Franco", 1234, "Masculino")
print(franco.possui_maioridade_civil())
extends Node
const IDADE_MINIMA_MAIORIDADE_CIVIL = 18
var nome
var idade
var genero
func _init(nome = "", idade = 0, genero = ""):
self.nome = nome
self.idade = idade
self.genero = genero
func _ready():
nome = "Franco"
idade = 1234
genero = "Masculino"
var franco = self
printt(self, franco)
printt(possui_maioridade_civil(), franco.possui_maioridade_civil())
func possui_maioridade_civil():
# Ou: return (self.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
return (idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
Nos exemplos, linguagens com suporte a classes (e usando class
, no caso de JavaScript) permitem definir subrotinas dentro da definição da classe.
Isso foi feito previamente para polimorfismo e sobrecarga de operadores.
A inclusão em uma classe de uma subrotina arbitrária definida por uma programadora ou um programador define um método.
No exemplo para JavaScript Object, a chamada deve incluir a própria variável: franco.possui_maioridade_civil(franco)
.
O mesmo ocorre em um dos exemplos em Lua.
Para comodidade de uso, Lua define o operador :
para uso com table
.
O operador passa a própria variável usada como primeiro parâmetro para a subrotina chamada.
É por isso que subrotinas como table.insert(minha_tabela, valor)
podem ser chamadas como minha_tabela:insert(valor)
.
A criação de uma table
em Lua incluir insert()
e outras subrotinas para manipulação de tabelas como atributos (são variáveis que armazenam a função genérica por referência).
O mesmo ocorre com subrotinas para string
em Lua.
Caso se quisesse fazer algo similar ao que ocorre em Lua para JavaScript, uma possibilidade seria definir uma função anônima (lambda) que chamasse a função original.
const IDADE_MINIMA_MAIORIDADE_CIVIL = 18
function possui_maioridade_civil(pessoa) {
if (!pessoa.idade) {
return undefined
}
return (pessoa.idade >= IDADE_MINIMA_MAIORIDADE_CIVIL)
}
function crie_pessoa(nome = "", idade = 0, genero = "") {
let pessoa = {
"nome": nome,
"idade": idade,
"genero": genero
}
pessoa["possui_maioridade_civil"] = function() {
return possui_maioridade_civil(pessoa)
}
return pessoa
}
let franco = crie_pessoa("Franco", 1234, "Masculino")
console.log(franco.possui_maioridade_civil())
Com a alteração, a chamada franco.possui_maioridade_civil()
passaria a funcionar com JavaScript Object.
JavaScript define fechamentos (closures; documentação) para subrotinas, permitindo o uso da variável local pessoa
definida em crie_pessoa()
na função anônima criada.
Contudo, isso não é possível em toda linguagem de programação. Para fazer isso em outras linguagens de programação, é necessário que a linguagem forneça funções anônimas, e recursos como closures ou captura de variáveis.
Programação Orientada a Objetos (OOP) em Lua
Com as informações atuais, é possível começar a prototipar um sistemas simples de orientação a objetos em Lua. Esta seção serve mais como curiosidade neste momento; não é necessário entendê-la para seguir o restante deste tópico.
Com metatable
, pode-se tornar a versão do código em Lua mais próximo de código em OOP.
A abordagem é detalhada em Programming in Lua (Capítulo 16) e (Seção 16.1).
O código a seguir é uma adaptação da abordagem.
local MINIMUM_CIVIL_IDADE_OF_MAJORITY = 18
-- Pessoa serve como tipo e como metatable para o tipo.
local Pessoa = {
nome = "",
idade = 0,
genero = ""
}
function Pessoa:tem_maioridade_civil()
-- self é o parâmetro implícito provido pelo operador :.
if (self.idade == nil) then
return false
end
return (self.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
end
-- Este é o construtor.
function Pessoa:new(nome, idade, genero)
local result = {}
-- self atua como metatable para Pessoa.
setmetatable(result, self)
self.__index = self
result.nome = nome or self.nome
result.idade = idade or self.idade
result.genero = genero or self.genero
return result
end
-- NOTA A criação precisa usar : ao invés de .
local franco = Pessoa:new("Franco", 1234, "Male")
print(franco.nome, franco.idade, franco.genero)
local voce = Pessoa:new("Voce", 4321, "???")
print(voce.nome, voce.idade, voce.genero)
-- Todas essas chamadas acessam dados de franco.
print(franco.nome, franco.idade, franco.genero)
print(franco.tem_maioridade_civil(franco))
print(franco:tem_maioridade_civil())
A definição da subrotina usando :
(como em Pessoa:tem_maioridade_civil()
e Pessoa:new()
) adiciona um parâmetro implícito self
na assinatura.
Assim, por exemplo, Pessoa:new()
equivale a Pessoa:new(self)
.
A metatable
usa Pessoa
como definição da classe.
A configuração de __index
modifica o comportamento da indexação de tabelas.
Ela permite usar subrotinas e atributos de Pessoa
em uma nova variável (objeto) criada por Pessoa.new()
.
Como nome
, idade
e genero
são atributos de tipos primitivos, o resultado são cópias independentes.
Entretanto, se um atributo fosse uma tabela (por exemplo, um vetor ou dicionário), a variável seria compartilhada, requerendo o a criação de uma cópia profunda.
O próximo exemplo destaca alternativas para a definição de métodos em Lua.
Em particular, deve-se evitar uma das formas: NomeClasse.nome_metodo()
, porque ela usa a metatable
NomeClasse
ao invés da variável criada por NomeClasse:new()
.
As outras formas são equivalentes.
local MINIMUM_CIVIL_IDADE_OF_MAJORITY = 18
-- Pessoa serve como tipo e como metatable para o tipo.
local Pessoa = {
nome = "",
idade = 0,
genero = "",
-- Este é um método virtual.
tem_maioridade_civil = function(self)
if (self.idade == nil) then
return false
end
return (self.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
end
}
-- Este é um método virtual.
Pessoa.tem_maioridade_civil_alternativa_1 = function(self)
if (self.idade == nil) then
return false
end
return (self.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
end
-- Este é um método virtual.
function Pessoa:tem_maioridade_civil_alternativa_2()
-- self é o parâmetro implícito provido pelo operador :.
if (self.idade == nil) then
return false
end
return (self.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
end
-- Este é um método da classe base.
-- Ele terá um resultado diferente.
-- Em OOP, ele acessaria os dados da classe pai (superclasse).
function Pessoa.tem_maioridade_civil_alternativa_3()
-- Pessoa is the local variable defined previously.
if (Pessoa.idade == nil) then
return false
end
-- Isso significa que idade é 0.
-- print(Pessoa.idade)
return (Pessoa.idade >= MINIMUM_CIVIL_IDADE_OF_MAJORITY)
end
-- Este é o construtor.
function Pessoa:new(nome, idade, genero)
local result = {}
-- self atua como metatable para Pessoa.
setmetatable(result, self)
self.__index = self
result.nome = nome or self.nome
result.idade = idade or self.idade
result.genero = genero or self.genero
return result
end
-- NOTA A criação precisa usar : ao invés de .
local franco = Pessoa:new("Franco", 1234, "Male")
print(franco.nome, franco.idade, franco.genero)
local voce = Pessoa:new("Voce", 4321, "???")
print(voce.nome, voce.idade, voce.genero)
-- Todas essas chamadas acessam dados de franco.
print(franco.nome, franco.idade, franco.genero)
print(franco.tem_maioridade_civil(franco))
print(franco:tem_maioridade_civil())
print(franco.tem_maioridade_civil_alternativa_1(franco))
print(franco:tem_maioridade_civil_alternativa_1())
print(franco.tem_maioridade_civil_alternativa_2(franco))
print(franco:tem_maioridade_civil_alternativa_2())
-- Estas não fazem a chamada usando franco, elas realizam a chamada usando
-- valores da metatable Pessoa.
print(franco.tem_maioridade_civil_alternativa_3())
print(franco:tem_maioridade_civil_alternativa_3())
print(Pessoa.tem_maioridade_civil(Pessoa))
print(Pessoa:tem_maioridade_civil())
print(Pessoa.tem_maioridade_civil_alternativa_1(Pessoa))
print(Pessoa:tem_maioridade_civil_alternativa_1())
print(Pessoa.tem_maioridade_civil_alternativa_2(Pessoa))
print(Pessoa:tem_maioridade_civil_alternativa_2())
print(Pessoa.tem_maioridade_civil_alternativa_3())
print(Pessoa:tem_maioridade_civil_alternativa_3())
Para uma lista de bibliotecas para programação OOP em Lua, pode-se consultar esta entrada de lua-users.org.
Técnicas Usando Registros
Nesta seção, retorna-se aos básicos de registros como agrupamentos de dados. Recursos adicionais como polimorfismo e sobrecarga de operadores não são necessários para entender as técnicas. Construtores com ou sem parâmetros podem ser usados como se subrotinas simples.
Para focar em dados ao invés de OOP, a versão de implementação em JavaScript usará class
e a versão em GDScript usará classes internas.
Você pode escolher entre definir subrotinas como métodos, se preferir.
Os exemplos implementarão subrotinas como funções e procedimentos independentes.
Vetor de Registros (Array of Structures)
Uma forma de relacionar dados em diferentes vetores consiste em usar a técnica de vetores paralelos (structure of arrays). Por sinal, agora é possível explorar a parte structure do nome. Entretanto, a técnica mais comum é definir um vetor de registros (vetor de estruturas ou array of structures), especialmente em OOP.
Um vetor de registros combina todos os dados que devem ser representados sobre uma entidade um registro. Ao invés de compartilhar dados usando um índice, todos os dados da entidade estarão no registro definido.
Por exemplo, para armazenar o nome, a extensão e um exemplo de uma linguagem de programação arbitrária, poder-se-ia definir três vetores paralelos: linguagens_nome
, linguagens_extensao
e linguagens_exemplo
.
No caso, adotou-se linguagens_
como prefixo para os vetores.
Com um vetor de registros, pode-se definir um único vetor que contenha dados em um único registro (por exemplo, Linguagem
ou LinguagemProgramacao
) que tenha três campos: nome
, extensao
e exemplo
.
Cada posição do vetor armazenará um registro.
Todos os dados estarão armazenados em conjunto no registro.
class LinguagemProgramacao {
constructor(nome = "", extensao = "", exemplo = "") {
this.nome = nome
this.extensao = extensao
this.exemplo = exemplo
}
}
let linguagens = [
new LinguagemProgramacao("JavaScript", ".js", "console.log(\"Olá, meu nome é Franco!\")"),
new LinguagemProgramacao("Python", ".py", "print(\"Olá, meu nome é Franco!\")"),
new LinguagemProgramacao("Lua", ".lua", "print(\"Olá, meu nome é Franco!\")"),
new LinguagemProgramacao("GDScript", ".gd", "extends Node\nfunc _ready():\n print(\"Olá, meu nome é Franco!\")"),
]
for (let linguagem of linguagens) {
console.log("Linguagem de Programação: " + linguagem.nome)
console.log("Extensão: " + linguagem.extensao)
console.log("Exemplo:")
console.log(linguagem.exemplo)
console.log("---")
}
class LinguagemProgramacao:
def __init__(self, nome = "", extensao = "", exemplo = ""):
self.nome = nome
self.extensao = extensao
self.exemplo = exemplo
linguagens = [
LinguagemProgramacao("JavaScript", ".js", "console.log(\"Olá, meu nome é Franco!\")"),
LinguagemProgramacao("Python", ".py", "print(\"Olá, meu nome é Franco!\")"),
LinguagemProgramacao("Lua", ".lua", "print(\"Olá, meu nome é Franco!\")"),
LinguagemProgramacao("GDScript", ".gd", "extends Node\nfunc _ready():\n print(\"Olá, meu nome é Franco!\")"),
]
for linguagem in linguagens:
print("Linguagem de Programação: " + linguagem.nome)
print("Extensão: " + linguagem.extensao)
print("Exemplo:")
print(linguagem.exemplo)
print("---")
function crie_linguagem_programacao(nome, extensao, exemplo)
local resultado = {
nome = nome or "",
extensao = extensao or "",
exemplo = exemplo or ""
}
return resultado
end
local linguagens = {
crie_linguagem_programacao("JavaScript", ".js", "console.log(\"Olá, meu nome é Franco!\")"),
crie_linguagem_programacao("Python", ".py", "print(\"Olá, meu nome é Franco!\")"),
crie_linguagem_programacao("Lua", ".lua", "print(\"Olá, meu nome é Franco!\")"),
crie_linguagem_programacao("GDScript", ".gd", "extends Node\nfunc _ready():\n print(\"Olá, meu nome é Franco!\")"),
}
for _, linguagem in ipairs(linguagens) do
print("Linguagem de Programação: " .. linguagem.nome)
print("Extensão: " .. linguagem.extensao)
print("Exemplo:")
print(linguagem.exemplo)
print("---")
end
extends Node
class LinguagemProgramacao:
var nome
var extensao
var exemplo
func _init(nome = "", extensao = "", exemplo = ""):
self.nome = nome
self.extensao = extensao
self.exemplo = exemplo
func _ready():
var linguagens = [
LinguagemProgramacao.new("JavaScript", ".js", "console.log(\"Olá, meu nome é Franco!\")"),
LinguagemProgramacao.new("Python", ".py", "print(\"Olá, meu nome é Franco!\")"),
LinguagemProgramacao.new("Lua", ".lua", "print(\"Olá, meu nome é Franco!\")"),
LinguagemProgramacao.new("GDScript", ".gd", "extends Node\nfunc _ready():\n print(\"Olá, meu nome é Franco!\")"),
]
for linguagem in linguagens:
print("Linguagem de Programação: " + linguagem.nome)
print("Extensão: " + linguagem.extensao)
print("Exemplo:")
print(linguagem.exemplo)
print("---")
Vetores de registros são comuns em programação orientada a objetos. Para pessoas iniciantes em programação, eles fornecem uma boa forma de organizar e modelar soluções para problemas.
Registro de Vetores (Structure of Arrays) ou Vetores Paralelos em Registro
Para um registro de vetores propriamente dito, pode-se modificar a implementação usando vetores paralelos. Todos os vetores representado todas as entidades estarão armazenados em um único registro; cada entidade é acessada pelo índice compartilhado entre os diferentes vetores. Em outras palavras, a abordagem é contrária à abstração de dados proposta por OOP.
class LinguagensProgramacao {
constructor(nomes = [], extensoes = [], exemplos = []) {
this.nomes = nomes
this.extensoes = extensoes
this.exemplos = exemplos
}
}
let linguagens = new LinguagensProgramacao(
["JavaScript", "Python", "Lua", "GDScript"],
[".js", ".py", ".lua", ".gd"],
[
"console.log(\"Olá, meu nome é Franco!\")",
"print(\"Olá, meu nome é Franco!\")",
"print(\"Olá, meu nome é Franco!\")",
"extends Node\nfunc _ready():\n print(\"Olá, meu nome é Franco!\")"
]
)
for (let indice_linguagem in linguagens.nomes) {
console.log("Linguagem de Programação: " + linguagens.nomes[indice_linguagem])
console.log("Extensão: " + linguagens.extensoes[indice_linguagem])
console.log("Exemplo:")
console.log(linguagens.exemplos[indice_linguagem])
console.log("---")
}
class LinguagensProgramacao:
def __init__(self, nomes = "", extensoes = "", exemplos = ""):
self.nomes = nomes
self.extensoes = extensoes
self.exemplos = exemplos
linguagens = LinguagensProgramacao(
["JavaScript", "Python", "Lua", "GDScript"],
[".js", ".py", ".lua", ".gd"],
[
"console.log(\"Olá, meu nome é Franco!\")",
"print(\"Olá, meu nome é Franco!\")",
"print(\"Olá, meu nome é Franco!\")",
"extends Node\nfunc _ready():\n print(\"Olá, meu nome é Franco!\")"
]
)
for indice_linguagem in range(len(linguagens.nomes)):
print("Linguagem de Programação: " + linguagens.nomes[indice_linguagem])
print("Extensão: " + linguagens.extensoes[indice_linguagem])
print("Exemplo:")
print(linguagens.exemplos[indice_linguagem])
print("---")
function crie_linguagens_programacao(nomes, extensoes, exemplos)
local resultado = {
nomes = nomes or {},
extensoes = extensoes or {},
exemplos = exemplos or {}
}
return resultado
end
local linguagens = crie_linguagens_programacao(
{"JavaScript", "Python", "Lua", "GDScript"},
{".js", ".py", ".lua", ".gd"},
{
"console.log(\"Olá, meu nome é Franco!\")",
"print(\"Olá, meu nome é Franco!\")",
"print(\"Olá, meu nome é Franco!\")",
"extends Node\nfunc _ready():\n print(\"Olá, meu nome é Franco!\")"
}
)
for indice_linguagem = 1, #linguagens.nomes do
print("Linguagem de Programação: " .. linguagens.nomes[indice_linguagem])
print("Extensão: " .. linguagens.extensoes[indice_linguagem])
print("Exemplo:")
print(linguagens.exemplos[indice_linguagem])
print("---")
end
extends Node
class LinguagensProgramacao:
var nomes
var extensoes
var exemplos
func _init(nomes = "", extensoes = "", exemplos = ""):
self.nomes = nomes
self.extensoes = extensoes
self.exemplos = exemplos
func _ready():
var linguagens = LinguagensProgramacao.new(
["JavaScript", "Python", "Lua", "GDScript"],
[".js", ".py", ".lua", ".gd"],
[
"console.log(\"Olá, meu nome é Franco!\")",
"print(\"Olá, meu nome é Franco!\")",
"print(\"Olá, meu nome é Franco!\")",
"extends Node\nfunc _ready():\n print(\"Olá, meu nome é Franco!\")"
]
)
for indice_linguagem in range(len(linguagens.nomes)):
print("Linguagem de Programação: " + linguagens.nomes[indice_linguagem])
print("Extensão: " + linguagens.extensoes[indice_linguagem])
print("Exemplo:")
print(linguagens.exemplos[indice_linguagem])
print("---")
Dependendo das características de uso e iteração sobre dados, o desempenho de uma implementação com vetor de registros ou de um registro de vetores pode variar significativamente. Por exemplo, em jogos digitais com milhões de entidades, o uso de registro de vetores pode ter desempenho superior, por armazenar dados de cada vetor de forma seqüencial em memória. Isso contribui para otimização de uso de memória cache em laços com repetições para milhares ou milhões de entidades em um jogo. A abordagem é bastante usada em uma arquitetura de software chamada Entity-Component-System (ECS).
Por outro lado, sistemas empresarias podem processar uma entidade por vez (ao invés de milhares de entidades semelhantes por vez). Neste caso, um vetor de registros pode ter desempenho melhor, caso todos os dados da mesma entidade estejam em seqüência (todos eles poderiam ser obtidos de uma única vez). Contudo, na prática, isso nem sempre ocorre, porque objetos são comumente modelados como hierarquias ou composições de outros objetos. Ou seja, pode não haver garantia de que eles ocupem posições contíguas de memória. O uso de bons sistemas gerenciadores de bancos de dados (SGDBs) pode amenizar o problema, embora seja necessário tomar cuidado ao armazenar os dados em memória primária após a recuperação.
No geral, como em todos os casos, o ideal é medir e comparar o desempenho usando uma ferramenta como um profiler ao invés de assumir o comportamento para um programa em particular. Cada caso é um caso.
Composição de Registros: Registro Com Registros
Um registro pode ser definido por tipos primitivos e por tipos compostos. Em potencial, pode-se compor um registro por outros registros pré-existentes. Para isso, basta adicionar um atributo ao registro que seja do tipo de outro registro.
Por exemplo, uma receita pode ter um nome
, modo_preparo
, e uma lista de ingredientes
.
Cada ingrediente pode ter um nome
, uma quantidade
e uma unidade de medida (por exemplo, massa ou volume -- unidade_medida
).
Pode-se definir dois registros para a modelagem: um para Ingrediente
, um para Receita
.
O registro para Receita
pode ter um vetor (ou uma lista) de Ingrediente
.
class Ingrediente {
constructor(nome = "", quantidade = 0.0, unidade_medida = "") {
this.nome = nome
this.quantidade = quantidade
this.unidade_medida = unidade_medida
}
}
class Receita {
constructor(nome = "", modo_preparo = "", ingredientes = []) {
this.nome = nome
this.modo_preparo = modo_preparo
this.ingredientes = ingredientes
}
}
let agua = new Ingrediente("Água", 3.0, "Xícaras")
let farinha = new Ingrediente("Farinha", 4.0, "Xícaras")
let sal = new Ingrediente("Sal", 2.0, "Colheres de Sopa")
let fermento = new Ingrediente("Fermento", 2.0, "Colheres de Chá")
let pao = new Receita("Pão")
pao.ingredientes.push(agua)
pao.ingredientes.push(farinha)
pao.ingredientes.push(sal)
pao.ingredientes.push(fermento)
pao.modo_preparo = "..."
console.log(pao.nome)
console.log("Ingredientes:")
for (let ingrediente of pao.ingredientes) {
console.log("- " + ingrediente.nome + ": " + ingrediente.quantidade + " " + ingrediente.unidade_medida)
}
console.log("Modo de preparo:")
console.log(pao.modo_preparo)
class Ingrediente:
def __init__(self, nome = "", quantidade = 0.0, unidade_medida = ""):
self.nome = nome
self.quantidade = quantidade
self.unidade_medida = unidade_medida
class Receita:
def __init__(self, nome = "", modo_preparo = "", ingredientes = None):
self.nome = nome
self.modo_preparo = modo_preparo
self.ingredientes = ingredientes if (ingredientes != None) else []
agua = Ingrediente("Água", 3.0, "Xícaras")
farinha = Ingrediente("Farinha", 4.0, "Xícaras")
sal = Ingrediente("Sal", 2.0, "Colheres de Sopa")
fermento = Ingrediente("Fermento", 2.0, "Colheres de Chá")
pao = Receita("Pão")
pao.ingredientes.append(agua)
pao.ingredientes.append(farinha)
pao.ingredientes.append(sal)
pao.ingredientes.append(fermento)
pao.modo_preparo = "..."
print(pao.nome)
print("Ingredientes:")
for ingrediente in pao.ingredientes:
print("- " + ingrediente.nome + ": " + str(ingrediente.quantidade) + " " + ingrediente.unidade_medida)
print("Modo de preparo:")
print(pao.modo_preparo)
function crie_ingrediente(nome, quantidade, unidade_medida)
local resultado = {
nome = nome or "",
quantidade = quantidade or 0.0,
unidade_medida = unidade_medida or ""
}
return resultado
end
function crie_receita(nome, modo_preparo, ingredientes)
local resultado = {
nome = nome or "",
modo_preparo = quantidade or "",
ingredientes = ingredientes or {}
}
return resultado
end
local agua = crie_ingrediente("Água", 3.0, "Xícaras")
local farinha = crie_ingrediente("Farinha", 4.0, "Xícaras")
local sal = crie_ingrediente("Sal", 2.0, "Colheres de Sopa")
local fermento = crie_ingrediente("Fermento", 2.0, "Colheres de Chá")
local pao = crie_receita("Pão")
table.insert(pao.ingredientes, agua)
table.insert(pao.ingredientes, farinha)
table.insert(pao.ingredientes, sal)
table.insert(pao.ingredientes, fermento)
pao.modo_preparo = "..."
print(pao.nome)
print("Ingredientes:")
for _, ingrediente in ipairs(pao.ingredientes) do
print("- " .. ingrediente.nome .. ": " .. ingrediente.quantidade .. " " .. ingrediente.unidade_medida)
end
print("Modo de preparo:")
print(pao.modo_preparo)
extends Node
class Ingrediente:
var nome
var quantidade
var unidade_medida
func _init(nome = "", quantidade = 0.0, unidade_medida = ""):
self.nome = nome
self.quantidade = quantidade
self.unidade_medida = unidade_medida
class Receita:
var nome
var modo_preparo
var ingredientes
func _init(nome = "", modo_preparo = "", ingredientes = []):
self.nome = nome
self.modo_preparo = modo_preparo
self.ingredientes = ingredientes
func _ready():
var agua = Ingrediente.new("Água", 3.0, "Xícaras")
var farinha = Ingrediente.new("Farinha", 4.0, "Xícaras")
var sal = Ingrediente.new("Sal", 2.0, "Colheres de Sopa")
var fermento = Ingrediente.new("Fermento", 2.0, "Colheres de Chá")
var pao = Receita.new("Pão")
pao.ingredientes.append(agua)
pao.ingredientes.append(farinha)
pao.ingredientes.append(sal)
pao.ingredientes.append(fermento)
pao.modo_preparo = "..."
print(pao.nome)
print("Ingredientes:")
for ingrediente in pao.ingredientes:
print("- " + ingrediente.nome + ": " + str(ingrediente.quantidade) + " " + ingrediente.unidade_medida)
print("Modo de preparo:")
print(pao.modo_preparo)
É possível modificar o exemplo para alterar a inicialização.
Por exemplo, ao invés de inserir cada Ingrediente
no vetor ingredientes
, poder-se-ia fazer a inicialização diretamente na subrotina usada como construtor.
Por exemplo:
pao = new Receita("Pão",
"...",
[
agua,
farinha,
sal,
fermento
])
Em particular, seria ainda mais conveniente definir subrotinas para operações válidas para manipulação dos registros, como será comentado em uma das próximas subseções.
Abstração de Dados e Granularidade da Modelagem
Caso se quisesse, poder-se-ia definir registros para unidade de medida (UnidadeMedida
, que poderia combinar quantidade
e unidade_medida
um único tipo) ou para o modo de preparo (por exemplo, ele poderia ser dividido em preparos_iniciais
, instrucoes
, tempo_preparo
, dicas
, como_servir
...).
Assim, sempre é possível criar mais (ou menos) tipos de dados para abstrair valores. Quanto mais tipos, maior a abstração de dados e a granularidade da modelagem. A granularidade define o detalhamento e a estruturação do tipo de dados.
A escolha da granularidade depende do problema a ser modelado. Maior granularidade nem sempre é melhor; o ideal é encontrar um equilíbrio. Quanto maior a granularidade, mais pormenorizada e compartimentada será a solução, facilitando o acesso a dados (pois existirão mais campos para acesso e consulta). Embora mais estruturado, o programa pode ser mais difícil de usar e trabalhoso para implementar. Quanto menor a granularidade, mais próximo o tipo será de um tipo primitivo. Embora mais simples par ser armazenar, a extração de dados será mais complexa (pois, possivelmente, exigirá o processamento do tipo primitivo).
Em particular, um critério útil para definir granularidade é determinar quais valores serão buscados em um programa. Variáveis que representem um mesmo objeto ou uma mesma entidade podem ter uma mesma instância de um tipo, para facilitar buscas.
Por exemplo, pode-se imaginar a modelagem de um endereço.
Um endereço pode ser, simplesmente, uma cadeia de caracteres.
Ele também poderia ser um registro composto por campos como rua
, numero
, cidade
, estado
, pais
, codigo_postal
, complemento
...
No primeiro caso, a extração da rua precisaria manipular a cadeia de caracteres.
Caso não existisse um padrão para definir um formato de endereço, a extração poderia ser complexa ou mesmo impossível (onde está a cidade na cadeia de caracteres? Ela pode estar em qualquer lugar).
No segundo caso, a extração é simples: endereco.rua
.
Por outro lado, a construção do registro requer mais operações (tanto para implementação inicial, quanto para o uso do sistema).
Na forma mais simples, ao invés de solicitar a entrada de um único valor (o endereço completo), dever-se-ia ler um valor para rua
, outro para numero
, e assim por diante.
É interessante observar formulários em páginas da Internet ou em programas. Cada campo fornecido em um cadastro permite inferir como os dados são armazenados e processados no sistema, assim como são feitas consultas a cada um deles.
Por exemplo, na segunda abordagem, um formulário e modelo de dados mais sofisticados poderiam continuar a composição.
Poder-se-ia criar uma estrutura Pais
, composta por uma coleção de Estado
, por sua vez composta por uma coleção de Cidade
...
Quando mais estruturas únicas, maior a granularidade.
Também é possível economizar memória instanciando cada valor uma única vez, e referenciando a variável criada sempre que necessário.
Um benefício é centralizar o local de atualizações: por exemplo, para atualizar informações de uma Cidade
em todos os endereços que a mencionem, basta alterar a referência original (ao invés de cada endereço individual).
Registro Com Referência para o Próprio Registro
Assim como existem subrotinas recursivas, é possível definir tipos de dados recursivos (ou estruturas recursivas ou classes recursivas). Em outras palavras, um tipo de dados que possui um atributo do próprio tipo de dados.
Por exemplo, um sistema pode ser composto de subsistemas (que também são sistemas). Uma seção pode ser composta por subseções (que também são seções). Uma lista pode ser composta por sublistas (que também são listas e já foram usadas ao longo do tópico).
De fato, estruturas de dados são comumente implementadas com tipos de dados recursivos. Pode-se pensar uma lista como um registro que armazene um valor e uma referência para o próprio tipo da lista (a próxima entrada).
Em algumas linguagens de programação, pode ser necessário realizar uma forward declaration do novo tipo antes de adicionar um atributo do próprio registro. Isso ocorre porque alguns compiladores e interpretadores precisam saber da existência de um tipo antes de usá-lo (por exemplo, para saber a quantidade de memória necessária para alocar uma variável do tipo). A forward declaration avisa a ferramenta que o tipo existirá (em algum momento), mesmo que ainda não tenha implementação. Isso é possível porque referências são endereços de memória que costumam ter tamanho fixo para endereçamento (4 bytes em sistemas 32-bit, 8 bytes em sistemas 64-bit).
O exemplo a seguir apresenta um protótipo de registro para um tipo para um item de lista encadeada (ou lista ligada ou linked list).
O tipo ItemLista
possui dois campos: o valor
armazenado e uma referência para o proximo
item da lista.
Uma entrada da qual o valor de proximo
tenha como valor uma referência inválida ou vazia (null
or nil
, nas linguagens consideradas) significa o final da lista.
Para facilitar a leitura, poder-se-ia definir uma constante FINAL_LISTA = null
, embora o uso da referência vazia seja comum e idiomático.
class ItemLista {
constructor(valor, proximo = null) {
this.valor = valor
this.proximo = proximo
}
}
let numeros = new ItemLista(1,
new ItemLista(2,
new ItemLista(3)))
let item_lista = numeros
while (item_lista !== null) {
console.log(item_lista.valor)
item_lista = item_lista.proximo
}
class ItemLista:
def __init__(self, valor, proximo = None):
self.valor = valor
self.proximo = proximo
numeros = ItemLista(1,
ItemLista(2,
ItemLista(3)))
item_lista = numeros
while (item_lista != None):
print(item_lista.valor)
item_lista = item_lista.proximo
function crie_item_lista(valor, proximo)
local resultado = {
valor = valor,
proximo = proximo or nil
}
return resultado
end
local numeros = crie_item_lista(1,
crie_item_lista(2,
crie_item_lista(3)))
local item_lista = numeros
while (item_lista ~= nil) do
print(item_lista.valor)
item_lista = item_lista.proximo
end
extends Node
class ItemLista:
var valor
var proximo
func _init(valor, proximo = null):
self.valor = valor
self.proximo = proximo
func _ready():
var numeros = ItemLista.new(1,
ItemLista.new(2,
ItemLista.new(3)))
var item_lista = numeros
while (item_lista != null):
print(item_lista.valor)
item_lista = item_lista.proximo
Uma lista encadeada é uma das estruturas de dados dinâmicas mais básicas que se pode implementar.
O exemplo apresenta inserções apenas no final (push()
, push_back()
ou append()
).
Uma implementação completa adicionaria inserção em posições arbitrárias e remoções.
Para isso, convém integrar registros com subrotinas.
Registros e Subrotinas
Registros podem abstrair dados (abstração de dados). Subrotinas podem abstrair processamentos (abstração funcional). A combinação de registros e subrotinas permite abstrair dados e processamentos. Por sinal, a combinação é uma das características fundamentais de OOP.
Ao invés de re-escrever operações para manipular cada registro criado como tipos de dados, pode-se criar subrotinas. Subrotinas para registos funcionam como subrotinas e operadores para tipos primitivos: elas podem definir operações pré-definidas para manipular os dados com comodidade e segurança. Em OOP, isso faz parte de operações criadas com métodos (e, possivelmente, operadores sobrecarregados) e de uma característica desejável chamada de encapsulamento.
Por exemplo, é bastante comum que implementações de programas em OOP definam código com métodos chamados getters e setters.
Genericamente, esses métodos são chamados de métodos acessores (accessor methods).
O objetivo de um método get()
é obter o valor de atributo.
O objetivo de um método set()
é ajustar (inicializar ou modificar) um valor que seja válido para a classe, e proibir alterações inválidas.
Em outras palavras, eles garantem que um objeto sempre tenha um estado válido, agregando consistência e segurança para uso.
Para um exemplo introdutório, pode-se considerar um elevador.
Um elevador pode ter um andar_atual
, um andar_minimo
e um andar_maximo
.
O andar_atual
deve ser menor ou igual ao andar_maximo
.
Ele também ser deve maior ou igual ao andar_minimo
.
Caso se considerasse o subsolo como andares negativos para andares, os valores de andares poderiam ser negativos.
class Elevador {
constructor(andar_inicial = 0, andar_minimo = 0, andar_maximo = 5) {
let maximo = andar_minimo
let minimo = andar_minimo
if (andar_minimo > andar_maximo) {
minimo = andar_maximo
} else {
maximo = andar_maximo
}
let inicial = andar_inicial
if (andar_inicial < minimo) {
inicial = minimo
} else if (andar_inicial > maximo) {
inicial = maximo
}
this.andar_minimo = minimo
this.andar_maximo = maximo
this.andar_atual = inicial
}
}
// Ou set_andar(), para um nome mais tradicional.
function alterar_andar(elevador, novo_andar) {
if ((novo_andar >= elevador.andar_minimo) && (novo_andar <= elevador.andar_maximo)) {
elevador.andar_atual = novo_andar
}
}
function subir_um_andar(elevador) {
alterar_andar(elevador, elevador.andar_atual + 1)
}
function descer_um_andar(elevador) {
alterar_andar(elevador, elevador.andar_atual - 1)
}
function inteiro_aleatorio(minimo_inclusive, maximo_inclusive) {
let minimo = Math.ceil(minimo_inclusive)
let maximo = Math.floor(maximo_inclusive)
return Math.floor(minimo + Math.random() * (maximo + 1 - minimo))
}
let elevador = new Elevador(0, 0, 5)
for (i = 0; i < 10; ++i) {
let andar_inicial = elevador.andar_atual
console.log("O elevador está no " + andar_inicial + "º andar")
if (inteiro_aleatorio(0, 1) === 0) {
console.log("Próximo comando: descer")
descer_um_andar(elevador)
} else {
console.log("Próximo comando: subir")
subir_um_andar(elevador)
}
let andar_final = elevador.andar_atual
if (andar_inicial !== andar_final) {
console.log("Vruuum!")
} else {
console.log("... Nada acontece.")
}
}
console.log("O elevador está no " + elevador.andar_atual + "º andar")
import random
class Elevador:
def __init__(self, andar_inicial = 0, andar_minimo = 0, andar_maximo = 5):
maximo = andar_minimo
minimo = andar_minimo
if (andar_minimo > andar_maximo):
minimo = andar_maximo
else:
maximo = andar_maximo
inicial = andar_inicial
if (andar_inicial < minimo):
inicial = minimo
elif (andar_inicial > maximo):
inicial = maximo
self.andar_minimo = minimo
self.andar_maximo = maximo
self.andar_atual = inicial
# Ou set_andar(), para um nome mais tradicional.
def alterar_andar(elevador, novo_andar):
if ((novo_andar >= elevador.andar_minimo) and (novo_andar <= elevador.andar_maximo)):
elevador.andar_atual = novo_andar
def subir_um_andar(elevador):
alterar_andar(elevador, elevador.andar_atual + 1)
def descer_um_andar(elevador):
alterar_andar(elevador, elevador.andar_atual - 1)
random.seed()
elevador = Elevador(0, 0, 5)
for i in range(10):
andar_inicial = elevador.andar_atual
print("O elevador está no " + str(andar_inicial) + "º andar")
if (random.randint(0, 1) == 0):
print("Próximo comando: descer")
descer_um_andar(elevador)
else:
print("Próximo comando: subir")
subir_um_andar(elevador)
andar_final = elevador.andar_atual
if (andar_inicial != andar_final):
print("Vruuum!")
else:
print("... Nada acontece.")
print("O elevador está no " + str(elevador.andar_atual) + "º andar")
function crie_elevador(andar_inicial, andar_minimo, andar_maximo)
andar_inicial = andar_inicial or 0
andar_minimo = andar_minimo or 0
andar_maximo = andar_maximo or 0
local maximo = andar_minimo
local minimo = andar_minimo
if (andar_minimo > andar_maximo) then
minimo = andar_maximo
else
maximo = andar_maximo
end
local inicial = andar_inicial
if (andar_inicial < minimo) then
inicial = minimo
elseif (andar_inicial > maximo) then
inicial = maximo
end
local resultado = {
andar_minimo = minimo,
andar_maximo = maximo,
andar_atual = inicial
}
return resultado
end
-- Ou set_andar(), para um nome mais tradicional.
function alterar_andar(elevador, novo_andar)
if ((novo_andar >= elevador.andar_minimo) and (novo_andar <= elevador.andar_maximo)) then
elevador.andar_atual = novo_andar
end
end
function subir_um_andar(elevador)
alterar_andar(elevador, elevador.andar_atual + 1)
end
function descer_um_andar(elevador)
alterar_andar(elevador, elevador.andar_atual - 1)
end
math.randomseed(os.time())
local elevador = crie_elevador(0, 0, 5)
for i = 1, 10 do
local andar_inicial = elevador.andar_atual
print("O elevador está no " .. andar_inicial .. "º andar")
if (math.random(0, 1) == 0) then
print("Próximo comando: descer")
descer_um_andar(elevador)
else
print("Próximo comando: subir")
subir_um_andar(elevador)
end
local andar_final = elevador.andar_atual
if (andar_inicial ~= andar_final) then
print("Vruuum!")
else
print("... Nada acontece.")
end
end
print("O elevador está no " .. elevador.andar_atual .. "º andar")
extends Node
class Elevador:
var andar_atual
var andar_minimo
var andar_maximo
func _init(andar_inicial = 0, andar_minimo = 0, andar_maximo = 5):
var maximo = andar_minimo
var minimo = andar_minimo
if (andar_minimo > andar_maximo):
minimo = andar_maximo
else:
maximo = andar_maximo
var inicial = andar_inicial
if (andar_inicial < minimo):
inicial = minimo
elif (andar_inicial > maximo):
inicial = maximo
self.andar_minimo = minimo
self.andar_maximo = maximo
self.andar_atual = inicial
# Ou set_andar(), para um nome mais tradicional.
func alterar_andar(elevador, novo_andar):
if ((novo_andar >= elevador.andar_minimo) and (novo_andar <= elevador.andar_maximo)):
elevador.andar_atual = novo_andar
func subir_um_andar(elevador):
alterar_andar(elevador, elevador.andar_atual + 1)
func descer_um_andar(elevador):
alterar_andar(elevador, elevador.andar_atual - 1)
func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
var minimo = ceil(minimo_inclusive)
var maximo = floor(maximo_inclusive)
# randi(): [0.0, 1.0[
return randi() % int(maximo + 1 - minimo) + minimo
func _ready():
randomize()
var elevador = Elevador.new(0, 0, 5)
for i in range(10):
var andar_inicial = elevador.andar_atual
print("O elevador está no " + str(andar_inicial) + "º andar")
if (inteiro_aleatorio(0, 1) == 0):
print("Próximo comando: descer")
descer_um_andar(elevador)
else:
print("Próximo comando: subir")
subir_um_andar(elevador)
var andar_final = elevador.andar_atual
if (andar_inicial != andar_final):
print("Vruuum!")
else:
print("... Nada acontece.")
print("O elevador está no " + str(elevador.andar_atual) + "º andar")
O exemplo fornece o construtor mais robusto apresentado neste tópico. A implementação assegura que todos os valores usados sejam válidos para garantir a consistência de dados para processamento. As verificações apresentadas garantem que os valores sejam inicializados conforme a especificação para o problema. Tecnicamente, como as linguagens dos exemplos possuem tipagem dinâmica, existiria a possibilidade de atribuir valores de tipos incorretos; isso será abordado em uma das próximas subseções.
Para uma possível melhoria, poder-se-ia definir um procedimento escreva_andar( )
.
Assim, ao invés de andar zero, poder-se-ia escrever térreo.
De qualquer forma, linguagens procedurais normalmente não garantem a integridade de dados contra atribuições diretas.
Embora alterar_andar()
não permita a definição de um valor inválido para a variável andar_atual
, nada impediria o uso de elevador.andar_atual = 1234
(assumindo que 1234 seja maior que andar_maximo
) ou elevador.andar_atual = "Franco"
.
Em OOP, é possível impedir tal uso por meio de modificadores de acesso (access modifiers) ou especificadores de acesso (access specifiers).
Três dos mais comuns são private
(acesso privado), public
(acesso público) e protected
(acesso protegido), que aparecem, por exemplo, em linguagens como C++, Java e C#.
Um atributo com acesso público é como um atributo de registro: ela pode ser acessada e modificada em qualquer parte do código na qual o objeto esteja em escopo.
Um atributo com acesso privado pode ser modificado apenas pela classe que definir o atributo.
Um atributo com acesso protegido pode ser modificado apenas pela classe que definir o atributo ou por suas classes derivadas (subclasses).
Assim, private
e protected
poderiam ser usadas para impedir leituras e escritas de valores restritos (desde que usadas, que as subrotinas criadas fossem convertidas para métodos, e que se definisse um método obtenha_andar_atual()
ou get_andar_atual()
para acesso ao valor do atributo restrito).
Como o tópico não é sobre OOP, deve-se ter responsabilidade e maturidade para usar corretamente a implementação. As alterações devem ser feitas usando apenas as subrotinas definidas, mesmo que seja possível alterar valores diretamente. Isso é algo que deve ser autoimposto. De fato, muitas boas práticas de OOP podem ser aplicadas a outras linguagens de programação utilizando-se de bom senso e autocontrole.
Tipos Abstratos de Dados (Abstract Data Types ou ADTs)
A combinação de abstração de dados com abstração funcional pode ser usada para a criação de tipos abstratos de dados (abstract data type ou ADTs). Uma possível implementação de tipo abstrato de dados é chamada de tipo concreto de dados (concrete data type).
Um tipo abstrato de dados define uma interface para ocultar detalhes de implementação de um registro. Ao invés de manipular diretamente as variáveis internas, usa-se as subrotinas definidas para programar usando o ADT. A definição das subrotinas é a responsável por modificar corretamente os atributos. Em outras palavras, os dados e o código para processá-los são meros detalhes de implementação. O uso do tipo deve ser feito única e exclusivamente usando as subrotinas fornecidas.
A especificação de interfaces para estruturas de dados é um exemplo típico de tipo abstrato de dados. Por exemplo, uma lista pode incluir as seguintes operações:
- Criar a lista;
- Adicionar elemento à lista;
- Remover elemento da lista;
- Acessar elemento da lista;
- Iterar pela lista.
Para demostrar o potencial da abordagem, pode-se retomar o exemplo do tipo ItemLista
para a definição de um tipo Lista
, que implemente uma lista encadeada (por isso chamada ListaEncadeada
).
Embora a implementação não use nenhum recurso não comentado anteriormente (em Python, del
permite desalocar memória, como comentado para dicionários), ela é mais complexa que os demais exemplos apresentados até este momento.
Para este tópico, o importante não é entendê-la completamente, mas constatar que operações definidas como subrotinas podem ocultar os detalhes de implementação de um registro.
class ItemLista {
constructor(valor, proximo = null) {
this.valor = valor
this.proximo = proximo
}
}
class ListaEncadeada {
constructor() {
this.inicio = null
this.tamanho = 0
}
}
function acesse_item_lista(lista, indice) {
if ((indice < 0) || (indice >= lista.tamanho)) {
return null
}
let indice_atual = 0
let item_lista = lista.inicio
while (indice_atual < indice) {
item_lista = item_lista.proximo
++indice_atual
}
return item_lista
}
// Índice negativo insere no fim da lista.
function adicione_a_lista(lista, valor, indice = -1) {
if (indice > lista.tamanho) {
// Índice após o final da lista; erro de uso.
return lista
}
let novo_item = new ItemLista(valor)
if (!lista.inicio) {
lista.inicio = novo_item
} else {
if (indice === 0) {
novo_item.proximo = lista.inicio
lista.inicio = novo_item
} else {
if (indice < 0) {
indice = lista.tamanho
}
let item_anterior = acesse_item_lista(lista, indice - 1)
novo_item.proximo = item_anterior.proximo
item_anterior.proximo = novo_item
}
}
++lista.tamanho
return lista
}
// Índice negativo remove do fim da lista.
function remova_da_lista(lista, indice = -1) {
if (!lista.inicio) {
// Lista vazia, não há o que remover.
return lista
} else if (indice >= lista.tamanho) {
// Índice após o final da lista; erro de uso.
return lista
}
if (indice === 0) {
let item_remover = lista.inicio
lista.inicio = lista.inicio.proximo
item_remover = null
} else {
if (indice < 0) {
indice = lista.tamanho - 1
}
let item_anterior = acesse_item_lista(lista, indice - 1)
let item_remover = item_anterior.proximo
item_anterior.proximo = item_remover.proximo
item_remover = null
}
--lista.tamanho
return lista
}
function escreva_lista(lista) {
let item_lista = lista.inicio
let texto = "["
while (item_lista) {
texto += item_lista.valor + ", "
item_lista = item_lista.proximo
}
texto += "]"
console.log(texto)
}
let numeros = new ListaEncadeada()
adicione_a_lista(numeros, 1)
adicione_a_lista(numeros, 2)
adicione_a_lista(numeros, 3)
adicione_a_lista(numeros, 0, 0)
adicione_a_lista(numeros, 1.5, 2)
adicione_a_lista(numeros, 2.5, 4)
adicione_a_lista(numeros, 4, 6)
adicione_a_lista(numeros, 3.5, 6)
escreva_lista(numeros)
remova_da_lista(numeros)
remova_da_lista(numeros, 0)
remova_da_lista(numeros, 1)
remova_da_lista(numeros, 3)
escreva_lista(numeros)
class ItemLista:
def __init__(self, valor, proximo = None):
self.valor = valor
self.proximo = proximo
class ListaEncadeada:
def __init__(self):
self.inicio = None
self.tamanho = 0
def acesse_item_lista(lista, indice):
if ((indice < 0) or (indice >= lista.tamanho)):
return None
indice_atual = 0
item_lista = lista.inicio
while (indice_atual < indice):
item_lista = item_lista.proximo
indice_atual += 1
return item_lista
# Índice negativo insere no fim da lista.
def adicione_a_lista(lista, valor, indice = -1):
if (indice > lista.tamanho):
# Índice após o final da lista; erro de uso.
return lista
novo_item = ItemLista(valor)
if (not lista.inicio):
lista.inicio = novo_item
else:
if (indice == 0):
novo_item.proximo = lista.inicio
lista.inicio = novo_item
else:
if (indice < 0):
indice = lista.tamanho
item_anterior = acesse_item_lista(lista, indice - 1)
novo_item.proximo = item_anterior.proximo
item_anterior.proximo = novo_item
lista.tamanho += 1
return lista
# Índice negativo remove do fim da lista.
def remova_da_lista(lista, indice = -1):
if (not lista.inicio):
# Lista vazia, não há o que remover.
return lista
elif (indice >= lista.tamanho):
# Índice após o final da lista; erro de uso.
return lista
if (indice == 0):
item_remover = lista.inicio
lista.inicio = lista.inicio.proximo
del item_remover
else:
if (indice < 0):
indice = lista.tamanho - 1
item_anterior = acesse_item_lista(lista, indice - 1)
item_remover = item_anterior.proximo
item_anterior.proximo = item_remover.proximo
item_remover
lista.tamanho -= 1
return lista
def escreva_lista(lista):
item_lista = lista.inicio
texto = "["
while (item_lista):
texto += str(item_lista.valor) + ", "
item_lista = item_lista.proximo
texto += "]"
print(texto)
numeros = ListaEncadeada()
adicione_a_lista(numeros, 1)
adicione_a_lista(numeros, 2)
adicione_a_lista(numeros, 3)
adicione_a_lista(numeros, 0, 0)
adicione_a_lista(numeros, 1.5, 2)
adicione_a_lista(numeros, 2.5, 4)
adicione_a_lista(numeros, 4, 6)
adicione_a_lista(numeros, 3.5, 6)
escreva_lista(numeros)
remova_da_lista(numeros)
remova_da_lista(numeros, 0)
remova_da_lista(numeros, 1)
remova_da_lista(numeros, 3)
escreva_lista(numeros)
function crie_item_lista(valor, proximo)
local resultado = {
valor = valor,
proximo = proximo or nil
}
return resultado
end
function crie_lista_encadeada()
local resultado = {
inicio = nil,
tamanho = 0
}
return resultado
end
function acesse_item_lista(lista, indice)
if ((indice < 1) or (indice > lista.tamanho)) then
return nil
end
local indice_atual = 1
local item_lista = lista.inicio
while (indice_atual < indice) do
item_lista = item_lista.proximo
indice_atual = indice_atual + 1
end
return item_lista
end
-- Índice zero ou negativo insere no fim da lista.
function adicione_a_lista(lista, valor, indice)
indice = indice or -1
if (indice > (lista.tamanho + 1)) then
-- Índice após o final da lista; erro de uso.
return lista
end
local novo_item = crie_item_lista(valor)
if (not lista.inicio) then
lista.inicio = novo_item
else
if (indice == 1) then
novo_item.proximo = lista.inicio
lista.inicio = novo_item
else
if (indice <= 0) then
indice = lista.tamanho + 1
end
local item_anterior = acesse_item_lista(lista, indice - 1)
novo_item.proximo = item_anterior.proximo
item_anterior.proximo = novo_item
end
end
lista.tamanho = lista.tamanho + 1
return lista
end
-- Índice zero ou negativo remove do fim da lista.
function remova_da_lista(lista, indice)
indice = indice or -1
if (not lista.inicio) then
-- Lista vazia, não há o que remover.
return lista
elseif (indice > lista.tamanho) then
-- Índice após o final da lista; erro de uso.
return lista
end
if (indice == 1) then
local item_remover = lista.inicio
lista.inicio = lista.inicio.proximo
item_remover = nil
else
if (indice <= 0) then
indice = lista.tamanho
end
local item_anterior = acesse_item_lista(lista, indice - 1)
local item_remover = item_anterior.proximo
item_anterior.proximo = item_remover.proximo
item_remover = nil
end
lista.tamanho = lista.tamanho - 1
return lista
end
function escreva_lista(lista)
local item_lista = lista.inicio
local texto = "["
while (item_lista) do
texto = texto .. tostring(item_lista.valor) .. ", "
item_lista = item_lista.proximo
end
texto = texto .. "]"
print(texto)
end
local numeros = crie_lista_encadeada()
adicione_a_lista(numeros, 1)
escreva_lista(numeros)
adicione_a_lista(numeros, 2)
escreva_lista(numeros)
adicione_a_lista(numeros, 3)
escreva_lista(numeros)
adicione_a_lista(numeros, 0, 1)
escreva_lista(numeros)
adicione_a_lista(numeros, 1.5, 3)
escreva_lista(numeros)
adicione_a_lista(numeros, 2.5, 5)
escreva_lista(numeros)
adicione_a_lista(numeros, 4, 7)
escreva_lista(numeros)
adicione_a_lista(numeros, 3.5, 7)
escreva_lista(numeros)
remova_da_lista(numeros)
remova_da_lista(numeros, 1)
remova_da_lista(numeros, 2)
remova_da_lista(numeros, 4)
escreva_lista(numeros)
extends Node
class ItemLista:
var valor
var proximo
func _init(valor, proximo = null):
self.valor = valor
self.proximo = proximo
class ListaEncadeada:
var inicio
var tamanho
func _init():
self.inicio = null
self.tamanho = 0
func acesse_item_lista(lista, indice):
if ((indice < 0) or (indice >= lista.tamanho)):
return null
var indice_atual = 0
var item_lista = lista.inicio
while (indice_atual < indice):
item_lista = item_lista.proximo
indice_atual += 1
return item_lista
# Índice negativo insere no fim da lista.
func adicione_a_lista(lista, valor, indice = -1):
if (indice > lista.tamanho):
# Índice após o final da lista; erro de uso.
return lista
var novo_item = ItemLista.new(valor)
if (not lista.inicio):
lista.inicio = novo_item
else:
if (indice == 0):
novo_item.proximo = lista.inicio
lista.inicio = novo_item
else:
if (indice < 0):
indice = lista.tamanho
var item_anterior = acesse_item_lista(lista, indice - 1)
novo_item.proximo = item_anterior.proximo
item_anterior.proximo = novo_item
lista.tamanho += 1
return lista
# Índice negativo remove do fim da lista.
func remova_da_lista(lista, indice = -1):
if (not lista.inicio):
# Lista vazia, não há o que remover.
return lista
elif (indice >= lista.tamanho):
# Índice após o final da lista; erro de uso.
return lista
if (indice == 0):
var item_remover = lista.inicio
lista.inicio = lista.inicio.proximo
# item_remover.unreference()
item_remover = null
else:
if (indice < 0):
indice = lista.tamanho - 1
var item_anterior = acesse_item_lista(lista, indice - 1)
var item_remover = item_anterior.proximo
item_anterior.proximo = item_remover.proximo
# item_remover.unreference()
item_remover = null
lista.tamanho -= 1
return lista
func escreva_lista(lista):
var item_lista = lista.inicio
var texto = "["
while (item_lista):
texto += str(item_lista.valor) + ", "
item_lista = item_lista.proximo
texto += "]"
print(texto)
func _ready():
var numeros = ListaEncadeada.new()
adicione_a_lista(numeros, 1)
adicione_a_lista(numeros, 2)
adicione_a_lista(numeros, 3)
adicione_a_lista(numeros, 0, 0)
adicione_a_lista(numeros, 1.5, 2)
adicione_a_lista(numeros, 2.5, 4)
adicione_a_lista(numeros, 4, 6)
adicione_a_lista(numeros, 3.5, 6)
escreva_lista(numeros)
remova_da_lista(numeros)
remova_da_lista(numeros, 0)
remova_da_lista(numeros, 1)
remova_da_lista(numeros, 3)
escreva_lista(numeros)
O exemplo não fornece uma subrotina para iteração, embora ela pudesse ser definida, por exemplo, como uma função acessar_proximo_item()
.
Embora a implementação da lista encadeada seja mais complexa, a operação de uma variável do tipo ListaEncadeada
é simples e similar ao uso de vetores e listas em linguagens como JavaScript, Python, GDScript e Lua:
- A criação da lista usa o construtor fornecido pela linguagem ou a função de criação definida (
crie_lista_encadeada()
); - A inserção de um valor à lista usa a função
adicione_a_lista()
; - A remoção de um valor da lista usa a função
remova_da_lista()
; - O acesso a um valor da lista usa a função
acesse_item_lista()
.
Em linguagens que forneçam recursos de sobrecarga de operadores, seria possível, por exemplo, definir o operador colchetes para acessar o valor de um índice na lista. Isso tornaria a leitura de valores semelhante a feita em um vetor.
Registro como Parâmetro para Subrotina
Em problemas complexos, é possível que uma subrotina precise receber muitos parâmetros. Por exemplo, simulações complexas podem ter dezenas ou centenas de variáveis como parâmetros para configuração. Contudo, o uso de subrotinas com muitos parâmetros torna-se complexo. Em particular, introduzir ou remover um parâmetro da definição da subrotina pode tornar-se uma operação difícil.
Em casos assim, ao invés de uma lista de parâmetros longa, pode ser melhor definir um registro como único parâmetro da subrotina. Todos os parâmetros podem ser adicionados ao registro. Os valores padrão para o registro podem ser os valores mais usuais para uso da subrotina. Assim, bastaria alterar valores desejados em chamadas personalizadas.
class ParametrosFuncaoComplexa {
constructor() {
this.saudacao = "Olá"
this.nome = "Franco"
this.despedida = "Tchau."
}
}
function minha_subrotina_complexa(parametros) {
console.log(parametros.saudacao, parametros.nome, parametros.despedida)
}
var parametros = new ParametrosFuncaoComplexa()
parametros.saudacao = "Oi"
minha_subrotina_complexa(parametros)
class ParametrosFuncaoComplexa:
def __init__(self):
self.saudacao = "Olá"
self.nome = "Franco"
self.despedida = "Tchau."
def minha_subrotina_complexa(parametros):
print(parametros.saudacao, parametros.nome, parametros.despedida)
parametros = ParametrosFuncaoComplexa()
parametros.saudacao = "Oi"
minha_subrotina_complexa(parametros)
function crie_parametros_funcao_complexa()
local resultado = {
saudacao = "Olá!",
nome = "Franco",
despedida = "Tchau."
}
return resultado
end
function minha_subrotina_complexa(parametros)
print(parametros.saudacao, parametros.nome, parametros.despedida)
end
local parametros = crie_parametros_funcao_complexa()
parametros.saudacao = "Oi"
minha_subrotina_complexa(parametros)
extends Node
class ParametrosFuncaoComplexa:
var saudacao = "Olá!"
var nome = "Franco"
var despedida = "Tchau."
func minha_subrotina_complexa(parametros):
printt(parametros.saudacao, parametros.nome, parametros.despedida)
func _ready():
var parametros = ParametrosFuncaoComplexa.new()
parametros.saudacao = "Oi"
minha_subrotina_complexa(parametros)
O exemplo define apenas três parâmetros, mas eles poderiam ser 10 ou 20, por exemplo. É importante notar que valores não são definidos no construtor; caso contrário, a utilidade da técnica reduzir-se-ia. Afinal, a única alteração foi no local do problema.
Além disso, a inicialização com valores pré-definidos pode comprometer o desempenho do programa, caso ela seja computacionalmente cara e todos os valores sejam redefinidos em seqüência. Portanto, é prudente equilibrar valores adotados como padrão e os que devem ser, obrigatoriamente, definidos antes da chamada. Contudo, antes de supor, o ideal é medir usando um profiler para verificar se a otimização realmente é necessária.
Verificação de Tipos para Parâmetros
Em linguagens de tipagem dinâmica, pode-se usar asserções para verificar os tipos de parâmetros usados na construção de um registro. Isso pode garantir a inicialização de valores com tipos corretos (dado que a linguagem não restringe atribuição quanto a tipos).
class MeuNumero {
constructor(valor) {
console.assert(typeof(valor) === "number", "valor deve ser um número.")
this.valor = valor
}
}
var valido = new MeuNumero(1)
var invalido = new MeuNumero("Franco")
class MeuNumero:
def __init__(self, valor):
assert isinstance(valor, (int, float)), "valor deve ser um número."
self.valor = valor
valido = MeuNumero(1)
invalido = MeuNumero("Franco")
function crie_meu_numero(valor)
assert(type(valor) == "number", "valor deve ser um número.")
local resultado = {
valor = valor
}
return resultado
end
local valido = crie_meu_numero(1)
local invalido = crie_meu_numero("Franco")
extends Node
class MeuNumero:
var valor
func _init(valor):
assert(typeof(valor) == TYPE_INT or typeof(valor) == TYPE_REAL, "valor deve ser um número.")
self.valor = valor
func _ready():
var valido = MeuNumero.new(1)
var invalido = MeuNumero.new("Franco")
Como feito em Subrotinas (Funções e Procedimentos), asserções também poder ser usadas para verificação de valores. Assim, poder-se-ia impedir a criação inicial de um registro com tipos e/ou valores iniciais inválidos.
Exemplos
Esta seção fornece exemplos adicionais para complementar os fornecidos ao longo do texto. Os exemplos são programas completos, estruturados usando registros e processados por combinações subrotinas. O intuito é mostrar como organizar programas e que já é possível começar a escrever sistemas mais complexos e complexos usando o conhecimento adquirido até este ponto.
Programa de Alto Nível
A combinação de registros, subrotinas e coleções permite criar programas complexos que também sejam elegantes e simples de ler. Esta seção retoma o exemplo de receitas e ingredientes para acrescentar funcionalidades. Novas funcionalidades são definidas por subrotinas.
class Ingrediente {
constructor(nome = "", quantidade = 0.0, unidade_medida = "") {
this.nome = nome
this.quantidade = quantidade
this.unidade_medida = unidade_medida
}
}
class Receita {
constructor(nome = "", modo_preparo = "", ingredientes = []) {
this.nome = nome
this.modo_preparo = modo_preparo
this.ingredientes = ingredientes
}
}
function escreva_ingrediente(ingrediente) {
alert("- " + ingrediente.nome + ": " + ingrediente.quantidade + " " + ingrediente.unidade_medida)
}
function leia_ingrediente() {
let ingrediente = new Ingrediente()
ingrediente.nome = prompt("Nome do ingrediente:")
ingrediente.quantidade = prompt("Quantidade de " + ingrediente.nome + ":")
ingrediente.unidade_medida = prompt("Unidade de medida:")
return ingrediente
}
function escreva_receita(receita) {
alert(receita.nome)
alert("Ingredientes:")
for (let ingrediente of receita.ingredientes) {
escreva_ingrediente(ingrediente)
}
alert("Modo de preparo:")
alert(receita.modo_preparo)
}
function leia_receita() {
let receita = new Receita()
receita.nome = prompt("Nome da receita:")
alert("Ingredientes")
let adicionar_novo_ingrediente = true
while (adicionar_novo_ingrediente) {
let ingrediente = leia_ingrediente()
receita.ingredientes.push(ingrediente)
let resposta = prompt("Deseja adicionar um novo ingrediente?\nDigite Sim para continuar, qualquer outro valor para terminar")
adicionar_novo_ingrediente = (resposta.toLowerCase() === "sim")
}
receita.modo_preparo = prompt("Modo de preparo para " + receita.nome + ":")
return receita
}
function escreva_receitas(receitas) {
alert("Livro de Receitas")
let numero_receita = 1
for (receita of receitas) {
alert("Receita #" + numero_receita)
escreva_receita(receita)
alert("")
++numero_receita
}
}
function escreva_menu() {
let mensagem = "Opções\n\n"
mensagem += "1. Mostrar todas as receitas cadastradas;\n"
mensagem += "2. Cadastrar nova receita;\n"
mensagem += "\n"
mensagem += "Qualquer outro valor encerra o programa.\n"
alert(mensagem)
}
function carregue_receitas() {
let receitas = [
new Receita("Pão",
"...",
[
new Ingrediente("Água", 3.0, "Xícaras"),
new Ingrediente("Farinha", 4.0, "Xícaras"),
new Ingrediente("Sal", 2.0, "Colheres de Sopa"),
new Ingrediente("Fermento", 2.0, "Colheres de Chá")
]),
new Receita("Pão Doce",
"...",
[
new Ingrediente("Água", 3.0, "Xícaras"),
new Ingrediente("Farinha", 4.0, "Xícaras"),
new Ingrediente("Açúcar", 2.0, "Xícaras"),
new Ingrediente("Sal", 2.0, "Colheres de Sopa"),
new Ingrediente("Fermento", 2.0, "Colheres de Chá")
]),
]
return receitas
}
function main() {
let receitas = carregue_receitas()
let fim = false
while (!fim) {
escreva_menu()
let opcao_escolhida = prompt("Escolha uma opção:")
switch (opcao_escolhida) {
case "1": {
escreva_receitas(receitas)
break
}
case "2": {
let nova_receita = leia_receita()
receitas.push(nova_receita)
break
}
default: {
fim = true
}
}
}
}
main()
class Ingrediente:
def __init__(self, nome = "", quantidade = 0.0, unidade_medida = ""):
self.nome = nome
self.quantidade = quantidade
self.unidade_medida = unidade_medida
class Receita:
def __init__(self, nome = "", modo_preparo = "", ingredientes = None):
self.nome = nome
self.modo_preparo = modo_preparo
self.ingredientes = ingredientes if (ingredientes != None) else []
def escreva_ingrediente(ingrediente):
print("- " + ingrediente.nome + ": " + str(ingrediente.quantidade) + " " + ingrediente.unidade_medida)
def leia_ingrediente():
ingrediente = Ingrediente()
ingrediente.nome = input("Nome do ingrediente: ")
ingrediente.quantidade = float(input("Quantidade de " + ingrediente.nome + ": "))
ingrediente.unidade_medida = input("Unidade de medida: ")
return ingrediente
def escreva_receita(receita):
print(receita.nome)
print("Ingredientes: ")
for ingrediente in receita.ingredientes:
escreva_ingrediente(ingrediente)
print("Modo de preparo: ")
print(receita.modo_preparo)
def leia_receita():
receita = Receita()
receita.nome = input("Nome da receita: ")
print("Ingredientes")
adicionar_novo_ingrediente = True
while (adicionar_novo_ingrediente):
ingrediente = leia_ingrediente()
receita.ingredientes.append(ingrediente)
resposta = input("Deseja adicionar um novo ingrediente?\nDigite Sim para continuar, qualquer outro valor para terminar ")
adicionar_novo_ingrediente = (resposta.lower() == "sim")
receita.modo_preparo = input("Modo de preparo para " + receita.nome + ": ")
return receita
def escreva_receitas(receitas):
print("Livro de Receitas")
numero_receita = 1
for receita in receitas:
print("Receita #" + str(numero_receita))
escreva_receita(receita)
print("")
numero_receita += 1
def escreva_menu():
mensagem = "Opções\n\n"
mensagem += "1. Mostrar todas as receitas cadastradas;\n"
mensagem += "2. Cadastrar nova receita;\n"
mensagem += "\n"
mensagem += "Qualquer outro valor encerra o programa.\n"
print(mensagem)
def carregue_receitas():
receitas = [
Receita("Pão",
"...",
[
Ingrediente("Água", 3.0, "Xícaras"),
Ingrediente("Farinha", 4.0, "Xícaras"),
Ingrediente("Sal", 2.0, "Colheres de Sopa"),
Ingrediente("Fermento", 2.0, "Colheres de Chá")
]),
Receita("Pão Doce",
"...",
[
Ingrediente("Água", 3.0, "Xícaras"),
Ingrediente("Farinha", 4.0, "Xícaras"),
Ingrediente("Açúcar", 2.0, "Xícaras"),
Ingrediente("Sal", 2.0, "Colheres de Sopa"),
Ingrediente("Fermento", 2.0, "Colheres de Chá")
]),
]
return receitas
def main():
receitas = carregue_receitas()
fim = False
while (not fim):
escreva_menu()
opcao_escolhida = input("Escolha uma opção: ")
if (opcao_escolhida == "1"):
escreva_receitas(receitas)
elif (opcao_escolhida == "2"):
nova_receita = leia_receita()
receitas.append(nova_receita)
else:
fim = True
if (__name__ == "__main__"):
main()
function crie_ingrediente(nome, quantidade, unidade_medida)
local resultado = {
nome = nome or "",
quantidade = quantidade or 0.0,
unidade_medida = unidade_medida or ""
}
return resultado
end
function crie_receita(nome, modo_preparo, ingredientes)
local resultado = {
nome = nome or "",
modo_preparo = quantidade or "",
ingredientes = ingredientes or {}
}
return resultado
end
function escreva_ingrediente(ingrediente)
print("- " .. ingrediente.nome .. ": " .. ingrediente.quantidade .. " " .. ingrediente.unidade_medida)
end
function leia_ingrediente()
local ingrediente = crie_ingrediente()
io.write("Nome do ingrediente: ")
ingrediente.nome = io.read("*line")
io.write("Quantidade de " .. ingrediente.nome .. ": ")
ingrediente.quantidade = io.read("*number", "*line")
io.write("Unidade de medida: ")
ingrediente.unidade_medida = io.read("*line")
return ingrediente
end
function escreva_receita(receita)
print(receita.nome)
print("Ingredientes: ")
for _, ingrediente in ipairs(receita.ingredientes) do
escreva_ingrediente(ingrediente)
end
print("Modo de preparo: ")
print(receita.modo_preparo)
end
function leia_receita()
local receita = crie_receita()
io.write("Nome da receita: ")
receita.nome = io.read("*line")
print("Ingredientes")
local adicionar_novo_ingrediente = true
while (adicionar_novo_ingrediente) do
local ingrediente = leia_ingrediente()
table.insert(receita.ingredientes, ingrediente)
io.write("Deseja adicionar um novo ingrediente?\nDigite Sim para continuar, qualquer outro valor para terminar ")
local resposta = io.read("*line*")
adicionar_novo_ingrediente = (string.lower(resposta) == "sim")
end
io.write("Modo de preparo para " .. receita.nome .. ": ")
receita.modo_preparo = io.read("*line")
return receita
end
function escreva_receitas(receitas)
print("Livro de Receitas")
local numero_receita = 1
for _, receita in ipairs(receitas) do
print("Receita #" .. numero_receita)
escreva_receita(receita)
print("")
numero_receita = numero_receita + 1
end
end
function escreva_menu()
local mensagem = "Opções\n\n"
mensagem = mensagem .. "1. Mostrar todas as receitas cadastradas;\n"
mensagem = mensagem .. "2. Cadastrar nova receita;\n"
mensagem = mensagem .. "\n"
mensagem = mensagem .. "Qualquer outro valor encerra o programa.\n"
print(mensagem)
end
function carregue_receitas()
local receitas = {
crie_receita("Pão",
"...",
{
crie_ingrediente("Água", 3.0, "Xícaras"),
crie_ingrediente("Farinha", 4.0, "Xícaras"),
crie_ingrediente("Sal", 2.0, "Colheres de Sopa"),
crie_ingrediente("Fermento", 2.0, "Colheres de Chá")
}),
crie_receita("Pão Doce",
"...",
{
crie_ingrediente("Água", 3.0, "Xícaras"),
crie_ingrediente("Farinha", 4.0, "Xícaras"),
crie_ingrediente("Açúcar", 2.0, "Xícaras"),
crie_ingrediente("Sal", 2.0, "Colheres de Sopa"),
crie_ingrediente("Fermento", 2.0, "Colheres de Chá")
}),
}
return receitas
end
function main()
local receitas = carregue_receitas()
local fim = false
while (not fim) do
escreva_menu()
io.write("Escolha uma opção: ")
local opcao_escolhida = io.read("*line")
if (opcao_escolhida == "1") then
escreva_receitas(receitas)
elseif (opcao_escolhida == "2") then
local nova_receita = leia_receita()
table.insert(receitas, nova_receita)
else
fim = true
end
end
end
main()
extends Node
func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
var minimo = ceil(minimo_inclusive)
var maximo = floor(maximo_inclusive)
# randi(): [0.0, 1.0[
return randi() % int(maximo + 1 - minimo) + minimo
func input(mensagem, valor_esperado):
print(mensagem)
if (valor_esperado == "sorteio menu"):
randomize()
return str(inteiro_aleatorio(1, 3))
return valor_esperado
class Ingrediente:
var nome
var quantidade
var unidade_medida
func _init(nome = "", quantidade = 0.0, unidade_medida = ""):
self.nome = nome
self.quantidade = quantidade
self.unidade_medida = unidade_medida
class Receita:
var nome
var modo_preparo
var ingredientes
func _init(nome = "", modo_preparo = "", ingredientes = []):
self.nome = nome
self.modo_preparo = modo_preparo
self.ingredientes = ingredientes
func escreva_ingrediente(ingrediente):
print("- " + ingrediente.nome + ": " + str(ingrediente.quantidade) + " " + ingrediente.unidade_medida)
func leia_ingrediente():
var ingrediente = Ingrediente.new()
ingrediente.nome = input("Nome do ingrediente: ", "farinha")
ingrediente.quantidade = float(input("Quantidade de " + ingrediente.nome + ": ", "1.23"))
ingrediente.unidade_medida = input("Unidade de medida: ", "kg")
return ingrediente
func escreva_receita(receita):
print(receita.nome)
print("Ingredientes: ")
for ingrediente in receita.ingredientes:
escreva_ingrediente(ingrediente)
print("Modo de preparo: ")
print(receita.modo_preparo)
func leia_receita():
var receita = Receita.new()
receita.nome = input("Nome da receita: ", "Farinha")
print("Ingredientes")
var adicionar_novo_ingrediente = true
while (adicionar_novo_ingrediente):
var ingrediente = leia_ingrediente()
receita.ingredientes.append(ingrediente)
var resposta = input("Deseja adicionar um novo ingrediente?\nDigite Sim para continuar, qualquer outro valor para terminar ", "não")
adicionar_novo_ingrediente = (resposta.to_lower() == "sim")
receita.modo_preparo = input("Modo de preparo para " + receita.nome + ": ", "Não comestível!")
return receita
func escreva_receitas(receitas):
print("Livro de Receitas")
var numero_receita = 1
for receita in receitas:
print("Receita #" + str(numero_receita))
escreva_receita(receita)
print("")
numero_receita += 1
func escreva_menu():
var mensagem = "Opções\n\n"
mensagem += "1. Mostrar todas as receitas cadastradas;\n"
mensagem += "2. Cadastrar nova receita;\n"
mensagem += "\n"
mensagem += "Qualquer outro valor encerra o programa.\n"
print(mensagem)
func carregue_receitas():
var receitas = [
Receita.new("Pão",
"...",
[
Ingrediente.new("Água", 3.0, "Xícaras"),
Ingrediente.new("Farinha", 4.0, "Xícaras"),
Ingrediente.new("Sal", 2.0, "Colheres de Sopa"),
Ingrediente.new("Fermento", 2.0, "Colheres de Chá")
]),
Receita.new("Pão Doce",
"...",
[
Ingrediente.new("Água", 3.0, "Xícaras"),
Ingrediente.new("Farinha", 4.0, "Xícaras"),
Ingrediente.new("Açúcar", 2.0, "Xícaras"),
Ingrediente.new("Sal", 2.0, "Colheres de Sopa"),
Ingrediente.new("Fermento", 2.0, "Colheres de Chá")
]),
]
return receitas
func _ready():
var receitas = carregue_receitas()
var fim = false
while (not fim):
escreva_menu()
var opcao_escolhida = input("Escolha uma opção: ", "sorteio menu")
if (opcao_escolhida == "1"):
escreva_receitas(receitas)
elif (opcao_escolhida == "2"):
var nova_receita = leia_receita()
receitas.append(nova_receita)
else:
fim = true
Na versão para JavaScript, pode-se trocar alert()
por console.log()
para escrever o resultado no console do navegador (ao invés de um painel de alerta).
Uma melhoria desejável para o uso de alert()
seria retornar cadeias de caracteres para apresentar receitas em um único diálogo (ao invés de um diálogo por frase).
Para isso, poder-se-ia criar uma função ingrediente_para_string()
que retornasse o texto (ao invés de escrevê-lo como feito em escreva_ingrediente()
).
A lista de ingredientes como cadeias de caracteres poderia ser concatenada em uma única mensagem, para a geração do alerta com os outros dados da receita.
O procedimento escreva_ingrediente()
também poderia ser refatorado para usar a nova função.
Como GDScript não possui subrotinas para entrada em console (terminal), a implementação define uma função input()
com um segundo parâmetro como valor esperado para o retorno.
Para o uso do menu principal, a função retorna um valor pseudoaleatório entre 1 e 3, simulando a escolha de uma opção.
Assim, o resultado poderá ser diferente a cada uso do programa.
A técnica pode servir como ferramenta útil para automação de testes, como forma de simular entradas de usuárias ou usuários finais em um programa.
Para testes, ao invés de valores aleatórios, poder-se-ia definir um vetor com uma seqüência de entradas (e retornar o valor da próxima posição a cada solicitação).
Além disso, neste momento já é possível começar a implementar interfaces gráficas usando Godot Engine.
Elas serão exploradas em tópicos futuros, possivelmente após uma breve introdução sobre OOP.
Caso você já queira tentar, você poderia seguir os passos definidos na preparação do ambiente de desenvolvimento para GDScript (Godot) para começar com formulários mais simples que o deste exemplo.
Cessando a digressão, o programa resultante é modular, organizado e simples de ler.
Os nomes de subrotinas descrevem o que elas fazem, simplificando a leitura do código do programa.
Por exemplo, a leitura de main()
(criada como programa principal) permitem entender rapidamente o que o programa faz:
- Inicialização de receitas;
- Escrita de menu de opções;
- Leitura (entrada) de opção:
- Escreve receitas salvas;
- Adiciona nova receita.
- (Ou qualquer outro valor) Encerra o programa.
Também é fácil modificar e estender o programa, pois basta adicionar ou remover funcionalidades das respectivas subrotinas (ou criar novas opções para o menu).
Por exemplo, para adicionar validação de dados para a leitura de um ingrediente, poder-se-ia modificar a função leia_ingrediente()
.
Após a introdução de arquivos, também seria possível salvar e carregar receitas de arquivos para persistir dados entre diversas sessões de uso do programa.
Assim, não seria mais necessário predefinir receitas no código-fonte do programa.
Conforme adquire-se experiência em programação, deve-se começar a pensar em arquiteturas de software para organização e estruturação de programas. Boas arquiteturas facilitam a implementação e a manutenção de sistemas.
Por exemplo, é uma boa prática de programação separar funcionalidades de entrada e saída de dados de funcionalidades de modelagem de dados e de processamento de dados. Isso pode ser feito, dentre outros, usando um modelo de três camadas, uma arquitetura multicamada, ou o padrão de software (software pattern) Model-View-Controller (MVC). A próxima seção apresenta um exemplo para implementar uma simulação. Embora o conceito de camadas seja normalmente associado a OOP, ele pode ser explorado de forma procedural (e funcional).
Jogo da Vida (Conway's Game Of Life)
Para ilustrar a separação de lógica e apresentação de um programa, o próximo exemplo apresenta a implementação de um algoritmo chamada Jogo da Vida, mais conhecido pelo nome em Inglês definido pelo criador: Conway's Game of Life. O Jogo da Vida é um exemplo simples de autômato celular. Ele será o primeiro exemplo deste material com uma animação ao invés de saída estática (consequentemente, infelizmente o programa será inacessível para pessoas não videntes).
A citação a seguir contém as regras para o Jogo da Vida definidas no artigo da Wikipedia:
- Qualquer célula viva com menos de dois vizinhos vivos morre de solidão.
- Qualquer célula viva com mais de três vizinhos vivos morre de superpopulação.
- Qualquer célula morta com exatamente três vizinhos vivos se torna uma célula viva.
- Qualquer célula viva com dois ou três vizinhos vivos continua no mesmo estado para a próxima geração.
Para implementar o jogo, define-se uma matriz como um tabuleiro (também chamado de grelha ou grid em Inglês) e adiciona-se valores como células.
Para a implementação das regras, deve-se checar as células (da tabela) vizinhas a cada posição da matriz.
A comparação de valores é feita usando os índices da matriz.
Por exemplo, m[linha][coluna]
, m[linha][coluna + 1]
corresponde à célula na próxima coluna.
Cada posição tem até 8 vizinhos (caso do 5 na tabela a seguir), embora seja possível existir menos. Por exemplo, nas extremidades da tabela, a célula pode ter apenas 3 vizinhos (caso de 1, 3, 7 e 9 no exemplo a seguir).
| 1 | 2 | 3 |
| 4 | 5 | 6 |
| 7 | 8 | 9 |
Uma forma de facilitar a resolução do problema é inserir linhas e colunas adicionais antes da primeira e a após última.
| | | | | |
| | 1 | 2 | 3 | |
| | 4 | 5 | 6 | |
| | 7 | 8 | 9 | |
| | | | | |
Com a alteração, todas as células com dados terão 8 vizinhos. Isso facilita a implementação, porque é possível verificar todas as células adjacentes sem preocupações com casos particulares e exceções. Ou seja, é possível comparar todos os vizinhos da mesma forma, pois todos os valores válidos comportar-se-ão como 5 na tabela anterior. Pode ser mais fácil entender o motivo considerando-se deslocamentos do valor central:
| (-1, -1) | (-1, 0) | (-1, 1) |
| (0, -1) | (0, 0) | (0, 1) |
| (1, -1) | (1, 0) | (1, 1) |
O elemento central (o par linha 0, coluna 0, ou seja, (0, 0)
) é ignorado, pois uma célula não é vizinha dela mesma.
Os demais deslocamentos podem ser implementados como índices na matriz de valores.
tabuleiro[linha - 1][coluna - 1]
tabuleiro[linha - 1][coluna]
tabuleiro[linha - 1][coluna + 1]
tabuleiro[linha][coluna - 1]
tabuleiro[linha][coluna + 1]
tabuleiro[linha + 1][coluna - 1]
tabuleiro[linha + 1][coluna]
tabuleiro[linha + 1][coluna + 1]
Pode-se aplicar diretamente os valores anteriores na solução ou escrever dois laços aninhado para obter os índices.
for linha_vizinha in range(-1, 2):
for coluna_vizinha in range(-1, 2):
if (not ((linha_vizinha == 0) and (coluna_vizinha == 0))):
// ...
Ambas as variações são válidas. A primeira é mais simples; a segunda é mais genérica e permitiria modificar a solução mais facilmente caso se desejasse verificar por células mais distantes. Com as informações anteriores, basta, agora, implementar as regras definidas.
const LINHAS_TABULEIRO = 20
const COLUNAS_TABULEIRO = 20
const CELULA_MORTA = 0
const CELULA_VIVA = 1
function inteiro_aleatorio(minimo_inclusive, maximo_inclusive) {
let minimo = Math.ceil(minimo_inclusive)
let maximo = Math.floor(maximo_inclusive)
return Math.floor(minimo + Math.random() * (maximo + 1 - minimo))
}
class Tabuleiro {
constructor(linhas = LINHAS_TABULEIRO, colunas = COLUNAS_TABULEIRO) {
this.linhas = linhas
this.colunas = colunas
this.valores = []
linhas += 2
colunas += 2
for (let linha = 0; linha < linhas; ++linha) {
let nova_coluna = []
this.valores.push(nova_coluna)
for (let coluna = 0; coluna < colunas; ++coluna) {
nova_coluna.push(CELULA_MORTA)
}
}
}
}
function inicialize_tabuleiro(tabuleiro) {
let linhas = tabuleiro.linhas + 1
let colunas = tabuleiro.colunas + 1
for (let linha = 1; linha < linhas; ++linha) {
for (let coluna = 1; coluna < colunas; ++coluna) {
if (inteiro_aleatorio(0, 100) < 30) {
tabuleiro.valores[linha][coluna] = CELULA_VIVA
} else {
tabuleiro.valores[linha][coluna] = CELULA_MORTA
}
}
}
}
function atualize_tabuleiro(tabuleiro) {
let linhas = tabuleiro.linhas + 1
let colunas = tabuleiro.colunas + 1
// Referência.
let valores = tabuleiro.valores
// Contagem de vizinhos vivos.
let tabuleiro_contagem = new Tabuleiro(tabuleiro.linhas, tabuleiro.colunas)
let contagem_valores = tabuleiro_contagem.valores
for (let linha = 1; linha < linhas; ++linha) {
for (let coluna = 1; coluna < colunas; ++coluna) {
let vizinhos_vivos = 0
for (let linha_vizinha = -1; linha_vizinha < 2; ++linha_vizinha) {
for (let coluna_vizinha = -1; coluna_vizinha < 2; ++coluna_vizinha) {
if (!((linha_vizinha === 0) && (coluna_vizinha === 0))) {
if (valores[linha + linha_vizinha][coluna + coluna_vizinha] === CELULA_VIVA) {
++vizinhos_vivos
}
}
}
}
contagem_valores[linha][coluna] = vizinhos_vivos
}
}
// Atualização de estado baseada na contagem de vizinhos.
for (let linha = 1; linha < linhas; ++linha) {
for (let coluna = 1; coluna < colunas; ++coluna) {
if (valores[linha][coluna] === CELULA_VIVA) {
if (contagem_valores[linha][coluna] < 2) {
// Qualquer célula viva com menos de dois vizinhos vivos
// morre de solidão.
valores[linha][coluna] = CELULA_MORTA
} else if (contagem_valores[linha][coluna] > 3) {
// Qualquer célula viva com mais de três vizinhos vivos
// morre de superpopulação.
valores[linha][coluna] = CELULA_MORTA
} /* else {
// Redundante neste caso, já a mudança é in-place.
// Qualquer célula viva com dois ou três vizinhos vivos
// continua no mesmo estado para a próxima geração.
valores[linha][coluna] = valores[linha][coluna]
} */
} else {
if (contagem_valores[linha][coluna] === 3) {
// Qualquer célula morta com exatamente três vizinhos vivos
// se torna uma célula viva.
valores[linha][coluna] = CELULA_VIVA
}
}
}
}
}
function desenhe_tabuleiro(tabuleiro) {
let linhas = tabuleiro.linhas + 1
let colunas = tabuleiro.colunas + 1
let mensagem = ""
for (let linha = 1; linha < linhas; ++linha) {
for (let coluna = 1; coluna < colunas; ++coluna) {
if (tabuleiro.valores[linha][coluna] === CELULA_VIVA) {
mensagem += "⏹" // Ou "X" ou "*" (qualquer caractere).
} else {
mensagem += " "
}
}
mensagem += "\n"
}
clear()
console.log(mensagem)
// alert(mensagem)
}
async function main() {
let tabuleiro = new Tabuleiro()
inicialize_tabuleiro(tabuleiro)
let numero_iteracoes = 100
for (let iteracao = 0; iteracao < numero_iteracoes; ++iteracao) {
atualize_tabuleiro(tabuleiro)
desenhe_tabuleiro(tabuleiro)
// sleep(): Aguarda 300ms antes da próxima atualização.
await new Promise(resolve => setTimeout(resolve, 300))
}
}
main()
import random
import time
from typing import Final
LINHAS_TABULEIRO: Final = 20
COLUNAS_TABULEIRO: Final = 20
CELULA_MORTA: Final = 0
CELULA_VIVA: Final = 1
def limpe_console(numero_linhas = LINHAS_TABULEIRO):
for contador in range(numero_linhas):
print("\n")
class Tabuleiro:
def __init__(self, linhas = LINHAS_TABULEIRO, colunas = COLUNAS_TABULEIRO):
self.linhas = linhas
self.colunas = colunas
self.valores = []
linhas += 2
colunas += 2
for linha in range(linhas):
nova_coluna = []
self.valores.append(nova_coluna)
for coluna in range(colunas):
nova_coluna.append(CELULA_MORTA)
def inicialize_tabuleiro(tabuleiro):
linhas = tabuleiro.linhas + 1
colunas = tabuleiro.colunas + 1
for linha in range(1, linhas):
for coluna in range(1, colunas):
if (random.randint(0, 100) < 30):
tabuleiro.valores[linha][coluna] = CELULA_VIVA
else:
tabuleiro.valores[linha][coluna] = CELULA_MORTA
def atualize_tabuleiro(tabuleiro):
linhas = tabuleiro.linhas + 1
colunas = tabuleiro.colunas + 1
# Referência.
valores = tabuleiro.valores
# Contagem de vizinhos vivos.
tabuleiro_contagem = Tabuleiro(tabuleiro.linhas, tabuleiro.colunas)
contagem_valores = tabuleiro_contagem.valores
for linha in range(1, linhas):
for coluna in range(1, colunas):
vizinhos_vivos = 0
for linha_vizinha in range(-1, 2):
for coluna_vizinha in range(-1, 2):
if (not ((linha_vizinha == 0) and (coluna_vizinha == 0))):
if (valores[linha + linha_vizinha][coluna + coluna_vizinha] == CELULA_VIVA):
vizinhos_vivos += 1
contagem_valores[linha][coluna] = vizinhos_vivos
# Atualização de estado baseada na contagem de vizinhos.
for linha in range(1, linhas):
for coluna in range(1, colunas):
if (valores[linha][coluna] == CELULA_VIVA):
if (contagem_valores[linha][coluna] < 2):
# Qualquer célula viva com menos de dois vizinhos vivos
# morre de solidão.
valores[linha][coluna] = CELULA_MORTA
elif (contagem_valores[linha][coluna] > 3):
# Qualquer célula viva com mais de três vizinhos vivos
# morre de superpopulação.
valores[linha][coluna] = CELULA_MORTA
# else:
# Redundante neste caso, já a mudança é in-place.
# Qualquer célula viva com dois ou três vizinhos vivos
# continua no mesmo estado para a próxima geração.
# valores[linha][coluna] = valores[linha][coluna]
else:
if (contagem_valores[linha][coluna] == 3):
# Qualquer célula morta com exatamente três vizinhos vivos
# se torna uma célula viva.
valores[linha][coluna] = CELULA_VIVA
def desenhe_tabuleiro(tabuleiro):
linhas = tabuleiro.linhas + 1
colunas = tabuleiro.colunas + 1
mensagem = ""
for linha in range(1, linhas):
for coluna in range(1, colunas):
if (tabuleiro.valores[linha][coluna] == CELULA_VIVA):
mensagem += "⏹" # Ou "X" ou "*" (qualquer caractere).
else:
mensagem += " "
mensagem += "\n"
limpe_console()
print(mensagem)
def main():
random.seed()
tabuleiro = Tabuleiro()
inicialize_tabuleiro(tabuleiro)
numero_iteracoes = 100
for iteracao in range(numero_iteracoes):
atualize_tabuleiro(tabuleiro)
desenhe_tabuleiro(tabuleiro)
# sleep(): Aguarda 300ms antes da próxima atualização.
time.sleep(0.3)
if (__name__ == "__main__"):
main()
local LINHAS_TABULEIRO = 20
local COLUNAS_TABULEIRO = 20
local CELULA_MORTA = 0
local CELULA_VIVA = 1
-- Implementação ineficiente usando espera ocupada.
function sleep(tempo_segundos)
local fim = tonumber(os.clock() + tempo_segundos);
while (os.clock() < fim) do
-- Espera o tempo passar, desperdiçando ciclos do processador.
end
end
function limpe_console(numero_linhas)
numero_linhas = numero_linhas or LINHAS_TABULEIRO
for contador = 1, numero_linhas do
print("\n")
end
end
function crie_tabuleiro(linhas, colunas)
linhas = linhas or LINHAS_TABULEIRO
colunas = colunas or COLUNAS_TABULEIRO
local resultado = {
linhas = linhas,
colunas = colunas,
valores = {}
}
linhas = linhas + 2
colunas = colunas + 2
for linha = 2, linhas do
local nova_coluna = {}
table.insert(resultado.valores, nova_coluna)
for coluna = 2, colunas do
table.insert(nova_coluna, CELULA_MORTA)
end
end
return resultado
end
function inicialize_tabuleiro(tabuleiro)
local linhas = tabuleiro.linhas
local colunas = tabuleiro.colunas
for linha = 2, linhas do
for coluna = 2, colunas do
if (math.random(0, 100) < 30) then
tabuleiro.valores[linha][coluna] = CELULA_VIVA
else
tabuleiro.valores[linha][coluna] = CELULA_MORTA
end
end
end
end
function atualize_tabuleiro(tabuleiro)
local linhas = tabuleiro.linhas
local colunas = tabuleiro.colunas
-- Referência.
local valores = tabuleiro.valores
-- Contagem de vizinhos vivos.
local tabuleiro_contagem = crie_tabuleiro(tabuleiro.linhas, tabuleiro.colunas)
local contagem_valores = tabuleiro_contagem.valores
for linha = 2, linhas do
for coluna = 2, colunas do
local vizinhos_vivos = 0
for linha_vizinha = -1, 1 do
for coluna_vizinha = -1, 1 do
if (not ((linha_vizinha == 0) and (coluna_vizinha == 0))) then
if (valores[linha + linha_vizinha][coluna + coluna_vizinha] == CELULA_VIVA) then
vizinhos_vivos = vizinhos_vivos + 1
end
end
end
end
contagem_valores[linha][coluna] = vizinhos_vivos
end
end
-- Atualização de estado baseada na contagem de vizinhos.
for linha = 2, linhas do
for coluna = 2, colunas do
if (valores[linha][coluna] == CELULA_VIVA) then
if (contagem_valores[linha][coluna] < 2) then
-- Qualquer célula viva com menos de dois vizinhos vivos
-- morre de solidão.
valores[linha][coluna] = CELULA_MORTA
elseif (contagem_valores[linha][coluna] > 3) then
-- Qualquer célula viva com mais de três vizinhos vivos
-- morre de superpopulação.
valores[linha][coluna] = CELULA_MORTA
-- else:
-- Redundante neste caso, já a mudança é in-place.
-- Qualquer célula viva com dois ou três vizinhos vivos
-- continua no mesmo estado para a próxima geração.
-- valores[linha][coluna] = valores[linha][coluna]
end
else
if (contagem_valores[linha][coluna] == 3) then
-- Qualquer célula morta com exatamente três vizinhos vivos
-- se torna uma célula viva.
valores[linha][coluna] = CELULA_VIVA
end
end
end
end
end
function desenhe_tabuleiro(tabuleiro)
local linhas = tabuleiro.linhas
local colunas = tabuleiro.colunas
local mensagem = ""
for linha = 2, linhas do
for coluna = 2, colunas do
if (tabuleiro.valores[linha][coluna] == CELULA_VIVA) then
mensagem = mensagem .. "⏹" -- Ou "X" ou "*" (qualquer caractere).
else
mensagem = mensagem .. " "
end
end
mensagem = mensagem .. "\n"
end
limpe_console()
print(mensagem)
end
function main()
math.randomseed(os.time())
local tabuleiro = crie_tabuleiro()
inicialize_tabuleiro(tabuleiro)
local numero_iteracoes = 100
for iteracao = 1, numero_iteracoes do
atualize_tabuleiro(tabuleiro)
desenhe_tabuleiro(tabuleiro)
-- sleep(): Aguarda 300ms antes da próxima atualização.
sleep(0.3)
end
end
main()
extends Node
const LINHAS_TABULEIRO = 20
const COLUNAS_TABULEIRO = 20
const CELULA_MORTA = 0
const CELULA_VIVA = 1
func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
var minimo = ceil(minimo_inclusive)
var maximo = floor(maximo_inclusive)
# randi(): [0.0, 1.0[
return randi() % int(maximo + 1 - minimo) + minimo
func limpe_console(numero_linhas = LINHAS_TABULEIRO):
for contador in range(numero_linhas):
print("\n")
class Tabuleiro:
var linhas
var colunas
var valores
func _init(linhas = LINHAS_TABULEIRO, colunas = COLUNAS_TABULEIRO):
self.linhas = linhas
self.colunas = colunas
self.valores = []
linhas += 2
colunas += 2
for linha in range(linhas):
var nova_coluna = []
self.valores.append(nova_coluna)
for coluna in range(colunas):
nova_coluna.append(CELULA_MORTA)
func inicialize_tabuleiro(tabuleiro):
var linhas = tabuleiro.linhas + 1
var colunas = tabuleiro.colunas + 1
for linha in range(1, linhas):
for coluna in range(1, colunas):
if (inteiro_aleatorio(0, 100) < 30):
tabuleiro.valores[linha][coluna] = CELULA_VIVA
else:
tabuleiro.valores[linha][coluna] = CELULA_MORTA
func atualize_tabuleiro(tabuleiro):
var linhas = tabuleiro.linhas + 1
var colunas = tabuleiro.colunas + 1
# Referência.
var valores = tabuleiro.valores
# Contagem de vizinhos vivos.
var tabuleiro_contagem = Tabuleiro.new(tabuleiro.linhas, tabuleiro.colunas)
var contagem_valores = tabuleiro_contagem.valores
for linha in range(1, linhas):
for coluna in range(1, colunas):
var vizinhos_vivos = 0
for linha_vizinha in range(-1, 2):
for coluna_vizinha in range(-1, 2):
if (not ((linha_vizinha == 0) and (coluna_vizinha == 0))):
if (valores[linha + linha_vizinha][coluna + coluna_vizinha] == CELULA_VIVA):
vizinhos_vivos += 1
contagem_valores[linha][coluna] = vizinhos_vivos
# Atualização de estado baseada na contagem de vizinhos.
for linha in range(1, linhas):
for coluna in range(1, colunas):
if (valores[linha][coluna] == CELULA_VIVA):
if (contagem_valores[linha][coluna] < 2):
# Qualquer célula viva com menos de dois vizinhos vivos
# morre de solidão.
valores[linha][coluna] = CELULA_MORTA
elif (contagem_valores[linha][coluna] > 3):
# Qualquer célula viva com mais de três vizinhos vivos
# morre de superpopulação.
valores[linha][coluna] = CELULA_MORTA
# else:
# Redundante neste caso, já a mudança é in-place.
# Qualquer célula viva com dois ou três vizinhos vivos
# continua no mesmo estado para a próxima geração.
# valores[linha][coluna] = valores[linha][coluna]
else:
if (contagem_valores[linha][coluna] == 3):
# Qualquer célula morta com exatamente três vizinhos vivos
# se torna uma célula viva.
valores[linha][coluna] = CELULA_VIVA
func desenhe_tabuleiro(tabuleiro):
var linhas = tabuleiro.linhas + 1
var colunas = tabuleiro.colunas + 1
var mensagem = ""
for linha in range(1, linhas):
for coluna in range(1, colunas):
if (tabuleiro.valores[linha][coluna] == CELULA_VIVA):
mensagem += "X" # Ou "X" ou "*" (qualquer caractere).
else:
mensagem += " "
mensagem += "\n"
limpe_console()
print(mensagem)
func _ready():
randomize()
var tabuleiro = Tabuleiro.new()
inicialize_tabuleiro(tabuleiro)
var numero_iteracoes = 100
for iteracao in range(numero_iteracoes):
atualize_tabuleiro(tabuleiro)
desenhe_tabuleiro(tabuleiro)
# sleep(): Aguarda 300ms antes da próxima atualização.
yield(get_tree().create_timer(0.3), "timeout")
Para o exemplo, primeiro convém notar a estruturação da solução.
Pode-se observar que toda atualização de lógica do programa ocorre em atualize_tabuleiro()
.
Da mesma forma, a apresentação do tabuleiro ocorre em desenhe_tabuleiro()
.
O programa não lê entradas de usuários (ou usuárias) finais; caso o fizesse, poder-se-ia definir uma nova subrotina chamada leia_entrada()
.
Os valores lidos seriam, então, passados como parâmetros para atualize_tabuleiro()
(ou seja, a atualização da lógica do programa não faria a leitura de dados diretamente).
Além disso, o exemplo introduz alguns recursos novos, usadas para a animação quadro a quadro (frame a frame).
- Uma subrotina para limpar o console (terminal).
JavaScript fornece o
console.clear()
(documentação) para a limpeza. Em Python, Lua e GDScript, o procedimentolimpe_console()
escreve linhas vazias para simular uma alternativa. Pode-se interessante escrever ainda mais linhas para um resultado melhor; - Uma subrotina para esperar (pausar) o programa por um determinado tempo, normalmente chamada de
sleep()
(algo como durma). JavaScript utiliza umaPromise
(documentação) esetTimeout()
(documentação). Em JavaScript, também é importante observar quemain()
deve ser declarada como funçãoasync
para uso das subrotinas anteriores. Python fornecetime.sleep()
(documentação). GDScript provêget_tree().create_timer()
(documentação). Lua não possui nenhuma subrotina pré-definida; a implementação desleep()
utiliza uma técnica chamada espera ocupada. A técnica repete um bloco de código até que uma condição torne-se falsa, para desperdiçar tempo. No caso, isso é feito para esperar um certo tempo até encerrar a repetição. Um implementação mais simples poderia ser um bloco vazio repetido algumas milhares de vezes.
Nas implementações, o incremento de linhas e colunas permitem ignorar valores vazios para desenhar ou atualizar tabuleiro.
Ao invés de somar linhas = tabuleiro.linhas + 2
e usar uma condição linhas - 1
, pode-se simplificar o código fazendo-se tabuleiro.linhas + 1
diretamente.
Em Lua, como a indexação inicia em 1, pode-se usar o valor original diretamente.
Dependendo da seqüência inicial gerada, o Jogo da Vida pode gerar padrões que se repetem infinitamente. A página inglesa da Wikipedia apresenta alguns padrões. Caso você execute o programa algumas vezes, talvez você obtenha padrões assim (dependendo da sorte). Alternativamente, você pode criar um tabuleiro inicial com uma configuração adequada para o padrão que quiser.
Por exemplo, em um tabuleiro 48x48 (50x50 com os zeros inseridos nas extremidades):
Mostrar/Ocultar Tabuleiro
Para melhor visualização, é recomendável usar um caractere como X
ao invés do quadrado (para garantir espaçamento igual entre caracteres).
O exemplo provê as configurações para JavaScript, mas os princípios (e valores) são os mesmos para outras linguagens. Em Lua, deve-se substituir colchetes por chaves.
const LINHAS_TABULEIRO = 47
const COLUNAS_TABULEIRO = 47
const CELULA_MORTA = 0
const CELULA_VIVA = 1
// ...
function inicialize_tabuleiro(tabuleiro) {
tabuleiro.valores = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
}
Embora os exemplos adicionem um limite máximo de repetições, poder-se-ia usar um laço infinito para a simulação continuasse. Além disso, para melhor visualização, é interessante escolher um número de linhas que permita limpar totalmente a tela a cada iteração, para que o próximo desenho ocorra sempre na mesma região de tela.
O Jogo da Vida é interessante porque define uma simulação simples, porém completa. A implementação apresentada ilustra como sistemas como jogos digitais, animações, vídeos ou simulações funcionam: processa-se e desenha-se conteúdo repetidamente, criando-se a ilusão de animação. Um exemplo usando material cotidiano seriam desenhos animados em papel. Cada página altera um pouco o desenho; ao se folhear rapidamente a seqüência de páginas, gera-se a impressão de animação. Uma coleção de imagens assim chama-se folioscópio (ou flip book, em inglês).
Simulações digitais funcionam similarmente. Elas repetem um código continuamente. Alterações de estado entre uma repetição e outra podem ser desenhadas. O desenho rápido gera a ilusão de animação.
Como computadores modernos são máquinas rápidas, o uso de uma subrotina como sleep()
permite esperar um tempo arbitrário entre duas atualizações.
Caso contrário, exceto caso o número de repetições seja muito grande, o programa pode terminar quase instantaneamente.
A última imagem gerada será a exibida ao final do programa, resultando em um desenho estático (alternativamente, dependendo da solução, pode-se rolar a saída do terminal para verificar as saídas anteriores).
Por sinal, praticamente toda aplicação com interface gráfica opera de forma similar ao exemplo. Usa-se uma cor sólida de fundo para limpar a tela; desenha-se o novo conteúdo. Então, repete-se o processo a cada mudança de dados (ou intervalo de tempo). Este é o princípio usado para exibir esta página em seu monitor, qualquer programa em uso, jogos digitais e outros conteúdos multimídia.
Caso você esteja usando um leitor de tela e ouvindo o conteúdo desta página (ou ouvindo música), o funcionamento de som digital também é similar. Toca-se um ruído, avança-se os dados, toca-se o próximo ruído. O som é criado ao longo do tempo, quadro a quadro. Mais exatamente, amostra (sample) a amostra.
Novos Itens para Seu Inventário
Ferramentas:
- Função como simulador de entradas de usuário final em um programa;
- Folioscópio (flip book).
Habilidades:
- Criação de registros;
- Programação em alto nível.
Conceitos:
- Registros (structs ou records);
- Decomposição de dados;
- Abstração de dados;
- Atributos ou campos;
- Estado;
- Plain Old Data (POD) or Passive Data Structures (PDS);
- Classes;
- Métodos;
- Instância;
- Data hiding;
- Encapsulamento;
- Interfaces;
- Fluxo de dados;
- Construtor;
- Parâmetros nomeados;
- Vetor de Registros (Array of Structures);
- Granularidade;
- Tipos de dados recursivos;
- Consistência;
- Detalhe de implementação;
- Espera ocupada.
Recursos de programação:
- Registros;
- Definição e uso de parâmetros nomeados.
Pratique
Excetuando-se o uso de tipos de dados recursivos (com referências para o próprio tipo de registro), registros agregam maior comodidade para programação, mas não permitem resolver muitos problemas diferentes do que já era possível. Uma boa forma de praticar o uso é refatorar alguns de seus programas antigos para usar registros. Em particular, adotar um estilo de programação com abstrações de dados e funcionais permitirá programar em nível mais alto; as soluções resultantes poderão ser mais legíveis e elegantes. Revistar seus programas antigos também permitirá constatar sua evolução como programadora ou programador. Você perceberá situações em que poderia ter optado por uma solução mais simples ou uma técnica mais avançada (que você não conhecia até então).
Crie um registro chamado
Animal
. Adicione dados característicos de animais (por exemplo, espécie e nome científico) e monte uma pequena base de dados com informações sobre seus animais favoritos. Caso prefira plantas, você pode criar um registroVegetal
.Crie um registro chamado
Produto
e crie um pequeno sistema de controle de estoque. O sistema deve armazenar nomes, quantidades, números de lotes e preços de produtos.Crie um registro chamado
Livro
e um registro chamadoAutor
. Armazene seus livros favoritos. Cada livro deve possuir título, um vetor de autores e um resumo (ou comentário). Implemente subrotinas para adicionar, listar e procurar por livros. A busca pode ser feita por título ou por autores.Crie uma subrotina para verificar se dois registros são iguais. A subrotina deve retornar um valor lógico.
Como ordenar um vetor de registros? Dica: pode-se criar uma subrotina com um critério para informar se um registro é menor ou maior que outro. A ordenação é feita usando um ou mais atributos do registro. Atributos comparados primeiro terão maior prioridade na ordenação.
Crie um cardápio para um restaurante usando registros.
Quais vantagens você citaria para o uso de registros em um programa? Quais desvantagens?
É possível criar um registro vazio, isto é, sem nenhum atributo? Caso seja possível, você consegue imaginar alguma utilidade para ele? A resposta pode depender da linguagem de programação escolhia.
É possível adicionar todas as variáveis de um programa (mesmo que bastante simples) a um registro?
Implemente um jogo de batalha naval. Use registros para armazenar as informações sobre o tabuleiro e para cada cartela de jogadores.
Próximos Passos
Com a introdução de registros, agora você tem quase todos os recursos básicos providos por linguagens de programação para a resolução de problemas usando computadores. Antes, você era capaz de processar dados de tipos primitivos e coleções de tipos primitivos. Agora, você tornou-se capaz de criar seus próprios tipos de dados. Inclusive, você pode usar registros para criar novos tipos de coleções.
Registros permitem abstrair dados para pensar em problemas e soluções em nível mais alto. Pode-se inclusive agrupar todos os dados de um programa em um único registro, que pode ser pensado como a memória primária do programa. Na prática, contudo, é preferível armazenar apenas variáveis de interesse para o longo prazo do programa. Por exemplo, variáveis locais criadas como contadores em estruturas de repetição ou valores temporários em partes do código não precisam ser armazenadas por todo o programa. Embora seja possível, normalmente não há muitas boas razões para ocupar a memória com variáveis efêmeras (embora existem situações em que isso é válido, como para quando for necessário garantir que o orçamento de memória nunca ultrapasse um teto).
Além disso, após esta introdução sobre registros, a transição para Programação Orientada a Objetos (POO ou OOP, da sigla inglesa) tenderá a ser mais suave. O básico de OOP não é muito diferente do uso de subrotinas com registros, embora forneça recursos adicionais, como modificadores e especificadores de acesso. OOP também fornece recursos mais avançados, como polimorfismo, herança e delegação, que permitem definir hierarquias complexas de classes e (re-)definir métodos de acordo com contextos e particularidades de classes definidas na hierarquia. Linguagens de programação orientadas a objetos são populares, tanto na academia, quanto na indústria e mercado profissional. Além de Python e JavaScript, Java, C++, C#, PHP e Ruby são exemplos linguagens com alta demanda por profissionais.
Antes, contudo, existe uma limitação de todos os programas que você criou até este momento. Todos eles perdem os dados manipulados ao final da execução. Todo programa começa de um mesmo estado inicial (a única exceção são programas usando números pseudoaleatórios com sementes baseadas em tempo). Os dados de execuções prévias são perdidos quando o programa termina.
Com arquivos, você poderá usar memória secundária para armazenar dados para uso em múltiplas execuções de programa. Salvar e carregar dados tornar-se-ão termos em seu vocabulário de programação.
- Introdução;
- Ponto de entrada e estrutura de programa;
- Saída (para console ou terminal);
- Tipos de dados;
- Variáveis e constantes;
- Entrada (para console ou terminal);
- Aritmética e Matemática básica;
- Operações relacionais e comparações;
- Operações lógicas e Álgebra Booleana;
- Estruturas de condição (ou condicionais ou de seleção);
- Subrotinas: funções e procedimentos;
- Estruturas de repetição (ou laços ou loops);
- Vetores (arrays), cadeias de caracteres (strings), coleções (collections) e estruturas de dados;
- Registros (structs ou records);
- Arquivos e serialização (serialization ou marshalling);
- Bibliotecas;
- Entrada em linha de comando;
- Operações bit-a-bit (bitwise operations);
- Testes e depuração.