Skip to content

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:

privé
Um campo privado só pode ser acedido pelos métodos internos da classe
público
Um campo público pode ser acedido por qualquer método, definido ou não dentro da
protegido
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:

Personne p1;
p1.prenom="Jean";
p1.nom="Dupont";
p1.age=30;

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:

Personne p1;
p1.Initialise("Jean","Dupont",30);

Escrever p1.Initialise é válido porque Initializes é de acesso público.

4.1.5. O operador new

A sequência de instruções

Personne p1;
p1.Initialise("Jean","Dupont",30);

está incorreta. A instrução

    Personne p1;

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:

Personne p1=null;

onde a palavra-chave null indica que a variável p1 ainda não faz referência a nenhum objeto. Quando escreve

p1.Initialise("Jean","Dupont",30);

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:

Personne p1=new Personne();

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

p1.Initialise("Jean","Dupont",30);

é 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:

p1.Initialise("Jean","Dupont",30);

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:

[Jean, Dupont, 30]

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:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

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:

        C objet =new C(arg1,arg2, ... argn);

ou

        C objet;
        objet=new C(arg1,arg2, ... argn);

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:

    Personne p1;
    p1=new Personne();

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:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

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:

1
2
3
4
5
6
p1=[Jean, Dupont, 30]
p2=[Jean, Dupont, 30]
p3=[Jean, Dupont, 30]
p1=[Micheline, Benoît, 67]
p2=[Micheline, Benoît, 67]
p3=[Jean, Dupont, 30]

Ao declarar a variável p1 por

Personne p1=new Personne("Jean","Dupont",30);

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:

    p1=null;

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:

Personne p2=p1;

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:

Personne p3=new Personne(p1);

é 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:
string s="une chaîne";
s=s.ToUpperCase();

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:

1
2
3
4
Dans fonction appelante avant appel : sb0=essai0, sb1=essai1, sb2=essai2
Début fonction appelée : sbf0=essai0, sbf1=essai1, sbf2=essai2
Fin fonction appelée : sbf0=essai0*****, sbf1=essai1*****, sbf2=essai2*****, sbf3=essai3*****
Dans fonction appelante après appel : sb0=essai0*****, sb1=essai1, sb2=essai2*****, sb3=essai3*****

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:

1
2
3
Constructeur Personne(string, string, int)
Constructeur Personne(Personne)
[Jean, Dupont, 30]

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:

1
2
3
Constructeur Personne(string, string, int)
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)

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:

public Type Propriété{
    get {...}
    set {...}
}

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

        p.Age=56;

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:

1
2
3
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)
âge (-4) invalide

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:

Classe objet=new Classe (...) {Propriété1=val1, Propriété2=val2, ...}

Esta sintaxe é equivalente ao seguinte código:

1
2
3
4
Classe objet=new Classe(...);
objet.Propriété1=val1;
objet.Propriété2=val2;
...

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:

p2=(Arthur,Martin,7)

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:

public Type Propriété{ get ; set ; }

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:

public string Prenom {get; set;}

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:

public string Prenom;

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:

    Nombre de personnes créées : 3

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:

1
2
3
[Jean, Dupont, 30]
[Sylvie, Vartan, 52]
[Neil, Armstrong, 66]

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:

    public class Enseignant : Personne

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:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Jean, Dupont, 30]

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:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Jean, Dupont, 30],27]

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:

    Oi=Oj avec j>i

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

    Oi=Oj

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):

    public static void Affiche(Personne p){
        ….
    }

Mais vale escrever

    Personne p;
    ...
    Affiche(p);

isso

    Enseignant e;
    ...
    Affiche(e);

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:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Lucile, Dumas, 56]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

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:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

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:

    public class Personne

estamos implicitamente a escrever:

    public class Personne : System.Object

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:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Chap2.Enseignant
Constructeur Personne(string, string, int)
Chap2.Personne

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:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

4.3. Redefinir o significado de um operador para uma classe

4.3.1. Introdução

Considere a instrução

op1 + op2

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:

public static [type] operator +(C1 opérande1, C2 opérande2);

Quando o compilador encontra o

op1 + op2

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 :

public static [type] operator ++(C1 opérande1);

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

public ListeDePersonnes(){
}

será utilizado. Este construtor não faz nada além de chamar o construtor sem parâmetros da sua classe pai:

public ArrayList(){
...
}

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:

l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])

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:

    public object this[int i] { ... }

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:

1
2
3
4
5
6
7
l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])
l[1]=[franck, gallon, 5]
l=([jean, martin, 10],[franck, gallon, 5],Enseignant[[camille, germain, 27],60])
Personne(martin)=[jean, martin, 10]
Personne(germain)=Enseignant[[camille, germain, 27],60]
Personne(xx) n'existe pas

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:

struct NomStructure{
// attributs
    ...
// propriétés
...
// constructeurs
...
// méthodes
...
}

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:

1
2
3
4
5
6
7
8
sp1=SPersonne(paul,10)
sp2=SPersonne(paul,10)
sp1=SPersonne(paul,10)
sp2=SPersonne(nicole,30)
op1=CPersonne(paul,10)
op2=CPersonne(paul,10)
op1=CPersonne(nicole,30)
op2=CPersonne(nicole,30)

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:

1
2
3
4
5
6
p1=SPersonne(paul,10)
p2=SPersonne(paul,10)
p1=SPersonne(paul,10)
p2=SPersonne(nicole,30)
p3=SPersonne(amandin,18)
p4=SPersonne(x,10)

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.

1
2
3
4
5
6
public class C : IEnumerator{
    ...
    Object Current{ get {...}}
    bool MoveNext{...}
    void Reset(){...}
}
  • 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:

1
2
3
matière=anglais, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18],[Jérôme,Jaric,10]), Moyenne=14,5, Ecart-type=2,95803989154981

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:

1
2
3
IStats stats1=new TableauDeNotes(...);
...
stats1=new ListeDeNotes(...);

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

public class ClasseDérivée:ClasseDeBase,I1,I2,..,In{
...
}

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:

1
2
3
Utilisateur[observer,mdp1,observateur]
Utilisateur[admin,mdp2,admin]
Utilisateur non connu : [xx,yy]

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:

1
2
3
i1=2,i2=1
s1=s2,s2=s1
p1=[pauline, dard, 55],p2=[jean, clu, 20]

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:

Image

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:

Image

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:

Avant échange --> c1=43,c2=79
Après échange --> c1=43,c2=79
Avant échange --> c1=72,c2=56
Après échange --> c1=56,c2=72
Avant échange --> c1=92,c2=75
Après échange --> c1=75,c2=92
Avant échange --> c1=11,c2=47
Après échange --> c1=11,c2=47
Avant échange --> c1=31,c2=67
Après échange --> c1=31,c2=67
Avant échange --> c3=xxxxxxxxxxxxxx,c4=xx
Après échange --> c3=xx,c4=xxxxxxxxxxxxxx

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:

Image

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:

public static void Sort<T>(T[] tableau, IComparer<T> comparateur)

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:

1
2
3
public interface IComparer<T>{
    int Compare(T t1, T t2);
}

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:

Tableau à trier-----------------------------------
[claude, pollon, 25]
[valentine, germain, 35]
[paul, germain, 32]
Tableau après le tri selon les nom et prénom------
[paul, germain, 32]
[valentine, germain, 35]
[claude, pollon, 25]
Tableau après le tri selon l'âge------------------
[claude, pollon, 25]
[paul, germain, 32]
[valentine, germain, 35]

4.9. Espaços de nomes

Para escrever uma linha no ecrã, usamos a instrução

Console.WriteLine(...)

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:

System.Console.WriteLine(...)

Isto é evitado utilizando um using:

using System;
...
Console.WriteLine(...)

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:

Assembly: Mscorlib (in Mscorlib.dll)

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:

csc /r:fic1.dll /r:fic2.dll ... prog.cs

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:

csc /r:e1.dll /r:e2.dll prog.cs

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:

namespace EspaceDeNoms{
     // class definition
}

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

    8382        0.0683        291.09

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:

    0                0.4809    9505.54

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:

Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument marié incorrect : tapez o ou n
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument salaire incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d f
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros