4. Classes, Estruturas, Interfaces
4.1. O objeto no exemplo
4.1.1. Noções gerais
Abordaremos agora, por meio de exemplos, a programação orientada a objetos. Um objeto é uma entidade que contém dados que definem seu estado (chamados de campos, atributos, etc.) e funções (chamadas de métodos). Um objeto é criado de acordo com um modelo chamado de classe:
public class C1{
Type1 p1; // campo p1
Type2 p2; // campo p2
…
Type3 m3(…){ // método m3
…
}
Type4 m4(…){ // método m4
…
}
…
}
A partir da classe anterior C1, é possível criar vários objetos O1, O2,… Todos terão os campos p1, p2, … e os métodos m3, m4, … Mas terão valores diferentes para seus campos pi, possuindo, assim, cada um um estado próprio. Se o1 é um objeto do tipo C1, o1.p1 designa a propriedade p1 de o1 e o1.m1 o método m1 de O1.
Vamos considerar um primeiro modelo de objeto: a classe Personne.
4.1.2. Criação do projeto em C#
Nos exemplos anteriores, tínhamos em um projeto apenas um único arquivo-fonte: Program.cs. A partir de agora, poderemos ter vários arquivos-fonte em um mesmo projeto. Mostramos como proceder.
![]() |
Em [1], crie um novo projeto. Em [2], escolha uma “Aplicativo de Console”. Em [3], mantenha o valor padrão. Em [4], confirme. Em [5], o projeto que foi gerado. O conteúdo de Program.cs é o seguinte:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
}
}
}
Vamos salvar o projeto criado:
![]() |
Em [1], a opção de salvar. Em [2], indique a pasta onde salvar o projeto. Em [3], dê um nome ao projeto. Em [5], indique que deseja criar uma solução. Uma solução é um conjunto de projetos. Em [4], digite o nome da solução. Em [6], confirme o salvamento.
![]() |
No [1], o projeto foi salvo. No [2], adicione um novo elemento ao projeto.
![]() |
Em [1], indique que deseja adicionar uma classe. Em [2], o nome da classe. Em [3], confirme as informações. Em [4], o projeto [01] possui um novo arquivo-fonte Personne.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Personne {
}
}
Alteramos o namespace de cada um dos arquivos-fonte no Chap2 e removemos a importação dos namespaces desnecessários:
using System;
namespace Chap2 {
class Personne {
}
}
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
}
}
}
4.1.3. Definição da classe Pessoa
A definição da classe Personne no arquivo-fonte [Personne.cs] será a seguinte:
using System;
namespace Chap2 {
public class Personne {
// atributos
private string prenom;
private string nom;
private int age;
// método
public void Initialise(string P, string N, int age) {
this.prenom = P;
this.nom = N;
this.age = age;
}
// método
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Temos aqui a definição de uma classe, ou seja, de um tipo de dados. Ao criarmos variáveis desse tipo, elas serão chamadas de objetos ou instâncias da classe. Uma classe é, portanto, um modelo a partir do qual os objetos são construídos.
Os membros ou campos de uma classe podem ser dados (atributos), métodos (funções) ou propriedades. As propriedades são métodos específicos que servem para consultar ou definir o valor dos atributos do objeto. Esses campos podem ser acompanhados por uma das três palavras-chave a seguir:
Um campo privado (private) só é acessível pelos métodos internos da classe | |
Um campo público (public) é acessível por qualquer método, definido ou não dentro da classe | |
Um campo protegido (protected) só é acessível pelos métodos internos da classe ou de um objeto derivado (veja mais adiante o conceito de herança). |
Em geral, os dados de uma classe são declarados privados, enquanto seus métodos e propriedades são declarados públicos. Isso significa que o usuário de um objeto (o programador)
- não terá acesso direto aos dados privados do objeto
- poderá chamar os métodos públicos do objeto e, em especial, aqueles que darão acesso aos seus dados privados.
A sintaxe de declaração de uma classe C é a seguinte:
public class C{
private donnée ou méthode ou propriété privée;
public donnée ou méthode ou propriété publique;
protected donnée ou méthode ou propriété protégée;
}
A ordem de declaração dos atributos private, protected e public é arbitrária.
4.1.4. O método Initialise
Voltemos à nossa classe Pessoa declarada como:
using System;
namespace Chap2 {
public class Personne {
// atributos
private string prenom;
private string nom;
private int age;
// método
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
// método
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Qual é a função do método Initialise? Como nom, prenom e age são dados privados da classe Personne, as instruções:
são inválidas. Precisamos inicializar um objeto do tipo Personne por meio de um método público. Essa é a função do método Initialise. Escreveremos:
A sintaxe p1.Initialise é válida, pois Initialise é de acesso público.
4.1.5. O operador new
A sequência de instruções
está incorreta. A instrução
declara p1 como uma referência a um objeto do tipo Personne. Esse objeto ainda não existe e, portanto, p1 não está inicializado. É como se escrevêssemos:
onde se indica explicitamente, com a palavra-chave null, que a variável p1 ainda não faz referência a nenhum objeto. Quando, em seguida, escrevemos
, estamos chamando o método Initialise do objeto referenciado por p1. No entanto, esse objeto ainda não existe e o compilador sinalizará o erro. Para que p1 faça referência a um objeto, é preciso escrever:
Isso tem como efeito a criação de um objeto do tipo Personne ainda não inicializado: os atributos nom e prenom, que são referências a objetos do tipo String, terão o valor null, e age assumirá o valor 0. Portanto, há uma inicialização padrão. Agora que p1 faz referência a um objeto, a instrução de inicialização desse objeto
é válida.
4.1.6. A palavra-chave this
Vejamos o código do método initialise:
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
A instrução this.prenom=p significa que o atributo prenom do objeto atual (this) recebe o valor p. A palavra-chave this designa o objeto atual: aquele no qual se encontra o método executado. Como sabemos disso? Vejamos como ocorre a inicialização do objeto referenciado por p1 no programa chamador:
É o método Initialise do objeto p1 que é chamado. Quando, nesse método, fazemos referência ao objeto this, na verdade estamos fazendo referência ao objeto p1. O método Initialise também poderia ter sido escrito da seguinte forma:
public void Initialise(string p, string n, int age) {
prenom = p;
nom = n;
this.age = age;
}
Quando um método de um objeto faz referência a um atributo A desse objeto, a notação this.A é implícita. É necessário utilizá-la explicitamente quando houver conflito de identificadores. Esse é o caso da instrução:
this.age=age;
onde age designa um atributo do objeto atual, bem como o parâmetro age recebido pelo método. É necessário, então, eliminar a ambiguidade, designando o atributo age como this.age.
4.1.7. Um programa de teste
Aqui está um breve programa de teste. Ele está escrito no arquivo-fonte [Program.cs]:
using System;
namespace Chap2 {
class P01 {
static void Main() {
Personne p1 = new Personne();
p1.Initialise("Jean", "Dupont", 30);
p1.Identifie();
}
}
}
Antes de executar o projeto [01], pode ser necessário especificar o arquivo-fonte a ser executado:
![]() |
Nas propriedades do projeto [01], indica-se [1] como a classe a ser executada.
Os resultados obtidos na execução são os seguintes:
4.1.8. Outro método Initialise
Consideremos novamente a classe Personne e adicionemos a ela o seguinte método:
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
Agora temos dois métodos com o nome Initialise: isso é válido, desde que aceitem parâmetros diferentes. É o que ocorre neste caso. O parâmetro agora é uma referência p a uma pessoa. Os atributos da pessoa p são, então, atribuídos ao objeto atual (this). Observe-se que o método Initialise tem acesso direto aos atributos do objeto p, embora estes sejam do tipo private. Isso sempre é verdade: um objeto o1 de uma classe C sempre tem acesso aos atributos dos objetos da mesma classe C.
Aqui está um teste da nova classe Personne:
using System;
namespace Chap2 {
class Program {
static void Main() {
Personne p1 = new Personne();
p1.Initialise("Jean", "Dupont", 30);
p1.Identifie();
Personne p2 = new Personne();
p2.Initialise(p1);
p2.Identifie();
}
}
}
e seus resultados:
4.1.9. Construtores da classe Pessoa
Um construtor é um método que leva o nome da classe e é chamado durante a criação do objeto. Geralmente, ele é usado para inicializá-lo. É um método que pode aceitar argumentos, mas não retorna nenhum resultado. Seu protótipo ou sua definição não são precedidos por nenhum tipo (nem mesmo void).
Se uma classe C tiver um construtor que aceita n argumentos argi, a declaração e a inicialização de um objeto dessa classe poderão ser feitas da seguinte forma:
ou
Quando uma classe C possui um ou mais construtores, é obrigatório utilizar um desses construtores para criar um objeto dessa classe. Se uma classe C não tiver nenhum construtor, ela possui um construtor padrão, que é o construtor sem parâmetros: public C(). Os atributos do objeto são, então, inicializados com valores padrão. Foi isso que aconteceu nos programas anteriores, nos quais escrevemos:
Vamos criar dois construtores para nossa classe Personne:
using System;
namespace Chap2 {
public class Personne {
// atributos
private string prenom;
private string nom;
private int age;
// construtores
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne P) {
Initialise(P);
}
// método
public void Initialise(string p, string n, int age) {
...
}
public void Initialise(Personne p) {
...
}
// método
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Nossos dois construtores limitam-se a chamar os métodos Initialise estudados anteriormente. Vale lembrar que, quando em um construtor encontramos a notação Initialise(p), por exemplo, o compilador traduz como this.Initialise(p). No construtor, o método Initialise é, portanto, chamado para operar sobre o objeto referenciado por this, ou seja, o objeto atual, aquele que está sendo construído.
Aqui está um pequeno programa de teste:
using System;
namespace Chap2 {
class Program {
static void Main() {
Personne p1 = new Personne("Jean", "Dupont", 30);
p1.Identifie();
Personne p2 = new Personne(p1);
p2.Identifie();
}
}
}
e os resultados obtidos:
4.1.10. Referências de objetos
Sempre utilizamos a mesma classe Personne. O programa de teste fica assim:
using System;
namespace Chap2 {
class Program2 {
static void Main() {
// p1
Personne p1 = new Personne("Jean", "Dupont", 30);
Console.Write("p1="); p1.Identifie();
// p2 faz referência ao mesmo objeto que p1
Personne p2 = p1;
Console.Write("p2="); p2.Identifie();
// p3 aponta para um objeto que será uma cópia do objeto ao qual p1 aponta
Personne p3 = new Personne(p1);
Console.Write("p3="); p3.Identifie();
// altera-se o estado do objeto referenciado por p1
p1.Initialise("Micheline", "Benoît", 67);
Console.Write("p1="); p1.Identifie();
// como p2 = p1, o objeto referenciado por p2 deve ter mudado de estado
Console.Write("p2="); p2.Identifie();
// como p3 não aponta para o mesmo objeto que p1, o objeto ao qual p3 aponta não deve ter mudado
Console.Write("p3="); p3.Identifie();
}
}
}
Os resultados obtidos são os seguintes:
Quando declaramos a variável p1 por meio de
p1 faz referência ao objeto Personne("Jean","Dupont",30), mas não é o próprio objeto. Em C, diríamos que se trata de um ponteiro, c.a.d, que aponta para o endereço do objeto criado. Se, em seguida, escrevermos:
Não é o objeto Personne("Jean","Dupont",30) que é modificado, e sim a referência p1 que muda de valor. O objeto Personne("Jean","Dupont",30) será “perdido” se não for referenciado por nenhuma outra variável.
Quando se escreve:
inicializa-se o ponteiro p2: ele “aponta” para o mesmo objeto (designa o mesmo objeto) que o ponteiro p1. Assim, se modificarmos o objeto “apontado” (ou referenciado) por p1, também modificaremos aquele referenciado por p2.
Quando se escreve:
é criado um novo objeto Personne. Esse novo objeto será referenciado por p3. Se modificarmos o objeto “apontado” (ou referenciado) por p1, não alteraremos de forma alguma aquele referenciado por p3. É isso que mostram os resultados obtidos.
4.1.11. Passagem de parâmetros do tipo referência a objeto
No capítulo anterior, estudamos os modos de passagem de parâmetros de uma função quando estes representavam um tipo simples de C# representado por uma estrutura .NET. Vejamos o que acontece quando o parâmetro é uma referência a um objeto:
using System;
using System.Text;
namespace Chap1 {
class P12 {
public static void Main() {
// exemplo 4
StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
Console.WriteLine("Dans fonction appelante après appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);
}
private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
Console.WriteLine("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
sbf0.Append("*****");
sbf1 = new StringBuilder("essai1*****");
sbf2 = new StringBuilder("essai2*****");
sbf3 = new StringBuilder("essai3*****");
Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
}
}
}
- linha 8: define 3 objetos do tipo StringBuilder. Um objeto StringBuilder está próximo de um objeto string.. Ao manipular um objeto string, obtém-se em retorno um novo objeto string. Assim, na sequência de código:
A linha 1 cria um objeto string na memória, e s é seu endereço. Na linha 2, s.ToUpperCase() cria outro objeto string na memória. Assim, entre as linhas 1 e 2, s mudou de valor (agora aponta para o novo objeto). Já a classe StringBuilder permite transformar uma string sem que um segundo objeto seja criado. Esse é o exemplo dado acima:
- linha 8: 4 referências [sb0, sb1, sb2, sb3] a objetos do tipo StringBuilder
- linha 10: são passadas para o método ChangeStringBuilder com modos diferentes: sb0, sb1 com o modo padrão, sb2 com a palavra-chave ref, sb3 com a palavra-chave out.
- linhas 15-22: um método que possui os parâmetros formais [sbf0, sbf1, sbf2, sbf3]. As relações entre os parâmetros formais sbfi e os efetivos sbi são as seguintes:
- sbf0 e sb0 são, no início do método, duas referências distintas que apontam para o mesmo objeto (passagem por valor dos endereços)
- o mesmo se aplica a sbf1 e sb1
- sbf2 e sb2 são, no início do método, a mesma referência ao mesmo objeto (palavra-chave ref)
- sbf3 e sb3 são, após a execução do método, a mesma referência ao mesmo objeto (palavra-chave out)
Os resultados obtidos são os seguintes:
Explicações:
- sb0 e sbf0 são duas referências distintas para o mesmo objeto. Este foi modificado por meio de sbf0 – linha 3. Essa modificação pode ser visualizada por meio de sb0 – linha 4.
- sb1 e sbf1 são duas referências distintas para o mesmo objeto. O valor de sbf1 é alterado no método e agora aponta para um novo objeto — linha 3. Isso não altera de forma alguma o valor de sb1, que continua apontando para o mesmo objeto — linha 4.
- sb2 e sbf2 são a mesma referência para o mesmo objeto. O valor de sbf2 é alterado no método e agora aponta para um novo objeto — linha 3. Como sbf2 e sb2 são uma única e mesma entidade, o valor de sb2 também foi alterado e sb2 aponta para o mesmo objeto que sbf2 — linhas 3 e 4.
- Antes da chamada do método, sb3 não tinha valor. Após a chamada do método, sb3 recebe o valor de sbf3. Portanto, temos duas referências ao mesmo objeto — linhas 3 e 4
4.1.12. Os objetos temporários
Em uma expressão, é possível chamar explicitamente o construtor de um objeto: ele é criado, mas não temos acesso a ele (para modificá-lo, por exemplo). Esse objeto temporário é criado para fins de avaliação da expressão e, em seguida, descartado. O espaço de memória que ele ocupava será automaticamente recuperado posteriormente por um programa chamado “garimpo de lixo”, cuja função é recuperar o espaço de memória ocupado por objetos que não são mais referenciados pelos dados do programa.
Consideremos o seguinte novo programa de teste:
using System;
namespace Chap2 {
class Program {
static void Main() {
new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
}
}
}
e modifiquemos os construtores da classe Personne para que exibam uma mensagem:
// construtores
public Personne(String p, String n, int age) {
Console.WriteLine("Constructeur Personne(string, string, int)");
Initialise(p, n, age);
}
public Personne(Personne P) {
Console.Out.WriteLine("Constructeur Personne(Personne)");
Initialise(P);
}
Obtemos os seguintes resultados:
mostrando a construção sucessiva dos dois objetos temporários.
4.1.13. Métodos de leitura e gravação de atributos privados
Adicionamos à classe Personne os métodos necessários para ler ou alterar o estado dos atributos dos objetos:
using System;
namespace Chap2 {
public class Personne {
// atributos
private string prenom;
private string nom;
private int age;
// construtores
public Personne(String p, String n, int age) {
Console.WriteLine("Constructeur Personne(string, string, int)");
Initialise(p, n, age);
}
public Personne(Personne p) {
Console.Out.WriteLine("Constructeur Personne(Personne)");
Initialise(p);
}
// método
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
// acessores
public String GetPrenom() {
return prenom;
}
public String GetNom() {
return nom;
}
public int GetAge() {
return age;
}
//modificadores
public void SetPrenom(String P) {
this.prenom = P;
}
public void SetNom(String N) {
this.nom = N;
}
public void SetAge(int age) {
this.age = age;
}
// método
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Testamos a nova classe com o seguinte programa:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p = new Personne("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
p.SetAge(56);
Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
}
}
}
e obtemos os seguintes resultados:
4.1.14. As propriedades
Existe outra maneira de acessar os atributos de uma classe: criar propriedades. Elas nos permitem manipular atributos privados como se fossem públicos.
Consideremos a seguinte classe Personne, na qual os acessadores e modificadores anteriores foram substituídos por propriedades de leitura e gravação:
using System;
namespace Chap2 {
public class Personne {
// atributos
private string prenom;
private string nom;
private int age;
// construtores
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne p) {
Initialise(p);
}
// método
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
// propriedades
public string Prenom {
get { return prenom; }
set {
// nome válido?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//nome
public string Nom {
get { return nom; }
set {
// sobrenome válido?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//se
}//sobrenome
public int Age {
get { return age; }
set {
// idade válida?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//se
}//idade
// método
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Uma propriedade permite ler (get) ou definir (set) o valor de um atributo. Uma propriedade é declarada da seguinte forma:
onde Type deve ser o tipo do atributo gerenciado pela propriedade. Ela pode ter dois métodos chamados get e set. O método get geralmente é responsável por retornar o valor do atributo que gerencia (ele poderia retornar outra coisa; nada o impede). O método set recebe um parâmetro chamado **value**, que normalmente é atribuído ao atributo que ele gerencia. Ele pode aproveitar para verificar a validade do valor recebido e, eventualmente, lançar uma exceção caso o valor seja inválido. É isso que é feito aqui.
Como esses métodos get e set são chamados? Consideremos o seguinte programa de teste:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p = new Personne("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
p.Age = 56;
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
try {
p.Age = -4;
} catch (Exception ex) {
Console.Error.WriteLine(ex.Message);
}//try-catch
}
}
}
Na instrução
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
buscamos obter os valores das propriedades Prenom, Nom e Age da pessoa p. É o método get dessas propriedades que é chamado e que retorna o valor do atributo que elas gerenciam.
Na instrução
, deseja-se definir o valor da propriedade Age. Nesse caso, é o método set dessa propriedade que é chamado. Ele receberá 56 em seu parâmetro **value**.
Uma propriedade P de uma classe C que defina apenas o método get é chamada de “somente leitura”. Se c for um objeto da classe C, a operação c.P=valor será rejeitada pelo compilador.
A execução do programa de teste anterior produz os seguintes resultados:
As propriedades nos permitem, portanto, manipular atributos privados como se fossem públicos. Outra característica das propriedades é que elas podem ser utilizadas em conjunto com um construtor, de acordo com a seguinte sintaxe:
Essa sintaxe é equivalente ao código a seguir:
A ordem das propriedades não importa. Veja um exemplo.
À classe Personne é adicionado um novo construtor sem parâmetros:
public Personne() {
}
O construtor não inicializa os membros do objeto. É o que chamamos de construtor padrão. É ele que é usado quando a classe não define nenhum construtor.
O código a seguir cria e inicializa (linha 6) um novo objeto Personne com a sintaxe apresentada anteriormente:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" };
Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age);
}
}
}
Na linha 6 acima, é utilizado o construtor sem parâmetros Personne(). Nesse caso específico, também seria possível escrever
Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };
mas os parênteses do construtor Personne() sem parâmetros não são obrigatórios nessa sintaxe.
Os resultados da execução são os seguintes:
Em muitos casos, os métodos get e set de uma propriedade limitam-se a ler e gravar um campo privado, sem qualquer outro processamento. Nesse cenário, é possível utilizar uma propriedade automática declarada da seguinte forma:
O campo privado associado à propriedade não é declarado. Ele é gerado automaticamente pelo compilador. O acesso a ele é feito exclusivamente por meio de sua propriedade. Assim, em vez de escrever:
private string prenom;
...
// propriedade associada
public string Prenom {
get { return prenom; }
set {
// nome válido?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//nome
poderemos escrever:
sem declarar o campo privado prenom. A diferença entre as duas propriedades anteriores é que a primeira verifica a validade do nome no set, enquanto a segunda não realiza nenhuma verificação.
Usar a propriedade automática Prenom equivale a declarar um campo Prenom como público:
Pode-se questionar se há alguma diferença entre as duas declarações. Declarar public como um campo de uma classe não é recomendado. Isso rompe com o conceito de encapsulamento do estado de um objeto, estado esse que deve ser privado e exposto por meio de métodos públicos.
Se a propriedade automática for declarada como virtuelle,, ela poderá ser redefinida em uma classe filha:
class Class1 {
public virtual string Prop { get; set; }
}
class Class2 : Class1 {
public override string Prop { get { return base.Prop; } set {... } }
}
Na linha 2 acima, a classe filha Class2 pode incluir na set, um código que verifique a validade do valor atribuído à propriedade automática base.Prop da classe mãe Class1.
4.1.15. Métodos e atributos de classe
Suponhamos que queiramos contar o número de objetos Personne criados em um aplicativo. É possível gerenciar um contador manualmente, mas corre-se o risco de esquecer os objetos temporários criados aqui e ali. Parece mais seguro incluir, nos construtores da classe Personne, uma instrução que incremente um contador. O problema é passar uma referência desse contador para que o construtor possa incrementá-lo: é preciso passar um novo parâmetro a eles. Também é possível incluir o contador na definição da classe. Como se trata de um atributo da própria classe e não de uma instância específica dessa classe, ele é declarado de maneira diferente com a palavra-chave static:
private static long nbPersonnes;
Para referenciá-lo, escreve-se Personne.nbPersonnes para indicar que se trata de um atributo da própria classe Personne. Aqui, criamos um atributo privado ao qual não teremos acesso direto fora da classe. Portanto, criamos uma propriedade pública para dar acesso ao atributo de classe nbPersonnes. Para definir o valor de nbPersonnes, o método get dessa propriedade não precisa de um objeto Personne específico: na verdade, nbPersonnes é um atributo de toda uma classe. Portanto, é necessária uma propriedade também declarada como static:
public static long NbPersonnes {
get { return nbPersonnes; }
}
que, externamente, será chamada com a sintaxe Personne.NbPersonnes. Veja um exemplo.
A classe Personne fica da seguinte forma:
using System;
namespace Chap2 {
public class Personne {
// atributos de classe
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// atributos de instância
private string prenom;
private string nom;
private int age;
// construtores
public Personne(String p, String n, int age) {
Initialise(p, n, age);
nbPersonnes++;
}
public Personne(Personne p) {
Initialise(p);
nbPersonnes++;
}
...
}
Nas linhas 20 e 24, os construtores incrementam o campo estático da linha 7.
Com o seguinte programa:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p1 = new Personne("Jean", "Dupont", 30);
Personne p2 = new Personne(p1);
new Personne(p1);
Console.WriteLine("Nombre de personnes créées : " + Personne.NbPersonnes);
}
}
}
obtêm-se os seguintes resultados:
4.1.16. Uma tabela de pessoas
Um objeto é um dado como qualquer outro e, por isso, vários objetos podem ser agrupados em uma tabela:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// uma tabela de pessoas
Personne[] amis = new Personne[3];
amis[0] = new Personne("Jean", "Dupont", 30);
amis[1] = new Personne("Sylvie", "Vartan", 52);
amis[2] = new Personne("Neil", "Armstrong", 66);
// exibição
foreach (Personne ami in amis) {
ami.Identifie();
}
}
}
}
- linha 7: cria uma matriz com 3 elementos do tipo Personne. Esses 3 elementos são inicializados aqui com os valores null e c.a.d, pois não referenciam nenhum objeto. Mais uma vez, por conveniência, fala-se em tabela de objetos, embora se trate apenas de uma tabela de referências a objetos. A criação da tabela de objetos, que é ela própria um objeto (presença de new), não cria nenhum objeto do tipo de seus elementos: isso deve ser feito posteriormente.
- linhas 8-10: criação dos 3 objetos do tipo Personne
- linhas 12-14: exibição do conteúdo da matriz amis
Obtêm-se os seguintes resultados:
4.2. Herança por exemplo
4.2.1. Generalidades
Abordamos aqui o conceito de herança. O objetivo da herança é “personalizar” uma classe existente para que ela atenda às nossas necessidades. Suponhamos que queiramos criar uma classe Enseignant: um professor é uma pessoa específica. Ele possui atributos que outra pessoa não terá: a disciplina que leciona, por exemplo. Mas também possui os atributos de qualquer pessoa: nome, sobrenome e idade. Um professor, portanto, faz parte integralmente da classe Personne, mas possui atributos adicionais. Em vez de criar uma classe Enseignant do zero, seria preferível aproveitar o que já existe na classe Personne e adaptá-la às características específicas dos professores. É o conceito de herança que nos permite fazer isso.
Para expressar que a classe Enseignant herda as propriedades da classe Personne, escreveremos:
Personne é chamada de classe pai (ou mãe) e Enseignant, de classe derivada (ou filha). Um objeto Enseignant possui todas as características de um objeto Personne: ele tem os mesmos atributos e os mesmos métodos. Esses atributos e métodos da classe pai não são repetidos na definição da classe filha: basta indicar os atributos e métodos adicionados pela classe filha:
Suponhamos que a classe Personne seja definida da seguinte forma:
using System;
namespace Chap2 {
public class Personne {
// atributos de classe
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// atributos de instância
private string prenom;
private string nom;
private int age;
// construtores
public Personne(String prenom, String nom, int age) {
Nom = nom;
Prenom = prenom;
Age = age;
nbPersonnes++;
Console.WriteLine("Constructeur Personne(string, string, int)");
}
public Personne(Personne p) {
Nom = p.Nom;
Prenom = p.Prenom;
Age = p.Age;
nbPersonnes++;
Console.WriteLine("Constructeur Personne(Personne)");
}
// propriedades
public string Prenom {
get { return prenom; }
set {
// nome válido?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//se
}//nome
public string Nom {
get { return nom; }
set {
// sobrenome válido?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//se
}//sobrenome
public int Age {
get { return age; }
set {
// idade válida?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//se
}//idade
// propriedade
public string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
}
}
}
O método Identifie foi substituído pela propriedade Identite, que é somente de leitura e identifica a pessoa. Criamos uma classe Enseignant herdando da classe Personne:
using System;
namespace Chap2 {
class Enseignant : Personne {
// atributos
private int section;
// construtor
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// a seção é armazenada por meio da propriedade Section
Section = section;
// rastreamento
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//construtor
// propriedade Section
public int Section {
get { return section; }
set { section = value; }
}// Seção
}
}
A classe Enseignant adiciona aos métodos e atributos da classe Personne:
- linha 4: a classe Enseignant deriva da classe Personne
- linha 6: um atributo section, que é o número da seção à qual o professor pertence no corpo docente (basicamente, uma seção por disciplina). Esse atributo privado é acessível por meio da propriedade pública Section das linhas 18 a 21
- linha 9: um novo construtor que permite inicializar todos os atributos de um professor
4.2.2. Construção de um objeto Professor
Uma classe filha não herda os construtores de sua classe pai. Portanto, ela deve definir seus próprios construtores. O construtor da classe Enseignant é o seguinte:
// construtor
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// a seção é memorizada
Section = section;
// acompanhamento
Console.WriteLine("Construction enseignant(string, string, int, int)");
}//fabricante
A declaração
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
indica que o construtor recebe quatro parâmetros: prenom, nom, age, section e passa três (prenom,nom,age) para sua classe base, neste caso a classe Personne. Sabe-se que essa classe possui um construtor Pessoa(string, string, int) que permitirá construir uma pessoa com os parâmetros passados (prenom,nom,age). Uma vez concluída a construção da classe base, a construção do objeto Enseignant prossegue com a execução do corpo do construtor:
Observe-se que, à esquerda do sinal =, não foi utilizado o atributo section do objeto, mas sim a propriedade Section a ele associada. Isso permite que o construtor aproveite eventuais verificações de validade que esse método possa realizar. Isso evita que essas verificações sejam colocadas em dois locais diferentes: o construtor e a propriedade.
Em resumo, o construtor de uma classe derivada:
- passa para sua classe base os parâmetros de que esta precisa para ser construída
- usa os demais parâmetros para inicializar os atributos que lhe são próprios
Poderíamos ter preferido escrever:
// construtor
public Enseignant(string prenom, string nom, int age, int section){
this.prenom=prenom;
this.nom=nom;
this.age=age;
this.section=section;
}
Isso é impossível. A classe Personne declarou como privados (private) seus três campos prenom, nom e age. Apenas objetos da mesma classe têm acesso direto a esses campos. Todos os outros objetos, incluindo objetos derivados como neste caso, devem utilizar métodos públicos para acessá-los. A situação teria sido diferente se a classe Personne tivesse declarado os três campos como protegidos (protected): nesse caso, ela permitiria que classes derivadas tivessem acesso direto aos três campos. Em nosso exemplo, usar o construtor da classe pai foi, portanto, a solução correta e é o método habitual: ao construir um objeto filho, primeiro chama-se o construtor do objeto pai e, em seguida, completam-se as inicializações específicas do objeto filho (section em nosso exemplo).
Vamos tentar um primeiro programa de teste [Program.cs]:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
Este programa se limita a criar um objeto Enseignant (new) e a identificá-lo. A classe Enseignant não possui o método Identite, mas sua classe pai possui um, que, além disso, é público: por herança, ele se torna um método público da classe Enseignant.
O projeto completo é o seguinte:
![]() |
Os resultados obtidos são os seguintes:
Percebe-se que:
- um objeto Personne (linha 1) foi criado antes do objeto Enseignant (linha 2)
- a identidade obtida é a do objeto Personne
4.2.3. Redefinição de um método ou de uma propriedade
No exemplo anterior, obtivemos a identidade da parte Personne do professor, mas faltam algumas informações específicas da classe Enseignant (a seção). Portanto, precisamos definir uma propriedade que permita identificar o professor:
using System;
namespace Chap2 {
class Enseignant : Personne {
// atributos
private int section;
// construtor
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// a seção é armazenada por meio da propriedade Section
Section = section;
// acompanhamento
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//construtor
// propriedade Section
public int Section {
get { return section; }
set { section = value; }
}// seção
// propriedade Identidade
public new string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
}
}
Nas linhas 24 a 26, a propriedade Identite da classe Enseignant baseia-se na propriedade Identite de sua classe pai (base.Identite) (linha 25) para exibir sua parte “Personne” e, em seguida, é complementada com o campo section, que é específico da classe Enseignant. Observe a declaração da propriedade Identite:
public new string Identite{
Suponha-se um objeto enseignant E. Esse objeto contém, em seu interior, um objeto Personne:
![]() |
A propriedade Identidade está definida tanto na classe Enseignant quanto em sua classe pai, Personne. Na classe filha Enseignant, a propriedade Identite deve ser precedida pela palavra-chave new para indicar que se está redefinindo uma nova propriedade Identite para a classe Enseignant.
public new string Identite{
A classe Enseignant agora possui duas propriedades Identite:
- aquela herdada da classe pai Personne
- e uma própria
Se E for um objeto Enseignant, E.Identite designa a propriedade Identite da classe Enseignant. Diz-se que a propriedade Identite da classe filha redefine ou oculta a propriedade Identite da classe pai. De modo geral, se O for um objeto e M um método, para executar o método O.M, o sistema procura um método M na seguinte ordem:
- na classe do objeto O
- na sua classe pai, se houver uma
- na classe-mãe da classe-mãe, se ela existir
- etc…
A herança permite, portanto, redefinir na classe filha métodos/propriedades com o mesmo nome que existem na classe pai. É isso que permite adaptar a classe filha às suas próprias necessidades. Associada ao polimorfismo, que veremos um pouco mais adiante, a redefinição de métodos/propriedades é o principal benefício da herança.
Vamos considerar o mesmo programa de teste de antes:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
Os resultados obtidos desta vez são os seguintes:
4.2.4. O polimorfismo
Consideremos uma hierarquia de classes: C0 ← C1 ← C2 ← … ← Cn
onde Ci ← Cj indica que a classe Cj é derivada da classe Ci. Isso implica que a classe Cj possui todas as características da classe Ci, além de outras. Sejam Oi objetos do tipo Ci. É válido escrever:
De fato, por herança, a classe Cj possui todas as características da classe Ci, além de outras. Portanto, um objeto Oj do tipo Cj contém em si um objeto do tipo Ci. A operação
faz com que Oi seja uma referência ao objeto do tipo Ci contido no objeto Oj.
O fato de uma variável Oi da classe Ci poder, na verdade, referenciar não apenas um objeto da classe Ci, mas qualquer objeto derivado da classe Ci, é chamado de polimorfismo: a capacidade de uma variável referenciar diferentes tipos de objetos.
Vamos dar um exemplo e considerar a seguinte função independente de qualquer classe (static):
Também seria possível escrever
quanto
Nesse último caso, o parâmetro formal p, do tipo Personne, do método estático Affiche, receberá um valor do tipo Enseignant. Como o tipo Enseignant deriva do tipo Personne, isso é válido.
4.2.5. Redefinição e polimorfismo
Vamos completar nosso método Affiche:
public static void Affiche(Personne p) {
// exibe identidade de p
Console.WriteLine(p.Identite);
}//exibe
A propriedade p.Identite retorna uma sequência de caracteres que identifica o objeto Pessoa p. O que acontece no exemplo anterior se o parâmetro passado para o método Affiche for um objeto do tipo Enseignant:
Enseignant e = new Enseignant(...);
Affiche(e);
Vejamos o exemplo a seguir:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// um professor
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// uma pessoa
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// exibe
public static void Affiche(Personne p) {
// exibe identidade de p
Console.WriteLine(p.Identite);
}//exibe
}
}
Os resultados obtidos são os seguintes:
A execução mostra que a instrução p.Identite (linha 17) executou, em cada ocasião, a propriedade Identite de um Personne, primeiro (linha 7) a pessoa contida na Enseignant e, em seguida (linha 10), a própria Personne. Ela não se adaptou ao objeto realmente passado como parâmetro para Affiche. Seria preferível ter a identidade completa do Enseignant e. Para isso, seria necessário que a notação p.Identite fizesse referência à propriedade Identite do objeto efetivamente apontado por p, em vez da propriedade Identite da parte “Personne” do objeto realmente apontado por p.
É possível obter esse resultado declarando Identite como uma propriedade virtual na classe base Personne:
public virtual string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
}
A palavra-chave **virtual torna Identite uma propriedade virtual. Essa palavra-chave também pode ser aplicada a métodos. As classes derivadas que redefinirem uma propriedade ou método virtual devem, então, utilizar a palavra-chave override em vez de new para qualificar sua propriedade/método redefinido. Assim, na classe Enseignant, a propriedade Identite é redefinida da seguinte forma:
public override string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
O programa anterior produz, então, os seguintes resultados:
Desta vez, na linha 3, obtivemos a identidade completa do professor. Vamos agora redefinir um método em vez de uma propriedade. A classe object (alias C# de System.Object) é a classe “pai” de todas as classes C#. Assim, quando escrevemos:
estamos escrevendo implicitamente:
A classe System.Object define um método virtual ToString:
![]() |
O método ToString retorna o nome da classe à qual o objeto pertence, conforme mostra o exemplo a seguir:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// um professor
Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
// uma pessoa
Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
}
}
}
Os resultados gerados são os seguintes:
É importante observar que, embora não tenhamos redefinido o método ToString nas classes Personne e Enseignant, é possível constatar que o método ToString da classe Object conseguiu exibir o nome real da classe do objeto.
Vamos redefinir o método ToString nas classes Personne e Enseignant:
// método ToString
public override string ToString() {
return Identite;
}
A definição é a mesma nas duas classes. Consideremos o seguinte programa de teste:
using System;
namespace Chap2 {
class Program3 {
public static void Main() {
// um professor
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// uma pessoa
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// cartaz
public static void Affiche(Personne p) {
// cartaz de identidade de p
Console.WriteLine(p);
}//Cartaz
}
}
Vamos nos concentrar no método Affiche, que recebe como parâmetro uma pessoa p. Na linha 15, o método WriteLine da classe Console não possui nenhuma variante que aceite um parâmetro do tipo Personne. Entre as diferentes variantes de Writeline, existe uma que aceita como parâmetro um tipo Object. O compilador utilizará esse método, WriteLine(Object o), pois essa assinatura significa que o parâmetro o pode ser do tipo Object ou derivado. Como Object é a classe pai de todas as classes, qualquer objeto pode ser passado como parâmetro para WriteLine e, portanto, um objeto do tipo Personne ou Enseignant. O método WriteLine(Object o) grava o.ToString() no fluxo de gravação Out. Como o método ToString é virtual, se o objeto o (do tipo Object ou derivado) tiver redefinido o método ToString, será este último que será utilizado. Esse é o caso das classes Personne e Enseignant.
É o que mostram os resultados da execução:
4.3. Redefinir o significado de um operador para uma classe
4.3.1. Introdução
Consideremos a instrução
onde op1 e op2 são dois operandos. É possível redefinir o significado do operador +. Se o operando op1 for um objeto da classe C1, é necessário definir um método estático na classe C1 com a seguinte assinatura:
Quando o compilador encontra a instrução
, ele a traduz para C1.operator+(op1,op2). O tipo retornado pelo método operator é importante. De fato, consideremos a operação op1+op2+op3. Ela é traduzida pelo compilador como (op1+op2)+op3. Seja res12 o resultado de op1+op2. A operação realizada em seguida é res12+op3. Se res12 for do tipo C1, ela também será traduzida por C1.operator+(res12,op3). Isso permite encadear as operações.
Também é possível redefinir os operadores unários que possuem apenas um operando. Assim, se op1 for um objeto do tipo C1, a operação op1++ pode ser redefinida por um método estático da classe C1:
O que foi dito aqui se aplica à maioria dos operadores, com algumas exceções:
- os operadores == e != devem ser redefinidos simultaneamente
- os operadores &&, ||, [], (), +=, -=, ... não podem ser redefinidos
4.3.2. Um exemplo
Criamos uma classe ListeDePersonnes derivada da classe ArrayList. Essa classe implementa uma lista dinâmica e é apresentada no capítulo a seguir. Dessa classe, utilizamos apenas os seguintes elementos:
- o método L.Add(Object o), que permite adicionar à lista L um objeto o. Aqui, o objeto o será um objeto Personne.
- a propriedade L.Count, que fornece o número de elementos da lista L
- a notação L[i], que fornece o elemento i da lista L
A classe ListeDePersonnes herdará todos os atributos, métodos e propriedades da classe ArrayList. Sua definição é a seguinte:
using System;
using System.Collections;
using System.Text;
namespace Chap2 {
class ListeDePersonnes : ArrayList{
// redefinição do operador +, para adicionar uma pessoa à lista
public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
// adiciona-se a Pessoa p à ListeDePersonnes l
l.Add(p);
// torna-se a ListeDePersonnes l
return l;
}// operador +
// ToString
public override string ToString() {
// retorna (él1, él2, ..., éln)
// parêntese de abertura
StringBuilder listeToString = new StringBuilder("(");
// percorre-se a lista de pessoas (this)
for (int i = 0; i < Count - 1; i++) {
listeToString.Append(this[i]).Append(",");
}//for
// último elemento
if (Count != 0) {
listeToString.Append(this[Count-1]);
}
// parêntese de fechamento
listeToString.Append(")");
// é preciso retornar uma string
return listeToString.ToString();
}//ToString
}
}
- linha 6: a classe ListeDePersonnes deriva da classe ArrayList
- linhas 8-13: definição do operador + para a operação l + p, em que l é do tipo ListeDePersonnes e p é do tipo Personne ou derivado.
- linha 10: a pessoa p é adicionada à lista l. Aqui é utilizado o método Add da classe pai ArrayList.
- linha 12: a referência à lista l é retornada para que seja possível encadear os operadores +, como em l + p1 + p2. A operação l+p1+p2 será interpretada (prioridade dos operadores) como (l+p1)+p2. A operação l+p1 resultará na referência l. A operação (l+p1)+p2 passa então a ser l+p2, que adiciona a pessoa p2 à lista de pessoas l.
- linha 16: redefinimos o método ToString para exibir uma lista de pessoas no formato (pessoa1, pessoa2, ..) em que personnei é, por sua vez, o resultado do método ToString da classe Personne.
- linha 19: utilizamos um objeto do tipo StringBuilder. Essa classe é mais adequada do que a classe string sempre que for necessário realizar várias operações na sequência de caracteres, neste caso, adições. De fato, cada operação em um objeto string retorna um novo objeto string, enquanto as mesmas operações em um objeto StringBuilder modificam o objeto, mas não criam um novo. Utilizamos o método Append para concatenar as cadeias de caracteres.
- linha 21: percorremos os elementos da lista de pessoas. Essa lista é aqui designada por this. Trata-se do objeto atual sobre o qual é executado o método ToString. A propriedade Count é uma propriedade da classe pai ArrayList.
- linha 22: o elemento nº i da lista atual this pode ser acessado por meio da notação this[i]. Mais uma vez, trata-se de uma propriedade da classe ArrayList. Como se trata de somar cadeias de caracteres, será utilizado o método this[i].ToString(). Como esse método é virtual, será utilizado o método ToString do objeto this, do tipo Personne ou derivado.
- linha 31: precisamos retornar um objeto do tipo string (linha 16). A classe StringBuilder possui um método ToString que permite passar de um tipo StringBuilder para um tipo string.
Observe-se que a classe ListeDePersonnes não possui construtor. Nesse caso, sabe-se que o construtor
será utilizado. Esse construtor não faz nada além de chamar o construtor sem parâmetros de sua classe pai:
Uma classe de teste poderia ser a seguinte:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// uma lista de pessoas
ListeDePersonnes l = new ListeDePersonnes();
// adição de pessoas
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// exibição
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
}
}
}
- linha 7: criação de uma lista de pessoas na
- linha 9: adição de 2 pessoas com o operador +
- linha 12: adição de um professor
- linhas 11 e 13: uso do método redefinido ListeDePersonnes.ToString().
Os resultados:
4.4. Definir um indexador para uma classe
Continuamos aqui a utilizar a classe ListeDePersonnes. Se l é um objeto ListeDePersonnes, queremos poder usar a notação l[i] para designar a pessoa nº i da lista l tanto na leitura (Pessoa p=l[i]) quanto na gravação (l[i]=new Pessoa(...)).
Para poder escrever l[i], onde l[i] designa um objeto Personne, precisamos definir na classe ListeDePersonnes o seguinte método this:
public Personne this[int i] {
get { ... }
set { ... }
}
Chamamos o método this[int i] de indexador, pois ele atribui um significado à expressão obj[i], que lembra a notação de tabelas, embora obj não seja uma tabela, mas um objeto. O método get do método this doobjeto obj é chamado quando se escreve variável=obj[i] e o método set quando se escreve obj[i]=valor.
A classe ListeDePersonnes deriva da classe ArrayList, que, por sua vez, possui um indexador:
Há um conflito entre o método this da classe ListeDePersonnes:
public Personne this[int i]
e o método this da classe ArrayList
public object this[int i]
porque elas têm o mesmo nome e aceitam o mesmo tipo de parâmetro (int). Para indicar que o método this da classe ListeDePersonnes “oculta” o método de mesmo nome da classe ArrayList, é necessário adicionar a palavra-chave new à declaração do indexador de ListeDePersonnes. Portanto, escreveremos:
public new Personne this[int i]{
get { ... }
set { ... }
}
Vamos completar esse método. O método this.get é chamado quando se escreve, por exemplo, variable=l[i], onde l é do tipo ListeDePersonnes. Nesse caso, deve-se retornar a pessoa nº i da lista l. Isso é feito com a notação base[i], que torna o objeto nº i da classe ArrayList subjacente à classe ListeDePersonnes. Como o objeto retornado é do tipo Object, é necessária uma conversão de tipo para a classe Personne.
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { ... }
}
O método set é chamado quando se escreve l[i]=p, onde p é um Personne. Trata-se, portanto, de atribuir a pessoa p ao elemento i da lista l.
public new Personne this[int i]{
get { ... }
set { base[i]=value; }
}
Aqui, a pessoa p, representada pela palavra-chave value, é atribuída ao elemento nº i da classe base ArrayList.
O indexador da classe ListeDePersonnes será, portanto, o seguinte:
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { base[i]=value; }
}
Agora, queremos poder escrever também Pessoa p=l["nom"], c.a.d e indexar a lista l não mais por um número de elemento, mas por um nome de pessoa. Para isso, definimos um novo indexador:
// pesquisa por nome
public int this[string nom] {
get {
// procurando a pessoa
for (int i = 0; i < Count; i++) {
if (((Personne)base[i]).Nom == nom)
return i;
}//para
return -1;
}//obter
}
A primeira linha
public int this[string nom]
indica que se indexa a classe ListeDePersonnes por uma sequência de caracteres nom e que o resultado de l[nom] é um número inteiro. Esse número inteiro será a posição na lista da pessoa com o nome nom ou -1, caso essa pessoa não conste na lista. Define-se apenas a propriedade get,, impedindo assim a escrita l["nom"]=valor, o que teria exigido a definição da propriedade set. A palavra-chave new não é necessária na declaração do indexador, pois a classe base ArrayList não define um indexador this[string].
No corpo do get, percorre-se a lista de pessoas em busca do nome passado como parâmetro. Se ele for encontrado na posição i, retorna-se i; caso contrário, retorna-se -1.
O programa de teste anterior é complementado da seguinte forma:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// uma lista de pessoas
ListeDePersonnes l = new ListeDePersonnes();
// adição de pessoas
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// exibição
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
// alteração do elemento 1
l[1] = new Personne("franck", "gallon",5);
// exibição do elemento 1
Console.WriteLine("l[1]=" + l[1]);
// exibição da lista l
Console.WriteLine("l=" + l);
// busca de pessoas
string[] noms = { "martin", "germain", "xx" };
for (int i = 0; i < noms.Length; i++) {
int inom = l[noms[i]];
if (inom != -1)
Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
else
Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
}//para
}
}
}
Sua execução produz os seguintes resultados:
4.5. As estruturas
A estrutura em C# é análoga à estrutura da linguagem C e é muito semelhante ao conceito de classe. Uma estrutura é definida da seguinte forma:
Apesar da semelhança na declaração, existem diferenças importantes entre classe e estrutura. O conceito de herança, por exemplo, não existe nas estruturas. Se criarmos uma classe que não deva ser derivada, quais são as diferenças entre estrutura e classe que nos ajudarão a escolher entre as duas? Vamos usar o exemplo a seguir para descobrir:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// uma estrutura sp1
SPersonne sp1;
sp1.Nom = "paul";
sp1.Age = 10;
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
// uma estrutura sp2
SPersonne sp2 = sp1;
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// sp2 é modificado
sp2.Nom = "nicole";
sp2.Age = 30;
// verificação de sp1 e sp2
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// um objeto op1
CPersonne op1=new CPersonne();
op1.Nom = "paul";
op1.Age = 10;
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
// um objeto op2
CPersonne op2=op1;
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
// op2 é modificado
op2.Nom = "nicole";
op2.Age = 30;
// verificação de op1 e op2
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
}
}
// estrutura SPersonne
struct SPersonne {
public string Nom;
public int Age;
}
// classe CPersonne
class CPersonne {
public string Nom;
public int Age;
}
}
- linhas 38-41: uma estrutura com dois campos públicos: Nom, Age
- linhas 44-47: uma classe com dois campos públicos: Nom, Age
Ao executar este programa, obtêm-se os seguintes resultados:
Onde antes se utilizava uma classe Personne, agora utilizamos uma estrutura SPersonne:
struct SPersonne {
public string Nom;
public int Age;
}
A estrutura não possui um construtor neste caso. Ela poderia ter um, como mostraremos mais adiante. Por padrão, ela sempre dispõe do construtor sem parâmetros, neste caso SPersonne().
- linha 7 do código: a declaração
SPersonne sp1;
é equivalente à instrução:
SPersonne sp1=new Spersonne();
Uma estrutura (Nome,Idade) é criada e o valor de sp1 é essa própria estrutura. No caso da classe, a criação do objeto (Nome,Idade) deve ser feita explicitamente pelo operador new (linha 22):
CPersonne op1=new CPersonne();
A instrução anterior cria um objeto CPersonne (grosso modo, o equivalente à nossa estrutura) e o valor de p1 é, então, o endereço (a referência) desse objeto.
Resumindo
- no caso da estrutura, o valor de sp1 é a própria estrutura
- no caso da classe, o valor de op1 é o endereço do objeto criado
![]() |
Quando, no programa, escrevemos na linha 12:
SPersonne sp2 = sp1;
uma nova estrutura sp2(Nome,Idade) é criada e inicializada com o valor de sp1,, ou seja, a própria estrutura.
![]() |
A estrutura de sp1 é duplicada em sp2 [1]. Trata-se de uma cópia de valor. Consideremos agora a instrução da linha 27:
CPersonne op2=op1;
No caso das classes, o valor de op1 é copiado para op2, mas como esse valor é, na verdade, o endereço do objeto, este não é duplicado [2].
No caso da estrutura [1], se alterarmos o valor de sp2, não alteramos o valor de sp1, o que é demonstrado pelo programa. No caso do objeto [2], se modificarmos o objeto apontado por op2, aquele apontado por op1 será modificado, pois se trata do mesmo objeto. É isso que os resultados do programa também demonstram.
Portanto, podemos concluir dessas explicações que:
- o valor de uma variável do tipo estrutura é a própria estrutura
- o valor de uma variável do tipo objeto é o endereço do objeto ao qual ela aponta
Uma vez compreendida essa diferença fundamental, a estrutura se mostra muito semelhante à classe, como demonstra o novo exemplo a seguir:
using System;
namespace Chap2 {
// estrutura SPersonne
struct SPersonne {
// atributos privados
private string nom;
private int age;
// propriedades
public string Nom {
get { return nom; }
set { nom = value; }
}//nome
public int Age {
get { return age; }
set { age = value; }
}//idade
// Fabricante
public SPersonne(string nom, int age) {
this.nom = nom;
this.age = age;
}//fabricante
// ToString
public override string ToString() {
return "SPersonne(" + Nom + "," + Age + ")";
}//ToString
}//estrutura
}//namespace
- linhas 8-9: dois campos privados
- linhas 12-20: as propriedades públicas associadas
- linhas 23-26: define-se um construtor. Observe que o construtor sem parâmetros SPersonne() está sempre presente e não precisa ser declarado. Sua declaração é rejeitada pelo compilador. No construtor das linhas 23-26, poderíamos ficar tentados a inicializar os campos privados nom e age por meio de suas propriedades públicas Nom e Age. Isso é rejeitado pelo compilador. Os métodos da estrutura não podem ser utilizados durante a construção da mesma.
- linhas 29-31: redefinição do método ToString.
Um programa de teste poderia ser o seguinte:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// uma pessoa p1
SPersonne p1=new SPersonne();
p1.Nom="paul";
p1.Age= 10;
Console.WriteLine("p1={0}",p1);
// uma pessoa p2
SPersonne p2 = p1;
Console.WriteLine("p2=" + p2);
// p2 é alterado
p2.Nom = "nicole";
p2.Age = 30;
// verificação de p1 e p2
Console.WriteLine("p1=" + p1);
Console.WriteLine("p2=" + p2);
// uma pessoa p3
SPersonne p3 = new SPersonne("amandin", 18);
Console.WriteLine("p3=" + p3);
// uma pessoa p4
SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
Console.WriteLine("p4=" + p4);
}
}
}
- linha 7: é necessário usar explicitamente o construtor sem parâmetros, pois existe outro construtor na estrutura. Se a estrutura não tivesse nenhum construtor, a instrução
SPersonne p1;
teria sido suficiente para criar uma estrutura vazia.
- linhas 8-9: a estrutura é inicializada por meio de suas propriedades públicas
- linha 10: o método p1.ToString será utilizado no WriteLine.
- linha 21: criação de uma estrutura com o construtor SPersonne(string, int)
- linha 24: criação de uma estrutura com o construtor sem parâmetros SPersonne(), com, entre chaves, a inicialização dos campos privados por meio de suas propriedades públicas.
Obtêm-se os seguintes resultados de execução:
A única diferença notável aqui entre estrutura e classe é que, com uma classe, os objetos p1 e p2 teriam apontado para o mesmo objeto ao final do programa.
4.6. As interfaces
Uma interface é um conjunto de protótipos de métodos ou propriedades que forma um contrato. Uma classe que decide implementar uma interface se compromete a fornecer uma implementação de todos os métodos definidos na interface. É o compilador que verifica essa implementação.
Veja, por exemplo, a definição da interface System.Collections.IEnumerator:
public interface System.Collections.IEnumerator
{
// Propriedades
Object Current { get; }
// Métodos
bool MoveNext();
void Reset();
}
As propriedades e os métodos da interface são definidos apenas por suas assinaturas. Eles não são implementados (não possuem código). São as classes que implementam a interface que fornecem o código para os métodos e propriedades da interface.
- linha 1: a classe C implementa a interface IEnumerator. Observe-se que o sinal : utilizado para a implementação de uma interface é o mesmo que o utilizado para a herança de uma classe.
- linhas 3-5: a implementação dos métodos e propriedades da interface IEnumerator.
Consideremos a seguinte interface:
namespace Chap2 {
public interface IStats {
double Moyenne { get; }
double EcartType();
}
}
A interface IStats apresenta:
- uma propriedade somente leitura Moyenne: para calcular a média de uma série de valores
- um método EcartType: para calcular o desvio-padrão
Observe-se que não é especificado em nenhum lugar de qual série de valores se trata. Pode ser a média das notas de uma turma, a média mensal das vendas de um produto específico, a temperatura média em um determinado local, etc. Esse é o princípio das interfaces: pressupõe-se a existência de métodos no objeto, mas não a de dados específicos.
Uma primeira classe de implementação da interface IStats poderia ser uma classe destinada a armazenar as notas dos alunos de uma turma em uma determinada disciplina. Um aluno seria caracterizado pela seguinte estrutura Elève:
public struct Elève {
public string Nom { get; set; }
public string Prénom { get; set; }
}//Aluno
O aluno seria identificado por seu nome e sobrenome. Nas linhas 2 e 3, encontram-se as propriedades automáticas para esses dois atributos.
Uma nota seria caracterizada pela seguinte estrutura Note:
public struct Note {
public Elève Elève { get; set; }
public double Valeur { get; set; }
}//Nota
A nota seria identificada pelo aluno avaliado e pela própria nota. Nas linhas 2 e 3, encontram-se as propriedades automáticas para esses dois atributos.
As notas de todos os alunos em uma determinada disciplina são agrupadas na seguinte classe TableauDeNotes:
using System;
using System.Text;
namespace Chap2 {
public class TableauDeNotes : IStats {
// atributos
public string Matière { get; set; }
public Note[] Notes { get; set; }
public double Moyenne { get; private set; }
private double ecartType;
// Construtor
public TableauDeNotes(string matière, Note[] notes) {
// armazenamento por meio de propriedades públicas
Matière = matière;
Notes = notes;
// cálculo da média das notas
double somme = 0;
for (int i = 0; i < Notes.Length; i++) {
somme += Notes[i].Valeur;
}
if (Notes.Length != 0) Moyenne = somme / Notes.Length;
else Moyenne = -1;
// desvio padrão
double carrés = 0;
for (int i = 0; i < Notes.Length; i++) {
carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
}//for
if (Notes.Length != 0)
ecartType = Math.Sqrt(carrés / Notes.Length);
else ecartType = -1;
}//construtor
public double EcartType() {
return ecartType;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// concatenamos todas as notas
for (i = 0; i < Notes.Length-1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
};
//última nota
if (Notes.Length != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
}
valeur.Append(")");
// fim
return valeur.ToString();
}//ToString
}//classe
}
- linha 6: a classe TableauDeNotes implementa a interface IStats. Portanto, ela deve implementar a propriedade Moyenne e o método EcartType. Estes são implementados nas linhas 10 (Moyenne) e 35-37 (EcartType)
- linhas 8-10: três propriedades automáticas
- linha 8: a disciplina cujas notas são armazenadas pelo objeto
- linha 9: a tabela de notas dos alunos (Aluno, Nota)
- linha 10: a média das notas — propriedade que implementa a propriedade Moyenne da interface IStats.
- linha 11: campo que armazena o desvio-padrão das notas - o método get associado a EcartType das linhas 35-37 implementa o método EcartType da interface IStats.
- linha 9: as notas são armazenadas em um array. Esse array é passado, durante a construção da classe TableauDeNotes, para o construtor das linhas 14 a 33.
- linhas 14-33: o construtor. Supõe-se aqui que as notas passadas para o construtor não sofrerão mais alterações posteriormente. Por isso, utiliza-se o construtor para calcular imediatamente a média e o desvio-padrão dessas notas e armazená-los nos campos das linhas 10-11. A média é armazenada no campo privado subjacente à propriedade automática Moyenne da linha 10 e o desvio-padrão no campo privado da linha 11.
- linha 10: o método get da propriedade automática Moyenne retornará o campo privado subjacente.
- linhas 35-37: o método EcartType retorna o valor do campo privado da linha 11.
Existem algumas sutilezas neste código:
- linha 23: o método set da propriedade Moyenne é usado para realizar a atribuição. Esse método foi declarado privado na linha 10 para que a atribuição de um valor à propriedade Moyenne só seja possível dentro da classe.
- linhas 40-54: utilizam um objeto StringBuilder para construir a string que representa o objeto TableauDeNotes, a fim de melhorar o desempenho. É importante observar que a legibilidade do código é bastante prejudicada. Esse é o outro lado da moeda.
Na classe anterior, as notas eram armazenadas em um array. Não era possível adicionar uma nova nota após a construção do objeto TableauDeNotes. Propomos agora uma segunda implementação da interface IStats, chamada ListeDeNotes, na qual, desta vez, as notas seriam armazenadas em uma lista, com a possibilidade de adicionar notas após a criação inicial do objeto ListeDeNotes.
O código da classe ListeDeNotes é o seguinte:
using System;
using System.Text;
using System.Collections.Generic;
namespace Chap2 {
public class ListeDeNotes : IStats {
// atributos
public string Matière { get; set; }
public List<Note> Notes { get; set; }
public double moyenne = -1;
public double ecartType = -1;
// construtor
public ListeDeNotes(string matière, List<Note> notes) {
// armazenamento por meio de propriedades públicas
Matière = matière;
Notes = notes;
}//construtor
// adição de uma nota
public void Ajouter(Note note) {
// adição da nota
Notes.Add(note);
// média e desvio padrão reinicializados
moyenne = -1;
ecartType = -1;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// concatenamos todas as notas
for (i = 0; i < Notes.Count - 1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
};
//última nota
if (Notes.Count != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
}
valeur.Append(")");
// fim
return valeur.ToString();
}//ToString
// média das notas
public double Moyenne {
get {
if (moyenne != -1) return moyenne;
// cálculo da média das notas
double somme = 0;
for (int i = 0; i < Notes.Count; i++) {
somme += Notes[i].Valeur;
}
// calcula-se a média
if (Notes.Count != 0) moyenne = somme / Notes.Count;
return moyenne;
}
}
public double EcartType() {
// desvio-padrão
if (ecartType != -1) return ecartType;
// média
double moyenne = Moyenne;
double carrés = 0;
for (int i = 0; i < Notes.Count; i++) {
carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
}//for
// exibe o desvio-padrão
if (Notes.Count != 0)
ecartType = Math.Sqrt(carrés / Notes.Count);
return ecartType;
}
}//turma
}
- linha 7: a classe ListeDeNotes implementa a interface IStats
- linha 10: as notas agora são colocadas em uma lista, em vez de um array
- linha 11: a propriedade automática Moyenne da classe TableauDeNotes foi abandonada aqui em favor de um campo privado moyenne, linha 11, associado à propriedade pública somente leitura Moyenne das linhas 48-60
- linhas 22 a 28: agora é possível adicionar uma nota às que já estão armazenadas, o que não era possível anteriormente.
- linhas 15-19: consequentemente, a média e o desvio-padrão não são mais calculados no construtor, mas nos próprios métodos da interface: Moyenne (linhas 48-60) e EcartType (62-76). No entanto, o recálculo só é reiniciado se a média e o desvio-padrão forem diferentes de -1 (linhas 50 e 64).
Uma classe de teste poderia ser a seguinte:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// alguns alunos e notas de inglês
Elève[] élèves1 = { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
// que são registradas em um objeto TableauDeNotes
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// exibição da média e do desvio-padrão
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
// colocamos os alunos e a disciplina em um objeto ListeDeNotes
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// exibição da média e do desvio-padrão
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
// adiciona-se uma nota
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// exibição da média e do desvio-padrão
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
}
}
}
- linha 8: criação de uma matriz de alunos utilizando o construtor sem parâmetros e inicialização por meio das propriedades públicas
- linha 9: criação de um array de notas seguindo a mesma técnica
- linha 11: um objeto TableauDeNotes cuja média e desvio-padrão são calculados na linha 13
- linha 15: um objeto ListeDeNotes cuja média e desvio-padrão são calculados na linha 17. A classe List<Note> possui um construtor que aceita um objeto que implemente a interface IEnumerable<Note>. A matriz notes1 implementa essa interface e pode ser usada para construir o objeto List<Note>.
- linha 19: adição de uma nova nota
- linha 21: recálculo da média e do desvio-padrão
Os resultados da execução são os seguintes:
No exemplo anterior, duas classes implementam a interface IStats. No entanto, o exemplo não demonstra a utilidade da interface IStats. Vamos reescrever o programa de teste da seguinte maneira:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// alguns alunos e notas de inglês
Elève[] élèves1 = { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
// que são gravadas em um objeto TableauDeNotes
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// exibição da média e do desvio-padrão
AfficheStats(anglais);
// colocamos os alunos e a disciplina em um objeto ListeDeNotes
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// exibição da média e do desvio-padrão
AfficheStats(français);
// adiciona-se uma nota
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// exibição da média e do desvio-padrão
AfficheStats(français);
}
// exibição da média e do desvio-padrão de um tipo IStats
static void AfficheStats(IStats valeurs) {
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
}
}
}
- linhas 25-27: o método estático AfficheStats recebe como parâmetro um tipo IStats, ou seja, um tipo Interface. Isso significa que o parâmetro efetivo pode ser qualquer objeto que implemente a interface IStats. Quando se utiliza um dado com o tipo de uma interface, isso significa que serão utilizados apenas os métodos da interface implementados por esse dado. O restante é ignorado. Trata-se de uma propriedade semelhante ao polimorfismo visto para as classes. Se um conjunto de classes Ci não relacionadas entre si por herança (portanto, não é possível utilizar o polimorfismo de herança) apresentar um conjunto de métodos com a mesma assinatura, pode ser interessante agrupar esses métodos em uma interface I que seria implementada por todas as classes em questão. Instâncias dessas classes Ci podem, então, ser utilizadas como parâmetros efetivos de funções que admitem um parâmetro formal do tipo I, c.a.d — funções que utilizam apenas os métodos dos objetos Ci definidos na interface I e não os atributos e métodos específicos das diferentes classes Ci.
- linha 13: o método AfficheStats é chamado com um tipo TableauDeNotes que implementa a interface IStats
- linha 17: o mesmo com um tipo ListeDeNotes
Os resultados da execução são idênticos aos da anterior.
Uma variável pode ser do tipo de uma interface. Assim, pode-se escrever:
A declaração da linha 1 indica que stats1 é a instância de uma classe que implementa a interface IStats. Essa declaração implica que o compilador permitirá, em stats1, apenas o acesso aos métodos da interface: a propriedade Moyenne e o método EcartType.
Por fim, observe que a implementação de interfaces pode ser múltipla, como em c.a.d. Pode-se escrever
onde os Ij são interfaces.
4.7. As classes abstratas
Uma classe abstrata é uma classe que não pode ser instanciada. É necessário criar classes derivadas que, por sua vez, possam ser instanciadas.
É possível utilizar classes abstratas para refatorar o código de uma linhagem de classes. Examinemos o seguinte caso:
using System;
namespace Chap2 {
abstract class Utilisateur {
// campos
private string login;
private string motDePasse;
private string role;
// fabricante
public Utilisateur(string login, string motDePasse) {
// as informações são registradas
this.login = login;
this.motDePasse = motDePasse;
// identifica-se o usuário
role=identifie();
// identificado?
if (role == null) {
throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
}
}
// toString
public override string ToString() {
return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role);
}
// identifica
abstract public string identifie();
}
}
- linhas 11-21: o construtor da classe Utilisateur. Essa classe armazena informações sobre o usuário de um aplicativo web. O aplicativo possui diversos tipos de usuários autenticados por login/senha (linhas 6-7). Essas duas informações são verificadas junto a um serviço LDAP para alguns usuários, junto a um SGBD para outros, etc...
- linhas 13-14: as informações de autenticação são armazenadas
- linha 16: elas são verificadas por um método de identificação. Como o método de identificação não é conhecido, ele é declarado abstrato na linha 29 com a palavra-chave abstract. O método identifie retorna uma sequência de caracteres que especifica a função do usuário (basicamente, o que ele tem permissão para fazer). Se essa string for o ponteiro null, uma exceção é lançada na linha 19.
- linha 4: como possui um método abstrato, a própria classe é declarada como abstrata com a palavra-chave `abstract`.
- linha 29: o método abstrato `identify` não possui definição. São as classes derivadas que lhe darão uma definição.
- linhas 24-26: o método ToString, que identifica uma instância da classe.
Supõe-se aqui que o desenvolvedor deseja ter controle sobre a criação de instâncias da classe Utilisateur e das classes derivadas, talvez porque queira garantir que uma exceção de determinado tipo seja lançada caso o usuário não seja reconhecido (linha 19). As classes derivadas poderão utilizar esse construtor. Para isso, elas deverão fornecer o método **identifica**.
A classe ExceptionUtilisateurInconnu é a seguinte:
using System;
namespace Chap2 {
class ExceptionUtilisateurInconnu : Exception {
public ExceptionUtilisateurInconnu(string message) : base(message){
}
}
}
- linha 3: ela deriva da classe Exception
- linhas 4-6: ela possui apenas um único construtor que aceita como parâmetro uma mensagem de erro. Essa mensagem é passada para a classe pai (linha 5), que possui esse mesmo construtor.
Agora derivamos a classe Utilisateur da classe filha Administrateur:
namespace Chap2 {
class Administrateur : Utilisateur {
// fabricante
public Administrateur(string login, string motDePasse)
: base(login, motDePasse) {
}
// identifica
public override string identifie() {
// identificação LDAP
// ...
return "admin";
}
}
}
- linhas 4-6: o construtor se limita a passar para sua classe pai os parâmetros que recebe
- linhas 9-12: o método `identifica` da classe Administrateur. Suponha-se que um administrador seja identificado por um sistema LDAP. Esse método redefine o método `identifica` de sua classe pai. Como ele redefine um método abstrato, não é necessário usar a palavra-chave override.
Agora derivamos a classe Utilisateur da classe filha Observateur:
namespace Chap2 {
class Observateur : Utilisateur{
// fabricante
public Observateur(string login, string motDePasse)
: base(login, motDePasse) {
}
//identifica
public override string identifie() {
// identificação SGBD
// ...
return "observateur";
}
}
}
- linhas 4-6: o construtor limita-se a passar para sua classe pai os parâmetros que recebe
- linhas 9-13: o método de identificação da classe Observateur. Supõe-se que um observador seja identificado por meio da verificação de seus dados de identificação em um banco de dados.
Por fim, os objetos Administrateur e Observateur são instanciados pelo mesmo construtor, o da classe pai Utilisateur.. Esse construtor utilizará o método identifica que essas classes fornecem.
Uma terceira classe, Inconnu, também deriva da classe Utilisateur:
namespace Chap2 {
class Inconnu : Utilisateur{
// fabricante
public Inconnu(string login, string motDePasse)
: base(login, motDePasse) {
}
//identifica
public override string identifie() {
// usuário desconhecido
// ...
return null;
}
}
}
- linha 13: o método identifie retorna o ponteiro null para indicar que o usuário não foi reconhecido.
Um programa de teste poderia ser o seguinte:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Observateur("observer","mdp1"));
Console.WriteLine(new Administrateur("admin", "mdp2"));
try {
Console.WriteLine(new Inconnu("xx", "yy"));
} catch (ExceptionUtilisateurInconnu e) {
Console.WriteLine("Utilisateur non connu : "+ e.Message);
}
}
}
}
Observe que nas linhas 6, 7 e 9, o método [Utilisateur].ToString() será utilizado pelo método WriteLine.
Os resultados da execução são os seguintes:
4.8. Classes, interfaces e métodos genéricos
Suponhamos que queiramos escrever um método que troque a ordem de dois números inteiros. Esse método poderia ser o seguinte:
public static void Echanger1(ref int value1, ref int value2){
// trocamos as referências value1 e value2
int temp = value2;
value2 = value1;
value1 = temp;
}
Agora, se quiséssemos permutar duas referências a objetos Personne, escreveríamos:
public static void Echanger2(ref Personne value1, ref Personne value2){
// trocamos as referências value1 e value2
Personne temp = value2;
value2 = value1;
value1 = temp;
}
O que diferencia os dois métodos é o tipo T dos parâmetros: int em Echanger1, Personne em Echanger2. As classes e interfaces genéricas atendem à necessidade de métodos que diferem apenas pelo tipo de alguns de seus parâmetros.
Com uma classe genérica, o método Echanger poderia ser reescrito da seguinte forma:
namespace Chap2 {
class Generic1<T> {
public static void Echanger(ref T value1, ref T value2){
// trocamos as referências value1 e value2
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- linha 2: a classe Generic1 é parametrizada por um tipo denominado T. É possível atribuir-lhe o nome que se desejar. Esse tipo T é, em seguida, reutilizado na classe nas linhas 3 e 5. Diz-se que a classe Generic1 é uma classe genérica.
- linha 3: define as duas referências a um tipo T a ser permutado
- linha 5: a variável temporária temp é do tipo T.
Um programa de teste da classe poderia ser o seguinte:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic1<int>.Echanger(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic1<string>.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Pessoa
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic1<Personne>.Echanger(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- linha 8: ao utilizar uma classe genérica parametrizada pelos tipos T1, T2, ... estes devem ser “instanciados”. Na linha 8, utiliza-se o método estático Echanger do tipo Generic1<int> para indicar que as referências passadas ao método Echanger são do tipo int.
- linha 12: utiliza-se o método estático Echanger do tipo Generic1<string> para indicar que as referências passadas ao método Echanger são do tipo string.
- linha 16: utiliza-se o método estático Echanger do tipo Generic1<Personne> para indicar que as referências passadas ao método Echanger são do tipo Personne.
Os resultados da execução são os seguintes:
O método Echanger também poderia ter sido escrito da seguinte forma:
namespace Chap2 {
class Generic2 {
public static void Echanger<T>(ref T value1, ref T value2){
// trocam-se as referências value1 e value2
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- linha 2: a classe Generic2 não é mais genérica
- linha 3: o método estático Echanger é genérico
O programa de teste passa a ser o seguinte:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic2.Echanger<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic2.Echanger<string>(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Pessoa
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic2.Echanger<Personne>(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- linhas 8, 12 e 16: chama-se o método Echanger, especificando em <> o tipo dos parâmetros. Na verdade, o compilador é capaz de deduzir, com base no tipo dos parâmetros efetivos, qual variante do método Echanger deve ser utilizada. Portanto, a seguinte sintaxe é válida:
Generic2.Echanger(ref i1, ref i2);
...
Generic2.Echanger(ref s1, ref s2);
...
Generic2.Echanger(ref p1, ref p2);
Linhas 1, 3 e 5: a variante do método Echanger chamada não é mais especificada. O compilador é capaz de deduzi-la a partir da natureza dos parâmetros efetivos utilizados.
É possível impor restrições aos parâmetros genéricos:

Consideremos o seguinte novo método genérico Echanger:
namespace Chap2 {
class Generic3 {
public static void Echanger<T>(ref T value1, ref T value2) where T : class {
// trocam-se as referências value1 e value2
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- linha 3: exige-se que o tipo T seja uma referência (classe, interface)
Consideremos o seguinte programa de teste:
using System;
namespace Chap2 {
class Program4 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic3.Echanger<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic3.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Pessoa
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic3.Echanger(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
O compilador reporta um erro na linha 8, pois o tipo int não é uma classe nem uma interface, mas sim uma estrutura:

Consideremos o seguinte novo método genérico Echanger:
namespace Chap2 {
class Generic4 {
public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
// recupera-se o valor dos dois elementos
int value1 = element1.Value();
int value2 = element2.Value();
// se o primeiro elemento for maior que o segundo, troca-se os elementos
if (value1 > value2) {
T temp = element2;
element2 = element1;
element1 = temp;
}
}
}
}
- linha 3: o tipo T deve implementar a interface Interface1. Essa interface possui um método Value, utilizado nas linhas 5 e 6, que retorna o valor do objeto do tipo T.
- linhas 8-12: as duas referências element1 e element2 só são trocadas se o valor de element1 for maior que o valor de element2.
A interface Interface1 é a seguinte:
namespace Chap2 {
interface Interface1 {
int Value();
}
}
Ela é implementada pela seguinte classe Class1:
using System;
using System.Threading;
namespace Chap2 {
class Class1 : Interface1 {
// valor do objeto
private int value;
// construtor
public Class1() {
// espera 1 ms
Thread.Sleep(1);
// valor aleatório entre 0 e 99
value = new Random(DateTime.Now.Millisecond).Next(100);
}
// acessador do campo privado value
public int Value() {
return value;
}
// estado da instância
public override string ToString() {
return value.ToString();
}
}
}
- linha 5: Class1 implementa a interface Interface1
- linha 7: o valor de uma instância de Class1
- linhas 10-14: o campo value é inicializado com um valor aleatório entre 0 e 99
- linhas 18-20: o método Value da interface Interface1
- linhas 23-25: o método ToString da classe
A interface Interface1 também é implementada pela classe Class2:
using System;
namespace Chap2 {
class Class2 : Interface1 {
// valores do objeto
private int value;
private String s;
// construtor
public Class2(String s) {
this.s = s;
value = s.Length;
}
// acessador de valor de campo privado
public int Value() {
return value;
}
// estado da instância
public override string ToString() {
return s;
}
}
}
- linha 4: Class2 implementa a interface Interface1
- linha 6: o valor de uma instância de Class2
- linhas 10-13: o campo value é inicializado com o comprimento da sequência de caracteres passada ao construtor
- linhas 16-18: o método Value da interface Interface1
- linhas 21-22: o método ToString da classe
Um programa de teste poderia ser o seguinte:
using System;
namespace Chap2 {
class Program5 {
static void Main(string[] args) {
// trocar instâncias do tipo Class1
Class1 c1, c2;
for (int i = 0; i < 5; i++) {
c1 = new Class1();
c2 = new Class1();
Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);
Generic4.Echanger(ref c1, ref c2);
Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
}
// trocar instâncias do tipo Class2
Class2 c3, c4;
c3 = new Class2("xxxxxxxxxxxxxx");
c4 = new Class2("xx");
Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
Generic4.Echanger(ref c3, ref c4);
Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
}
}
}
- linhas 8-14: instâncias do tipo Class1 são trocadas
- linhas 16-22: instâncias do tipo Class2 são trocadas
Os resultados da execução são os seguintes:
Para ilustrar o conceito de interface genérica, vamos ordenar uma matriz de pessoas primeiro por seus nomes e, em seguida, por suas idades. O método que nos permite ordenar uma matriz é o método estático Sort da classe Array:

Vale lembrar que um método estático é utilizado prefixando-o com o nome da classe, e não com o nome de uma instância da classe. O método Sort possui diferentes assinaturas (ele é sobrecarregado). Utilizaremos a seguinte assinatura:
Sort é um método genérico, em que T representa qualquer tipo. O método recebe dois parâmetros:
- T[] array: o array de elementos do tipo T a ser classificado
- IComparer<T> comparador: uma referência a um objeto que implementa a interface IComparer<T>.
IComparer<T> é uma interface genérica definida da seguinte forma:
A interface IComparer<T> possui apenas um único método. O método Compare:
- recebe como parâmetros dois elementos t1 e t2 do tipo T
- retorna 1 se t1 > t2, 0 se t1 == t2, -1 se t1 < t2. Cabe ao desenvolvedor definir o significado dos operadores <, ==, >. Por exemplo, se p1 e p2 são dois objetos Personne, poderemos dizer que p1 > p2 se o nome de p1 vier antes do nome de p2 na ordem alfabética. Teremos, então, uma ordenação crescente de acordo com o nome das pessoas. Se quisermos uma ordenação por idade, diremos que p1 > p2 se a idade de p1 for maior que a idade de p2.
- Para obter uma ordenação decrescente, basta inverter os resultados +1 e -1
Já sabemos o suficiente para ordenar uma matriz de pessoas. O programa é o seguinte:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program6 {
static void Main(string[] args) {
// uma matriz de pessoas
Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
// exibição
Affiche("Tableau à trier", personnes1);
// classificação por nome
Array.Sort(personnes1, new CompareNoms());
// exibição
Affiche("Tableau après le tri selon les nom et prénom", personnes1);
// classificação por idade
Array.Sort(personnes1, new CompareAges());
// exibição
Affiche("Tableau après le tri selon l'âge", personnes1);
}
static void Affiche(string texte, Personne[] personnes) {
Console.WriteLine(texte.PadRight(50, '-'));
foreach (Personne p in personnes) {
Console.WriteLine(p);
}
}
}
// classe de comparação dos nomes e sobrenomes das pessoas
class CompareNoms : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// comparação de nomes
int i = p1.Nom.CompareTo(p2.Nom);
if (i != 0)
return i;
// igualdade de nomes — compara-se os nomes próprios
return p1.Prenom.CompareTo(p2.Prenom);
}
}
// classe de comparação das idades das pessoas
class CompareAges : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// compara-se as idades
if (p1.Age > p2.Age)
return 1;
else if (p1.Age == p2.Age)
return 0;
else
return -1;
}
}
}
- linha 8: a matriz de pessoas
- linha 12: a ordenação da matriz de pessoas por nome e sobrenome. O segundo parâmetro do método genérico Sort é uma instância de uma classe CompareNoms que implementa a interface genérica IComparer<Pessoa>.
- linhas 30-39: a classe CompareNoms que implementa a interface genérica IComparer<Pessoa>.
- linhas 31-38: implementação do método genérico int CompareTo(T,T) da interface IComparer<T>. O método utiliza o método String.CompareTo, apresentado no parágrafo 3.3.5.4, para comparar duas cadeias de caracteres.
- linha 16: classificação da tabela de pessoas por idade. O segundo parâmetro do método genérico Sort é uma instância da classe CompareAges, que implementa a interface genérica IComparer<Pessoa> e está definida nas linhas 42 a 51.
Os resultados da execução são os seguintes:
4.9. Espaços de nomes
Para escrever uma linha na tela, usamos a instrução
Se analisarmos a definição da classe Console
Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)
descobrimos que ela faz parte do espaço de nomes System. Isso significa que a classe Console deveria ser designada por System.Console e, na verdade, deveríamos escrever:
Isso é evitado usando uma cláusula using:
Diz-se que se importa o namespace System com a cláusula using. Quando o compilador encontrar o nome de uma classe (neste caso, Console), ele procurará por ela nos diferentes espaços de nomes importados pelas cláusulas using. Nesse caso, ele encontrará a classe Console no espaço de nomes System. Observemos agora a segunda informação associada à classe Console:
Essa linha indica em qual “assembly” se encontra a definição da classe Console. Ao compilar fora do Visual Studio e precisar fornecer as referências dos diferentes dll que contêm as classes a serem utilizadas, essa informação pode ser útil. Para referenciar os dll necessários para a compilação de uma classe, escreve-se:
onde csc é o compilador C#. Ao criar uma classe, é possível criá-la dentro de um namespace. O objetivo desses espaços de nomes é evitar conflitos de nomes entre classes quando estas são comercializadas, por exemplo. Consideremos duas empresas, E1 e E2, que distribuem classes empacotadas, respectivamente, nos dll, e1.dll e e2.dll. Suponhamos que um cliente C compre esses dois conjuntos de classes, nos quais ambas as empresas definiram uma classe Personne. O cliente C compila um programa da seguinte maneira:
Se o código-fonte prog.cs utilizar a classe Personne, o compilador não saberá se deve utilizar a classe Personne de e1.dll ou a de e2.dll. Ele sinalizará um erro. Se a empresa E1 se certificar de criar suas classes em um espaço de nomes chamado E1 e a empresa E2 em um espaço de nomes chamado E2, as duas classes Personne passarão a se chamar E1.Personne e E2.Personne. O cliente deverá utilizar em suas classes ou E1.Personne, ou E2.Personne, mas não Personne. O espaço de nomes permite eliminar a ambiguidade.
Para criar uma classe em um espaço de nomes, escreve-se:
4.10. Exemplo de aplicação — V2
Retomamos o cálculo do imposto já estudado no capítulo anterior, parágrafo 3.6, e agora o tratamos utilizando classes e interfaces. Recorde-se o problema:
Propõe-se escrever um programa que permita calcular o imposto de um contribuinte. Consideramos o caso simplificado de um contribuinte que tenha apenas seu salário a declarar (dados de 2004 referentes à renda de 2003):
- calcula-se o número de cotas do funcionário nbParts = nbEnfants/2 + 1 se ele for solteiro, nbEnfants/2 + 2 se ele for casado, onde nbEnfants é o número de filhos dele.
- se ele tiver pelo menos três filhos, recebe mais meia parcela
- calcula-se sua renda tributável R = 0,72 * S, onde S é seu salário anual
- calcula-se seu coeficiente familiar QF = R / nbParts
- calcula-se seu imposto I. Consideremos a tabela a seguir:
4262 | 0 | 0 |
8382 | 0,0683 | 291,09 |
14.753 | 0,1914 | 1.322,92 |
23.888 | 0,2826 | 2.668,39 |
38.868 | 0,3738 | 4.846,98 |
47.932 | 0,4262 | 6.883,66 |
0 | 0,4809 | 9505,54 |
Cada linha possui 3 campos. Para calcular o imposto I, procura-se a primeira linha em que QF ≤ campo1. Por exemplo, se QF = 5000, será encontrada a linha
O imposto I é, então, igual a 0,0683*R - 291,09*nbParts. Se QF for tal que a relação QF <= campo1 nunca for verificada, então serão utilizados os coeficientes da última linha. Aqui:
o que resulta no imposto I = 0,4809 * R - 9.505,54 * nbParts.
Primeiramente, definimos uma estrutura capaz de encapsular uma linha da tabela anterior:
namespace Chap2 {
// uma faixa de imposto
struct TrancheImpot {
public decimal Limite { get; set; }
public decimal CoeffR { get; set; }
public decimal CoeffN { get; set; }
}
}
Em seguida, definimos uma interface IImpot capaz de calcular o imposto:
namespace Chap2 {
interface IImpot {
int calculer(bool marié, int nbEnfants, int salaire);
}
}
- linha 3: o método de cálculo do imposto a partir de três dados: o estado civil do contribuinte, o número de filhos e o salário
Em seguida, definimos uma classe abstrata que implementa essa interface:
namespace Chap2 {
abstract class AbstractImpot : IImpot {
// as faixas de imposto necessárias para o cálculo do imposto
// provêm de uma fonte externa
protected TrancheImpot[] tranchesImpot;
// cálculo do imposto
public int calculer(bool marié, int nbEnfants, int salaire) {
// cálculo do número de cotas
decimal nbParts;
if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
else nbParts = (decimal)nbEnfants / 2 + 1;
if (nbEnfants >= 3) nbParts += 0.5M;
// cálculo do rendimento tributável e do quociente familiar
decimal revenu = 0.72M * salaire;
decimal QF = revenu / nbParts;
// cálculo do imposto
tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
int i = 0;
while (QF > tranchesImpot[i].Limite) i++;
// retorno do resultado
return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
}//calcular
}//classe
}
- linha 2: a classe AbstractImpot implementa a interface IImpot.
- linha 7: os dados anuais do cálculo do imposto na forma de um campo protegido. A classe AbstractImpot não sabe como esse campo será inicializado. Ela deixa essa tarefa a cargo das classes derivadas. É por isso que ela é declarada abstrata (linha 2), a fim de impedir qualquer instanciação.
- linhas 10-25: a implementação do método calculer da interface IImpot. As classes derivadas não precisarão reescrever esse método. A classe AbstractImpot serve, assim, como classe de fatoração das classes derivadas. Nela é colocado o que é comum a todas as classes derivadas.
Uma classe que implemente a interface IImpot pode ser criada derivando-se da classe AbstractImpot. É isso que faremos agora:
using System;
namespace Chap2 {
class HardwiredImpot : AbstractImpot {
// tabelas de dados necessárias para o cálculo do imposto
decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
public HardwiredImpot() {
// criação da tabela de faixas de imposto
tranchesImpot = new TrancheImpot[limites.Length];
// preenchimento
for (int i = 0; i < tranchesImpot.Length; i++) {
tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
}
}
}// classe
}// espaço de nomes
A classe HardwiredImpot define, nas linhas 7 a 9, os dados necessários para o cálculo do imposto. Seu construtor (linhas 11 a 18) utiliza esses dados para inicializar o campo protegido tranchesImpot da classe pai AbstractImpot.
Um programa de teste poderia ser o seguinte:
using System;
namespace Chap2 {
class Program {
static void Main() {
// programa interativo de cálculo de imposto
// o usuário digita três dados no teclado: casado nbEnfants salário
// o programa exibe então o imposto a pagar
const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
+ "Marié : o pour marié, n pour non marié\n"
+ "NbEnfants : nombre d'enfants\n"
+ "Salaire : salaire annuel en F";
// criação de um objeto IImpot
IImpot impot = new HardwiredImpot();
// loop infinito
while (true) {
// solicita-se os parâmetros para o cálculo do imposto
Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
string paramètres = Console.ReadLine().Trim();
// O que fazer?
if (paramètres == null || paramètres == "") break;
// verificação do número de argumentos na linha inserida
string[] args = paramètres.Split(null);
int nbParamètres = args.Length;
if (nbParamètres != 3) {
Console.WriteLine(syntaxe);
continue;
}//if
// verificação da validade dos parâmetros
// casado
string marié = args[0].ToLower();
if (marié != "o" && marié != "n") {
Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
continue;
}//if
// nbEnfants
int nbEnfants = 0;
bool dataOk = false;
try {
nbEnfants = int.Parse(args[1]);
dataOk = nbEnfants >= 0;
} catch {
}//if
// dado correto?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
continue;
}
// salário
int salaire = 0;
dataOk = false;
try {
salaire = int.Parse(args[2]);
dataOk = salaire >= 0;
} catch {
}//try-catch
// dados corretos?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
continue;
}
// os parâmetros estão corretos — calculamos o imposto
Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
// próximo contribuinte
}//enquanto
}
}
}
O programa acima permite que o usuário realize simulações repetidas de cálculo de imposto.
- linha 16: criação de um objeto impot que implementa a interface IImpot. Esse objeto é obtido por instanciação de um tipo HardwiredImpot, um tipo que implementa a interface IImpot. Observe-se que não foi atribuído à variável impot o tipo HardwiredImpot, mas sim o tipo IImpot. Ao escrever isso, indicamos que estamos interessados apenas no método calculer do objeto impot e não no restante.
- linhas 19-68: o ciclo de simulações para o cálculo do imposto
- linha 22: os três parâmetros necessários para o método calculer são solicitados em uma única linha digitada no teclado.
- linha 26: o método [chaine].Split(null) permite decompor [chaine] em palavras. Estas são armazenadas em um array args.
- linha 66: chamada do método calculer do objeto impot, que implementa a interface IImpot.
Aqui está um exemplo de execução do programa:









