Implementación de clases privadas, estáticas y anidadas

Completado

Los constructores de instancia se usan para especificar el código que se ejecuta al crear una nueva instancia de una clase con el operador new. Cuando los constructores de instancia usan el modificador de acceso public, el objeto resultante es accesible desde cualquier otro código de la aplicación. Este tipo de acceso a un objeto y sus miembros suele ser deseable, pero hay ocasiones en las que se quiere restringir el acceso a una clase o a los miembros de una clase.

Cuando necesite ocultar o reducir la exposición de una clase, puede usar modificadores de acceso para controlar la visibilidad de la clase y sus miembros.

Modificadores de acceso

Todos los tipos y miembros de tipo tienen un nivel de accesibilidad que controla si son accesibles desde otro código de la aplicación compilada (u otros ensamblados).

Las clases declaradas directamente dentro de un espacio de nombres pueden tener acceso public, internal o file. Las clases se asignan internal acceso de forma predeterminada cuando no se especifica ningún modificador de acceso.

Use los siguientes modificadores de acceso para especificar la accesibilidad de un tipo o miembro al declararlo:

  • public: el código de cualquier ensamblado puede tener acceso a este tipo o miembro. El nivel de accesibilidad del tipo contenedor controla el nivel de accesibilidad de los miembros públicos del tipo.
  • private: solo el código declarado en el mismo class o struct puede acceder a este miembro.
  • protected: solo el código de la misma class o en un class derivado puede acceder a este tipo o miembro.
  • internal: solo el código del mismo ensamblado puede tener acceso a este tipo o miembro.
  • protected internal: solo el código del mismo ensamblado o de una clase derivada de otro ensamblado puede tener acceso a este tipo o miembro.
  • private protected: solo el código del mismo ensamblado y en la misma clase o una clase derivada puede tener acceso al tipo o miembro.
  • file: solo el código del mismo archivo puede tener acceso al tipo o al miembro.

El modificador record en un tipo hace que el compilador sintetice miembros adicionales. El modificador record no afecta a la accesibilidad predeterminada para una record class o un record struct.

Constructores de clase privada

Un constructor privado es un constructor de instancia especial. Los constructores privados se suelen usar en clases que solo contienen miembros estáticos. Si una clase tiene uno o varios constructores privados y ningún constructor público, otras clases (excepto las clases anidadas) no pueden crear instancias de la clase. Por ejemplo:


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

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

La declaración del constructor vacío impide la generación automática de un constructor sin parámetros. Si no especifica un modificador de acceso para un constructor, el valor predeterminado es private. Sin embargo, el modificador private debe usarse para aclarar que no se puede crear una instancia de la clase.

Los constructores privados se usan para evitar la creación de instancias de una clase cuando no hay ningún campo o método de instancia, como la clase Math en la biblioteca de .NET o cuando se llama a un método para obtener una instancia de una clase. Si todos los métodos de la clase son estáticos, considere la posibilidad de hacer que la clase completa sea estática.

En el ejemplo siguiente se muestra una clase mediante un constructor 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

Si quita la marca de comentario de la instrucción siguiente del ejemplo, el compilador genera un error de compilación. El compilador entiende que un constructor privado no es accesible.


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

Clases estáticas

Una clase estática es básicamente la misma que una clase no estática, con una diferencia importante: no se puede crear una instancia de una clase estática. En otras palabras, no se puede usar el operador new para crear una variable a partir de una clase estática. Aunque no se puede crear una instancia de la clase, puede acceder a los miembros de una clase estática haciendo referencia al nombre de clase. Por ejemplo, si tiene una clase estática denominada UtilityClass que tiene un método estático público denominado MethodA, puede llamar al método mediante una combinación de los nombres de clase y método. Esta sintaxis se muestra en el ejemplo siguiente:


UtilityClass.MethodA();

Una clase estática se puede usar como contenedor para los métodos que operan en parámetros de entrada y no tiene que get ni set ningún campo de instancia interno.

Por ejemplo, en la biblioteca de clases de .NET, la clase System.Math estática contiene métodos que realizan operaciones matemáticas. Estos métodos funcionan sin necesidad de almacenar o recuperar datos que sean únicos para una instancia determinada de la clase Math. Para llamar a los miembros de la clase, especifique el nombre de clase y el nombre del método, como se muestra en el ejemplo siguiente:


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 sucede con todos los tipos de clase, el entorno de ejecución de .NET carga la información de tipos de una clase estática cuando se carga el programa que hace referencia a la clase. El programa no puede especificar exactamente cuándo se carga la clase. Sin embargo, se garantiza que se carguen y tengan sus campos inicializados y su constructor estático llamado antes de que se haga referencia a la clase por primera vez en el programa. Un constructor estático solo se llama una vez y una clase estática permanece en memoria durante la vigencia del dominio de aplicación en el que reside el programa.

En la lista siguiente se proporcionan las características principales de una clase estática:

  • Contiene solo miembros estáticos.
  • No se pueden crear instancias.
  • Está sellado.
  • No puede contener constructores de instancia.

La creación de una clase estática es básicamente la misma que la creación de una clase que contiene solo miembros estáticos y un constructor privado. Un constructor privado impide que se cree una instancia de la clase. La ventaja de usar una clase estática es que el compilador puede comprobar para asegurarse de que no se agregan accidentalmente miembros de instancia. El compilador garantiza que no se pueden crear instancias de esta clase.

Las clases estáticas están selladas y, por tanto, no se pueden heredar. No pueden heredar de ninguna clase o interfaz, excepto Object. Las clases estáticas no pueden contener un constructor de instancia. Sin embargo, pueden contener un constructor static. Las clases no estáticas también deben definir un constructor de static si la clase contiene static miembros que requieren inicialización no trivial.

Este es un ejemplo de una clase estática que contiene dos métodos que convierten la temperatura de Celsius a Fahrenheit y de Fahrenheit a 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.
 */

Miembros estáticos

Una clase no estática puede contener métodos estáticos, campos, propiedades o eventos. El miembro estático se puede llamar en una clase incluso cuando no existe ninguna instancia de la clase. El nombre de la clase siempre tiene acceso al miembro estático, no al nombre de la instancia. Solo existe una copia de un miembro estático, independientemente de cuántas instancias de la clase se creen. Los métodos y propiedades estáticos no pueden tener acceso a campos y eventos no estáticos en su tipo contenedor y no pueden tener acceso a una variable de instancia de ningún objeto a menos que se pase explícitamente en un parámetro de método.

Es más habitual declarar una clase no estática con algunos miembros estáticos, que declarar una clase completa como estática. Dos usos comunes de campos estáticos son mantener un recuento del número de objetos que se crean instancias o para almacenar un valor que se debe compartir entre todas las instancias.

Los métodos estáticos se pueden sobrecargar pero no invalidar, ya que pertenecen a la clase y no a ninguna instancia de la clase .

Aunque un campo no se puede declarar como static const, un campo de const es esencialmente estático en su comportamiento. Pertenece al tipo , no a instancias del tipo . Por lo tanto, se puede acceder a const campos mediante la misma notación de ClassName.MemberName usada para los campos estáticos. No se requiere ninguna instancia de objeto.

C# no admite variables locales estáticas (es decir, variables declaradas en el ámbito del método).

Los miembros de clase estática se declaran mediante la palabra clave static antes del tipo de valor devuelto del miembro, como se muestra en el ejemplo siguiente:


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...
}

Los miembros estáticos se inicializan antes de que se acceda al miembro estático por primera vez y antes del constructor estático, si hay uno, se llama a . Para acceder a un miembro de clase estática, use el nombre de la clase en lugar de un nombre de variable para especificar la ubicación del miembro, como se muestra en el ejemplo siguiente:


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

Si la clase contiene campos estáticos, proporcione un constructor estático que los inicialice cuando se cargue la clase.

Una llamada a un método estático genera una instrucción de llamada en lenguaje intermedio común (CIL), mientras que una llamada a un método de instancia genera una instrucción callvirt, que también comprueba si hay referencias de objeto null. Sin embargo, la mayoría de las veces la diferencia de rendimiento entre los dos no es significativa.

Clases anidadas

Un tipo de class definido dentro de otra clase se denomina clase anidada. Por ejemplo:


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

La accesibilidad de una clase anidada tiene como valor predeterminado private. Esto significa que las clases anidadas solo son accesibles desde su clase contenedora. En el ejemplo anterior, la clase Nested no es accesible para los tipos externos.

Puede especificar un modificador de acceso para definir la accesibilidad de un tipo anidado, como se indica a continuación:

  • Los tipos anidados de una clase pueden ser public, protected, internal, protected internal, private o private protected.

Sin embargo, definir un protected, protected internal o private protected clase anidada dentro de una clase sellada genera la advertencia del compilador CS0628, "nuevo miembro protegido declarado en la clase sealed".

Cautela

Tenga en cuenta que hacer que un tipo anidado sea visible externamente infringe la regla de calidad de código CA1034 "Los tipos anidados no deben estar visibles".

En el ejemplo siguiente se hace pública la clase Nested:


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

La clase anidada, o interna, puede tener acceso a la clase contenedora o externa. Para acceder a la clase contenedora, pásela como argumento al constructor de la clase anidada. Por ejemplo:


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

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

Una clase anidada tiene acceso a todos los miembros a los que se puede acceder a su clase contenedora. Puede acceder a miembros privados y protegidos de la clase contenedora, incluidos los miembros protegidos heredados.

En la declaración anterior, el nombre completo de la clase Nested es Container.Nested. Este es el nombre que se usa para crear una nueva instancia de la clase anidada, como se indica a continuación:


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