Обязательные члены
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих собраниях по проектированию языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Проблема чемпиона: https://github.com/dotnet/csharplang/issues/3630
Сводка
Это предложение добавляет способ указания того, что свойство или поле необходимо задать во время инициализации объекта, заставляя создателя экземпляра предоставлять начальное значение элемента в инициализаторе объектов на сайте создания.
Мотивация
Иерархии объектов сегодня требуют большого количества стандартных данных для передачи данных на всех уровнях иерархии. Рассмотрим простую иерархию, включающую Person
, как можно определить в C# 8:
class Person
{
public string FirstName { get; }
public string MiddleName { get; }
public string LastName { get; }
public Person(string firstName, string lastName, string? middleName = null)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName ?? string.Empty;
}
}
class Student : Person
{
public int ID { get; }
public Student(int id, string firstName, string lastName, string? middleName = null)
: base(firstName, lastName, middleName)
{
ID = id;
}
}
Здесь происходит много повторений:
- В корне иерархии тип каждого свойства должен повторяться дважды, и имя должно повторяться четыре раза.
- На производном уровне тип каждого унаследованного свойства должен повторяться один раз, а имя — дважды.
Это простая иерархия с 3 свойствами и 1 уровнем наследования, но многие реальные примеры этих типов иерархий идут гораздо глубже, накапливая большее и большее количество свойств для передачи по мере их выполнения. Roslyn является одной из таких баз кода, например, в различных типах деревьев, которые формируют наши CSTs и ASTs (синтаксические и абстрактные синтаксические деревья). Это вложение настолько утомительное, что у нас есть генераторы кода для создания конструкторов и определений этих типов, и многие клиенты используют аналогичные подходы к решению этой проблемы. В C# 9 представлены записи, которые для некоторых сценариев могут сделать это лучше:
record Person(string FirstName, string LastName, string MiddleName = "");
record Student(int ID, string FirstName, string LastName, string MiddleName = "") : Person(FirstName, LastName, MiddleName);
record
исключают первый источник дублирования, но второй источник дублирования остается неизменным: к сожалению, это источник дублирования, который растет по мере роста иерархии и является самой болезненной частью дублирования, чтобы исправить после внесения изменений в иерархию, так как это требует прослеживания иерархии во всех его расположениях, возможно, даже в проектах, и может нарушить работу потребителей.
В качестве обходного решения, чтобы избежать этого дублирования, мы уже давно видим, что потребители используют инициализаторы объектов как способ избегания написания конструкторов. Однако до C# 9 это имело 2 основных недостатка:
- Иерархия объектов должна быть полностью мутируемой, с
set
аксессорами для каждого свойства. - Невозможно убедиться, что каждый экземпляр объекта из графа задает каждый элемент.
C# 9 снова обратился к первой проблеме, введя init
метод доступа: с ним эти свойства можно задать для создания или инициализации объектов, но не впоследствии. Однако у нас все еще есть вторая проблема: свойства в C# были необязательными с версии C# 1.0. Ссылочные типы, допускающие значение NULL и представленные в C# 8.0, частично решают эту проблему: если конструктор не инициализирует свойство ссылочного типа, не допускающего значение NULL, то пользователю об этом будет выдано предупреждение. Тем не менее, это не решает проблему: пользователь хочет избежать повторения больших частей своего типа в конструкторе, им необходимо передать требование , чтобы установить свойства для своих потребителей. Он также не предоставляет никаких предупреждений о ID
из Student
, так как это тип значения. Эти сценарии чрезвычайно распространены в моделях баз данных ORM, таких как EF Core, которым нужен публичный конструктор без параметров, но потом они определяют значение NULL строк на основе возможности нулевого значения у свойств.
Это предложение направлено на решение этих проблем путем введения новой функции в C#: обязательные члены. Необходимые члены должны быть инициализированы пользователями, а не автором типа, с различными настройками, которые обеспечивают гибкость для нескольких конструкторов и дополнительных сценариев.
Подробный дизайн
class
, struct
и record
типы получают возможность объявлять required_member_list. Этот список представляет собой список всех свойств и полей типа, которые считаются необходимых, и их необходимо инициализировать во время построения и инициализации экземпляра типа. Типы наследуют эти списки от их базовых типов автоматически, обеспечивая простой интерфейс, который удаляет стандартный и повторяющийся код.
модификатор required
Мы добавим 'required'
в список модификаторов в field_modifier и property_modifier.
required_member_list типа состоит из всех элементов, к которым было применено required
. Таким образом, тип Person
из более ранних версий выглядит следующим образом:
public class Person
{
// The default constructor requires that FirstName and LastName be set at construction time
public required string FirstName { get; init; }
public string MiddleName { get; init; } = "";
public required string LastName { get; init; }
}
Все конструкторы типа с required_member_list автоматически указывают контракт , согласно которому потребители типа должны инициализировать все свойства в этом списке. Это ошибка, если конструктор объявляет контракт, который требует элемента, не являющегося таким же доступным, как сам конструктор. Например:
public class C
{
public required int Prop { get; protected init; }
// Advertises that Prop is required. This is fine, because the constructor is just as accessible as the property initer.
protected C() {}
// Error: ctor C(object) is more accessible than required property Prop.init.
public C(object otherArg) {}
}
required
допустим только в типах class
, struct
и record
. Недопустимо в типах interface
.
required
нельзя объединить со следующими модификаторами:
fixed
ref readonly
ref
const
static
required
нельзя применять к индексаторам.
Компилятор выдает предупреждение при применении Obsolete
к обязательному элементу типа и:
- Тип не отмечен как
Obsolete
или - Любой конструктор, не приписываемый
SetsRequiredMembersAttribute
, не помеченObsolete
.
SetsRequiredMembersAttribute
Все конструкторы в типе с обязательными элементами, или в типе, базовый тип которого определяет обязательные элементы, должны иметь эти элементы, заданные пользователем при вызове этого конструктора. Чтобы освободить конструкторы от данных требований, конструктор может быть помечен SetsRequiredMembersAttribute
, что снимает эти требования. Текст конструктора не проверяется, чтобы убедиться, что он определенно задает необходимые элементы типа.
SetsRequiredMembersAttribute
удаляет все требования конструктора, и эти требования не проверяются на корректность каким-либо образом. Примечание: это выход, если необходимо наследование от типа с недопустимым списком обязательных членов: пометьте конструктор этого типа как SetsRequiredMembersAttribute
, и ошибки не будут отображаться.
Если конструктор C
ссылается на конструктор base
или this
, аннотированный SetsRequiredMembersAttribute
, то C
также должен быть аннотирован SetsRequiredMembersAttribute
.
Для типов записей мы будем выдавать SetsRequiredMembersAttribute
в синтезированный конструктор копии записи, если тип записи или любой из его базовых типов имеют необходимые элементы.
NB: Более ранняя версия этого предложения имела более обширный метаязык касательно инициализации, позволяя добавлять и удалять отдельные необходимые элементы из конструктора, а также проверять, что конструктор устанавливал все необходимые элементы. Это было признано слишком сложным для первоначального выпуска и удалено. Мы можем рассмотреть добавление более сложных контрактов и изменений в качестве последующей функции.
Принуждение
Для каждого конструктора Ci
типа T
с требуемыми элементами R
потребители, осуществляющие вызовы к Ci
, должны выполнять одно из следующих действий:
- Задайте все элементы
R
в инициализаторе объекта в выражении создания объекта . - Или задайте все элементы
R
с помощью раздела named_argument_list в категории attribute_target.
Если Ci
не будет отнесен к SetsRequiredMembers
.
Если текущий контекст не разрешает object_initializer или не является attribute_target, а Ci
не атрибутируется SetsRequiredMembers
, то вызов Ci
является ошибкой.
ограничение new()
Тип с конструктором без параметров, который объявляет контракт , не может быть использован вместо параметра типа, ограниченного new()
, так как универсальному экземпляру невозможно гарантировать выполнение требований.
struct
default
s
Обязательные элементы не применяются к экземплярам типов struct
, созданных с помощью default
или default(StructType)
. Они применяются для экземпляров struct
, созданных с new StructType()
, даже если StructType
не имеет конструктора без параметров и используется конструктор структуры по умолчанию.
Доступность
Это ошибка пометить элемент, необходимый, если элемент не может быть задан в любом контексте, где отображается содержащий тип.
- Если член является полем, ему нельзя присвоить
readonly
. - Если элемент является свойством, он должен иметь сеттер или инициализатор с уровнем доступа не ниже, чем уровень доступа содержащего его типа.
Это означает, что следующие случаи не допускаются:
interface I
{
int Prop1 { get; }
}
public class Base
{
public virtual int Prop2 { get; set; }
protected required int _field; // Error: _field is not at least as visible as Base. Open question below about the protected constructor scenario
public required readonly int _field2; // Error: required fields cannot be readonly
protected Base() { }
protected class Inner
{
protected required int PropInner { get; set; } // Error: PropInner cannot be set inside Base or Derived
}
}
public class Derived : Base, I
{
required int I.Prop1 { get; } // Error: explicit interface implementions cannot be required as they cannot be set in an object initializer
public required override int Prop2 { get; set; } // Error: this property is hidden by Derived.Prop2 and cannot be set in an object initializer
public new int Prop2 { get; }
public required int Prop3 { get; } // Error: Required member must have a setter or initer
public required int Prop4 { get; internal set; } // Error: Required member setter must be at least as visible as the constructor of Derived
}
Это ошибка скрытия элемента required
, так как этот элемент больше не может быть задан потребителем.
При переопределении элемента required
ключевое слово required
должно быть включено в сигнатуру метода. Это делается так, чтобы если бы мы когда-либо захотели сделать свойство необязательным с помощью переопределения в будущем, у нас была возможность для реализации этого.
Переопределения могут быть разрешены, чтобы пометить член required
в случае, если он не был required
в базовом типе. Элемент, помеченный таким образом, добавляется в список обязательных элементов производного типа.
Типам разрешено переопределять необходимые виртуальные свойства. Это означает, что если базовое виртуальное свойство имеет хранилище, а производный тип пытается получить доступ к базовой реализации этого свойства, он может наблюдать неинициализированное хранилище. NB: Это общий антишаблон C#, и мы не думаем, что стоит пытаться его решать в этом предложении.
Влияние на анализ, допускающий значение NULL
Элементы, помеченные required
, не обязательно инициализированы в допустимое состояние null в конце конструктора. Все элементы required
из этого типа и любые базовые типы считаются по умолчанию в начале любого конструктора в этом типе, если только не привязка к this
или конструктору base
, который относится к SetsRequiredMembersAttribute
.
Анализ, допускающий значение NULL, будет предупреждать обо всех элементах required
из текущих и базовых типов, которые не имеют допустимого состояния NULL в конце конструктора, атрибутом которого является SetsRequiredMembersAttribute
.
#nullable enable
public class Base
{
public required string Prop1 { get; set; }
public Base() {}
[SetsRequiredMembers]
public Base(int unused) { Prop1 = ""; }
}
public class Derived : Base
{
public required string Prop2 { get; set; }
[SetsRequiredMembers]
public Derived() : base()
{
} // Warning: Prop1 and Prop2 are possibly null.
[SetsRequiredMembers]
public Derived(int unused) : base()
{
Prop1.ToString(); // Warning: possibly null dereference
Prop2.ToString(); // Warning: possibly null dereference
}
[SetsRequiredMembers]
public Derived(int unused, int unused2) : this()
{
Prop1.ToString(); // Ok
Prop2.ToString(); // Ok
}
[SetsRequiredMembers]
public Derived(int unused1, int unused2, int unused3) : base(unused1)
{
Prop1.ToString(); // Ok
Prop2.ToString(); // Warning: possibly null dereference
}
}
Представление метаданных
Следующие 2 атрибута известны компилятору C# и требуются для работы этой функции:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class RequiredMemberAttribute : Attribute
{
public RequiredMemberAttribute() {}
}
}
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
public sealed class SetsRequiredMembersAttribute : Attribute
{
public SetsRequiredMembersAttribute() {}
}
}
Неправильно вручную применять RequiredMemberAttribute
к типу.
К любому элементу, помеченным required
, применяется RequiredMemberAttribute
. Кроме того, любой тип, определяющий такие элементы, помечается RequiredMemberAttribute
как маркер, указывающий на наличие необходимых элементов в этом типе. Обратите внимание, что если тип B
является производным от A
и A
определяет required
члены, но B
не добавляет какие-либо новые или переопределяет существующие required
члены, B
не будет помечен RequiredMemberAttribute
.
Чтобы полностью определить наличие необходимых элементов в B
, необходимо проверить полную иерархию наследования.
Любой конструктор в типе с элементами required
, к которому не применен SetsRequiredMembersAttribute
, отмечается двумя атрибутами:
-
System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute
с именем функции"RequiredMembers"
. -
System.ObsoleteAttribute
со строкой"Types with required members are not supported in this version of your compiler"
, а атрибут помечается как ошибка, чтобы предотвратить использование этих конструкторов старыми компиляторами.
Мы не используем modreq
здесь, потому что целью является поддержание двоичной совместимости: если последнее свойство required
было удалено из типа, компилятор больше не будет синтезировать этот modreq
, что является двоично-несовместимым изменением, и все потребители должны быть перекомпилированы. Компилятор, который понимает элементы required
, проигнорирует этот устаревший атрибут. Обратите внимание, что члены также могут поступать из базовых типов: даже если в текущем типе нет новых required
членов, если любой базовый тип имеет required
членов, этот Obsolete
атрибут будет создан. Если конструктор уже имеет атрибут Obsolete
, дополнительный атрибут Obsolete
не будет создан.
Мы используем как ObsoleteAttribute
, так и CompilerFeatureRequiredAttribute
, так как последний является новым выпуском, и старые компиляторы не понимают его. В будущем мы можем удалить ObsoleteAttribute
и (или) не использовать его для защиты новых функций, но на данный момент нам нужно и для полной защиты.
Чтобы создать полный список элементов required
R
для заданного типа T
, включая все базовые типы, выполняется следующий алгоритм:
- Для каждого
Tb
, начиная сT
и проходя по цепочке базовых типов, пока не будет достигнутobject
. - Если
Tb
помеченRequiredMemberAttribute
, все членыTb
, помеченныеRequiredMemberAttribute
, собраны вRb
- Для каждого
Ri
вRb
, еслиRi
переопределён любым членомR
, он пропускается. - В противном случае, если любой
Ri
скрыт членомR
, поиск обязательных элементов завершается ошибкой, и дальнейшие действия не выполняются. Вызов любого конструктораT
, не имеющего атрибутаSetsRequiredMembers
, вызывает ошибку. - В противном случае
Ri
добавляется вR
.
- Для каждого
Открытые вопросы
Инициализаторы вложенных элементов
Какие механизмы принуждения будут применяться для инициализаторов вложенных элементов? Будут ли они полностью запрещены?
class Range
{
public required Location Start { get; init; }
public required Location End { get; init; }
}
class Location
{
public required int Column { get; init; }
public required int Line { get; init; }
}
_ = new Range { Start = { Column = 0, Line = 0 }, End = { Column = 1, Line = 0 } } // Would this be allowed if Location is a struct type?
_ = new Range { Start = new Location { Column = 0, Line = 0 }, End = new Location { Column = 1, Line = 0 } } // Or would this form be necessary instead?
Обсуждаемые вопросы
Уровень применения положений init
Функция предложения init
не реализована в C# 11. Остается активным предложением.
Требуем ли мы строго, чтобы члены, указанные в предложении init
без инициализатора, инициализировали все члены? Кажется вероятным, что мы так поступаем, иначе мы создадим простой риск провала. Однако мы также рискуем повторно вводить те же проблемы, которые мы решили с помощью MemberNotNull
в C# 9. Если мы хотим строго внедрять это, скорее всего, нам потребуется способ для вспомогательного метода, указывающего, что он устанавливает элемент. Некоторые возможные синтаксисы, которые мы обсуждали для этого:
- Разрешить методы
init
. Эти методы могут вызываться только из конструктора или из другого методаinit
и могут получить доступ кthis
, как в конструкторе (например, задатьreadonly
иinit
поля или свойства). Это можно объединить с предложениямиinit
для таких методов. Предложениеinit
будет считаться удовлетворенным, если член в предложении определенно назначен в тексте метода или конструктора. Вызов метода с предложениемinit
, включающим члена, приравнивается к присвоению значения этому члену. Если мы решим, что это путь, по которому мы хотим пойти сейчас или в будущем, скорее всего, мы не должны использоватьinit
в качестве ключевого слова для предложения init для конструктора, так как это может вызвать путаницу. - Разрешите оператору
!
явно подавлять предупреждение или ошибку. При инициализации элемента сложным способом (например, в общем методе) пользователь может добавить!
в предложение init, чтобы указать компилятору не следует проверять инициализацию.
Заключение: по итогам обсуждения нам нравится идея оператора !
. Он позволяет пользователю целенаправленно подходить к более сложным сценариям, а также не создавать большой пробел в дизайне вокруг методов и аннотирования каждого метода как установки элементов X или Y. !
был выбран, поскольку мы уже используем его для подавления предупреждений о значениях NULL, и использование его для того, чтобы сообщить компилятору "Я умнее вас" в другом месте является естественным расширением формы синтаксиса.
Обязательные элементы интерфейса
Это предложение не позволяет интерфейсам пометить элементы по мере необходимости. Это защищает нас от необходимости разбираться в сложных сценариях вокруг new()
и ограничений интерфейсов в обобщенных шаблонах прямо сейчас и напрямую связано как с фабриками, так и с обобщёнными конструкциями. Для обеспечения того, чтобы у нас было пространство конструктора в этой области, мы запрещаем required
в интерфейсах и запрещаем типы с required_member_lists заменять параметры типа, ограниченные new()
. Когда мы хотим более широко рассмотреть универсальные сценарии строительства с заводами, мы можем пересмотреть эту проблему.
Вопросы синтаксиса
Функция предложения init
не реализована в C# 11. Остается активным предложением.
- Является ли
init
правильным словом?init
как постфиксный модификатор конструктора может создать затруднения, если мы захотим вновь использовать его для фабрик, а также задействовать методыinit
с префиксным модификатором. Другие возможности:set
- Является ли
required
правильным модификатором для указания, что все члены инициализированы? Другие предложили:default
all
- С ! для обозначения сложной логики
- Следует ли нам требовать разделитель между
base
/this
иinit
?- разделитель
:
- Разделитель ','
- разделитель
- Является ли
required
правильным модификатором? Другие альтернативные варианты, которые были предложены:req
require
mustinit
must
explicit
заключение: мы удалили предложение конструктора init
и продолжаем использовать required
в качестве модификатора свойств.
Ограничения условий инициализации
Функция предложения init
не реализована в C# 11. Остается активным предложением.
Следует ли разрешить доступ к this
в предложении init? Если мы хотим, чтобы назначение в init
являлось сокращённой формой для назначения элемента в самом конструкторе, похоже, что это именно то, что следует сделать.
Кроме того, создается ли новая область, как base()
, или используется та же область, что и тело метода? Это особенно важно для таких функций, как локальные функции, к которым хочет получить доступ конструкция init, или для затенения имен, если выражение инициализации вводит переменную через параметр out
.
заключение: предложение init
было удалено.
Требования к доступности и init
Функция предложения init
не реализована в C# 11. Остается активным предложением.
В версиях этого предложения с предложением init
мы говорили о том, чтобы иметь возможность иметь следующий сценарий:
public class Base
{
protected required int _field;
protected Base() {} // Contract required that _field is set
}
public class Derived : Base
{
public Derived() : init(_field = 1) // Contract is fulfilled and _field is removed from the required members list
{
}
}
Тем не менее, мы удалили предложение init
из предложения на данный момент, поэтому нам нужно решить, следует ли разрешить этот сценарий в ограниченном порядке. Доступны следующие варианты:
- Запретить сценарий. Это самый консервативный подход, и правила в доступности в настоящее время написаны, исходя из этого предположения. Правило заключается в том, что любой необходимый элемент должен быть не менее видимым, чем содержащий его тип.
- Требовать, чтобы все конструкторы были либо:
- Не более видимый, чем наименее видимый требуемый элемент.
- Примените
SetsRequiredMembersAttribute
к конструктору. Это будет гарантировать, что любой пользователь, который может видеть конструктор, может либо задать всё, что он экспортирует, либо нечего задавать. Это может быть полезно для типов, которые создаются только с помощью статическихCreate
методов или аналогичных построителей, но полезность в общем ограничена.
- Прочитал способ удаления определенных частей контракта в предложение, как описано в LDM ранее.
Заключение: Вариант 1, все обязательные члены должны быть как минимум такими же видимыми, как и их содержащий тип.
Переопределение правил
Текущая спецификация говорит, что ключевое слово required
необходимо скопировать, и что переопределения могут сделать элемент более обязательным, но не менее обязательным. Это то, что мы хотим сделать?
Разрешение на удаление требований нуждается в больших возможностях для изменения контракта, чем те, которые мы предлагаем в настоящее время.
заключение: Допускается добавление required
в случае переопределения. В случае, если переопределяемый элемент — это required
, переопределяющий элемент также должен быть required
.
Альтернативное представление метаданных
Мы также могли бы использовать другой подход к представлению метаданных, взяв пример с методов расширения. Мы можем поместить RequiredMemberAttribute
в тип, чтобы указать, что тип содержит обязательные элементы, а затем поместить RequiredMemberAttribute
на каждый обязательный элемент. Это упростит последовательность поиска (не нужно выполнять поиск элементов, только искать элементы с атрибутом).
заключение: альтернатива утверждена.
Представление метаданных
Необходимо утвердить представление метаданных . Кроме того, необходимо решить, следует ли включить эти атрибуты в BCL.
- Для
RequiredMemberAttribute
этот атрибут больше соответствует общим встроенным атрибутам, которые мы используем для имен элементов nullable/nint/tuple, и не будет применяться пользователем в C# вручную. Возможно, другие языки могут захотеть вручную применить этот атрибут. -
SetsRequiredMembersAttribute
, с другой стороны, напрямую используется потребителями, и поэтому, скорее всего, должен находиться в BCL.
Если воспользоваться альтернативным представлением в предыдущем разделе, это может изменить расчеты для RequiredMemberAttribute
: вместо того, чтобы быть похожими на общие встроенные атрибуты для имен элементов nint
/nullable/кортежей, это ближе к System.Runtime.CompilerServices.ExtensionAttribute
, который присутствует в платформе с момента появления методов расширения.
заключение: мы поместим оба атрибута в BCL.
Предупреждение и ошибка
Не следует задавать обязательный элемент как предупреждение или ошибку? Конечно, можно обмануть систему с помощью Activator.CreateInstance(typeof(C))
или аналогичного, что означает, что мы не можем полностью гарантировать, что все свойства всегда заданы. Мы также разрешаем подавление диагностики на сайте конструктора с помощью !
, что обычно не допускает ошибок. Однако возможность аналогична полям только для чтения или свойствам инициализации, поскольку мы выдаём жёсткую ошибку, если пользователи пытаются установить такой член после инициализации, но их можно обойти, используя рефлексию.
Заключение: ошибки.
C# feature specifications