Типы структур (справочник по C#)

Тип структуры представляет собой тип значения, который может инкапсулировать данные и связанные функции. Для определения типа структуры используется ключевое слово 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})";
}

Типы структуры имеют семантики значений. То есть переменная типа структуры содержит экземпляр этого типа. По умолчанию значения переменных копируются при назначении, передаче аргумента в метод и возврате результата метода. Для переменных типа структуры копируется экземпляр типа. Дополнительные сведения см. в разделе Типы значений.

Как правило, типы структуры используются для проектирования небольших ориентированных на данные типов, которые предоставляют минимум поведения или не предоставляют его вовсе. Например, платформа .NET использует типы структуры для представления числа (как целого, так и вещественного), логического значения, символа Юникода, экземпляра времени. Если вы сконцентрированы на поведении типа, рекомендуется определить класс. Типы классов имеют семантики ссылок. То есть переменная типа класса содержит ссылку на экземпляр этого типа, а не сам экземпляр.

Поскольку типы структуры имеют семантику значений, рекомендуется определять неизменяемые типы структуры.

Структура readonly

Начиная с C# версии 7.2, чтобы объявить, что тип структуры является неизменяемым, используйте модификатор readonly. Все элементы данных структуры readonly должны быть доступны только для чтения:

  • Любое объявление поля должно иметь readonly модификатор.
  • Все свойства, включая автоматические реализованные, должны быть доступны только для чтения. В C# 9.0 и более поздних версий свойство может иметь метод доступа init.

Это гарантирует, что ни один из элементов структуры readonly не изменит состояние структуры. В C# 8.0 и более поздних версиях это означает, что другие члены экземпляра, кроме конструкторов, неявно readonly.

Примечание

В структуре readonly элемент данных изменяемого ссылочного типа по-прежнему может изменять свое собственное состояние. Например, вы не можете заменить экземпляр List<T>, но можете добавить в него новые элементы.

В следующем коде определяется структура readonly с методами задания свойств только для инициализации, которые доступны в C# 9.0 и более поздних версий:

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

Члены экземпляров readonly

Начиная с C# 8.0, можно также использовать модификатор readonly, чтобы объявить, что член экземпляра не изменяет состояние структуры. Если не удается объявить весь тип структуры как readonly, используйте модификатор readonly, чтобы пометить члены экземпляров, которые не изменяют состояние структуры.

В члене экземпляра readonly невозможно назначать поля экземпляра структуры. Однако член readonly может вызвать член, не являющийся readonly. В этом случае компилятор создает копию экземпляра структуры и вызывает в ней член, не являющийся readonly. В результате исходный экземпляр структуры не изменяется.

Как правило, модификатор readonly применяется к следующим типам элементов экземпляров.

  • Методы.

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

    Можно также применить модификатор readonly к методам, переопределяющим методы, объявленные в System.Object.

    public readonly override string ToString() => $"({X}, {Y})";
    
  • Свойства и индексаторы.

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

    Если необходимо применить модификатор readonly к методам доступа свойства или индексатора, примените его в объявлении свойства или индексатора.

    Примечание

    Компилятор объявляет метод доступа getавтоматически реализуемого свойства как readonly независимо от наличия модификатора readonly в объявлении свойства.

    В C# 9.0 и более поздних версий вы можете применить модификатор readonly к свойству или индексатору с помощью метода доступа init:

    public readonly double X { get; init; }
    

Модификатор readonly можно применить к статическим полям типа структуры, но не к другим статическим элементам, таким как свойства или методы.

Компилятор может использовать модификатор readonly для оптимизации производительности. Дополнительные сведения см. в статье Написание безопасного и эффективного кода C#.

Обратимое изменение

Начиная с C# 10 можно использовать выражение with для создания копии экземпляра с типом структуры, в котором изменяются указанные свойства и поля. Как показано в следующем примере, для указания элементов для изменения и их новых значений используется синтаксис инициализатора объектов.

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

Структура record

Начиная с C# 10 можно определить типы структур записей. Типы записей предоставляют встроенные функции для инкапсулирования данных. Можно определить оба record struct типа и readonly record struct типы. Запись не может быть структуройref. Дополнительные сведения и примеры см. в разделе "Записи".

Инициализация структуры и значения по умолчанию

Переменная struct типа напрямую содержит данные для этого struct. Это создает различие между неинициализированным structзначением по умолчанию и инициализированным struct, в котором хранятся значения, заданные путем создания. Например, рассмотрим следующий код:

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 ()
}

Как показано в предыдущем примере, выражение значения по умолчанию игнорирует конструктор без параметров и создает значение по умолчанию типа структуры. При создании экземпляра массива типа структуры также игнорируется конструктор без параметров и создается массив, заполненный значениями по умолчанию для типа структуры.

Наиболее распространенная ситуация, когда значения по умолчанию отображаются в массивах или в других коллекциях, где внутреннее хранилище содержит блоки переменных. В следующем примере создается массив из 30 TemperatureRange структур, каждый из которых имеет значение по умолчанию:

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

Все поля элементов структуры должны быть определенно назначены при его создании, так как struct типы хранят их данные напрямую. default Значение структуры определенно присвоило всем полям значение 0. Все поля должны быть определенно назначены при вызове конструктора. Вы инициализируете поля с помощью следующих механизмов:

  • Инициализаторы полей можно добавлять в любое поле или автоматически реализованное свойство.
  • Вы можете инициализировать любые поля или автоматические свойства в теле конструктора.

Начиная с C# 11, если не инициализировать все поля в структуре, компилятор добавляет код в конструктор, который инициализирует эти поля в значение по умолчанию. Компилятор выполняет обычный анализ определенного назначения. Все поля, к которым осуществляется доступ до назначения, или не назначены определенно, когда конструктор завершает выполнение, назначаются значения по умолчанию перед выполнением текста конструктора. Если this доступ осуществляется до назначения всех полей, то она инициализируется значением по умолчанию перед выполнением текста конструктора.

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 ()
}

Каждый struct имеет public конструктор без параметров. Если вы пишете конструктор без параметров, он должен быть открытым. Если вы не напишете открытый конструктор без параметров, компилятор создаст его. Созданный компилятор конструктор без параметров выполняет все инициализации поля и создает значение по умолчанию для всех остальных полей. При объявлении инициализаторов полей необходимо объявить один явный конструктор. Один явный конструктор может быть конструктором без параметров. Он может иметь пустое тело. Дополнительные сведения см. в примечании к предложению новой возможности в разделе Конструкторы структур без параметров.

Если все поля экземпляров типа структуры доступны, можно также создать его экземпляр без оператора new. В этом случае необходимо инициализировать все поля экземпляров перед первым использованием экземпляра. Следующий пример показывает, как это сделать:

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

В случае встроенных типов значения используйте соответствующие литералы, чтобы указать значение типа.

Ограничения при проектировании типа структуры

Структуры имеют большую часть возможностей типа класса . Существуют некоторые исключения и некоторые исключения, которые были удалены в более поздних версиях:

  • Тип структуры не может наследовать от другого типа класса или структуры и не может быть базовым для класса. Однако тип структуры может реализовывать интерфейсы.
  • Вы не можете объявить метод завершения в типе структуры.
  • До C# 11 конструктор типа структуры должен инициализировать все поля экземпляра типа.
  • До C# 10 нельзя объявить конструктор без параметров.
  • До C# 10 нельзя инициализировать поле экземпляра или свойство в его объявлении.

Передача переменных типа структуры по ссылке

При передаче переменной типа структуры в метод в качестве аргумента или возврате значения типа структуры из метода копируется весь экземпляр типа структуры. Передача по значению может повлиять на производительность кода в сценариях с высокой производительностью, в которых используются большие типы структур. Копирования значений можно избежать, передав переменную типа структуры по ссылке. Используйте модификаторы параметра метода ref, out или in, чтобы указать, что аргумент должен передаваться по ссылке. Чтобы возвратить результат метода по ссылке, используйте ref returns. Дополнительные сведения см. в статье Написание безопасного и эффективного кода C#.

Структура ref

Начиная с C# 7.2 в объявлении типа структуры можно использовать модификатор ref. Экземпляры типа структуры ref выделяются в стеке и не могут временно перейти в управляемую кучу. Для этого компилятор ограничивает использование типов структуры ref следующим образом:

  • Структура ref не может быть типом элемента массива.
  • Структура ref не может быть объявленным типом поля класса или структурой, отличной отref.
  • Структура ref не может реализовывать интерфейсы.
  • Структура ref не может быть упакована в System.ValueType или System.Object.
  • Структура ref не может быть аргументом типа.
  • Переменная структуры ref не может быть зафиксирована лямбда-выражением или локальной функцией.
  • Переменную структуры ref нельзя использовать в методе async. Однако переменные структуры можно использовать ref в синхронных методах, например в методах, возвращающих Task или Task<TResult>.
  • Переменную структуры ref нельзя использовать в итераторах.

Начиная с C# 8.0 вы можете определить разовую структуру ref. Для этого убедитесь, что структура ref соответствует разовому шаблону, то есть имеет экземпляр или расширение метода Dispose, который доступен, не имеет параметров и имеет тип возвращаемого значения void.

Как правило, тип структуры ref определяется, если требуется тип, который также содержит члены данных типов структуры ref:

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

Чтобы объявить структуру ref как readonly, объедините модификаторы readonly и ref в объявлении типа (модификатор readonly должен предшествовать модификатору 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; }
}

В .NET примерами структуры ref являются System.Span<T> и System.ReadOnlySpan<T>.

Ограничение struct

Вы можете использовать ключевое слово struct в ограничении struct, чтобы указать, что параметр типа является типом значения, не допускающим значения NULL. Типы структуры и перечисления удовлетворяют ограничению struct.

Преобразования

Для любого типа структуры (за исключением типов структурыref) существует упаковка-преобразование и распаковка-преобразование в типы System.ValueType и System.Object и из них. Существуют упаковка-преобразование и распаковка-преобразование между типом структуры и любым интерфейсом, который он реализует.

Спецификация языка C#

Дополнительные сведения см. в разделе Структуры в спецификации языка C#.

Дополнительные сведения о функциях, появившихся в C# 7.2 и более поздних версиях, см. в следующих заметках о функциях.

См. также