Implementar classes privadas, estáticas e aninhadas

Concluído

Construtores de instância são usados para especificar o código executado quando você cria uma nova instância de uma classe com o operador new. Quando construtores de instância usam o modificador de acesso public, o objeto resultante fica acessível de qualquer outro código em seu aplicativo. Esse tipo de acesso a um objeto e seus membros geralmente é desejável, mas há momentos em que você deseja restringir o acesso a uma classe ou aos membros de uma classe.

Quando precisar ocultar ou reduzir a exposição de uma classe, você poderá usar modificadores de acesso para controlar a visibilidade da classe e de seus membros.

Modificadores de acesso

Todos os tipos e membros de tipo têm um nível de acessibilidade que controla se eles estão acessíveis de outro código em seu aplicativo compilado (ou em outros assemblies).

Classes declaradas diretamente em um namespace podem ter acesso public, internal ou file. Classes são atribuídas internal acesso por padrão quando nenhum modificador de acesso é especificado.

Use os seguintes modificadores de acesso para especificar a acessibilidade de um tipo ou membro ao declará-lo:

  • public: o código em qualquer assembly pode acessar esse tipo ou membro. O nível de acessibilidade do tipo que contém controla o nível de acessibilidade dos membros públicos do tipo.
  • private: somente o código declarado no mesmo class ou struct pode acessar esse membro.
  • protected: somente o código no mesmo class ou em uma class derivada pode acessar esse tipo ou membro.
  • internal: somente o código no mesmo assembly pode acessar esse tipo ou membro.
  • protected internal: somente o código no mesmo assembly ou em uma classe derivada em outro assembly pode acessar esse tipo ou membro.
  • private protected: somente o código no mesmo assembly e na mesma classe ou em uma classe derivada pode acessar o tipo ou membro.
  • file: somente o código no mesmo arquivo pode acessar o tipo ou membro.

O modificador record em um tipo faz com que o compilador sintetize membros extras. O modificador de record não afeta a acessibilidade padrão para um record class ou um record struct.

Construtores de classe privada

Um construtor privado é um construtor de instância especial. Construtores privados geralmente são usados em classes que contêm apenas membros estáticos. Se uma classe tiver um ou mais construtores privados e nenhum construtor público, outras classes (exceto classes aninhadas) não poderão criar instâncias da classe. Por exemplo:


class NLog
{
    // Private Constructor:
    private NLog() { }

    public static double e = Math.E;  //2.71828...
}

A declaração do construtor vazio impede a geração automática de um construtor sem parâmetros. Se você não especificar um modificador de acesso para um construtor, ele usará como padrão private. No entanto, o modificador de private deve ser usado para esclarecer que a classe não pode ser instanciada.

Construtores privados são usados para impedir a criação de instâncias de uma classe quando não há campos ou métodos de instância, como a classe Math na biblioteca .NET ou quando um método é chamado para obter uma instância de uma classe. Se todos os métodos da classe forem estáticos, considere tornar a classe completa estática.

O exemplo a seguir mostra uma classe usando um construtor privado.


public class Counter
{
    private Counter() { }

    public static int currentCount;

    public static int IncrementCount()
    {
        return ++currentCount;
    }
}

class TestCounter
{
    static void Main()
    {
        // If you uncomment the following statement, it generates
        // an error because the constructor is inaccessible:
        // Counter aCounter = new Counter();   // Error

        Counter.currentCount = 100;
        Counter.IncrementCount();
        Console.WriteLine("New count: {0}", Counter.currentCount);

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
// Output: New count: 101

Se você descompactar a instrução a seguir do exemplo, o compilador gerará um erro de build. O compilador entende que um construtor privado está inacessível.


// Counter aCounter = new Counter();   // Error

Classes estáticas

Uma classe estática é basicamente a mesma que uma classe não estática, com uma diferença importante: uma classe estática não pode ser instanciada. Em outras palavras, você não pode usar o operador new para criar uma variável de uma classe estática. Embora não seja possível criar uma instância da classe, você pode acessar os membros de uma classe estática fazendo referência ao nome da classe. Por exemplo, se você tiver uma classe estática chamada UtilityClass que tenha um método estático público chamado MethodA, você poderá chamar o método usando uma combinação dos nomes de classe e método. Essa sintaxe é mostrada no exemplo a seguir:


UtilityClass.MethodA();

Uma classe estática pode ser usada como um contêiner para métodos que operam em parâmetros de entrada e não precisam get ou set campos de instância interna.

Por exemplo, na Biblioteca de Classes do .NET, a classe System.Math estática contém métodos que executam operações matemáticas. Esses métodos operam sem qualquer requisito para armazenar ou recuperar dados exclusivos de uma instância específica da classe Math. Você chama os membros da classe especificando o nome da classe e o nome do método, conforme mostrado no exemplo a seguir:


double dub = -3.14;
Console.WriteLine(Math.Abs(dub));
Console.WriteLine(Math.Floor(dub));
Console.WriteLine(Math.Round(Math.Abs(dub)));

// Output:
// 3.14
// -4
// 3

Como é o caso de todos os tipos de classe, o runtime do .NET carrega as informações de tipo para uma classe estática quando o programa que faz referência à classe é carregado. O programa não pode especificar exatamente quando a classe é carregada. No entanto, é garantido carregar e ter seus campos inicializados e seu construtor estático chamado antes que a classe seja referenciada pela primeira vez em seu programa. Um construtor estático é chamado apenas uma vez e uma classe estática permanece na memória durante o tempo de vida do domínio do aplicativo no qual o programa reside.

A lista a seguir fornece os principais recursos de uma classe estática:

  • Contém apenas membros estáticos.
  • Não pode ser instanciado.
  • Está lacrado.
  • Não é possível conter construtores de instância.

A criação de uma classe estática é, portanto, basicamente a mesma que a criação de uma classe que contém apenas membros estáticos e um construtor privado. Um construtor privado impede que a classe seja instanciada. A vantagem de usar uma classe estática é que o compilador pode verificar se nenhum membro de instância foi adicionado acidentalmente. O compilador garante que as instâncias dessa classe não possam ser criadas.

As classes estáticas são lacradas e, portanto, não podem ser herdadas. Eles não podem herdar de nenhuma classe ou interface, exceto Object. Classes estáticas não podem conter um construtor de instância. No entanto, eles podem conter um construtor static. Classes não estáticas também devem definir um construtor static se a classe contiver static membros que exigem inicialização não trivial.

Aqui está um exemplo de uma classe estática que contém dois métodos que convertem a temperatura de Celsius em Fahrenheit e de Fahrenheit para Celsius:


public static class TemperatureConverter
{
    public static double CelsiusToFahrenheit(string temperatureCelsius)
    {
        // Convert argument to double for calculations.
        double celsius = Double.Parse(temperatureCelsius);

        // Convert Celsius to Fahrenheit.
        double fahrenheit = (celsius * 9 / 5) + 32;

        return fahrenheit;
    }

    public static double FahrenheitToCelsius(string temperatureFahrenheit)
    {
        // Convert argument to double for calculations.
        double fahrenheit = Double.Parse(temperatureFahrenheit);

        // Convert Fahrenheit to Celsius.
        double celsius = (fahrenheit - 32) * 5 / 9;

        return celsius;
    }
}

class TestTemperatureConverter
{
    static void Main()
    {
        Console.WriteLine("Please select the convertor direction");
        Console.WriteLine("1. From Celsius to Fahrenheit.");
        Console.WriteLine("2. From Fahrenheit to Celsius.");
        Console.Write(":");

        string? selection = Console.ReadLine();
        double F, C = 0;

        switch (selection)
        {
            case "1":
                Console.Write("Please enter the Celsius temperature: ");
                F = TemperatureConverter.CelsiusToFahrenheit(Console.ReadLine() ?? "0");
                Console.WriteLine("Temperature in Fahrenheit: {0:F2}", F);
                break;

            case "2":
                Console.Write("Please enter the Fahrenheit temperature: ");
                C = TemperatureConverter.FahrenheitToCelsius(Console.ReadLine() ?? "0");
                Console.WriteLine("Temperature in Celsius: {0:F2}", C);
                break;

            default:
                Console.WriteLine("Please select a convertor.");
                break;
        }

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/* Example Output:
    Please select the convertor direction
    1. From Celsius to Fahrenheit.
    2. From Fahrenheit to Celsius.
    :2
    Please enter the Fahrenheit temperature: 20
    Temperature in Celsius: -6.67
    Press any key to exit.
 */

Membros estáticos

Uma classe não estática pode conter métodos estáticos, campos, propriedades ou eventos. O membro estático pode ser chamado em uma classe mesmo quando nenhuma instância da classe existe. O membro estático sempre é acessado pelo nome da classe, não pelo nome da instância. Existe apenas uma cópia de um membro estático, independentemente de quantas instâncias da classe são criadas. Métodos e propriedades estáticos não podem acessar campos e eventos não estáticos em seu tipo de contenção e não podem acessar uma variável de instância de qualquer objeto, a menos que seja explicitamente passado em um parâmetro de método.

É mais comum declarar uma classe não estática com alguns membros estáticos do que declarar uma classe inteira como estática. Dois usos comuns de campos estáticos são manter uma contagem do número de objetos instanciados ou armazenar um valor que deve ser compartilhado entre todas as instâncias.

Métodos estáticos podem ser sobrecarregados, mas não substituídos, porque pertencem à classe e não a nenhuma instância da classe.

Embora um campo não possa ser declarado como static const, um campo const é essencialmente estático em seu comportamento. Ele pertence ao tipo, não a instâncias do tipo. Portanto, const campos podem ser acessados usando a mesma notação de ClassName.MemberName usada para campos estáticos. Nenhuma instância de objeto é necessária.

O C# não dá suporte a variáveis locais estáticas (ou seja, variáveis que são declaradas no escopo do método).

Você declara membros de classe estáticos usando a palavra-chave static antes do tipo de retorno do membro, conforme mostrado no exemplo a seguir:


public class Automobile
{
    public static int NumberOfWheels = 4;

    public static int SizeOfGasTank
    {
        get
        {
            return 15;
        }
    }

    public static void Drive() { }

    public static event EventType? RunOutOfGas;

    // Other nonstatic fields and properties...
}

Os membros estáticos são inicializados antes que o membro estático seja acessado pela primeira vez e antes que o construtor estático, se houver um, seja chamado. Para acessar um membro de classe estático, use o nome da classe em vez de um nome de variável para especificar o local do membro, conforme mostrado no exemplo a seguir:


Automobile.Drive();
int i = Automobile.NumberOfWheels;

Se a classe contiver campos estáticos, forneça um construtor estático que os inicialize quando a classe for carregada.

Uma chamada para um método estático gera uma instrução de chamada em CIL (linguagem intermediária comum), enquanto uma chamada para um método de instância gera uma instrução callvirt, que também verifica referências de objeto nulos. No entanto, na maioria das vezes, a diferença de desempenho entre os dois não é significativa.

Classes aninhadas

Um tipo de class definido em outra classe é chamado de classe aninhada. Por exemplo:


public class Container
{
    class Nested
    {
        Nested() { }
    }
}

A acessibilidade de uma classe aninhada é padrão para private. Isso significa que as classes aninhadas só podem ser acessadas de sua classe de contenção. No exemplo anterior, a classe Nested é inacessível para tipos externos.

Você pode especificar um modificador de acesso para definir a acessibilidade de um tipo aninhado, da seguinte maneira:

  • Tipos aninhados de uma classe podem ser public, protected, internal, protected internal, private ou private protected.

No entanto, definir um protected, protected internal ou private protected classe aninhada dentro de uma classe lacrada gera o aviso do compilador CS0628, "novo membro protegido declarado na classe lacrada".

Cuidado

Lembre-se de que tornar um tipo aninhado visível externamente viola a regra de qualidade de código CA1034 "Tipos aninhados não devem ser visíveis".

O exemplo a seguir torna a classe Nested pública:


public class Container
{
    public class Nested
    {
        Nested() { }
    }
}

A classe aninhada ou interna pode acessar a classe independente ou externa. Para acessar a classe que contém, passe-a como um argumento para o construtor da classe aninhada. Por exemplo:


public class Container
{
    public class Nested
    {
        private Container? parent;

        public Nested()
        {
        }
        public Nested(Container parent)
        {
            this.parent = parent;
        }
    }
}

Uma classe aninhada tem acesso a todos os membros que são acessíveis à sua classe independente. Ele pode acessar membros privados e protegidos da classe que contém, incluindo todos os membros protegidos herdados.

Na declaração anterior, o nome completo da classe Nested é Container.Nested. Esse é o nome usado para criar uma nova instância da classe aninhada, da seguinte maneira:


Container.Nested nest = new Container.Nested();