Tipos y miembros de C#

En cuanto lenguaje orientado a objetos, C# admite los conceptos de encapsulación, herencia y polimorfismo. Una clase puede heredar directamente de una clase primaria e implementar cualquier número de interfaces. Los métodos que invalidan los métodos virtuales en una clase primaria requieren la palabra clave override como una manera de evitar redefiniciones accidentales. En C#, un struct es como una clase ligera; es un tipo asignado en la pila que puede implementar interfaces pero que no admite la herencia. C# proporciona tipos de record class y de record struct cuyo propósito es, principalmente, almacenar valores de datos.

Todos los tipos se inicializan a través de un constructor, que es un método que se encarga de inicializar una instancia. Dos declaraciones de constructor tienen un comportamiento único:

  • Un constructor sin parámetros, que inicializa todos los campos con su valor predeterminado.
  • Un constructor principal, que declara los parámetros necesarios para una instancia de ese tipo.

Clases y objetos

Las clases son los tipos más fundamentales de C#. Una clase es una estructura de datos que combina estados (campos) y acciones (métodos y otros miembros de función) en una sola unidad. Una clase proporciona una definición para instancias de la clase, también conocidas como objetos. Las clases admiten herencia y polimorfismo, mecanismos por los que las clases derivadas pueden extender y especializar clases base.

Las clases nuevas se crean mediante declaraciones de clase. Una declaración de clase comienza con un encabezado. El encabezado especifica lo siguiente:

  • Atributos y modificadores de la clase
  • Nombre de la clase
  • Clase base (al heredar de una clase base)
  • Interfaces implementadas por la clase

Al encabezado le sigue el cuerpo de la clase, que consta de una lista de declaraciones de miembros escritas entre los delimitadores { y }.

En el código siguiente se muestra una declaración de una clase simple denominada Point:

public class Point
{
    public int X { get; }
    public int Y { get; }
    
    public Point(int x, int y) => (X, Y) = (x, y);
}

Las instancias de clases se crean mediante el operador new, que asigna memoria para una nueva instancia, invoca un constructor para inicializar la instancia y devuelve una referencia a la instancia. Las instrucciones siguientes crean dos objetos Point y almacenan las referencias en esos objetos en dos variables:

var p1 = new Point(0, 0);
var p2 = new Point(10, 20);

La memoria ocupada por un objeto se reclama automáticamente cuando el objeto ya no es accesible. En C#, no es necesario ni posible desasignar objetos de forma explícita.

var p1 = new Point(0, 0);
var p2 = new Point(10, 20);

Es posible que las aplicaciones o las pruebas de los algoritmos necesiten crear varios objetos Point. La siguiente clase genera una secuencia de puntos aleatorios. El parámetro de constructor principal establece el número de puntos. El parámetro de constructor principal numPoints está en el ámbito de todos los miembros de la clase:

public class PointFactory(int numberOfPoints)
{
    public IEnumerable<Point> CreatePoints()
    {
        var generator = new Random();
        for (int i = 0; i < numberOfPoints; i++)
        {
            yield return new Point(generator.Next(), generator.Next());
        }
    }
}

Puede usar la clase tal como se muestra en el código siguiente:

var factory = new PointFactory(10);
foreach (var point in factory.CreatePoints())
{
    Console.WriteLine($"({point.X}, {point.Y})");
}

Parámetros de tipo

Las clases genéricas definen parámetros de tipos. Los parámetros de tipo son una lista de nombres de parámetros de tipo entre paréntesis angulares. Los parámetros de tipo siguen el nombre de la clase. Los parámetros de tipo pueden usarse luego en el cuerpo de las declaraciones de clase para definir a los miembros de la clase. En el ejemplo siguiente, los parámetros de tipo de Pair son TFirst y TSecond:

public class Pair<TFirst, TSecond>
{
    public TFirst First { get; }
    public TSecond Second { get; }
    
    public Pair(TFirst first, TSecond second) => 
        (First, Second) = (first, second);
}

Un tipo de clase que se declara para tomar parámetros de tipo se conoce como tipo de clase genérica. Los tipos de estructura, interfaz y delegado también pueden ser genéricos. Cuando se usa la clase genérica, se deben proporcionar argumentos de tipo para cada uno de los parámetros de tipo:

var pair = new Pair<int, string>(1, "two");
int i = pair.First;     //TFirst int
string s = pair.Second; //TSecond string

Un tipo genérico con argumentos de tipo proporcionado, como Pair<int,string> anteriormente, se conoce como tipo construido.

Clases base

Una declaración de clase puede especificar una clase base. Tras el nombre de clase y los parámetros de tipo, agregue un signo de dos puntos y el nombre de la clase base. Omitir una especificación de la clase base es igual que derivarla del tipo object. En el ejemplo siguiente, la clase base de Point3D es Point. En el primer ejemplo, la clase base de Point es object:

public class Point3D : Point
{
    public int Z { get; set; }
    
    public Point3D(int x, int y, int z) : base(x, y)
    {
        Z = z;
    }
}

Una clase hereda a los miembros de su clase base. La herencia significa que una clase contiene implícitamente casi todos los miembros de su clase base. Una clase no hereda la instancia, los constructores estáticos ni el finalizador. Una clase derivada puede agregar nuevos miembros a aquellos de los que hereda, pero no puede quitar la definición de un miembro heredado. En el ejemplo anterior, Point3D hereda los miembros X y Y de Point, y cada instancia de Point3D contiene tres miembros: X, Y y Z.

Existe una conversión implícita de un tipo de clase a cualquiera de sus tipos de clase base. Una variable de un tipo de clase puede hacer referencia a una instancia de esa clase o a una instancia de cualquier clase derivada. Por ejemplo, dadas las declaraciones de clase anteriores, una variable de tipo Point puede hacer referencia a una instancia de Point o Point3D:

Point a = new(10, 20);
Point b = new Point3D(10, 20, 30);

Estructuras

Las clases definen tipos que admiten la herencia y el polimorfismo. Permiten crear comportamientos sofisticados basados en jerarquías de clases derivadas. Por el contrario, los tipos struct son tipos más simples, cuyo propósito principal es almacenar valores de datos. Dichos tipos struct no pueden declarar un tipo base; se derivan implícitamente de System.ValueType. No se pueden derivar otros tipos de struct a partir de un tipo de struct. Están sellados implícitamente.

public struct Point
{
    public double X { get; }
    public double Y { get; }
    
    public Point(double x, double y) => (X, Y) = (x, y);
}

Interfaces

Una interfaz define un contrato que se puede implementar mediante clases y structs. Una interfaz se define para declarar capacidades que se comparten entre tipos distintos. Por ejemplo, la interfaz System.Collections.Generic.IEnumerable<T> define una manera coherente de recorrer todos los elementos de una colección, como una matriz. Una interfaz puede contener métodos, propiedades, eventos e indexadores. Normalmente, una interfaz no proporciona implementaciones de los miembros que define, sino que simplemente especifica los miembros que se deben proporcionar mediante clases o estructuras que implementan la interfaz.

Las interfaces pueden usar la herencia múltiple. En el ejemplo siguiente, la interfaz IComboBox hereda de ITextBox y IListBox.

interface IControl
{
    void Paint();
}

interface ITextBox : IControl
{
    void SetText(string text);
}

interface IListBox : IControl
{
    void SetItems(string[] items);
}

interface IComboBox : ITextBox, IListBox { }

Las clases y los structs pueden implementar varias interfaces. En el ejemplo siguiente, la clase EditBox implementa IControl y IDataBound.

interface IDataBound
{
    void Bind(Binder b);
}

public class EditBox : IControl, IDataBound
{
    public void Paint() { }
    public void Bind(Binder b) { }
}

Cuando una clase o un struct implementan una interfaz determinada, las instancias de esa clase o struct se pueden convertir implícitamente a ese tipo de interfaz. Por ejemplo

EditBox editBox = new();
IControl control = editBox;
IDataBound dataBound = editBox;

Enumeraciones

Un tipo de enumeración define un conjunto de valores constantes. En el elemento enum siguiente se declaran constantes que definen diferentes verduras de raíz:

public enum SomeRootVegetable
{
    HorseRadish,
    Radish,
    Turnip
}

También puede definir un elemento enum que se usará de forma combinada como marcas. La declaración siguiente declara un conjunto de marcas para las cuatro estaciones. Se puede aplicar cualquier combinación de estaciones, incluido un valor All que incluya todas las estaciones:

[Flags]
public enum Seasons
{
    None = 0,
    Summer = 1,
    Autumn = 2,
    Winter = 4,
    Spring = 8,
    All = Summer | Autumn | Winter | Spring
}

En el ejemplo siguiente se muestran las declaraciones de ambas enumeraciones anteriores:

var turnip = SomeRootVegetable.Turnip;

var spring = Seasons.Spring;
var startingOnEquinox = Seasons.Spring | Seasons.Autumn;
var theYear = Seasons.All;

Tipos que aceptan valores NULL

Las variables de cualquier tipo se pueden declarar para que no acepten valores NULL o sí acepten valores NULL. Una variable que acepta valores NULL puede contener un valor null adicional que no indica valor alguno. Los tipos de valor que aceptan valores NULL (estructuras o enumeraciones) se representan mediante System.Nullable<T>. Los tipos de referencia que no aceptan valores NULL y los que sí aceptan valores NULL se representan mediante el tipo de referencia subyacente. La distinción se representa mediante metadatos leídos por el compilador y algunas bibliotecas. El compilador proporciona advertencias cuando se desreferencian las referencias que aceptan valores NULL sin comprobar primero su valor con null. El compilador también proporciona advertencias cuando las referencias que no aceptan valores NULL se asignan a un valor que puede ser null. En el ejemplo siguiente se declara un elemento int que admite un valor NULL, y que se inicializa en null. A continuación, establece el valor en 5. Muestra el mismo concepto con una cadena que acepta valores NULL. Para más información, consulte Tipos de valor que admiten un valor NULL y Tipos de referencia que aceptan valores NULL.

int? optionalInt = default; 
optionalInt = 5;
string? optionalText = default;
optionalText = "Hello World.";

Tuplas

C# admite tuplas, lo que proporciona una sintaxis concisa para agrupar varios elementos de datos en una estructura de datos ligera. Puede crear una instancia de una tupla declarando los tipos y los nombres de los miembros entre ( y ), como se muestra en el ejemplo siguiente:

(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
//Output:
//Sum of 3 elements is 4.5.

Las tuplas proporcionan una alternativa para la estructura de datos con varios miembros sin usar los bloques de creación que se describen en el siguiente artículo.