4. Classes, Estruturas, Interfaces
4.1. O objeto por exemplo
4.1.1. Geral
Passamos agora à programação orientada a objetos por meio de um exemplo. Um objeto é uma entidade que contém dados que definem o seu estado (chamados campos, atributos, ...) e funções (chamadas métodos). Um objeto é criado de acordo com um modelo chamado classe:
public class C1{
Type1 p1 ; // field p1
Type2 p2 ; // p2 field
…
Type3 m3(… ) { // m3 method
…
}
Type4 m4(… ) { // m4 method
…
}
…
}
A partir da classe C1 acima, pode criar vários objetos O1, O2,.. Todos terão campos p1, p2,.. e métodos m3, m4, .. Mas terão valores diferentes para os seus campos pi, cada um com o seu próprio estado. 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 Pessoa.
4.1.2. Criar um projeto C#
Nos exemplos anteriores, tínhamos apenas um único ficheiro: Program.cs. A partir de agora, poderemos ter vários ficheiros de código num único projeto. Vamos mostrar-lhe como.
![]() |
Em [1], crie um novo projeto. Em [2], escolha uma Consola de Aplicação. Em [3], mantenha o valor predefinido. Em [4], confirme. Em [5], o projeto 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 guardar o projeto criado:
![]() |
Em [1], a opção a ser guardada. Em [2], selecione a pasta na qual guardar o projeto. Em [3], dê um nome ao projeto. Em [5], indique que pretende criar uma solução. Uma solução é um conjunto de projetos. Em [4], dê um nome à solução. Em [6], confirme o armazenamento.
![]() |
Em [1], o projeto guardado. Em [2], adicione um novo elemento ao projeto.
![]() |
Em [1], indique que pretende adicionar uma classe. Em [2], introduza o nome da classe. Em [3], valide as informações. Em [4], o projeto [01] tem um novo ficheiro de código-fonte, Personne.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Personne {
}
}
Altere o namespace de cada ficheiro fonte para Chap2 e elimine a necessidade de importar 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 Person
A definição da classe Pessoa no ficheiro fonte [Personne.cs] será a seguinte:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// method
public void Initialise(string P, string N, int age) {
this.prenom = P;
this.nom = N;
this.age = age;
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Temos aqui a definição de uma classe, ou seja, um tipo de dados. Quando criamos variáveis deste tipo, chamamos-lhes objetos ou instâncias da classe. Uma classe é, portanto, um molde 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 especiais utilizados para obter ou definir o valor dos atributos de um objeto. Estes campos podem ser acompanhados por uma das três palavras-chave seguintes:
Um campo privado só pode ser acedido pelos métodos internos da classe | |
Um campo público pode ser acedido por qualquer método, definido ou não dentro da | |
Um campo protegido (protected) só pode ser acedido pelos métodos internos da classe ou por um objeto derivado (ver o conceito de herança mais adiante). |
Em geral, os dados da classe são declarados como privados, enquanto os seus métodos e propriedades são declarados como públicos. Isto significa que o utilizador de um objeto (o programador)
- não terá acesso direto aos dados privados do objeto
- poderá invocar os métodos públicos do objeto e, em particular, aqueles que fornecem acesso aos seus dados privados.
A sintaxe para declarar uma classe em 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 Initialize
De volta à nossa classe Person declarada como:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// method
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Qual é o papel do Initializes? Como o nome, o apelido e a idade são dados privados da classe Pessoa, instruções:
são ilegais. Precisamos de inicializar um objeto do tipo Pessoa através de um método público. Esta é a função do Initializes. Escrevemos:
Escrever p1.Initialise é válido porque Initializes é de acesso público.
4.1.5. O operador new
A sequência de instruções
está incorreta. A instrução
define p1 como uma referência a um objeto do tipo Pessoa. Este objeto ainda não existe, pelo que p1 não é inicializado. É como escrever:
onde a palavra-chave null indica que a variável p1 ainda não faz referência a nenhum objeto. Quando escreve
utilizamos o método Initialize do objeto referenciado por p1. Caso esse objeto ainda não exista, o compilador sinalizará o erro. Para garantir que p1 refira um objeto, escreva:
Isto cria um objeto do tipo Pessoa ainda não inicializado: os atributos «nome» e «prénome», que são referências a objetos do tipo String, terão o valor null, e o atributo «idade» terá o valor 0. Portanto, há uma inicialização por predefinição. Agora que p1 se refere a um objeto, a instrução de inicialização para este objeto
é válida.
4.1.6. A palavra-chave this
Vejamos o código para a inicialização:
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 primeiro nome do objeto atual (this) recebe o valor p. A palavra-chave this designa o objeto atual: aquele no qual o método executado está localizado. Como sabemos isso? Vamos dar uma olhada na inicialização do objeto referenciado por p1 no programa de chamada:
Este é o método Initialize do objeto p1. Quando este método faz referência a this, na verdade está a referir-se ao objeto p1. O método Initialize 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 o método de um objeto faz referência a um atributo A desse objeto, escrever this.A é implícito. Deve ser usado explicitamente quando houver conflito de identificadores. É o caso de:
this.age=age;
onde «age» designa um atributo do objeto atual e o parâmetro «age» recebido pelo método. A ambiguidade deve então ser resolvida designando o atributo «age» como «this.age».
4.1.7. Um programa de teste
Eis um pequeno programa de teste. Está escrito no ficheiro 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], poderá ser necessário especificar o ficheiro fonte a ser executado:
![]() |
Nas propriedades do projeto [01], a classe a ser executada é indicada em [1].
Os resultados obtidos após a conclusão são os seguintes:
4.1.8. Outro método Inicializar
Vamos considerar a classe Person e adicionar o seguinte método:
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
Temos agora dois métodos com o nome Initializes: isto é válido desde que admitam parâmetros diferentes. É o caso aqui. 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). Note-se que o Initializes tem acesso direto aos atributos do objeto p, embora estes sejam privados. Isto é sempre verdade: um objeto o1 de uma classe C tem sempre acesso aos atributos de objetos da mesma classe C.
Aqui está um teste da nova classe Person:
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 os seus resultados:
4.1.9. Construtores da classe Pessoa
Um construtor é um método com o nome da classe e chamado quando o objeto é criado. É geralmente utilizado para inicializar o objeto. Pode aceitar argumentos, mas não retorna nenhum resultado. O seu protótipo ou definição não é precedido 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 podem ser feitas da seguinte forma:
ou
Quando uma classe C tem um ou mais construtores, um desses construtores deve ser utilizado para criar um objeto dessa classe. Se uma classe C não tiver nenhum construtor, 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, onde:
Vamos criar dois construtores para a nossa classe Pessoa:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne P) {
Initialise(P);
}
// method
public void Initialise(string p, string n, int age) {
...
}
public void Initialise(Personne p) {
...
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Os nossos dois construtores utilizam simplesmente os Initializes anteriormente estudados. Recorde-se que quando um programador utiliza a notação Initialise(p), por exemplo, o compilador traduz isto em this.Initialise(p). No construtor, o Initializes é chamado para atuar sobre o objeto referenciado por this, ou seja, o objeto atual, aquele que está a ser construído.
Eis 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 a objetos
Utilizamos sempre a mesma Pessoa. O programa de teste passa a ser:
using System;
namespace Chap2 {
class Program2 {
static void Main() {
// p1
Personne p1 = new Personne("Jean", "Dupont", 30);
Console.Write("p1="); p1.Identifie();
// p2 references the same object as p1
Personne p2 = p1;
Console.Write("p2="); p2.Identifie();
// p3 references an object that will be a copy of the object referenced by p1
Personne p3 = new Personne(p1);
Console.Write("p3="); p3.Identifie();
// change the state of the object referenced by p1
p1.Initialise("Micheline", "Benoît", 67);
Console.Write("p1="); p1.Identifie();
// as p2=p1, the object referenced by p2 must have changed state
Console.Write("p2="); p2.Identifie();
// as p3 does not reference the same object as p1, the object referenced by p3 must not have changed
Console.Write("p3="); p3.Identifie();
}
}
}
Os resultados são os seguintes:
Ao declarar a variável p1 por
p1 é uma referência ao objeto Personne("John", "Smith", 30), mas não é o próprio objeto. Em C, diríamos que é um ponteiro, ou seja, o endereço do objeto criado. Se escrever então:
Não é o objeto Person("John", "Smith",30) que é modificado, mas sim a referência p1 que muda de valor. O objeto Person("John", "Smith",30) será "perdido" se não for referenciado por nenhuma outra variável.
Quando escrevemos:
inicializamos 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 modificamos aquele referenciado por p2.
Quando escrevemos:
é criado um novo objeto Pessoa. Este novo objeto será referenciado por p3. Se modificar o objeto «apontado» (ou referenciado) por p1, o mesmo será alterado no objeto referenciado por p3. É isso que os resultados mostram.
4.1.11. Passagem de parâmetros de referência de objeto
No capítulo anterior, vimos como os parâmetros de função são passados quando representam um tipo C# simples representado por uma estrutura .NET. Vamos ver o que acontece quando o parâmetro é um :
using System;
using System.Text;
namespace Chap1 {
class P12 {
public static void Main() {
// example 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 StringBuilder. Um objeto StringBuilder é semelhante a um objeto string. Ao manipular um objeto string, obtém-se um novo objeto string em troca. Assim, na sequência de código:
A linha 1 cria uma cadeia de caracteres e s é o seu endereço. Na linha 2, s.ToUpperCase() cria outro objeto de cadeia de caracteres na memória. Assim, entre as linhas 1 e 2, s alterou o seu valor (apontando agora para o novo objeto). A classe StringBuilder permite transformar uma cadeia de caracteres sem criar um segundo objeto. Este é o exemplo apresentado acima:
- linha 8: 4 referências [sb0, sb1, sb2, sb3] a objetos do tipo StringBuilder
- linha 10: são passados para o 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 com parâmetros formais [sbf0, sbf1, sbf2, sbf3]. As relações entre os parâmetros formais sbfi e a força de trabalho 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 de valor de endereço)
- 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 são os seguintes:
Explicações:
- sb0 e sbf0 são duas referências distintas para o mesmo objeto. Este foi modificado através de sbf0 - linha 3. Esta modificação pode ser visualizada através de sb0 - linha 4.
- sb1 e sbf1 são duas referências distintas para o mesmo objeto. O método sbf1 agora aponta para um novo objeto - linha 3. Isto não altera o valor de sb1, que continua a apontar para o mesmo objeto - linha 4.
- sb2 e sbf2 são a mesma referência ao mesmo objeto. sbf2 é modificado no método e agora aponta para um novo objeto - linha 3. Como sbf2 e sb2 são uma única entidade, o valor de sb2 também foi modificado e sb2 aponta para o mesmo objeto que sbf2 - linhas 3 e 4.
- Antes de chamar o método, sb3 era inútil. Após o método, sb3 recebe o valor de sbf3. Temos, portanto, duas referências ao mesmo objeto - linhas 3 e 4
4.1.12. Objetos temporários
Numa expressão, podemos invocar explicitamente o construtor de um objeto: este é criado, mas não temos acesso a ele (para o modificar, por exemplo). Este objeto temporário é criado com o objetivo de avaliar a expressão e, em seguida, é descartado. O espaço de memória que ocupa é automaticamente recuperado mais tarde por um programa chamado «garbage collector», cuja função é recuperar o espaço de memória ocupado por objetos que já não são referenciados pelos dados do programa.
Considere o seguinte novo programa de teste:
using System;
namespace Chap2 {
class Program {
static void Main() {
new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
}
}
}
e modifique os construtores da classe Pessoa para exibir uma mensagem:
// manufacturers
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 para ler e escrever atributos privados
Adicionamos à classe Pessoa métodos para ler ou modificar o estado dos atributos do objeto:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// manufacturers
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);
}
// method
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;
}
// accessors
public String GetPrenom() {
return prenom;
}
public String GetNom() {
return nom;
}
public int GetAge() {
return age;
}
//modifiers
public void SetPrenom(String P) {
this.prenom = P;
}
public void SetNom(String N) {
this.nom = N;
}
public void SetAge(int age) {
this.age = age;
}
// method
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 resultados:
4.1.14. As propriedades
Outra forma de aceder aos atributos de uma classe é criar propriedades. Estas permitem-nos manipular atributos privados como se fossem públicos.
Considere a classe P, na qual os acessores e modificadores anteriores foram substituídos por propriedades de leitura e escrita:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne p) {
Initialise(p);
}
// method
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;
}
// properties
public string Prenom {
get { return prenom; }
set {
// valid first name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//first name
public string Nom {
get { return nom; }
set {
// valid name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//name
public int Age {
get { return age; }
set {
// valid age?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//if
}//age
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Uma propriedade permite-lhe ler (obter) ou definir (estabelecer) o valor de um atributo. Uma propriedade é declarada da seguinte forma:
onde Type deve ser o tipo do atributo gerido pela propriedade. Pode ter dois métodos chamados get e set. O método get é normalmente responsável por apresentar o valor do atributo que gere (pode apresentar outra coisa, nada o impede). O método set recebe um parâmetro chamado value, que normalmente atribui ao atributo que gere. Pode aproveitar isto para verificar a validade do valor recebido e, se necessário, lançar uma exceção caso o valor se revele inválido. É isto que o método ici faz.
Como é que estes métodos get e set são chamados? Considere 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
}
}
}
No
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
Estamos à procura dos valores das propriedades Prenown, Nom e Age da pessoa p. Trata-se do método get destas propriedades, que é então chamado e devolve o valor do atributo que gere.
No
queremos definir o valor da propriedade Age. Este é o conjunto que é então chamado. Receberá 56 no valor do seu parâmetro.
Uma propriedade P de uma classe C que apenas defina o get é considerada de leitura única. 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 dá os seguintes resultados:
As propriedades permitem-nos manipular atributos privados como se fossem públicos. Outra característica das propriedades é que podem ser utilizadas em conjunto com um construtor, utilizando a seguinte sintaxe:
Esta sintaxe é equivalente ao seguinte código:
A ordem das propriedades não importa. Aqui está um exemplo.
A classe Pessoa adiciona um novo construtor sem parâmetros:
public Personne() {
}
O construtor não inicializa os membros do objeto. Este é conhecido como o construtor padrão. É utilizado quando a classe não define um construtor.
O código a seguir cria e inicializa (linha 6) uma nova Pessoa com a sintaxe apresentada acima:
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 Person(). Neste caso específico, também poderíamos ter escrito
Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };
mas os parênteses do construtor Pessoa() sem parâmetros não são obrigatórios nesta sintaxe.
Os resultados são os seguintes:
Em muitos casos, as propriedades get e set limitam-se a ler e a escrever num campo privado, sem qualquer processamento adicional. Neste cenário, podemos utilizar uma declaração automática da seguinte forma:
O campo privado associado à propriedade não é declarado. É gerado automaticamente pelo compilador. Só pode ser acedido através da sua propriedade. Assim, em vez de escrever:
private string prenom;
...
// propriété associée
public string Prenom {
get { return prenom; }
set {
// prénom valide ?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//prenom
podemos escrever:
sem declarar o campo privado first name. A diferença entre as duas propriedades anteriores é que a primeira verifica a validade do nome próprio no set, enquanto a segunda não o faz.
Utilize a propriedade automática «First name» para declarar o campo «First name» como público:
Perguntamo-nos se existe alguma diferença entre as duas declarações. Não é recomendável declarar um campo de classe como público. Isto contraria o conceito de encapsulamento do estado de um objeto, que deve ser privado e exposto por métodos públicos.
Se a propriedade automática for declarada como virtual, pode então ser redefinida numa classe derivada:
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 no conjunto o código que verifica a validade do valor atribuído à propriedade automática base.Prop da classe pai Class1.
4.1.15. Métodos e atributos de classe
Suponhamos que queremos contar o número de objetos Person criados numa aplicação. Pode gerir um contador por conta própria, mas corre o risco de esquecer os objetos temporários que são criados aqui ou ali. Pareceria mais seguro incluir nos construtores da classe Person uma instrução que incremente um contador. O problema é passar uma referência a este contador para que o construtor o possa incrementar: é necessário passar um novo parâmetro. Em alternativa, pode incluir o contador na definição da classe. Uma vez que se trata de um atributo da própria classe e não de uma instância específica dessa classe, declaramo-lo de forma diferente com a palavra-chave static :
private static long nbPersonnes;
Para referenciá-lo, escrevemos Personne.nbPersonnes para indicar que é um atributo da própria classe Person. Aqui, criámos um atributo privado que não será acedido diretamente fora da classe. Criamos, portanto, uma propriedade pública para dar acesso ao atributo de classe nbPersonnes. Para devolver o valor de nbPersonnes, o método get desta propriedade não necessita de um objeto Person específico: na verdade, nbPersonnes é o atributo de toda a classe. Por isso, precisamos de uma propriedade declarada também como static:
public static long NbPersonnes {
get { return nbPersonnes; }
}
que, externamente, será chamado com a sintaxe Personne.NbPersonnes. Aqui está um exemplo.
A classe P fica da seguinte forma:
using System;
namespace Chap2 {
public class Personne {
// class attributes
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// instance attributes
private string prenom;
private string nom;
private int age;
// manufacturers
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 na 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);
}
}
}
obtemos os seguintes resultados:
4.1.16. Uma tabela de pessoas
Um objeto é um dado como qualquer outro e, como tal, vários objetos podem ser reunidos numa tabela:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// a table of people
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);
// display
foreach (Personne ami in amis) {
ami.Identifie();
}
}
}
}
- linha 7: cria uma matriz de 3 elementos do tipo Pessoa. Estes 3 elementos são inicializados aqui com o valor nulo, ou seja, não referenciam quaisquer objetos. Mais uma vez, trata-se de um uso incorreto do termo «matriz de objetos», quando na verdade é apenas uma matriz de referências a objetos. A criação da matriz de objetos, que é ela própria um objeto (presença de new), não cria quaisquer objetos do mesmo tipo que os seus elementos.
- linhas 8-10: criação de 3 objetos do tipo Pessoa
- linhas 12-14: exibição do conteúdo da tabela friends
Obtemos os seguintes resultados:
4.2. Herança pelo exemplo
4.2.1. Geral
Introduzimos aqui a noção de herança. O objetivo da herança é «personalizar» uma classe existente para se adequar às nossas necessidades. Suponhamos que queremos criar uma classe Enstructor: um professor é uma pessoa especial. Ele tem atributos que nenhuma outra pessoa teria: a disciplina que leciona, por exemplo. Mas também tem os atributos de qualquer outra pessoa: nome próprio , apelido e idade. Um professor faz, portanto, parte integrante da classe Pessoa, mas possui atributos adicionais. Em vez de escrever uma classe Enstructor a partir do zero, preferimos aproveitar o que aprendemos na classe Pessoa e adaptá-la ao caráter específico dos professores. É o conceito de herança que torna isso possível.
Para expressar que a classe Professor herda propriedades da classe Pessoa, escrevemos:
Person é chamada de classe pai, e Enstructor de classe derivada (ou filha). Um objeto Enstructor possui todas as qualidades de um objeto Person: tem os mesmos atributos e métodos. Esses atributos e métodos da classe pai não são repetidos na definição da classe filha: indicamos simplesmente os atributos e métodos adicionados pela classe filha:
Partimos do princípio de que a classe Pessoa está definida da seguinte forma:
using System;
namespace Chap2 {
public class Personne {
// class attributes
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// instance attributes
private string prenom;
private string nom;
private int age;
// manufacturers
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)");
}
// properties
public string Prenom {
get { return prenom; }
set {
// valid first name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//first name
public string Nom {
get { return nom; }
set {
// valid name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//name
public int Age {
get { return age; }
set {
// valid age?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//if
}//age
// property
public string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
}
}
}
O método Identifica foi substituído pela Identidade que identifica a pessoa. Criamos uma classe Enstructor que herda da classe Pessoa:
using System;
namespace Chap2 {
class Enseignant : Personne {
// attributes
private int section;
// manufacturer
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// the section is saved using the Section property
Section = section;
// follow-up
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//manufacturer
// property Section
public int Section {
get { return section; }
set { section = value; }
}// Section
}
}
A classe Professor adiciona aos métodos e atributos da classe Pessoa:
- linha 4: a classe Professor deriva da classe Pessoa
- linha 6: um atributo Section, que é o número da secção a que o professor pertence no corpo docente (aproximadamente uma secção por disciplina). Este atributo privado é acessível através da propriedade pública Section, linhas 18-21
- linha 9: um novo construtor para inicializar todos os atributos do professor
4.2.2. Criação de um objeto Professor
Uma turma de raparigas não herda os construtores da sua classe Parent. Deve, portanto, definir os seus próprios construtores. O construtor do Enstructor é o seguinte:
// manufacturer
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// section is memorized
Section = section;
// follow-up
Console.WriteLine("Construction enseignant(string, string, int, int)");
}//manufacturer
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 — nome próprio, apelido, idade e secção — e mais três (nome próprio, apelido, idade) da sua classe base, neste caso a classe Pessoa. Sabemos que esta classe possui um construtor Pessoa(string, string, int), que irá criar uma pessoa com os parâmetros passados (nome próprio, apelido, idade). Assim que a construção da classe base estiver concluída, a construção da classe Professor prossegue com a execução do corpo do construtor:
// on mémorise la section
Section = section;
Note-se que, à esquerda do sinal =, não é a propriedade section do objeto que é utilizada, mas sim a Section a ela associada. Isto permite que o construtor tire partido de quaisquer verificações de validade que possam ser realizadas por este método. Isto evita a necessidade de as colocar em dois locais diferentes: o construtor e a propriedade.
Em resumo, o construtor de uma classe derivada:
- passa à sua classe base os parâmetros de que necessita para se construir
- usa os outros parâmetros para inicializar os seus próprios atributos
Talvez tivéssemos preferido escrever:
// constructeur
public Enseignant(string prenom, string nom, int age, int section){
this.prenom=prenom;
this.nom=nom;
this.age=age;
this.section=section;
}
Isso não é possível. A classe Person declarou como privados (private) os seus três campos: first name, name e age. Apenas objetos da mesma classe têm acesso direto a esses campos. Todos os outros objetos, incluindo objetos derivados como ici, devem usar métodos públicos para aceder a eles. Teria sido diferente se a classe Person tivesse declarado os três campos como protegidos (protected): isso permitiria que as classes derivadas tivessem acesso direto aos três campos. No nosso exemplo, utilizar o construtor da classe pai foi, portanto, a solução correta, e este é o método habitual: ao construir um objeto filho, chamamos primeiro o construtor do objeto pai e, em seguida, completamos as inicializações específicas do objeto filho (section no nosso exemplo).
Vamos experimentar 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 simplesmente cria um Enstructor (novo) e identifica-o. A classe Enstructor não tem nenhum método Identite, mas a sua classe pai tem um que também é público: por herança, torna-se um método público da classe Enstructor.
O projeto completo é o seguinte:
![]() |
Os resultados são os seguintes:
Podemos ver que:
- um objeto Pessoa (linha 1) foi construído antes do objeto Professor (linha 2)
- a identidade obtida é a do objeto Pessoa
4.2.3. Redefinir um método ou propriedade
No exemplo anterior, tínhamos a identidade da parte Pessoa, mas faltam algumas informações específicas da classe Enstructor (a secção). Isto leva-nos a escrever uma propriedade que identifica o professor:
using System;
namespace Chap2 {
class Enseignant : Personne {
// attributes
private int section;
// manufacturer
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// the section is saved using the Section property
Section = section;
// follow-up
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//manufacturer
// property Section
public int Section {
get { return section; }
set { section = value; }
}// section
// property Identity
public new string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
}
}
Linhas 24-26, a propriedade Identite da classe Enstructor baseia-se na Identite da sua classe pai (baseidentity) (linha 25) para apresentar a sua "Pessoa" e, em seguida, completa com a secção específica do Enstructor. Observe a declaração da propriedade Identity:
public new string Identite{
Seja um objeto professor E. Este objeto contém uma Pessoa:
![]() |
A propriedade Identity está definida tanto na classe Teacher como na sua classe pai, Person. Na classe Teacher, a propriedade Identity deve ser precedida pela palavra-chave new para indicar que uma nova propriedade Identity está a ser redefinida para a classe Teacher.
public new string Identite{
A classe Enstructor tem agora duas propriedades Identite:
- a herdada da classe pai Pessoa
- a sua própria
Se E é um Enstructor, E.Identite designa a propriedade Identite da classe Enstructor. Dizemos que a propriedade Identite da classe Enstructor redefine ou oculta a propriedade Identite da classe pai. Em geral, se O é um objeto e M um método, para executar O.M, o sistema procura o método M na seguinte ordem:
- no O
- na sua classe pai, se tiver uma
- na classe pai da sua classe pai, se existir
- etc.
A herança permite-lhe redefinir métodos/propriedades com o mesmo nome da classe pai na classe filha. Isto permite-lhe adaptar a classe filha às suas próprias necessidades. Combinada com o polimorfismo, que veremos daqui a pouco, a redefinição de métodos/propriedades é a principal vantagem da herança.
Considere o mesmo programa de teste que acima:
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. Polimorfismo
Considere uma linhagem de classes: C0 ← C1 ← C2 ← … ←Cn
onde Ci ← Cj indica que Cj deriva de Ci. Isto significa que Cj possui todas as características de Ci, além de outras. Sejam os objetos Oi do tipo Ci. É válido escrever:
De facto, por herança, Cj possui todas as características da classe Ci, além de outras. Assim, um Oj do tipo Cj contém um objeto do tipo Ci. A operação
significa que Oi é uma referência ao objeto de tipo Ci contido no objeto Oj.
O facto de uma variável Oi da classe Ci poder, na verdade, referenciar não só um objeto da classe Ci, mas também qualquer objeto derivado da classe Ci, denomina-se polimorfismo: a capacidade de uma variável referenciar diferentes tipos de objetos.
Vamos dar um exemplo e considerar a seguinte função independente de classe (estática):
Mais vale escrever
isso
Neste último caso, o método estático Affiche do parâmetro formal p do tipo Person receberá um valor do tipo Enstructor. Como o tipo Teacher deriva de Person, isso é válido.
4.2.5. Redefinição e polimorfismo
Vamos completar o nosso método Affiche:
public static void Affiche(Personne p) {
// displays identity of p
Console.WriteLine(p.Identite);
}//poster
A propriedade p.Identite devolve uma cadeia de caracteres que identifica o objeto Pessoa p. O que acontece no exemplo anterior se o parâmetro passado para o Poster for um objeto do tipo Professor :
Enseignant e = new Enseignant(...);
Affiche(e);
Vejamos o seguinte exemplo:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a teacher
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// a person
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// poster
public static void Affiche(Personne p) {
// displays identity of p
Console.WriteLine(p.Identite);
}//poster
}
}
Os resultados são os seguintes:
A execução mostra que o p.Identite (linha 17) executou a Identidade de uma Pessoa, primeiro (linha 7) a pessoa contida no Professor e, depois (linha 10) a própria Pessoa p. Não se adaptou ao objeto efetivamente passado como parâmetro para Poster. Teríamos preferido ter a identidade completa do Professor e. Isso teria exigido a notação p.Identite, referência à propriedade Identity do objeto efetivamente apontado por p, em vez da propriedade Identity da parte "Pessoa" do objeto efetivamente apontado por p.
Este resultado pode ser obtido declarando Identity como uma propriedade virtual (virtual) na classe base Person :
public virtual string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
}
A palavra-chave virtual transformou Identity numa propriedade virtual. Esta 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 a sua propriedade/método redefinido. Assim, na classe Teacher, a propriedade Identity é 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, temos a identidade completa do professor. Agora vamos redefinir um método em vez de uma propriedade. A classe object (alias C# de System.Object) é a classe «mãe» de todas as classes C#. Portanto, quando escreve:
estamos implicitamente a escrever:
A classe System.Object define um método virtual ToString:
![]() |
O método ToString devolve o nome da classe a que o objeto pertence, conforme ilustrado no exemplo seguinte:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a teacher
Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
// a person
Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
}
}
}
Os resultados são os seguintes:
Note que, embora não tenhamos redefinido o ToString nas classes Pessoa e Professor, podemos ver que o método ToString da classe Object conseguiu exibir o nome real da classe do objeto.
Vamos redefinir o ToString nas classes Pessoa e Professor:
// méthode ToString
public override string ToString() {
return Identite;
}
A definição é a mesma em ambas as classes. Considere o seguinte programa de teste:
using System;
namespace Chap2 {
class Program3 {
public static void Main() {
// a teacher
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// a person
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// poster
public static void Affiche(Personne p) {
// displays identity of p
Console.WriteLine(p);
}//Poster
}
}
Vejamos o método Poster, cujo parâmetro é uma pessoa p. Na linha 15, a classe WriteLine da Console não possui nenhuma variante que admita um parâmetro do tipo Pessoa. Entre os vários WriteLine, existe um que aceita um Object. O compilador utilizará este método, WriteLine(Object o), porque esta assinatura significa que o o pode ser do tipo Object ou de uma classe derivada. Uma vez que Object é a classe pai de todas as classes, qualquer objeto pode ser passado como parâmetro para WriteLine e, portanto, um objeto do tipo Person ou Teacher. O método WriteLine(Object o) escreve o.ToString() no fluxo de saída Out. Sendo o método ToString virtual, se o objeto o (do tipo Object ou derivado) tiver redefinido o ToString, este último será utilizado. É este o caso aqui com Person e Teacher.
Eis o que mostram os resultados de desempenho:
4.3. Redefinir o significado de um operador para uma classe
4.3.1. Introdução
Considere 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, deve ser definido um método estático na C1 com a seguinte assinatura:
Quando o compilador encontra o
Em seguida, traduz-a como C1.operator+(op1,op2). O tipo representado pelo operador é importante. Considere a operação op1+op2+op3. É traduzida pelo compilador como (op1+op2)+op3. Seja res12 o resultado de op1+op2. A operação seguinte é res12+op3. Se o tipo de res12 for C1, também será traduzida como C1.operator+(res12,op3). Isto permite encadear operações.
Os operadores unários com um único operando também podem ser redefinidos. Por exemplo, se op1 for um objeto do tipo C1, a operação op1++ pode ser redefinida por um método estático do C1 :
O que foi dito aqui é válido para a maioria dos operadores, com algumas exceções:
- os operadores == e != devem ser redefinidos em simultâneo
- os operadores &&, ||, [], (), +=, -=, ... não podem ser redefinidos
4.3.2. Um exemplo
Criamos uma ListeDePersonnes derivada da ArrayList. Esta classe implementa uma lista dinâmica e é apresentada no capítulo seguinte. Utilizamos apenas os seguintes elementos desta classe:
- o método L.Add(Object o) para adicionar à L um objeto o. Aqui, o objeto o será um objeto Person.
- a propriedade L.Count, que fornece o número de elementos na lista L
- a notação L[i], que indica o elemento i da lista L
A classe ListeDePersonnes herdará todos os atributos, métodos e propriedades da ArrayList. A sua definição é a seguinte:
using System;
using System.Collections;
using System.Text;
namespace Chap2 {
class ListeDePersonnes : ArrayList{
// redefine + operator, to add a person to the list
public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
// person p is added to the ListeDePersonnes l
l.Add(p);
// we return the ListeDePersonnes l
return l;
}// operator +
// ToString
public override string ToString() {
// render (él1, él2, ..., éln)
// opening parenthesis
StringBuilder listeToString = new StringBuilder("(");
// browse the list of people (this)
for (int i = 0; i < Count - 1; i++) {
listeToString.Append(this[i]).Append(",");
}//for
// last element
if (Count != 0) {
listeToString.Append(this[Count-1]);
}
// closing parenthesis
listeToString.Append(")");
// you must return a 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 Person ou derivado.
- linha 10: a pessoa p é adicionada à lista l. Trata-se do método Add da classe pai ArrayList, que é aqui utilizado.
- linha 12: a referência à lista l é apresentada de forma a que os operadores + possam ser concatenados, como em l + p1 + p2. A operação l+p1+p2 será interpretada (prioridade do operador) como (l+p1)+p2. A operação l+p1 cria a referência l. A operação (l+p1)+p2 torna-se então l+p2, o que adiciona a pessoa p2 à lista de pessoas l.
- linha 16: redefinimos o ToString para apresentar uma lista de pessoas como (pessoa1, pessoa2, ..), em que pessoa1 é, por sua vez, o resultado do ToString da classe Pessoa.
- linha 19: utilizamos um objeto do tipo StringBuilder. Esta classe é mais adequada do que a string sempre que são necessárias várias operações com strings, neste caso, adições. De facto, cada operação numa string cria um novo objeto string, enquanto as mesmas operações num StringBuilder modificam o objeto sem criar um novo. Utilizamos o método Append para concatenar strings.
- linha 21: percorremos os elementos da lista de pessoas. Esta lista é aqui designada por this. Trata-se do objeto atual sobre o qual é chamado o método ToString. A propriedade Count é uma propriedade da classe pai ArrayList.
- linha 22: o elemento n.º i na lista atual this é acessível através da notação this[i]. Mais uma vez, trata-se de uma propriedade da ArrayList. Como envolve a adição de cadeias de caracteres, será utilizado o this[i].ToString(). Como este é um método virtual, será utilizado o ToString do objeto this, do tipo Person ou derivado.
- linha 31: precisamos de devolver um objeto do tipo string (linha 16). A classe StringBuilder possui um método ToString que permite converter um StringBuilder num tipo string.
Note que a ListeDePersonnes não tem construtor. Neste caso, sabemos que o
será utilizado. Este construtor não faz nada além de chamar o construtor sem parâmetros da sua classe pai:
Uma classe de teste pode ter o seguinte aspeto:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// a list of people
ListeDePersonnes l = new ListeDePersonnes();
// add people
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// display
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 l
- linha 9: adicionar 2 pessoas com o operador +
- linha 12: professor adicionado
- linhas 11 e 13: utilização 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 utilizar l[i] para designar a pessoa n.º i na lista l, tanto na leitura (Person p=l[i]) como na escrita (l[i]=new Person(...)).
Para poder escrever l[i], onde l[i] designa um objeto Pessoa, da classe Pessoa, precisamos de definir o seguinte método na classe ListaDePessoas:
public Personne this[int i] {
get { ... }
set { ... }
}
O método chama-se this[int i], um indexador, porque dá sentido à expressão obj[i], que lembra a notação de matriz, embora obj não seja uma matriz, mas um objeto. O método get deste objeto obj é chamado quando se escreve variable=obj[i] e o método set quando se escreve obj[i]=value.
A classe ListeDePersonnes deriva da classe ArrayList, que por sua vez possui um indexador:
Existe um conflito entre esta classe ListeDePersonnes:
public Personne this[int i]
e a classe ArrayList this
public object this[int i]
porque têm o mesmo nome e o mesmo tipo de parâmetro (int). Para indicar que a classe ListeDePersonnes «armazena» o método com o mesmo nome da classe ArrayList, temos de adicionar a palavra-chave new à declaração da classe ListeDePersonnes. Escrevemos, portanto:
public new Personne this[int i]{
get { ... }
set { ... }
}
Vamos completar este método. O método this.get é chamado quando variable = l[i], por exemplo, onde l é do tipo ListeDePersonnes. Devemos então devolver a pessoa n.º i da lista l. Isto é feito com a notação base[i], que cria o objeto n.º i da classe ArrayList subjacente à classe ListeDePersonnes. O objeto devolvido é do tipo Object, sendo necessário um transtyping para a classe Person.
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { ... }
}
O método set é chamado quando l[i]=p, sendo que p é uma Person. O objetivo é atribuir a pessoa p ao elemento i em 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 de 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 Person p=l["name"], ou seja, indexar a lista l não por um número de elemento, mas pelo nome de uma pessoa. Para isso, definimos um novo indexador:
// indexeur via un nom
public int this[string nom] {
get {
// on recherche la personne
for (int i = 0; i < Count; i++) {
if (((Personne)base[i]).Nom == nom)
return i;
}//for
return -1;
}//get
}
A primeira linha
public int this[string nom]
indica que a ListeDePersonnes é indexada por um nome de cadeia de caracteres e que o resultado de l[name] é um inteiro. Este inteiro corresponderá à posição na lista da pessoa com o nome name ou a -1, caso a pessoa não conste na lista. Apenas permite a leitura (get), proibindo a escrita l["name"]=value, o que teria exigido a definição de um conjunto. A palavra-chave new não é necessária na declaração do indexador, uma vez que a classe base ArrayList não define um indexador this[string].
No corpo do get, percorre-se a lista de pessoas à procura do nome passado no parâmetro. Se for encontrado na posição i, i é devolvido; caso contrário, é devolvido -1.
O programa de teste anterior é concluído da seguinte forma:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a list of people
ListeDePersonnes l = new ListeDePersonnes();
// add people
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// display
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
// change item 1
l[1] = new Personne("franck", "gallon",5);
// display element 1
Console.WriteLine("l[1]=" + l[1]);
// display list l
Console.WriteLine("l=" + l);
// people search
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");
}//for
}
}
}
A sua execução produz os seguintes resultados:
4.5. As estruturas
A estrutura em C# é análoga à estrutura da linguagem C e está muito próxima da noção de classe. Uma estrutura é definida da seguinte forma:
Apesar das declarações semelhantes, existem diferenças significativas entre classe e estrutura. Por exemplo, o conceito de herança não existe nas estruturas. Se estivermos a escrever uma classe que não precisa de 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 seguinte para descobrir:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// a sp1 structure
SPersonne sp1;
sp1.Nom = "paul";
sp1.Age = 10;
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
// a sp2 structure
SPersonne sp2 = sp1;
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// sp2 is modified
sp2.Nom = "nicole";
sp2.Age = 30;
// checking sp1 and sp2
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// an op1 object
CPersonne op1=new CPersonne();
op1.Nom = "paul";
op1.Age = 10;
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
// an op2 object
CPersonne op2=op1;
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
// op2 is modified
op2.Nom = "nicole";
op2.Age = 30;
// op1 and op2 verification
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
}
}
// structure SPersonne
struct SPersonne {
public string Nom;
public int Age;
}
// class 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
Se executarmos este programa, obtemos os seguintes resultados:
Onde antes utilizávamos uma Person, agora utilizamos uma SPersonne :
struct SPersonne {
public string Nom;
public int Age;
}
A estrutura não tem aqui nenhum construtor. Poderia ter um, como mostraremos mais tarde. Por predefinição, tem sempre o construtor sem parâmetros, aqui SPersonne().
- linha 7 do código: a declaração
SPersonne sp1;
é equivalente à instrução:
SPersonne sp1=new Spersonne();
É criada uma estrutura (Name,Age) e o valor de sp1 é essa própria estrutura. No caso da classe, a criação do objeto (Name,Age) deve ser feita explicitamente pelo operador new (linha 22):
CPersonne op1=new CPersonne();
A instrução anterior cria um CPersonne (aproximadamente 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 a linha 12:
SPersonne sp2 = sp1;
é criada uma nova estrutura sp2(Name,Age) e inicializada com o valor de sp1, a própria estrutura.
![]() |
A estrutura de sp1 é duplicada em sp2 [1]. Trata-se de uma cópia de um valor. Agora, considere a instrução na linha 27:
CPersonne op2=op1;
No caso das classes, o valor de op1 é copiado para op2, mas como este valor é, na verdade, o endereço do objeto, não é duplicado [2].
No caso da estrutura [1], se alterarmos o valor de sp2, o valor de sp1 também é alterado, conforme mostrado no programa. No caso do objeto [2], se modificarmos o objeto apontado por op2, o apontado por op1 também é modificado, pois são o mesmo. Isto também é demonstrado pelos resultados do programa.
Estas explicações mostram que:
- o valor de uma variável de estrutura é a própria estrutura
- o valor de uma variável de objeto é o endereço do objeto apontado
Uma vez compreendida esta diferença fundamental, a estrutura é muito semelhante à classe, como se pode ver no seguinte novo exemplo:
using System;
namespace Chap2 {
// structure SPersonne
struct SPersonne {
// private attributes
private string nom;
private int age;
// properties
public string Nom {
get { return nom; }
set { nom = value; }
}//name
public int Age {
get { return age; }
set { age = value; }
}//age
// Manufacturer
public SPersonne(string nom, int age) {
this.nom = nom;
this.age = age;
}//manufacturer
// ToString
public override string ToString() {
return "SPersonne(" + Nom + "," + Age + ")";
}//ToString
}//structure
}//namespace
- linhas 8-9: dois campos privados
- linhas 12-20: propriedades públicas associadas
- linhas 23-26: definem um construtor. Note-se que o construtor sem parâmetros SPersonne() está sempre presente e não precisa de ser declarado. A sua declaração é rejeitada pelo compilador. No construtor das linhas 23-26, poderá sentir-se tentado a inicializar os campos privados name e age através das suas propriedades públicas Name e Age. Isto é rejeitado pelo compilador. Os métodos da estrutura não podem ser utilizados durante a construção da estrutura.
- linhas 29-31: redefinição do método ToString.
Um programa de teste pode ter o seguinte aspeto:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// one person p1
SPersonne p1=new SPersonne();
p1.Nom="paul";
p1.Age= 10;
Console.WriteLine("p1={0}",p1);
// one person p2
SPersonne p2 = p1;
Console.WriteLine("p2=" + p2);
// p2 is modified
p2.Nom = "nicole";
p2.Age = 30;
// checking p1 and p2
Console.WriteLine("p1=" + p1);
Console.WriteLine("p2=" + p2);
// one person p3
SPersonne p3 = new SPersonne("amandin", 18);
Console.WriteLine("p3=" + p3);
// one person p4
SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
Console.WriteLine("p4=" + p4);
}
}
}
- linha 7: somos obrigados a utilizar explicitamente o construtor sem parâmetros, porque 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 através das 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: criar uma estrutura utilizando o construtor sem parâmetros SPersonne() com, entre chaves, a inicialização dos campos privados através das suas propriedades públicas.
Os seguintes resultados são obtidos:
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 no final do programa.
4.6. Interfaces
Uma interface é um conjunto de métodos ou propriedades protótipos que formam um contrato. Uma classe que decide implementar uma interface compromete-se a fornecer uma implementação de todos os métodos definidos na interface. O compilador verifica esta implementação.
Por exemplo, eis a definição da interface System.Collections.IEnumerator :
public interface System.Collections.IEnumerator
{ // Prop
e rties Object Curren
t { get; }
// Methods
bool MoveNe
xt(); void Reset(); }
As propriedades e métodos da interface são definidos apenas pelas suas assinaturas. Não são implementados (não têm código). São as classes que implementam a interface que fornecem código aos métodos e propriedades da interface.
- linha 1: a classe C implementa a interface IEnumerator. Note-se que o sinal : utilizado para implementar uma interface é o mesmo que o utilizado para derivar uma classe.
- linhas 3-5: implementação dos métodos e propriedades da interface IEnumerator.
Considere a seguinte interface:
namespace Chap2 {
public interface IStats {
double Moyenne { get; }
double EcartType();
}
}
A interface IStats apresenta:
- uma propriedade de leitura Average: para calcular a média de uma série de valores
- um método EcartType: para calcular o desvio padrão
Note-se que em nenhum lugar é especificado quais as séries de valores envolvidas. Pode ser a média das notas de uma turma, a média das vendas mensais de um determinado produto, a temperatura média num determinado local, etc. Este é o princípio das interfaces: assumimos a existência de métodos no objeto, mas não a existência de dados específicos.
Uma primeira implementação da IStats poderia ser uma classe utilizada para memorizar as notas dos alunos de uma turma numa determinada disciplina. Um aluno seria caracterizado pela estrutura Student a seguir:
public struct Elève {
public string Nom { get; set; }
public string Prénom { get; set; }
}//Student
O aluno seria identificado pelo nome e apelido. As linhas 2-3 mostram as propriedades automáticas para estes dois atributos.
Uma nota seria caracterizada pela estrutura Nota a seguir:
public struct Note {
public Elève Elève { get; set; }
public double Valeur { get; set; }
}//Note
A nota seria identificada pelo aluno avaliado e pela própria nota. As linhas 2-3 mostram as propriedades automáticas para estes dois atributos.
As notas de todos os alunos numa determinada disciplina são reunidas na classe TableauDeNotes a seguir:
using System;
using System.Text;
namespace Chap2 {
public class TableauDeNotes : IStats {
// attributes
public string Matière { get; set; }
public Note[] Notes { get; set; }
public double Moyenne { get; private set; }
private double ecartType;
// manufacturer
public TableauDeNotes(string matière, Note[] notes) {
// saving via public properties
Matière = matière;
Notes = notes;
// calculating the average score
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;
// standard deviation
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;
}//manufacturer
public double EcartType() {
return ecartType;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// concatenate all the notes
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("],");
};
//final note
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(")");
// end
return valeur.ToString();
}//ToString
}//class
}
- linha 6: a classe TableauDeNotes implementa a interface IStats. Por conseguinte, deve implementar os métodos Average e EcartType. Estes são implementados na linha 10 (Average) e nas linhas 35-37 (EcartType)
- linhas 8-10: três propriedades automáticas
- linha 8: o material cujas notas o objeto memoriza
- linha 9: tabela de notas dos alunos (Student, Grade)
- linha 10: pontuação média - propriedade que implementa a interface Average de IStats.
- linha 11: campo que armazena o desvio padrão das notas - o método get associado a EcartType nas linhas 35-37 implementa a interface EcartType de IStats.
- linha 9: as notas são armazenadas numa tabela. Esta é transmitida quando a classe TableauDeNotes é criada para o construtor das linhas 14-33.
- linhas 14-33: o construtor. Presume-se aqui que as notas transmitidas ao construtor não irão alterar-se no futuro. Por isso, utilizamos o construtor para calcular imediatamente a média e o desvio padrão dessas notas e armazená-las nos campos das linhas 10-11. A média é armazenada no campo privado subjacente à propriedade automática Average na linha 10 e o desvio padrão no campo privado da linha 11.
- linha 10: o método get da propriedade automática Average irá renderizar o campo privado subjacente.
- linhas 35-37: o método EcartType devolve o valor do campo privado na linha 11.
Existem algumas subtilezas neste código:
- linha 23: o método set property Average é utilizado para efetuar a atribuição. Este método foi declarado privado na linha 10, pelo que a atribuição de um valor a Average só é possível dentro da sala de aula.
- linhas 40-54: utilize um objeto StringBuilder para construir a string que representa o TableauDeNotes, a fim de melhorar o desempenho. Deve-se notar, no entanto, que a legibilidade do código é consideravelmente prejudicada. Este é o outro lado da moeda.
Na classe anterior, as notas eram armazenadas numa tabela. Não era possível adicionar uma nova nota depois de criado o TableauDeNotes. Propomos agora uma segunda implementação do IStats, chamada ListeDeNotes, onde, desta vez, as notas seriam guardadas numa lista, com a possibilidade de adicionar notas após a construçã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 {
// attributes
public string Matière { get; set; }
public List<Note> Notes { get; set; }
public double moyenne = -1;
public double ecartType = -1;
// manufacturer
public ListeDeNotes(string matière, List<Note> notes) {
// saving via public properties
Matière = matière;
Notes = notes;
}//manufacturer
// add a note
public void Ajouter(Note note) {
// add note
Notes.Add(note);
// mean and standard deviation reset
moyenne = -1;
ecartType = -1;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// concatenate all the notes
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("],");
};
//final note
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(")");
// end
return valeur.ToString();
}//ToString
// average score
public double Moyenne {
get {
if (moyenne != -1) return moyenne;
// calculating the average score
double somme = 0;
for (int i = 0; i < Notes.Count; i++) {
somme += Notes[i].Valeur;
}
// we return the average
if (Notes.Count != 0) moyenne = somme / Notes.Count;
return moyenne;
}
}
public double EcartType() {
// standard deviation
if (ecartType != -1) return ecartType;
// average
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
// we return the standard deviation
if (Notes.Count != 0)
ecartType = Math.Sqrt(carrés / Notes.Count);
return ecartType;
}
}//class
}
- linha 7: a classe ListeDeNotes implementa a interface IStats
- linha 10: as notas são agora apresentadas numa lista em vez de numa tabela
- linha 11: a propriedade automática «Average» da classe TableauDeNotes foi abandonada aqui em favor de um campo privado «average», na linha 11, associado à propriedade pública de leitura apenas «Average» nas linhas 48-60
- linhas 22-28: agora é possível adicionar uma nota às já memorizadas, o que antes era impossível.
- linhas 15-19: consequentemente, a média e o desvio padrão já não são calculados no construtor, mas nos próprios métodos da interface: Average (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 pode ter o seguinte aspeto:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// some students & english notes
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 } };
// which we save in a TableauDeNotes object
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// average and standard deviation display
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
// we put the students and the material in a ListeDeNotes object
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// average and standard deviation display
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
// we add a note
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// average and standard deviation display
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
}
}
}
- linha 8: criar uma matriz de alunos utilizando o construtor sem parâmetros e a inicialização através de propriedades públicas
- linha 9: criação de uma tabela de notas utilizando 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 admite um objeto que implemente a interface IEnumerable<Note>. A tabela notes1 implementa esta interface e pode ser utilizada para construir a List<Note>.
- linha 19: nova nota adicionada
- linha 21: recálculo da média e do desvio padrão
Os resultados são os seguintes:
No exemplo anterior, duas classes implementam a interface IStats. Dito isto, o exemplo não mostra a utilidade da interface IStats. Vamos reescrever o programa de teste da seguinte forma:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// some students & english notes
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 } };
// which we save in a TableauDeNotes object
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// average and standard deviation display
AfficheStats(anglais);
// we put the students and the material in a ListeDeNotes object
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// average and standard deviation display
AfficheStats(français);
// we add a note
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// average and standard deviation display
AfficheStats(français);
}
// display mean and standard deviation of a type 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 um IStats, tipo Interface. Isto significa que o parâmetro efetivo pode ser qualquer objeto que implemente o IStats. Quando se utilizam dados com um tipo de interface, isto significa que se utilizam apenas os métodos da interface implementados pelos dados. O resto é ignorado. Esta é uma propriedade semelhante ao polimorfismo observado nas classes. Se um conjunto de Ci não ligado por herança (pelo que não se pode utilizar o polimorfismo de herança) apresentar um conjunto de métodos com a mesma assinatura, pode ser interessante agrupar esses métodos numa interface I implementada por todas as classes em causa. As instâncias destas classes Ci podem então ser utilizadas como parâmetros efetivos de funções que admitem um parâmetro formal do tipo I, ou seja, funções que utilizam apenas métodos de objeto Ci definidos na I e não os atributos e métodos das classes Ci individuais.
- linha 13: o método AfficheStats é chamado com um TableauDeNotes que implementa a IStats
- linha 17: idem com um tipo ListeDeNotes
Os resultados desta execução são idênticos aos da anterior.
Uma variável pode ser do tipo interface. Assim, podemos escrever:
A instrução na linha 1 indica que stats1 é a instância de uma classe que implementa a interface IStats. Esta instrução implica que o compilador só permitirá o acesso aos métodos da interface stats1: Average e EcartType.
Por fim, é importante notar que as interfaces podem ser implementadas de várias formas, ou seja, podem ser escritas como
onde os Ij são interfaces.
4.7. Classes abstratas
Uma classe abstrata é aquela que não pode ser instanciada. É necessário criar classes derivadas que possam ser instanciadas.
As classes abstratas podem ser usadas para factorizar o código de um conjunto de classes. Considere o seguinte caso:
using System;
namespace Chap2 {
abstract class Utilisateur {
// fields
private string login;
private string motDePasse;
private string role;
// manufacturer
public Utilisateur(string login, string motDePasse) {
// information is recorded
this.login = login;
this.motDePasse = motDePasse;
// on identifie l'utilisateur
role=identifie();
// identified?
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);
}
// identifies
abstract public string identifie();
}
}
- linhas 11-21: a classe construtora User. Esta classe armazena informações sobre o utilizador de uma aplicação web. Esta aplicação tem vários tipos de utilizadores autenticados por um nome de utilizador/palavra-passe (linhas 6-7). Estas duas informações são verificadas com um serviço LDAP para alguns utilizadores, com um SGBD para outros, etc...
- linhas 13-14: as informações de autenticação são armazenadas na memória
- linha 16: são verificadas pelo método identifies. Como o método de identificação não é conhecido, é declarado abstrato na linha 29 com a palavra-chave abstract. O método identifies devolve uma cadeia de caracteres que especifica a função do utilizador (basicamente, o que ele está autorizado a fazer). Se esta cadeia for nula, é lançada uma exceção na linha 19.
- linha 4: como possui um método abstrato, a própria classe é declarada como abstracta com a palavra-chave abstract.
- linha 29: o método abstrato identifies não tem definição. As classes derivadas irão atribuir-lhe uma.
- linhas 24-26: o método ToString identifica uma instância da classe.
Presume-se aqui que o programador deseja controlar a construção de instâncias da classe User e das classes derivadas, talvez porque queira garantir que seja lançada uma exceção de um determinado tipo se o utilizador não for reconhecido (linha 19). As classes derivadas podem basear-se neste construtor. Para tal, devem fornecer o método ToString.
A classe ExceptionUtilisateurInconnu é a seguinte:
using System;
namespace Chap2 {
class ExceptionUtilisateurInconnu : Exception {
public ExceptionUtilisateurInconnu(string message) : base(message){
}
}
}
- linha 3: derivada da classe Exception
- linhas 4-6: possui um único construtor que aceita uma mensagem de erro como parâmetro. Esta é passada para a classe pai (linha 5), que possui o mesmo construtor.
Agora derivamos o User na classe Director das meninas:
namespace Chap2 {
class Administrateur : Utilisateur {
// manufacturer
public Administrateur(string login, string motDePasse)
: base(login, motDePasse) {
}
// identifies
public override string identifie() {
// identification LDAP
// ...
return "admin";
}
}
}
- linhas 4-6: o construtor simplesmente passa os parâmetros que recebe para a sua classe pai
- linhas 9-12: o método identifica a classe Director. Presume-se que um administrador é identificado por um sistema LDAP. Este método redefine os identificadores da sua classe pai. Como redefine um método abstrato, é desnecessário utilizar a palavra-chave override.
Derivamos agora a classe User da classe Observer:
namespace Chap2 {
class Observateur : Utilisateur{
// manufacturer
public Observateur(string login, string motDePasse)
: base(login, motDePasse) {
}
//identifies
public override string identifie() {
// identification SGBD
// ...
return "observateur";
}
}
}
- linhas 4-6: o construtor simplesmente passa os parâmetros que recebe para a sua classe pai
- linhas 9-13: o método identifica a classe Observer. Presume-se que um observador é identificado através da verificação dos seus dados de identificação numa base de dados.
No final, os objetos Director e Observer são instanciados pelo mesmo construtor que a classe pai User. Este construtor utilizará as identificações fornecidas por estas classes.
Uma terceira classe, Unknown, também deriva da classe User:
namespace Chap2 {
class Inconnu : Utilisateur{
// manufacturer
public Inconnu(string login, string motDePasse)
: base(login, motDePasse) {
}
//identifies
public override string identifie() {
// unknown user
// ...
return null;
}
}
}
- linha 13: o método define o ponteiro como nulo para indicar que o utilizador não foi reconhecido.
Um programa de teste poderia ter o seguinte aspecto:
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);
}
}
}
}
Note que as linhas 6, 7 e 9 utilizam o [User].ToString(), que será utilizado pelo WriteLine.
Os resultados são os seguintes:
4.8. Classes, interfaces e métodos genéricos
Suponhamos que queremos escrever um método que troque dois números inteiros. Este método poderia ser o seguinte:
public static void Echanger1(ref int value1, ref int value2){
// on échange les références value1 et value2
int temp = value2;
value2 = value1;
value1 = temp;
}
Agora, se quiséssemos trocar duas referências ao objeto Pessoa, escreveríamos:
public static void Echanger2(ref Personne value1, ref Personne value2){
// on échange les références value1 et value2
Personne temp = value2;
value2 = value1;
value1 = temp;
}
A diferença entre os dois métodos reside no tipo T dos parâmetros: int em Exchange1, Person em Exchange2. As classes genéricas e as interfaces satisfazem a necessidade de métodos que diferem apenas no tipo de alguns dos seus parâmetros.
Com uma classe genérica, o Exchange poderia ser reescrito da seguinte forma:
namespace Chap2 {
class Generic1<T> {
public static void Echanger(ref T value1, ref T value2){
// exchange the value1 and value2 references
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- linha 2: a classe Generic1 é parametrizada por um tipo denotado por T. Pode dar-lhe qualquer nome que desejar. Este tipo T é então reutilizado na classe nas linhas 3 e 5. Dizemos que a Generic1 é uma classe genérica.
- linha 3: define as duas referências de tipo T a serem trocadas
- linha 5: a variável temporária temp tem o tipo T.
Um programa de teste para a 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);
// Person
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». Linha 8: utilize o método estático Exchange do tipo Generic1<int> para indicar que as referências passadas para o Exchange são do tipo int.
- linha 12: o método estático Exchange de tipo Generic1<string> é utilizado para indicar que as referências passadas para o Exchange são do tipo string.
- linha 16: o método estático Exchange do tipo Generic1<Person> é utilizado para indicar que as referências passadas para o Exchange são do tipo Person.
Os resultados são os seguintes:
O método Exchange também poderia ter sido escrito da seguinte forma:
namespace Chap2 {
class Generic2 {
public static void Echanger<T>(ref T value1, ref T value2){
// exchange the value1 and value2 references
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- linha 2: a classe Generic2 já não é genérica
- linha 3: o método estático Exchange é genérico
O programa de teste é então 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);
// Person
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: chamam o Exchange especificando o tipo de parâmetro em <>. Na verdade, o compilador consegue deduzir a variante do Exchange a utilizar. A seguinte entrada é, portanto, 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 Exchange já não é especificada. O compilador consegue deduzi-la a partir da natureza dos parâmetros reais utilizados.
Podem ser impostas restrições aos parâmetros genéricos:

Considere o novo método genérico Exchange a seguir:
namespace Chap2 {
class Generic3 {
public static void Echanger<T>(ref T value1, ref T value2) where T : class {
// exchange the value1 and value2 references
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- linha 3: o tipo T deve ser uma referência (classe, interface)
Considere 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);
// Person
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 declara um erro na linha 8 porque o tipo int não é uma classe nem uma interface, é uma estrutura:

Considere o novo método genérico Exchange a seguir:
namespace Chap2 {
class Generic4 {
public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
// retrieve the value of the 2 elements
int value1 = element1.Value();
int value2 = element2.Value();
// if 1st element > 2nd element, exchange elements
if (value1 > value2) {
T temp = element2;
element2 = element1;
element1 = temp;
}
}
}
}
- linha 3: o tipo T deve implementar a Interface1. Possui um método Value, utilizado nas linhas 5 e 6, que fornece o valor do objeto do tipo T.
- linhas 8-12: as duas referências element1 e element2 são trocadas apenas se o valor de element1 for maior do que o valor de element2.
A interface Interface1 é a seguinte:
namespace Chap2 {
interface Interface1 {
int Value();
}
}
É implementada pela Classe1 a seguir:
using System;
using System.Threading;
namespace Chap2 {
class Class1 : Interface1 {
// object value
private int value;
// manufacturer
public Class1() {
// wait 1 ms
Thread.Sleep(1);
// random value between 0 and 99
value = new Random(DateTime.Now.Millisecond).Next(100);
}
// accessor private field value
public int Value() {
return value;
}
// instance status
public override string ToString() {
return value.ToString();
}
}
}
- linha 5: Class1 implementa a Interface1
- linha 7: o valor de uma instância da Classe1
- linhas 10-14: o valor do campo é 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 Classe2 :
using System;
namespace Chap2 {
class Class2 : Interface1 {
// object values
private int value;
private String s;
// manufacturer
public Class2(String s) {
this.s = s;
value = s.Length;
}
// accessor private field value
public int Value() {
return value;
}
// instance status
public override string ToString() {
return s;
}
}
}
- linha 4: Class2 implementa a Interface1
- linha 6: o valor de uma instância da Class2
- linhas 10-13: o valor do campo é inicializado com o comprimento da string 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 pode ter o seguinte aspeto:
using System;
namespace Chap2 {
class Program5 {
static void Main(string[] args) {
// exchange instances of type 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);
}
// exchange Class2 instances
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 da Classe1 são trocadas
- linhas 16-22: instâncias do tipo Class2 são trocadas
Os resultados são os seguintes:
Para ilustrar o conceito de interfei , vamos ordenar uma matriz de pessoas primeiro pelos seus nomes e, em seguida, pelas suas idades. O método que usamos para ordenar uma matriz é o método estático da classe Array:

Lembre-se de que um método estático é utilizado prefixando o método com o nome da classe e não com o nome de uma instância da classe. O método Spell tem diferentes assinaturas (é sobrecarregado). Vamos utilizar a seguinte assinatura:
Spell é um método genérico em que T denota qualquer tipo. O método recebe dois parâmetros:
- T[] table : a matriz de elementos T a ser ordenada
- 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> tem apenas um método. O método Compare :
- recebe dois elementos como parâmetros, t1 e t2, do tipo T
- retorna 1 se t1>t2, 0 se t1==t2, -1 se t1<t2. Cabe ao programador atribuir significado aos operadores <, ==, >. Por exemplo, se p1 e p2 são dois objetos Person, podemos dizer que p1>p2 se o nome de p1 preceder o nome de p2 por ordem alfabética. Iremos então ordenar por ordem crescente de nome. Se quiser ordenar por idade, considere p1>p2 se a idade de p1 for maior do que a idade de p2.
- Para ordenar por ordem decrescente, basta inverter os resultados +1 e -1
Sabemos o suficiente para ordenar uma tabela de pessoas. O programa é o seguinte:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program6 {
static void Main(string[] args) {
// a table of people
Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
// display
Affiche("Tableau à trier", personnes1);
// sort by name
Array.Sort(personnes1, new CompareNoms());
// display
Affiche("Tableau après le tri selon les nom et prénom", personnes1);
// sorted by age
Array.Sort(personnes1, new CompareAges());
// display
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);
}
}
}
// first and last name comparison class
class CompareNoms : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// compare names
int i = p1.Nom.CompareTo(p2.Nom);
if (i != 0)
return i;
// equal names - first names are compared
return p1.Prenom.CompareTo(p2.Prenom);
}
}
// age comparison class
class CompareAges : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// comparing ages
if (p1.Age > p2.Age)
return 1;
else if (p1.Age == p2.Age)
return 0;
else
return -1;
}
}
}
- linha 8: a tabela de pessoas
- linha 12: ordena a tabela de pessoas por nome e apelido. O segundo parâmetro do método genérico Spell é uma instância de CompareNoms que implementa a interface genérica IComparer<Person>.
- linhas 30-39: a classe CompareNoms que implementa a interface genérica IComparer<Person>.
- linhas 31-38: implementação do método genérico int CompareTo(T,T) da interface IComparer<T>. O método utiliza o String.CompareTo, apresentado na secção 3.3.5.4, para comparar duas cadeias de caracteres.
- linha 16: ordenar a tabela de pessoas por idade. O segundo parâmetro do método genérico Spell é uma instância de CompareAges que implementa a interface genérica IComparer<Person> e está definido nas linhas 42-51.
Os resultados são os seguintes:
4.9. Espaços de nomes
Para escrever uma linha no ecrã, usamos a instrução
Se olharmos para a definição do Console
Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)
descobrimos que faz parte do System. Isto significa que a Console deve ser designada por System.Console e que, na verdade, devemos escrever:
Isto é evitado utilizando um using:
Dizemos que importamos o namespace System com a cláusula using. Quando o compilador encontra o nome de uma classe (aqui Console), tenta localizá-la nos vários namespaces importados pela cláusula using. Aqui, encontrará a classe Console no namespace System. Agora, reparemos na segunda informação associada à classe Console:
Esta linha indica em que «assembly» se encontra a definição da classe Console. Ao compilar fora do Visual Studio e quando é necessário fornecer referências às várias DLL que contêm as classes a utilizar, esta informação pode ser útil. Para referenciar as DLL necessárias para compilar uma classe, escrevemos:
onde csc é o compilador C#. Quando criamos uma classe, podemos fazê-lo dentro de um namespace. O objetivo destes namespaces é evitar conflitos de nomes entre classes quando estas são comercializadas, por exemplo. Consideremos duas empresas, E1 e E2, que distribuem classes empacotadas, respetivamente, nas DLL e1.dll e e2.dll. Suponhamos que um cliente C adquira estes dois conjuntos de classes, nos quais ambas as empresas definiram uma classe Person. O cliente C compila um programa da seguinte forma:
Se o código-fonte prog.cs utilizar a classe Person, o compilador não saberá se deve utilizar a Person da e1.dll ou a da e2.dll. Irá sinalizar um erro. Se a empresa E1 tiver o cuidado de criar as suas classes num namespace chamado E1 e a empresa E2 num namespace chamado E2, as duas classes Person passarão a chamar-se E1.Person e E2.Personne. O cliente deve utilizar E1.Personne ou E2.Personne, mas não Person. O namespace elimina qualquer ambiguidade.
Para criar uma classe num namespace, escreva:
4.10. Aplicação de exemplo - V2
Repetimos o cálculo de impostos já estudado no capítulo anterior, parágrafo 3.6, e agora abordamo-lo utilizando classes e interfaces. Recordemos o problema:
Propomos escrever um programa para calcular o imposto sobre o rendimento de um contribuinte. O caso simplificado é o de um contribuinte que tem apenas o seu salário para declarar (valores de 2004 relativos ao rendimento de 2003):
- o número de quotas de empregado é calculado como nbParts = nbEnfants/2 + 1 se solteiro, nbEnfants/2 + 2 se casado, onde nbEnfants é o número de filhos.
- se tiver pelo menos três filhos, recebe mais meia quota
- calcule o seu rendimento tributável R=0,72*S, em que S é o seu salário anual
- calcule o seu coeficiente familiar QF=R/nbParts
- Calcule o seu imposto I. Considere a seguinte tabela:
4262 | 0 | 0 |
8382 | 0,0683 | 291,09 |
14753 | 0,1914 | 1322,92 |
23888 | 0,2826 | 2668,39 |
38868 | 0,3738 | 4846,98 |
47932 | 0,4262 | 6883,66 |
0 | 0,4809 | 9505,54 |
Cada linha tem 3 campos. Para calcular o imposto I, procure a primeira linha em que QF <= champ1. Por exemplo, se QF = 5000, encontramos a linha
O imposto I é, então, igual a 0,0683*R - 291,09*nbParts. Se QF for tal que a relação QF<=champ1 nunca seja verificada, então são utilizados os coeficientes da última linha. Aqui:
o que dá o imposto I = 0,4809*R - 9505,54*nbParts.
Primeiro, definimos uma estrutura capaz de encapsular uma linha da matriz anterior:
namespace Chap2 {
// a tax bracket
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: método de cálculo do imposto com base em três dados: se o contribuinte é casado ou não, número de filhos, salário
Em seguida, definimos uma classe abstrata que implementa esta interface:
namespace Chap2 {
abstract class AbstractImpot : IImpot {
// tax brackets required to calculate tax
// come from an external source
protected TrancheImpot[] tranchesImpot;
// tAX CALCULATION
public int calculer(bool marié, int nbEnfants, int salaire) {
// calculating the number of shares
decimal nbParts;
if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
else nbParts = (decimal)nbEnfants / 2 + 1;
if (nbEnfants >= 3) nbParts += 0.5M;
// calculation of taxable income & family quota
decimal revenu = 0.72M * salaire;
decimal QF = revenu / nbParts;
// tAX CALCULATION
tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
int i = 0;
while (QF > tranchesImpot[i].Limite) i++;
// return result
return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
}//calculate
}//class
}
- linha 2: a classe AbstractImpot implementa a interface IImpot.
- linha 7: dados de cálculo do imposto anual na forma de um campo protegido. A classe AbstractImpot não sabe como este campo será inicializado. Deixa isso a cargo das classes derivadas. É por isso que é declarada como abstract (linha 2), para impedir qualquer instanciação.
- linhas 10-25: implementação da interface de cálculo IImpot. As classes derivadas não terão de reescrever este método. A AbstractImpot serve como uma classe de fatorização para as classes derivadas. É aqui que colocamos o que é comum a todas as classes derivadas.
Uma classe que implemente a interface IImpot pode ser construída derivando-se da AbstractImpot. É isso que estamos a fazer agora:
using System;
namespace Chap2 {
class HardwiredImpot : AbstractImpot {
// data tables for tax calculations
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() {
// creation of tax bracket table
tranchesImpot = new TrancheImpot[limites.Length];
// filling
for (int i = 0; i < tranchesImpot.Length; i++) {
tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
}
}
}// class
}// namespace
A classe HardwiredImpot define, nas linhas 7-9, os dados concretos necessários para o cálculo do imposto. O seu construtor (linhas 11-18) utiliza estes 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() {
// interactive Tax calculation program
// l'user types three data into keyboard: married nbEnfants salary
// the program then displays Tax payable
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";
// creation of a IImpot object
IImpot impot = new HardwiredImpot();
// infinite loop
while (true) {
// tax calculation parameters are requested
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();
// anything to do?
if (paramètres == null || paramètres == "") break;
// check number of arguments in the input line
string[] args = paramètres.Split(null);
int nbParamètres = args.Length;
if (nbParamètres != 3) {
Console.WriteLine(syntaxe);
continue;
}//if
// checking the validity of parameters
// married
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
// correct data?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
continue;
}
// salary
int salaire = 0;
dataOk = false;
try {
salaire = int.Parse(args[2]);
dataOk = salaire >= 0;
} catch {
}//try-catch
// correct data?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
continue;
}
// parameters are correct - Tax is calculated
Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
// next taxpayer
}//while
}
}
}
O programa acima permite ao utilizador executar simulações repetidas de cálculo de impostos.
- linha 16: criação do objeto tax que implementa a interface IImpot. Este objeto é obtido através da instanciação de um HardwiredImpot, um tipo que implementa a interface IImpot. Note-se que não atribuímos à variável tax o tipo HardwiredImpot, mas sim a interface IImpot. Isto indica que estamos apenas interessados no objeto tax e não no resto.
- linhas 19-68: o ciclo de simulação do cálculo do imposto
- linha 22: os três parâmetros necessários para o método calculate são solicitados numa única linha digitada no teclado.
- linha 26: o método [string].Split(null) divide [string] em palavras. Estas são armazenadas numa matriz args.
- linha 66: chama o objeto calculate que implementa a interface IImpot.
Eis um exemplo de como executar o programa:









