Автоматические структуры по умолчанию
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Это включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих заметках с собрания по дизайну языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
https://github.com/dotnet/csharplang/issues/5737
Сводка
Эта функция позволяет в конструкторах структур определять поля, которые не были явно назначены пользователем перед возвратом или перед использованием, и инициализировать их неявно для default
вместо предоставления определенных ошибок назначения.
Мотивация
Это предложение предлагается как возможное устранение проблем удобства использования, найденных в dotnet/csharplang#5552 и dotnet/csharplang#5635, а также для устранения #5563 (все поля должны быть однозначно назначены, но field
недоступен в конструкторе).
С момента выхода C# 1.0, для конструкторов структур требовалось обязательно назначать this
, как если бы он был параметром out
.
public struct S
{
public int x, y;
public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
{
}
}
При этом возникают проблемы, когда сеттеры задаются вручную в полуавтоматических свойствах, так как компилятор не может рассматривать присвоение свойства как эквивалентное присвоению вспомогательного поля.
public struct S
{
public int X { get => field; set => field = value; }
public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
{
}
}
Мы предполагаем, что введение более точных ограничений для сеттеров, таких как схема, в которой сеттер принимает не ref this
, а out field
в качестве параметра, будет слишком узкой и неполной для некоторых вариантов использования.
Одна из основных трудностей заключается в том, что когда свойства структуры имеют вручную реализованные сеттеры, пользователям часто приходится делать некоторые виды "повторения": либо повторного назначения, либо повторения логики.
struct S
{
private int _x;
public int X
{
get => _x;
set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
}
// Solution 1: assign some value in the constructor before "really" assigning through the property setter.
public S(int x)
{
_x = default;
X = x;
}
// Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
public S(int x)
{
_x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
}
}
Предыдущее обсуждение
Небольшая группа рассмотрела эту проблему и рассмотрела несколько возможных решений:
- Требовать, чтобы пользователи назначали
this = default
, когда полуавтоматические свойства имеют вручную реализованные установщики. Мы согласны с тем, что это неправильное решение, так как оно перезаписывает значения, заданные в инициализаторах полей. - Неявно инициализировать все вспомогательные поля авто/полуавтоматических свойств.
- Это решает проблему "полуавтоматических установщиков свойств", и прямо подчеркивает, что для явно объявленных полей действуют другие правила: "не выполнять неявную инициализацию моих полей, но выполнять неявную инициализацию моих автосвойств".
- Предоставьте способ назначить резервное поле полуавтоматового свойства и требовать от пользователей назначить его.
- Это может быть громоздким по сравнению с (2). Автоматическое свойство должно быть "автоматическим" и, возможно, включает "автоматическую" инициализацию поля. Это может привести к путанице в том, когда базовое поле назначается назначением свойству, а также при вызове метода задания свойств.
Мы также получили отзывы от пользователей, которые хотят, например, включить несколько инициализаторов полей в структуры без необходимости явно назначать все. Эту проблему можно решить одновременно с проблемой полуавтоматического свойства с вручную реализованным сеттером.
struct MagnitudeVector3d
{
double X, Y, Z;
double Magnitude = 1;
public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
{
}
}
Настройка определенного назначения
Вместо выполнения анализа инициализации для выявления ошибок из-за неназначенных полей в this
, мы делаем это, чтобы определить, какие поля необходимо инициализировать неявно. Такая инициализация вставляется в начале конструктора.
struct S
{
int x, y;
// Example 1
public S()
{
// ok. Compiler inserts an assignment of `this = default`.
}
// Example 2
public S()
{
// ok. Compiler inserts an assignment of `y = default`.
x = 1;
}
// Example 3
public S()
{
// valid since C# 1.0. Compiler inserts no implicit assignments.
x = 1;
y = 2;
}
// Example 4
public S(bool b)
{
// ok. Compiler inserts assignment of `this = default`.
if (b)
x = 1;
else
y = 2;
}
// Example 5
void M() { }
public S(bool b)
{
// ok. Compiler inserts assignment of `y = default`.
x = 1;
if (b)
M();
y = 2;
}
}
В примерах (4) и (5) результирующий кодеген иногда имеет "двойные назначения" полей. Как правило, это хорошо, но для пользователей, которые обеспокоены такими двойными назначениями, мы можем вывести то, что раньше было определенной диагностикой ошибок назначения, так как отключен по умолчанию диагностику предупреждений.
struct S
{
int x;
public S() // warning: 'S.x' is implicitly initialized to 'default'.
{
}
}
Пользователи, которые устанавливают уровень серьезности этой диагностики на "ошибка", выбирают поведение, существовавшее до C# 11. Такие пользователи, по сути, исключаются из полуавтоматических свойств с вручную реализованными сеттерами.
struct S
{
public int X
{
get => field;
set => field = field < value ? value : field;
}
public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
{
X = 1;
}
}
На первый взгляд, это кажется "недостатком" в функции, но это на самом деле так и должно быть. Включив диагностику, пользователь сообщает нам, что компилятор не хочет неявно инициализировать свои поля в конструкторе. Здесь нет способа избежать неявной инициализации, поэтому решение для них заключается в использовании другого способа инициализации поля, чем вручную реализованное средство задания, например объявление поля вручную и назначение его или включение инициализатора полей.
В настоящее время JIT не устраняет мертвые хранилища через ссылки, что означает, что эти неявные инициализации имеют реальную стоимость. Но это может быть исправлено. https://github.com/dotnet/runtime/issues/13727
Стоит отметить, что инициализация отдельных полей вместо всего экземпляра на самом деле является всего лишь оптимизацией. Компилятор, вероятно, должен быть свободен реализовывать любую эвристику, если он удовлетворяет инварианту, что поля, которые не обязательно назначаются во всех точках возврата или до любого обращения к члену, не являющемуся полем, this
, инициализируются неявно.
Например, если у структуры есть 100 полей, и только одна из них явно инициализирована, это может быть более целесообразно сделать initobj
ко всему, чем неявно выдавать initobj
для 99 других полей. Однако реализация, которая неявно выдает initobj
для 99 других полей, по-прежнему будет допустимой.
Изменения спецификации языка
Мы корректируем следующий раздел стандарта:
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access
Если объявление конструктора не имеет инициализатора конструктора, переменная
this
ведет себя точно так же, как и параметрout
типа структуры. В частности, это означает, что переменная должна быть однозначно назначена на каждом пути выполнения конструктора экземпляра.
Мы изменяем формулировку так:
Если объявление конструктора не имеет инициализатора конструктора, переменная this
ведет себя аналогично параметру out
типа структуры, за исключением того, что это не является ошибкой, если определенные требования к назначению (§9.4.1) не выполняются. Вместо этого мы введем следующее поведение:
- Если сама переменная
this
не соответствует требованиям, то все неназначенные переменные экземпляра вthis
во всех точках, где требования нарушены, неявно инициализируются значением по умолчанию (§9.3) на этапе инициализации перед выполнением любого другого кода в конструкторе. - Если переменная экземпляра v в
this
не соответствует требованиям, или любая переменная экземпляра на любом уровне вложенности в v не соответствует требованиям, то v неявно инициализирована в значение по умолчанию на этапе инициализации перед выполнением любого другого кода в конструкторе.
Совещания по дизайну
C# feature specifications