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})";
}

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

A partir de C# 7.2, 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:

  • Cualquier declaración de campo debe tener el modificador readonly
  • Cualquier propiedad, incluidas las implementadas automáticamente, debe ser de solo lectura. En C# 9.0 y versiones posteriores, una propiedad puede tener un descriptor de acceso init.

Esto garantiza que ningún miembro de una estructura readonly modifique el estado de la misma. En C# 8.0 y en versiones posteriores, 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.

En el código siguiente se define una estructura readonly con establecedores de propiedad de solo inicialización, disponibles en C# 9.0 y versiones posteriores:

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

A partir de C# 8.0, 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 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 descriptor de acceso get de una propiedad implementada automáticamente como readonly, con independencia de la presencia del modificador readonly en la declaración de una propiedad.

    En C# 9.0 y versiones posteriores, puede aplicar el modificador readonly a una propiedad o un indizador con un descriptor de acceso init:

    public readonly double X { get; init; }
    

Puede aplicar el readonly modificador 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 Escritura de código C# seguro y eficaz.

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 registros. Los tipos de registro proporcionan funcionalidad integrada para encapsular datos. Puede definir ambos record struct tipos y readonly record struct . Un struct de registro no puede ser un ref struct. Para obtener más información y ejemplos, vea Registros.

Inicialización de estructura y valores predeterminados

Una variable de un struct tipo contiene directamente los datos de .struct Esto crea una distinción entre un elemento sin inicializar struct, que tiene su valor predeterminado y un inicializado struct, 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 predeterminada 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á valores predeterminados es en matrices o en otras colecciones donde el almacenamiento interno incluye bloques de variables. En el ejemplo siguiente se crea una matriz de 30 TemperatureRange estructuras, cada una de las cuales tiene el valor predeterminado:

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

Todos los campos de miembro de un struct deben asignarse definitivamente cuando se crean porque struct los tipos almacenan directamente sus datos. El default valor 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 siguientes mecanismos:

  • 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 no se asignan definitivamente cuando el constructor termina de ejecutarse se asignan sus valores predeterminados antes de que se ejecute el cuerpo del constructor. Si this se tiene acceso antes de asignar 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 public constructor sin parámetros. Si escribe un constructor sin parámetros, debe ser público. Si no escribiría un constructor público sin parámetros, el compilador genera uno. El constructor sin parámetros generado por el compilador ejecuta cualquier campo inicializa y genera el valor predeterminado para todos los demás campos. Si declara algún inicializador de campo, debe declarar un constructor explícito. El único constructor explícito puede ser el constructor sin parámetros. Puede tener un cuerpo vacío. Para obtener más información, vea la nota propuesta de la característica Constructores de structs sin parámetros.

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 de clase . Hay algunas excepciones y 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.
  • Antes de C# 10, no se puede declarar un constructor sin parámetros.
  • Antes de C# 10, no se puede inicializar un campo o propiedad de instancia en su declaración.

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. Pasar por valor puede afectar al rendimiento del código en escenarios de alto rendimiento que implican 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 o in para indicar que un argumento se debe pasar por referencia. Use valores devueltos de tipo ref para devolver el resultado de un método por referencia. Para más información, consulte Escritura de código C# seguro y eficaz.

Estructura ref

A partir de C# 7.2, puede usar el modificador ref en la declaración de un tipo de estructura. Las instancias de un tipo de estructura ref se asignan en la pila y no pueden escapar al montón administrado. Para asegurarse de eso, el compilador limita el uso de tipos de estructura ref de la manera siguiente:

  • Una estructura ref no puede ser el tipo de elemento de una matriz.
  • Una estructura ref no puede ser un tipo declarado de un campo de una clase o una estructura que no sea ref.
  • Una estructura ref no puede implementar interfaces.
  • En una estructura ref no se puede aplicar una conversión boxing a System.ValueType ni System.Object.
  • Una estructura ref no puede ser un argumento de tipo.
  • Una ref variable de estructura no se puede capturar mediante una expresión lambda o una función local.
  • Una variable de estructura ref no se puede usar en un método async. Sin embargo, puede usar ref variables de estructura en métodos sincrónicos, por ejemplo, en métodos que devuelven Task o Task<TResult>.
  • Una variable de estructura ref no se puede usar en iteradores.

A partir de C# 8.0, puede definir una estructura ref descartable. Para ello, asegúrese de que una estructura ref se ajuste al patrón descartable. Es decir, tiene una instancia o un método Dispose de extensión, que es accesible, no tiene parámetros y tiene un tipo de valor devuelto void.

Normalmente, se define un tipo de estructura ref cuando se necesita un tipo que también incluye miembros de datos de tipos de estructura ref:

public ref struct CustomRef
{
    public bool IsValid;
    public Span<int> Inputs;
    public Span<int> Outputs;
}

Para declarar una estructura ref como readonly, combine los modificadores readonly y ref en la declaración de tipos (el modificador readonly debe ir delante del modificador ref):

public readonly ref struct ConversionRequest
{
    public ConversionRequest(double rate, ReadOnlySpan<double> values)
    {
        Rate = rate;
        Values = values;
    }

    public double Rate { get; }
    public ReadOnlySpan<double> Values { get; }
}

En .NET, System.Span<T> y System.ReadOnlySpan<T> son ejemplos de una estructura ref.

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 estructura ref), existen conversiones boxing y unboxing a y desde los tipos System.ValueType y System.Object. 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 obtener más información sobre de las características presentadas en C# 7.2 y versiones posteriores, vea las siguientes notas de propuesta de características:

Vea también