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. En el caso de una variable de tipo de 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 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 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; }
    

No se puede aplicar el modificador readonly a los miembros estáticos de un tipo de estructura.

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 para generar una copia de una instancia de tipo de estructura con las propiedades y los campos especificados modificados. with 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)
}

Limitaciones del diseño de un tipo de estructura

Al diseñar un tipo de estructura, tiene las mismas funciones que con un tipo de clase, con las siguientes excepciones:

  • No se puede declarar un constructor sin parámetros. Cada tipo de estructura ya proporciona un constructor sin parámetros implícito que genera el valor predeterminado del tipo.

    Nota

    A partir de C# 10, puede declarar un constructor sin parámetros en un tipo de estructura. Para obtener más información, vea la sección Constructores sin parámetros e inicializadores de campo.

  • No se puede inicializar una propiedad o un campo de instancia en su declaración. Pero se puede inicializar un campo static o const, o una propiedad estática en su declaración.

    Nota

    A partir de C# 10, puede inicializar un campo de instancia o una propiedad en su declaración. Para obtener más información, vea la sección Constructores sin parámetros e inicializadores de campo.

  • Un constructor de un tipo de estructura debe inicializar todos los campos de instancia del tipo.

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

Constructores sin parámetros e inicializadores de campo

A partir de C# 10, se puede declarar un constructor de instancia sin parámetros en un tipo de estructura, tal como se muestra en el siguiente ejemplo:

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 muestra el ejemplo anterior, la expresión de valor predeterminado ignora un constructor sin parámetros y produce el valor predeterminado de un tipo de estructura, que es el valor producido al establecer todos los campos de tipo valor en sus valores predeterminados (el patrón de 0 bits) y todos los campos de tipo referencia en null. 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.

A partir de C# 10, también puede inicializar un campo o propiedad de instancia en su declaración, tal como se muestra en el ejemplo siguiente:

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

    public Measurement(double value, string description)
    {
        Value = value;
        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 ()
}

Si no se declara explícitamente un constructor sin parámetros, un tipo de estructura proporciona un constructor sin parámetros cuyo comportamiento es el siguiente:

  • Si un tipo de estructura tiene constructores de instancia explícitos o no tiene inicializadores de campo, un constructor sin parámetros implícito genera el valor predeterminado de un tipo de estructura, independientemente de los inicializadores de campo, como se muestra en el ejemplo anterior.

  • Si un tipo de estructura no tiene constructores de instancia explícitos y tiene inicializadores de campo, el compilador sintetiza un constructor público sin parámetros que realiza las inicializaciones de campo especificadas, como se muestra en el ejemplo siguiente:

    public struct Coords
    {
        public double X = double.NaN;
        public double Y = double.NaN;
    
        public override string ToString() => $"({X}, {Y})";
    }
    
    public static void Main()
    {
        var p1 = new Coords();
        Console.WriteLine(p1);  // output: (NaN, NaN)
    
        var p2 = default(Coords);
        Console.WriteLine(p2);  // output: (0, 0)
    
        var ps = new Coords[3];
        Console.WriteLine(string.Join(", ", ps));  // output: (0, 0), (0, 0), (0, 0)
    }
    

Como se muestra en el ejemplo anterior, la expresión de valor predeterminado y la creación de instancias de matriz omiten los inicializadores de campo.

Para obtener más información, vea la nota propuesta de la característica Constructores de structs sin parámetros.

Creación de instancias de un tipo de estructura

En C#, debe inicializar una variable declarada antes de poder usarla. Como una variable de tipo de estructura no puede ser null (a menos que sea una variable de un tipo de valor que admite valores NULL), tendrá que crear instancias de una instancia del tipo correspondiente. Existen varias formas de hacerlo.

Normalmente, para crear una instancia de un tipo de estructura, se llama a un constructor adecuado con el operador new. Todos los tipos de estructura tienen al menos un constructor. Se trata de un constructor sin parámetros implícito, que genera el valor predeterminado del tipo. También puede usar una expresión de valor predeterminado para generar el valor predeterminado de un tipo.

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.

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. Esto 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 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. Aunque se pueden usar variables de estructura ref en métodos sincrónicos, como, por ejemplo, en los que devuelven TaskTask<TResult>.
  • Una variable de estructura ref no se puede usar en iteradores.

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