Classes são "pacotes" de código que definem classsificações de dados (igual os tipos), os quais possuem proiedades e operações (métodos) em linguagens orientadas a objetos (OOP). As propriedades (atributos) armazenam valores e características dos dados, já os métoos são funções internas às classes, que permitem agir sobre elas para executar ações.
Objetos são instancias de uma classe que possuem características e comportamentos. Podemos criar múltiplas instâncias de uma mesma classe. As propriedades, métodos e eventos de uma classe são alocadas em memória através da instância de um objeto.
As vantagens da POO, incluem reutilização de códigos, modularidade, uso mais simples (realístico) e código mais limpo e claro.
Para definir classes, usamos a palavra-chave class, por exemplo:
class Caixa {
double lado;
double volume() {
return lado * lado * lado;
}
}
Abra um novo projeto console do C#, note que mesmo no programa principal, já temos uma classe (Program), mas para trabalharmos com classes em C#, é recomendado criar arquivos separados de classes, e os arquivos terão os mesmos nomes das classes, em algumas linguagens POO não é possível criar várias classes no mesmo arquivo.
No gerenciador de soluções, escolha a opção classe e coloque o nome Caixa, esse será o código dela:
using System;
namespace ClassesCriandoEInstanciando {
class Caixa {
double lado;
double volume() {
return lado * lado * lado;
}
}
}
E para criar a instância da classe (o objeto), vá no programa principal da classe principal e coloque o código assim.
using System;
namespace ClassesCriandoEInstanciando {
class Program {
static void Main(string[] args) {
Caixa cx;
cx = new Caixa();
}
}
}
Mas o mais comum é chamar o objeto assim:
Caixa cx = new Caixa();
Quando criamos uma classe, temos mais trabalho para fazê-la, mas depois que ela está pronta, podemos criar quantos objetos precisarmos vindos dessa mesma classe, e o programa principal fica mais simples, menor e mais natural. Os objetos criados dessa mesma classe são independentes entre si e o status de um não interfere no outro. Essa é uma das vantagens da orientação a objetos.
PS: Caso queira retornar o nome do objeto em C#, use o método this.GetType().Name no objeto.
Os atributos podem ser de qualquer tipo, incluindo classes invólucro, o mesmo vale para parâmetros de construtores. É possível também chamar um atributo ou método de objeto agregado, por exemplo objeto1.metodo2().metodo1().
Quando temos que acessar algum atributo ou método da classe, ele não aparece por padrão no programa principal, devido ao fator de atributos e métodos por padrão, no C#, serem privados.
Para resolver isso, colocamos a indicação public, para colocar esses métodos públicos, dessa forma:
class Caixa {
private double lado;
public double volume() {
return lado * lado * lado;
}
}
Como vimos, além do método public (acesso total), temos o private (acesso apenas dentro da classe) e o protected (acesso dentro da classe e das descendentes, é como um meio termo entre o public e private).
Dessa forma, podemos mexer nesse atributo e usar os métodos na classe principal apenas se forem públicos, assim:
Caixa cx = new Caixa();
cx.lado = 5; // Só podemos mexer nele se for público
Console.WriteLine(cx.volume().ToString());
Console.Read();
Colocando os atributos privados, eles estarão encapsulados, mas para utilizar eles utilizamos dois tipos de métodos, os getters (que retornam o conteúdo) e os setters (que mudam o valor do conteúdo).
Veja como encapsular os dados usando o getter e o setter:
class Caixa {
private double lado;
public double volume() {
return lado * lado * lado;
}
public double getLado() {
return this.lado;
}
public void setLado(double lado) {
this.lado = lado;
}
}
PS: O this vai ser substituído pelo objeto.
Dessa forma, protegemos o acesso à variável, mas podemos pegar o valor dela com o getter e mudar ela pelo setter, assim:
Caixa cx = new Caixa();
cx.setLado(5);
Console.WriteLine(cx.getLado().ToString());
Console.Read();
No entanto, o mais comum é usarmos o getter e setter dessa forma no C#:
class Caixa {
private double lado;
public double volume() {
return lado * lado * lado;
}
public double Lado {
get {return this.lado;}
set {this.lado = value;}
}
}
Assim também:
class Caixa {
private double lado;
public double volume() {
return lado * lado * lado;
}
public double Lado {
get => lado;
set => lado = value;
}
}
Ou mais simplificado:
class Caixa {
private double lado;
public double volume() {
return lado * lado * lado;
}
public double Lado {
get;
set;
}
}
PS: O último método acima pode dar erro em objetos agregados, nesse caso use um dos dois modos anteriores.
Inclusive, podemos colocar um private no set ou no get caso queiramos que um deles seja restrito à classe.
E pra exibir:
Caixa cx = new Caixa();
cx.Lado = 5; // Setter
Console.WriteLine(cx.Lado); // Getter
Console.Read();
PS: Assim como no Java e no PHP, o C# permite trabalhar com interfaces, que faz o encapsulamento a nível de classe, nesse caso, podemos fazer a interface assim:
interface Estoque {
void defineLado(int l);
double volume();
}
E na classe que implementará os elementos, basta usar como uma herança, assim:
class Caixa : Estoque {
private double lado;
public void defineLado(int l) {
this.lado = l;
}
public double volume() {
return lado * lado * lado;
}
public double Lado {
get;
set;
}
}
Em outras palavras, as interfaces estabelecem contratos entre as classes que as implementam, garantindo que elas tenham um certo comportamento, por isso, podemos declarar um objeto do tipo da interface e iniciar com a classe que a implementa, por exemplo:
Estoque cx = new Caixa();
cx.defineLado(5);
Console.WriteLine(cx.volume());
Console.Read();
As interfaces também garantem o encapsulamento, já que podemos usar somente os métodos descritos na interface nos objetos, e não permitir a visualização de atributos da classe implementadora.
Lembrando que podemos implementar mais de uma interface no programa, separando por vírgulas, algo tipo class NomeDaClasse : NomeDaInterface1, NomeDaInterface2. Lembrando também que muitos programadores colocam as interfaces com um "I" no começo para facilitar a identificação das mesmas (como no caso ficaria IEstoque), mas não é obrigatório.
PS: Podemos verificar se uma classe implementa uma interface usando um if e else, isso também vale pra classes:
Estoque cx = new Caixa();
if(typeof(Estoque).IsInstanceOfType(cx))) {
cx.defineLado(5);
Console.WriteLine(cx.volume());
}
else {
Console.WriteLine("O objeto não implementa a interface Estoque!");
}
Para que possamos usar essa classe criada ainda é necessário que os campos privados sejam inicializados, pois é inacessível pro exterior. Faremos isso usando um construtor.
Quando usamos a palavra new para criar um objeto, o runtime constrói o objeto usando a definição da classe. O runtime toma um pedaço da memória RAM, a preenche com os campos definidos pela classe e então invoca um construtor que realizará as inicializações requeridas.
Um construtor é um método especial que roda automaticamente quando uma classe é instanciada. Possui o mesmo nome da classe, e pode receber parâmetros, mas não retorna valores, nem mesmo void. Toda classe deve ter um construtor, se um não for escrito, o compilador gerará um automaticamente (que não faz absolutamente nada).
Veja abaixo o exemplo simples de construtor:
public Caixa() {
lado = 10;
}
Na classe Caixa, ele ficaria assim (deixe o atributo lado privado):
class Caixa : Estoque {
private double lado;
public Caixa() {
lado = 10;
}
public void defineLado(int l) {
this.lado = l;
}
public double volume() {
return lado * lado * lado;
}
}
E no programa principal, faça isso:
Estoque cx = new Caixa();
double vol;
vol = cx.volume();
Console.WriteLine(vol.ToString());
Da mesma forma, podemos criar destrutores, mas eles são dispensáveis em C#, que faz a coleta de lixo automaticamente, mas para usar, fazemos assim na classe:
~Caixa {
Console.WriteLine("Objeto Destruído");
}
PS: Não existe delete em C#.
Podemos ter sobrecarga de métodos construtores no C# também. Basicamente, como em qualquer outra função, é criar um segundo construtor (que obviamente, também terá o mesmo nome da classe), mas com parâmetros diferentes.
No nosso exemplo, a classe Caixa terá o construtor padrão sem parâmetros (que adiciona 10 ao atributo lado automaticamente) e o segundo construtor que receberá um parâmetro para definiir esse mesmo atributo, dessa forma:
public Caixa() {
lado = 10;
}
public Caixa(double l) {
lado = l;
}
E no programa principal, fazemos assim (veja a diferença dos objetos):
Estoque cx1 = new Caixa();
double volume1;
volume1 = cx1.volume();
Console.WriteLine(volume1.ToString());
Estoque cx2 = new Caixa(8);
double volume2;
volume2 = cx2.volume();
Console.WriteLine(volume2.ToString());
PS: Podemos ter mais de dois construtores sobrecarregados, desde que não sejam do mesmo tipo de parâmetros. Caso num construtor precise inicializa um atributo como nulo, use null.
Podemos também chamar um construtor dentro do outro com o this, fazendo o encadeamento, assim:
public Caixa() : this(10) { // Chama o construtor abaixo e atribui 10 ao atributo lado
// Deixa vazio mesmo, nesse caso
}
public Caixa(double l) {
lado = l;
}