Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Это включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия зафиксированы на соответствующих собраниях по проектированию языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Проблема чемпиона: https://github.com/dotnet/csharplang/issues/2691
Сводка
Классы и структуры могут иметь список параметров, а их спецификация базового класса может иметь список аргументов. Основные параметры конструктора находятся в области действия в объявлении класса или структуры, и, если они фиксируются членом функции или анонимной функцией, они хранятся соответствующим образом (например, как недоступные частные поля объявленного класса или структуры).
Предложение ретроспективно изменяет первичные конструкторы, уже доступные для записей, с помощью этой более общей функции, синтезируя некоторые дополнительные элементы.
Мотивация
Способность класса или структуры в C# иметь несколько конструкторов обеспечивает гибкость, но за счет некоторой утомительности в синтаксисе объявления, так как входные данные конструктора и состояние класса должны быть четко разделены.
Первичные конструкторы помещают параметры одного конструктора в область действия для всего класса или структуры, используемой для инициализации или непосредственно в качестве состояния объекта. Ограничение заключается в том, что любые другие конструкторы должны вызываться через основной конструктор.
public class B(bool b) { } // base class
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(S));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Подробный дизайн
В этом разделе описывается обобщенная конструкция для записей и не записей, а затем сведения о том, как указываются существующие первичные конструкторы для записей путем добавления набора синтезированных элементов в присутствии первичного конструктора.
Синтаксис
Объявления классов и структур дополняются, чтобы разрешить список параметров в имени типа, список аргументов базового класса и текст, состоящий только из ;
:
class_declaration
: attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
parameter_list? class_base? type_parameter_constraints_clause* class_body
;
class_designator
: 'record' 'class'?
| 'class'
class_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
class_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
struct_declaration
: attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
;
struct_body
: '{' struct_member_declaration* '}' ';'?
| ';'
;
interface_declaration
: attributes? interface_modifier* 'partial'? 'interface'
identifier variant_type_parameter_list? interface_base?
type_parameter_constraints_clause* interface_body
;
interface_body
: '{' interface_member_declaration* '}' ';'?
| ';'
;
enum_declaration
: attributes? enum_modifier* 'enum' identifier enum_base? enum_body
;
enum_body
: '{' enum_member_declarations? '}' ';'?
| '{' enum_member_declarations ',' '}' ';'?
| ';'
;
Примечание. эти рабочие среды заменяют record_declaration
в записях и record_struct_declaration
в структуры записей, которые оба становятся устаревшими.
Ошибкой для class_base
будет иметь argument_list
, если окружающий class_declaration
не содержит parameter_list
. Максимум одно частичное объявление типа частичного класса или структуры может содержать parameter_list
. Параметры в parameter_list
объявления record
должны быть параметрами значений.
Обратите внимание, что в соответствии с этим предложением class_body
, struct_body
, interface_body
и enum_body
могут состоять только из ;
.
Класс или структура с parameter_list
имеет неявный открытый конструктор, подпись которого соответствует параметрам значения объявления типа. Это называется первичным конструктором для типа и приводит к тому, что неявно объявленный конструктор без параметров, если он присутствует, подавляется. Это ошибка, когда в объявлении типа присутствует основной конструктор и конструктор с той же сигнатурой.
Поиск
поиск простых имен дополнен для обработки параметров основного конструктора. Изменения выделены в полужирным шрифтом в следующем фрагменте:
- В противном случае для каждого типа экземпляра
T
(§15.3.2), начиная с типа экземпляра ближайшего к нему объявления типа и продолжая типом экземпляра каждого окружающего класса или структуры (если таковые имеются):
- Если объявление
T
включает в себя основной параметр конструктораI
и ссылка возникает вargument_list
class_base
T
или в рамках инициализатора поля, свойства или событияT
, то результатом является основной параметр конструктораI
- В противном случае, если
e
равно нулю, а объявлениеT
включает параметр типа с именемI
, то simple_name ссылается на этот параметр типа.- В противном случае, если запрос элемента (§12.5)
I
вT
с аргументами типаe
создает совпадение:
- Если
T
является типом экземпляра для непосредственно содержащего класса или типа структуры, и поиск определяет один или несколько методов, результатом является группа методов с ассоциированным выражением экземпляраthis
. Если указан список аргументов типа, он используется при вызове универсального метода (§12.8.10.2).- В противном случае, если
T
является типом экземпляра непосредственно содержащего класса или типа структуры, если поиск идентифицирует элемент экземпляра, и если ссылка возникает в блоке конструктора экземпляра, метода экземпляра или метода доступа экземпляра (§12.2.1), результатом является то же, что и доступ к члену (§12.8.7) формыthis.I
. Это может произойти только в том случае, еслиe
равно нулю.- В противном случае результат совпадает с доступом к члену (§12.8.7) формы
T.I
илиT.I<A₁, ..., Aₑ>
.- В противном случае, если объявление
T
содержит основной параметр конструктораI
, результатом является основной параметр конструктораI
.
Первое добавление соответствует изменению, произведенному основными конструкторами для записей, и гарантирует, что параметры первичного конструктора указаны перед любыми соответствующими полями в инициализаторах и аргументах базового класса. Это правило также расширяется на статические инициализаторы. Однако, поскольку записи всегда имеют член экземпляра с тем же именем, что и указанный параметр, расширение может привести только к изменению сообщения об ошибке. Незаконный доступ к параметру против незаконного доступа к члену экземпляра.
Второе нововведение позволяет находить параметры первичного конструктора в другом месте тела типа, но только в том случае, если они не затенены элементами.
Ошибка заключается в обращении к основному параметру конструктора, если обращение не происходит в одном из следующих случаев.
- аргумент
nameof
- инициализатор поля экземпляра, свойства или события объявляющего типа (тип, объявляющий первичный конструктор с параметром).
-
argument_list
class_base
декларативного типа. - Текст метода экземпляра (обратите внимание, что конструкторы экземпляров исключены) деклараированного типа.
- текст метода доступа экземпляра декларативного типа.
Другими словами, основные параметры конструктора находятся в области в тексте декларативного типа. Они скрывают члены объявляющего типа внутри инициализатора поля, свойства или события объявляющего типа, или внутри argument_list
типа class_base
объявляющего типа. Они затеняются членами декларативного типа везде.
Таким образом, в следующем объявлении:
class C(int i)
{
protected int i = i; // references parameter
public int I => i; // references field
}
Инициализатор для поля i
ссылается на параметр i
, а текст свойства I
ссылается на поле i
.
Предупреждать о тени члена из базы
Компилятор выдает предупреждение об использовании идентификатора, когда базовый элемент теняет параметр первичного конструктора, если этот основной параметр конструктора не был передан базовому типу через его конструктор.
Основной параметр конструктора считается передаваемым в базовый тип с помощью конструктора, если все следующие условия верны для аргумента в class_base:
- Аргумент представляет неявное или явное преобразование идентичности основного параметра конструктора.
- Аргумент не является частью развернутого
params
аргумента;
Семантика
Основной конструктор ведет к созданию конструктора экземпляра в охватывающем типе с заданными параметрами. Если class_base
содержит список аргументов, конструктор созданного экземпляра будет иметь инициализатор base
с тем же списком аргументов.
Основные параметры конструктора в объявлениях класса или структуры можно объявить ref
, in
или out
. Объявление параметров ref
или out
остается недопустимым в первичных конструкторах объявления записей.
Все инициализаторы членов экземпляра в теле класса будут преобразованы в присвоения в созданном конструкторе.
Если элемент экземпляра ссылается на основной параметр конструктора, и ссылка не находится внутри аргумента nameof
, она фиксируется в состоянии содержащего типа, чтобы параметр оставался доступным после окончания конструктора. Скорее всего, стратегия реализации заключается в использовании частного поля с искажённым именем. В структуре только для чтения поля захвата будут только для чтения. Таким образом, доступ к захваченным параметрам структуры только для чтения будет иметь аналогичные ограничения, как доступ к полям только для чтения. Доступ к захваченным параметрам в члене readonly будет иметь аналогичные ограничения, как доступ к полям экземпляра в том же контексте.
Запись не допускается для параметров, имеющих тип ссылок, и запись не допускается для ref
, in
или out
параметров. Это аналогично ограничению на захват переменных в лямбда-функциях.
Если параметр первичного конструктора используется только в инициализаторах членов экземпляра, они могут напрямую ссылаться на параметр сгенерированного конструктора, поскольку выполняются как часть его процесса.
Основной конструктор выполняет следующую последовательность операций:
- Значения параметров хранятся в полях записи, если таковые имеются.
- Инициализаторы экземпляров выполняются
- Инициализатор базового конструктора вызывается
Ссылки на параметры в любом пользовательском коде заменяются соответствующими ссылками на поля записи.
Например, это объявление:
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Создает код, аналогичный следующему:
public class C : B
{
public int I { get; set; }
public string S
{
get => __s;
set => __s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(0, s) { ... } // must call this(...)
// generated members
private string __s; // for capture of s
public C(bool b, int i, string s)
{
__s = s; // capture s
I = i; // run I's initializer
B(b) // run B's constructor
}
}
Ошибкой является, если объявление неосновного конструктора имеет тот же список параметров, что и основного конструктора. Все непервичные объявления конструктора должны использовать инициализатор this
, чтобы первичный конструктор в конечном счете вызывался.
Записи создают предупреждение, если первичный параметр конструктора не используется в инициализаторах экземпляра (возможно, созданных) или базовом инициализаторе. Аналогичные предупреждения будут выдаваться для параметров первичного конструктора в классах и структурах.
- для передаваемого по значению параметра, если параметр не захвачен и не считывается в инициализаторах экземпляра или базовом инициализаторе.
- для параметра
in
, если параметр не считывается ни в одном из инициализаторов экземпляра, ни в базовом инициализаторе. - для параметра
ref
, если параметр не считывается или записывается ни в какие инициализаторы экземпляров, ни в базовый инициализатор.
Идентичные простые имена и имена типов
Существует специальное правило языка для сценариев, которые часто называются сценариями "Цвет цвета" — идентичные простые имена и имена типов.
В доступе к члену формы
E.I
, еслиE
является одним идентификатором, а значениеE
в качестве simple_name (§12.8.4) является константой, полем, свойством, локальной переменной или параметром с таким же типом, что и значениеE
type_name (§7.8.1), тогда разрешены оба возможных значенияE
. Поиск элементовE.I
никогда не является неоднозначным, так какI
обязательно должен быть членом типаE
в обоих случаях. Другими словами, правило просто разрешает доступ к статическим элементам и вложенным типамE
, где в противном случае возникла бы ошибка времени компиляции.
Что касается первичных конструкторов, правило влияет на то, следует ли рассматривать идентификатор в элементе экземпляра в качестве ссылки на тип или как ссылку на параметр первичного конструктора, который, в свою очередь, сохраняет параметр для включения в состояние окружающего типа. Несмотря на то что "поиск члена E.I
никогда не является неоднозначным", когда поиск дает группу членов, в некоторых случаях невозможно определить, относится ли доступ к статическому элементу или члену экземпляра без полного разрешения (привязки) доступа к члену. В то же время захват основного параметра конструктора влияет на свойства окружающего типа таким образом, что затрагивает семантический анализ. Например, тип может стать неуправляемым и из-за этого нарушить определенные ограничения.
Существуют даже сценарии, для которых привязка может быть выполнена в любом случае, в зависимости от того, считается ли параметр захваченным или нет. Например:
struct S1(Color Color)
{
public void Test()
{
Color.M1(this); // Error: ambiguity between parameter and typename
}
}
class Color
{
public void M1<T>(T x, int y = 0)
{
System.Console.WriteLine("instance");
}
public static void M1<T>(T x) where T : unmanaged
{
System.Console.WriteLine("static");
}
}
Если мы будем рассматривать приемник Color
как значение, мы фиксируем параметр и S1 становится управляемым. Затем статический метод становится неприменимым из-за ограничения, и мы вызовем метод экземпляра. Однако если мы обрабатываем приемник как тип, мы не фиксируем параметр и "S1" остается неуправляемым, оба метода применимы, но статический метод "лучше", так как он не имеет необязательного параметра. Ни выбор не приводит к ошибке, но каждый из них приведет к отдельному поведению.
Учитывая это, компилятор выдаст ошибку неоднозначности для доступа к элементу E.I
при выполнении всех следующих условий:
- Поиск членов
E.I
возвращает группу членов, содержащую одновременно экземплярные и статические члены. Методы расширения, применяемые к типу приемника, для данной проверки рассматриваются как методы экземпляра. - Если
E
рассматривается как простое имя, а не имя типа, он будет ссылаться на первичный параметр конструктора и будет записывать параметр в состояние заключенного типа.
Предупреждения о двойном хранилище
Если основной параметр конструктора передается в базу и также захвачен, существует высокий риск того, что он непреднамеренно сохраняется дважды в объекте.
Компилятор выдаст предупреждение для in
или аргумента по значению в class_base
argument_list
, когда верны все следующие условия:
- Аргумент представляет собой неявную или явную идентичностную конверсию первичного параметра конструктора.
- Аргумент не является частью развернутого
params
аргумента; - Основной параметр конструктора сохраняется в состоянии вмещающего типа.
Компилятор создаст предупреждение для variable_initializer
, если все следующие условия верны:
- Инициализатор переменной представляет неявное или явное преобразование идентичности параметра первичного конструктора.
- Основной параметр конструктора фиксируется в состоянии заключенного типа.
Например:
public class Person(string name)
{
public string Name { get; set; } = name; // warning: initialization
public override string ToString() => name; // capture
}
Атрибуты, предназначенные для основных конструкторов
На https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md мы решили принять предложение https://github.com/dotnet/csharplang/issues/7047.
Целевое применение атрибута "method" разрешено на class_declaration/struct_declaration с parameter_list и приводит к тому, что соответствующий первичный конструктор получает этот атрибут.
Атрибуты с целевым объектом method
на class_declaration/struct_declaration без parameter_list игнорируются с предупреждением.
[method: FooAttr] // Good
public partial record Rec(
[property: Foo] int X,
[field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
public void Frobnicate()
{
...
}
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;
Основные конструкторы записей
С этим предложением записи больше не должны отдельно определять основной механизм конструктора. Вместо этого объявления классов и структур с первичными конструкторами будут следовать общим правилам с учетом следующих простых дополнений:
- Для каждого основного параметра конструктора, если элемент с тем же именем уже существует, он должен быть свойством экземпляра или полем. В противном случае синтезируется публичное свойство, доступное только для инициализации, с тем же именем, инициализируемое по значению параметра.
- Деконструктор синтезируется с параметрами out для сопоставления параметров первичного конструктора.
- Если явное объявление конструктора является "конструктором копирования" — конструктором, принимающим один параметр заключающего типа, то не требуется вызывать инициализатор
this
, и инициализаторы элементов, присутствующих в объявлении записи, не будут выполняться.
Недостатки
- Размер выделения созданных объектов менее очевиден, так как компилятор определяет, следует ли выделить поле для основного параметра конструктора на основе полного текста класса. Этот риск похож на неявный захват переменных лямбда-выражениями.
- Распространенный соблазн (или случайный шаблон) заключается в том, чтобы записать "один и тот же" параметр на нескольких уровнях наследования, так как он передается цепочке конструктора вместо явного выделения защищенного поля в базовом классе, что приводит к дублированию выделений для одних и того же данных в объектах. Это очень похоже на риск переопределения автоматических свойств сегодняшнего дня другими автоматическими свойствами.
- Как здесь предложено, не предусмотрено места для дополнительной логики, обычно выражаемой в телах конструктора. Расширение "тела основного конструктора" ниже устраняет эту проблему.
- Предлагается, что семантика порядка выполнения немного отличается от обычных конструкторов, откладывая инициализацию элементов до выполнения базовых вызовов. Это, вероятно, может быть исправлено, но по стоимости некоторых предложений расширения (в частности, "основного конструктора").
- Предложение работает только в сценариях, когда один конструктор может быть назначен первичным.
- Нет способа выразить раздельную доступность класса и основного конструктора. Пример заключается в том, что все общедоступные конструкторы делегируют задачи одному частному конструктору, создающему всё. При необходимости синтаксис можно предложить для этого позже.
Альтернативы
Нет записи
Гораздо более простая версия функции запрещает основным параметрам конструктора выполняться в органах членов. Ссылка на них будет ошибкой. Поля должны быть явно объявлены, если хранилище требуется за пределами кода инициализации.
public class C(string s)
{
public string S1 => s; // Nope!
public string S2 { get; } = s; // Still allowed
}
Это может быть доработано до полного предложения позже, что позволит избежать необходимости принимать ряд решений и усложнений, ценой того, что изначально будет убрано меньше стандартных элементов, и, возможно, будет выглядеть неинтуитивным.
Явные сгенерированные поля
Альтернативный подход заключается в том, что параметры первичного конструктора всегда автоматически создают поле с таким же именем. Вместо закрытия параметров таким же образом, как и локальные и анонимные функции, будет явно создано объявление члена, аналогичное общедоступным свойствам, созданным для основных параметров construcor в записях. Как и для записей, если подходящий элемент уже существует, он не будет создан.
Если созданное поле является закрытым, оно по-прежнему может быть исключено, если не используется в качестве поля внутри членов. Однако в классах частное поле часто не будет правильным выбором из-за дублирования состояния, которое может вызвать в производных классах. В качестве альтернативы можно было бы генерировать защищенное поле в классах, что способствует повторному использованию хранилища на уровнях наследования. Однако в таком случае мы не смогли бы опустить объявление и понесли бы затраты на выделение памяти для каждого параметра первичного конструктора.
Это приведет к более тесному выравниванию первичных конструкторов, которые не являются записями, с рекордами, в том смысле, что члены всегда (по крайней мере концептуально) генерируются, хотя и разные виды членов с различными уровнями доступа. Но это также приведет к удивительным отличиям от того, как параметры и местные переменные захватываются в других местах C#. Если бы мы когда-либо разрешили локальные классы, например, они захватывали бы внешние параметры и локальные переменные неявно. Видимое создание теневых полей для них, казалось бы, не является разумным поведением.
Другая проблема часто возникает при таком подходе, заключается в том, что многие разработчики имеют разные соглашения об именовании параметров и полей. Что следует использовать для основного параметра конструктора? Любой вариант приведет к несоответствию остальной части кода.
Наконец, явное создание объявлений членов действительно является важной частью для записей, но гораздо более удивительно и нехарактерно для классов и структур, отличных от записей. В целом, это причины, по которым основное предложение выбирает неявное захват, с разумным поведением (в соответствии с записями) для явных объявлений членов, когда они нужны.
Удаление элементов экземпляра из области инициализатора
Правила подстановки, изложенные выше, предназначены для того, чтобы учесть текущее поведение параметров первичного конструктора в рекордах, когда соответствующий член объявлен вручную, и пояснить поведение автоматически генерируемого члена, если он не объявлен. Для этого необходимо различать «область инициализации» (инициализаторы this/base, инициализаторы членов) и «область тела» (тела членов), что достигается в приведенном выше предложении путем изменения при поиске параметров основного конструктора в зависимости от места обращения.
Наблюдение заключается в том, что ссылка на член экземпляра с простым именем в области инициализатора всегда приводит к ошибке. Вместо того чтобы просто затенять члены экземпляра в указанных областях, можем ли мы просто вывести их за рамки видимости? Таким образом, не было бы такого странного условного порядка областей.
Эта альтернатива, вероятно, возможна, но она может иметь некоторые последствия, которые достаточно далеко идущие и потенциально нежелательны. Прежде всего, если удалить элементы экземпляра из области инициализатора, то простое имя, которое действительно соответствует члену экземпляра, но не основному параметру конструктора, может случайно привязаться к чему-то за пределами объявления типа! Кажется, вряд ли это было бы преднамеренным, и лучше, если это будет ошибкой.
Кроме того, статические члены могут корректно использоваться в области инициализации. Таким образом, нам придется различать статические и экземплярные члены в поиске, чего мы не делаем сегодня. (Мы проводим различие при разрешении перегрузки, но это здесь не имеет значения). Таким образом, это также должно быть изменено, что приведет к ещё большему числу ситуаций, когда, например, в статических контекстах что-то связывается "дальше от" источника, вместо ошибки, поскольку обнаружен член экземпляра.
Все это "упрощение" приведет к значительным последствиям, которых никто не просил.
Возможные расширения
Это варианты или дополнения к основному предложению, которые могут рассматриваться в сочетании с ним, или на более позднем этапе, если они считаются полезными.
Доступ к основному параметру конструктора в конструкторах
Правила, приведенные выше, делают ошибкой обращение к параметру основного конструктора в другом конструкторе. Это может быть разрешено в тексте других конструкторов, хотя, так как основной конструктор выполняется сначала. Однако в списке аргументов инициализатора this
это должно оставаться запрещенным.
public class C(bool b, int i, string s) : B(b)
{
public C(string s) : this(b, s) // b still disallowed
{
i++; // could be allowed
}
}
Такой доступ по-прежнему приведет к захвату, так как это единственный способ, которым тело конструктора может получить доступ к переменной после завершения выполнения основного конструктора.
Запрет на основные параметры конструктора в аргументах этого инициализатора может быть ослаблен, чтобы разрешить их, но сделать их не определенно назначенными, но это не кажется полезным.
Разрешить использование конструкторов без инициализатора this
Конструкторы без инициализатора this
(т. е. с неявным или явным инициализатором base
) могут быть разрешены. Такой конструктор не поля экземпляра, свойства и инициализаторы событий, так как они считаются частью только основного конструктора.
В присутствии таких конструкторов базовых вызовов существует несколько вариантов обработки записи параметров основного конструктора. Самое простое — полностью запретить захват в этой ситуации. Параметры основного конструктора предназначены только для инициализации, когда такие конструкторы существуют.
Кроме того, если в сочетании с ранее описанным вариантом разрешить доступ к основным параметрам конструктора в конструкторах, параметры могут присутствовать в теле конструктора как не определенные, и те, которые захвачены, должны быть определенно назначены в конце тела конструктора. Они, по сути, будут неявными выходными параметрами. Таким образом, захваченные параметры первичного конструктора всегда будут иметь разумное значение (т. е. явно назначенное) к моменту, когда они будут использованы другими членами функции.
Привлекательность этого расширения (в любой форме) заключается в том, что оно полностью расширяет текущее исключение для "конструкторов копирования объектов" в записях данных, не приводя к ситуациям, когда наблюдаются неинициализированные параметры первичного конструктора. По сути, конструкторы, которые инициализируют объект альтернативными способами, приемлемы. Ограничения, связанные с записью, не будут критическим изменением для существующих вручную определенных конструкторов копирования в записях, так как записи никогда не фиксируют свои основные параметры конструктора (они создают поля вместо этого).
public class C(bool b, int i, string s) : B(b)
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s2) : base(true) // cannot use `string s` because it would shadow
{
s = s2; // must initialize s because it is captured by S
}
protected C(C original) : base(original) // copy constructor
{
this.s = original.s; // assignment to b and i not required because not captured
}
}
Основные тела конструкторов
Конструкторы часто содержат логику проверки параметров или другой нетривиальный код инициализации, который нельзя выразить как инициализаторы.
Первичные конструкторы могут быть расширены, чтобы блоки инструкций отображались непосредственно в теле класса. Эти инструкции будут вставлены в созданный конструктор в той точке, где они появляются между инициализирующими присваиваниями, и таким образом будут выполняться в перемешку с инициализаторами. Например:
public class C(int i, string s) : B(s)
{
{
if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
}
int[] a = new int[i];
public int S => s;
}
Многие из этих сценариев могут быть адекватно охвачены, если мы представим "окончательные инициализаторы", которые запускаются после завершения всех инициализаторов объектов/коллекций в конструкторах и. Однако проверка аргументов — это одна вещь, которая в идеале произойдет как можно раньше.
Основной конструктор может также предоставить место для предоставления модификатора доступа для основного конструктора, что позволяет ему отклоняться от специальных возможностей заключенного типа.
Объединенные объявления параметров и элементов
Возможное и часто упоминаемое добавление может позволить первичным параметрам конструктора быть аннотированы таким образом, чтобы они также объявить элемент в типе. Чаще всего предлагается определить спецификатор доступа для параметров, чтобы активировать генерацию членов.
public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
void M()
{
... i ... // refers to the field i
... s ... // closes over the parameter s
}
}
Существуют некоторые проблемы:
- Что делать, если желается свойство, а не поле? Наличие встроенного синтаксиса
{ get; set; }
в списке параметров не кажется привлекательным. - Что делать, если для параметров и полей используются различные соглашения об именовании? Тогда эта функция будет бесполезной.
Это потенциальное будущее дополнение, которое может быть принято или нет. Текущее предложение оставляет возможность открытой.
Открытые вопросы
Порядок поиска для параметров типа
В разделе https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup указывается, что параметры типа объявляющего типа должны поступать перед параметрами основного конструктора типа в каждом контексте, где эти параметры находятся в области. Однако у нас уже есть существующее поведение с записями — параметры первичного конструктора идут перед параметрами типа в инициализаторе базового класса и инициализаторах полей.
Что мы должны делать с этим несоответствием?
- Измените правила, чтобы соответствовать поведению.
- Измените поведение (возможное критическое изменение).
- Запретить параметру первичного конструктора использовать имя параметра типа (возможно, изменение, нарушающее совместимость).
- Не делайте ничего, примите несоответствие между спецификацией и реализацией.
Заключение:
Настройте правила, чтобы соответствовать поведению (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).
Атрибуты целевого поля для захваченных параметров первичного конструктора
Следует ли разрешать атрибуты назначения полей для захваченных параметров первичного конструктора?
class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = x;
}
Сейчас атрибуты игнорируются с предупреждением независимо от того, фиксируется ли параметр.
Обратите внимание, что в записях целевые атрибуты полей разрешены, когда для них синтезируется свойство. Затем атрибуты переходят в поле резервного копирования.
record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = X;
}
Заключение:
Запрещено (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).
Предупреждать о тени члена из базы
Следует ли сообщать предупреждение, когда член из базового класса затеняет основной параметр конструктора в другом члене (см. https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?
Заключение:
Утвержден альтернативный дизайн — https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
Захват экземпляра вложенного типа в замыкании
Если параметр, захваченный в состояние окружающего типа, также используется в лямбда-выражении внутри инициализатора экземпляра или базового инициализатора, лямбда и состояние окружающего типа должны ссылаться на одно и то же местоположение параметра. Например:
partial class C1
{
public System.Func<int> F1 = Execute1(() => p1++);
}
partial class C1 (int p1)
{
public int M1() { return p1++; }
static System.Func<int> Execute1(System.Func<int> f)
{
_ = f();
return f;
}
}
Так как наивная реализация захвата параметра в состояние типа просто захватывает параметр в поле закрытого экземпляра, лямбда-выражение должно ссылаться на то же поле. В результате он должен иметь доступ к экземпляру типа. Для этого требуется запись this
в закрытие перед вызовом базового конструктора. Это, в свою очередь, приводит к безопасному, но непроверяемому IL. Это приемлемо?
Кроме того, мы могли бы:
- Запретить такие лямбда-выражения;
- Или, вместо этого, параметры можно захватывать в экземпляре отдельного класса (еще одной замыкания), и использовать этот экземпляр как для замыкания, так и для экземпляра обрабатываемого типа. Таким образом устраняется необходимость захвата
this
в замыкании.
Заключение:
Нам удобно захватывать this
в замыкание перед вызовом базового конструктора (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
Команда среды выполнения также не обнаружила проблемы с шаблоном IL.
Назначение this
в структуре
C# позволяет присваивать this
в структуре. Если структура захватывает первичный параметр конструктора, назначение перезаписывает его значение, что может быть не очевидно для пользователя. Хотим ли мы сообщить предупреждение о таких назначениях?
struct S(int x)
{
int X => x;
void M(S s)
{
this = s; // 'x' is overwritten
}
}
Заключение:
Разрешено, без предупреждения (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
Двойное предупреждение о хранении для инициализации и записи
У нас есть предупреждение, если основной параметр конструктора передается в базу и также записан, так как существует высокий риск того, что он непреднамеренно сохраняется дважды в объекте.
Похоже, что существует аналогичный риск, если параметр используется для инициализации элемента, а также фиксируется. Вот небольшой пример:
public class Person(string name)
{
public string Name { get; set; } = name; // initialization
public override string ToString() => name; // capture
}
Для данного экземпляра Person
изменения в Name
не будут отражены в выходных данных ToString
, что, вероятно, не было задумано разработчиком.
Следует ли ввести предупреждение о превышении лимита хранилища для этой ситуации?
Вот как это будет работать:
Компилятор выдает предупреждение для variable_initializer
, если выполняются все следующие условия:
- Инициализатор переменных представляет неявное или явное преобразование идентичности параметра главного конструктора.
- Основной параметр конструктора фиксируется в состоянии заключенного типа.
Заключение:
Утверждено, см. https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
Собрания LDM
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-10-17.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-01-18.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-22.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors
C# feature specifications