Tipos de estructura (Referencia de C#)

Un tipo de estructura (o tipo struct) es un tipo de valor que puede encapsular datos y funcionalidad relacionada. Para definir un tipo de estructura se usa la palabra clave struct:

public struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; }
    public double Y { get; }

    public override string ToString() => $"({X}, {Y})";
}

Para obtener información sobre los tipos ref struct y readonly ref struct, consulte el artículo tipos de estructura de referencia.

Los tipos de estructura tienen semántica de valores. Es decir, una variable de un tipo de estructura contiene una instancia del tipo. De forma predeterminada, los valores de variable se copian al asignar, pasar un argumento a un método o devolver el resultado de un método. Para las variables de tipo estructura, se copia una instancia del tipo. Para más información, vea Tipos de valor.

Normalmente, los tipos de estructura se usan para diseñar tipos de pequeño tamaño centrados en datos que proporcionan poco o ningún comportamiento. Por ejemplo, en .NET se usan los tipos de estructura para representar un número (entero y real), un valor booleano, un caracter Unicode, una instancia de tiempo. Si le interesa el comportamiento de un tipo, considere la posibilidad de definir una clase. Los tipos de clase tienen semántica de referencias. Es decir, una variable de un tipo de clase contiene una referencia a una instancia del tipo, no la propia instancia.

Dado que los tipos de estructura tienen semántica del valor, le recomendamos que defina tipos de estructura inmutables.

Estructura readonly

Se usa el modificador readonly para declarar que un tipo de estructura es inmutable. Todos los miembros de datos de una estructura readonly debe ser de solo lectura tal como se indica a continuación:

Esto garantiza que ningún miembro de una estructura readonly modifique el estado de la misma. Eso significa que otros miembros de instancia, excepto los constructores, son implícitamente readonly.

Nota

En una estructura readonly, un miembro de datos de un tipo de referencia mutable puede seguir mutando su propio estado. Por ejemplo, no puede reemplazar una instancia de List<T>, pero puede agregarle nuevos elementos.

El código siguiente define una estructura de readonly con establecedores de propiedades de solo inicialización:

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; init; }
    public double Y { get; init; }

    public override string ToString() => $"({X}, {Y})";
}

Miembros de instancia de readonly

También puede usar el modificador readonly para declarar que un miembro de instancia no modifica el estado de una estructura. Si no puede declarar el tipo de estructura completa como readonly, use el modificador readonly para marcar los miembros de instancia que no modifican el estado de la estructura.

Dentro de un miembro de instancia readonly, no se puede realizar la asignación a campos de instancia de la estructura. Pero un miembro readonly puede llamar a un miembro que no sea readonly. En ese caso, el compilador crea una copia de la instancia de la estructura y llama al miembro que no es readonly en esa copia. Como resultado, la instancia de la estructura original no se modifica.

Normalmente, se aplica el modificador readonly a los siguientes tipos de miembros de instancia:

  • Métodos:

    public readonly double Sum()
    {
        return X + Y;
    }
    

    También puede aplicar el modificador readonly a los métodos que invalidan los métodos declarados en System.Object:

    public readonly override string ToString() => $"({X}, {Y})";
    
  • Propiedades e indizadores:

    private int counter;
    public int Counter
    {
        readonly get => counter;
        set => counter = value;
    }
    

    Si tiene que aplicar el modificador readonly a los dos descriptores de acceso de una propiedad o un indizador, aplíquelo en la declaración de la propiedad o el indizador.

    Nota:

    El compilador declara un get descriptor de acceso de una propiedad implementada automáticamente como readonly, independientemente de la presencia del readonly modificador en una declaración de propiedad.

    Puede aplicar el modificador readonly a una propiedad o indexador con un descriptor de acceso init:

    public readonly double X { get; init; }
    

Puede aplicar el modificador readonly a campos estáticos de un tipo de estructura, pero no a ningún otro miembro estático, como propiedades o métodos.

Es posible que el compilador use el modificador readonly para optimizaciones de rendimiento. Para más información, consulte Cómo evitar asignaciones.

Mutación no destructiva

A partir de C# 10, puede usar la expresión with para generar una copia de una instancia de tipo de estructura con las propiedades y los campos especificados modificados. Como se muestra en el ejemplo siguiente, se usa la sintaxis del inicializador de objeto para especificar qué miembros se van a modificar y sus nuevos valores.

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; init; }
    public double Y { get; init; }

    public override string ToString() => $"({X}, {Y})";
}

public static void Main()
{
    var p1 = new Coords(0, 0);
    Console.WriteLine(p1);  // output: (0, 0)

    var p2 = p1 with { X = 3 };
    Console.WriteLine(p2);  // output: (3, 0)

    var p3 = p1 with { X = 1, Y = 4 };
    Console.WriteLine(p3);  // output: (1, 4)
}

Estructura record

A partir de C# 10, puede definir tipos de estructura de registro. Los tipos de registro proporcionan funcionalidad integrada para encapsular datos. Puede definir tipos record struct y readonly record struct. Una estructura de registro no puede ser una ref struct. Para más información y ver ejemplos, consulte Registros.

Matrices insertadas

A partir de C# 12, puede declarar matrices insertadas como un tipostruct:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBuffer
{
    private char _firstElement;
}

Una matriz insertada es una estructura que contiene un bloque contiguo de N elementos del mismo tipo. Es un código seguro equivalente a la declaración de búfer fijo disponible solo en código no seguro. Una matriz insertada es una struct con las siguientes características:

  • Contiene un único campo.
  • La estructura no especifica un diseño explícito.

Además, el compilador valida el atributo System.Runtime.CompilerServices.InlineArrayAttribute:

  • La longitud debe ser mayor que cero (> 0).
  • El tipo de destino debe ser una estructura.

En la mayoría de los casos, se puede tener acceso a una matriz insertada como una matriz, tanto para leer como escribir valores. Además, puede usar los operadores de rango e índice.

Hay restricciones mínimas en el tipo del único campo de una matriz insertada. No puede ser un tipo de puntero:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithPointer
{
    private unsafe char* _pointerElement;    // CS9184
}

pero puede ser cualquier tipo de referencia o cualquier tipo de valor:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithReferenceType
{
    private string _referenceElement;
}

Puede usar matrices insertadas con casi cualquier estructura de datos de C#.

Las matrices insertadas son una característica de lenguaje avanzada. Están diseñados para escenarios de alto rendimiento en los que un bloque de elementos insertado y contiguo es más rápido que otras estructuras de datos alternativas. Puede obtener más información sobre las matrices insertadas en el speclet de características

Inicialización de estructuras y valores predeterminados

Una variable de tipo struct contiene directamente los datos de ese struct. Esto crea una distinción entre un struct sin inicializar, que tiene su valor predeterminado y un struct inicializado, que almacena los valores establecidos mediante su construcción. Por ejemplo, considere el código siguiente:

public readonly struct Measurement
{
    public Measurement()
    {
        Value = double.NaN;
        Description = "Undefined";
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    public double Value { get; init; }
    public string Description { get; init; }

    public override string ToString() => $"{Value} ({Description})";
}

public static void Main()
{
    var m1 = new Measurement();
    Console.WriteLine(m1);  // output: NaN (Undefined)

    var m2 = default(Measurement);
    Console.WriteLine(m2);  // output: 0 ()

    var ms = new Measurement[2];
    Console.WriteLine(string.Join(", ", ms));  // output: 0 (), 0 ()
}

Como se muestra en el ejemplo anterior, la expresión de valor predeterminado omite un constructor sin parámetros y genera el valor predeterminado del tipo de estructura. La creación de instancias de matriz de tipo de estructura también omite un constructor sin parámetros y genera una matriz rellenada con los valores predeterminados de un tipo de estructura.

La situación más común en la que verá los valores predeterminados es en matrices o en otras colecciones en las que el almacenamiento interno incluye bloques de variables. En el ejemplo siguiente se crea una matriz de 30 estructuras TemperatureRange, cada una de las cuales tiene el valor predeterminado:

// All elements have default values of 0:
TemperatureRange[] lastMonth = new TemperatureRange[30];

Todos los campos miembros de una estructura deben asignarse definitivamente cuando se crean porque los tipos struct almacenan directamente sus datos. El valor default de una estructura ha asignado definitivamente todos los campos a 0. Todos los campos deben asignarse definitivamente cuando se invoca un constructor. Los campos se inicializan mediante los mecanismos siguientes:

  • Puede agregar inicializadores de campo a cualquier campo o propiedad implementada automáticamente.
  • Puede inicializar cualquier campo o propiedades automáticas en el cuerpo del constructor.

A partir de C# 11, si no inicializa todos los campos de una estructura, el compilador agrega código al constructor que inicializa esos campos en el valor predeterminado. El compilador realiza su análisis de asignación definitiva habitual. Los campos a los que se tiene acceso antes de asignarse o que no se asignan definitivamente cuando el constructor termina de ejecutarse tienen asignados sus valores predeterminados antes de que se ejecute el cuerpo del constructor. Si se accede a this antes de que se asignen todos los campos, la estructura se inicializa en el valor predeterminado antes de que se ejecute el cuerpo del constructor.

public readonly struct Measurement
{
    public Measurement(double value)
    {
        Value = value;
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    public Measurement(string description)
    {
        Description = description;
    }

    public double Value { get; init; }
    public string Description { get; init; } = "Ordinary measurement";

    public override string ToString() => $"{Value} ({Description})";
}

public static void Main()
{
    var m1 = new Measurement(5);
    Console.WriteLine(m1);  // output: 5 (Ordinary measurement)

    var m2 = new Measurement();
    Console.WriteLine(m2);  // output: 0 ()

    var m3 = default(Measurement);
    Console.WriteLine(m3);  // output: 0 ()
}

Cada struct tiene un constructor sin parámetros public. Si escribe un constructor sin parámetros, debe ser público. Si una estructura declara cualquier inicializador de campo, debe declarar explícitamente un constructor. No hace falta que dicho constructor no tenga parámetros. Si una estructura declara un inicializador de campo pero no declara ningún constructor, el compilador notifica un error. Cualquier constructor declarado explícitamente (con parámetros o sin parámetros) ejecuta todos los inicializadores de campo de esa estructura. Todos los campos sin un inicializador de campo o una asignación en un constructor se establecen en el valor predeterminado. Para obtener más información, vea la nota propuesta de la característica Constructores de structs sin parámetros.

A partir de C# 12, los tipos struct pueden definir un constructor principal como parte de su declaración. Los constructores principales proporcionan una sintaxis concisa para los parámetros de constructor que se pueden usar en todo el cuerpo struct, en cualquier declaración de miembro para esa estructura.

Si se puede acceder a todos los campos de instancia de un tipo de estructura, también puede crear una instancia de él sin el operador new. En ese caso, debe inicializar todos los campos de instancia antes del primer uso de la instancia. En el siguiente ejemplo se muestra cómo hacerlo:

public static class StructWithoutNew
{
    public struct Coords
    {
        public double x;
        public double y;
    }

    public static void Main()
    {
        Coords p;
        p.x = 3;
        p.y = 4;
        Console.WriteLine($"({p.x}, {p.y})");  // output: (3, 4)
    }
}

En el caso de los tipos de valor integrados, use los literales correspondientes para especificar un valor del tipo.

Limitaciones del diseño de un tipo de estructura

Las estructuras tienen la mayoría de las funcionalidades de un tipo class. Hay algunas excepciones que se han quitado en versiones más recientes:

  • Un tipo de estructura no puede heredar de otro tipo de estructura o clase ni puede ser la base de una clase. Pero un tipo de estructura puede implementar interfaces.
  • No se puede declarar un finalizador dentro de un tipo de estructura.
  • Antes de C# 11, un constructor de un tipo de estructura debe inicializar todos los campos de instancia del tipo.

Pasar variables de tipo de estructura por referencia

Cuando se pasa una variable de tipo de estructura a un método como argumento o se devuelve un valor de tipo de estructura a partir de un método, se copia toda la instancia de un tipo de estructura. El método de paso por valor puede afectar al rendimiento del código en escenarios de alto rendimiento que impliquen tipos de estructura grandes. Puede evitar la copia de valores si pasa una variable de tipo de estructura por referencia. Use los modificadores de parámetro de método ref, out, ino ref readonly para indicar que se debe pasar un argumento por referencia. Use valores devueltos de tipo ref para devolver el resultado de un método por referencia. Para más información, consulte Cómo evitar asignaciones.

Restricción struct

Use también la palabra clave struct de la restricción struct para especificar que un parámetro de tipo es un tipo de valor que no acepta valores NULL. Los tipos de estructura y enumeración satisfacen la restricción struct.

Conversiones

Para cualquier tipo de estructura (excepto los tipos de ref struct), hay conversiones boxing y unboxing a los tipos System.ValueType y System.Object y también desde ellos. También existen conversiones boxing y unboxing entre un tipo de estructura y cualquier interfaz que implemente.

Especificación del lenguaje C#

Para más información, vea la sección Estructuras de la especificación del lenguaje C#.

Para más información sobre las características de struct, consulte las siguientes notas de propuestas de características:

Consulte también