Заметка
Доступ к этой странице требует авторизации. Вы можете попробовать войти в систему или изменить каталог.
Доступ к этой странице требует авторизации. Вы можете попробовать сменить директорию.
Замечание
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Этот документ включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия отражены в соответствующих заметках с заседания по дизайну языка (LDM) .
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Вопрос чемпиона: https://github.com/dotnet/csharplang/issues/9101
Сводка
Разрешить типам пользователей настраивать поведение операторов составных назначений таким образом, чтобы целевой объект назначения был изменен на месте.
Мотивация
C# поддерживает реализации оператора перегрузки разработчика для определяемого пользователем типа.
Кроме того, она обеспечивает поддержку операторов составных назначений, которые позволяют пользователю писать код аналогичным x += y образом, x = x + yа не . Однако язык в настоящее время не позволяет разработчику перегружать эти составные операторы присваивания, и хотя поведение по умолчанию делает правильное, особенно так как оно относится к неизменяемым типам значений, это не всегда является "оптимальным".
В приведенном ниже примере
class C1
{
static void Main()
{
var c1 = new C1();
c1 += 1;
System.Console.Write(c1);
}
public static C1 operator+(C1 x, int y) => new C1();
}
с текущими правилами языка оператор составного назначения c1 += 1 вызывает определяемый + пользователем оператор, а затем назначает возвращаемое значение локальной переменной c1. Обратите внимание, что реализация оператора должна выделять и возвращать новый экземпляр C1, в то время как, с точки зрения потребителя, изменение на исходный экземпляр C1 вместо этого будет работать как хорошо (оно не используется после назначения) с дополнительным преимуществом, чтобы избежать дополнительного выделения.
Если программа использует составную операцию назначения, наиболее распространенное действие заключается в том, что исходное значение "потеряно" и больше не доступно программе. С типами, которые имеют большие данные (например, BigInteger, Tensors и т. д.) стоимость создания чистого назначения, итерации и копирования памяти, как правило, довольно дорого. Мутация на месте позволит пропустить эти расходы во многих случаях, что может обеспечить значительное улучшение таких сценариев.
Поэтому для C# может быть полезно разрешить типам пользователей настраивать поведение операторов составных назначений и оптимизировать сценарии, которые в противном случае необходимо выделить и скопировать.
Подробный дизайн
Синтаксис
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#15101-general Грамматика настраивается следующим образом.
Операторы объявляются с помощью operator_declarations:
operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;
operator_modifier
: 'public'
| 'static'
| 'extern'
| unsafe_modifier // unsafe code support
| 'abstract'
| 'virtual'
| 'sealed'
+ | 'override'
+ | 'new'
+ | 'readonly'
;
operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
+ | increment_operator_declarator
+ | compound_assignment_operator_declarator
;
unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
;
logical_negation_operator
: '!'
;
overloadable_unary_operator
- : '+' | 'checked'? '-' | logical_negation_operator | '~' | 'checked'? '++' | 'checked'? '--' | 'true' | 'false'
+ : '+' | 'checked'? '-' | logical_negation_operator | '~' | 'true' | 'false'
;
binary_operator_declarator
: type 'operator' overloadable_binary_operator
'(' fixed_parameter ',' fixed_parameter ')'
;
overloadable_binary_operator
: 'checked'? '+' | 'checked'? '-' | 'checked'? '*' | 'checked'? '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;
conversion_operator_declarator
: 'implicit' 'operator' type '(' fixed_parameter ')'
| 'explicit' 'operator' type '(' fixed_parameter ')'
;
+increment_operator_declarator
+ : type 'operator' overloadable_increment_operator '(' fixed_parameter ')'
+ | 'void' 'operator' overloadable_increment_operator '(' ')'
+ ;
+overloadable_increment_operator
+ : 'checked'? '++' | 'checked'? '--'
+ ;
+compound_assignment_operator_declarator
+ : 'void' 'operator' overloadable_compound_assignment_operator
+ '(' fixed_parameter ')'
+ ;
+overloadable_compound_assignment_operator
+ : 'checked'? '+=' | 'checked'? '-=' | 'checked'? '*=' | 'checked'? '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
+ | right_shift_assignment
+ | unsigned_right_shift_assignment
+ ;
operator_body
: block
| '=>' expression ';'
| ';'
;
Существует пять категорий перегруженных операторов: унарные операторы, двоичные операторы, операторы преобразования, операторы добавочного увеличения, составные операторы назначения.
Следующие правила применяются ко всем объявлениям операторов:
- Объявление оператора должно включать
как модификатор,модификатор.publicиstatic
Составные операторы присваивания и добавочного экземпляра могут скрывать операторы, объявленные в базовом классе. Таким образом, следующий абзац больше не является точным и должен быть скорректирован соответствующим образом, или его можно удалить:
Поскольку объявления операторов требуют, чтобы класс или структура, в которых оператор объявлен, участвовали в подписи оператора, невозможно, чтобы оператор, объявленный в производном классе, скрывал оператор, объявленный в базовом классе. Таким образом,
newмодификатор никогда не требуется и поэтому никогда не допускается в объявлении оператора.
Унарные операторы
См. https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#15102-unary-operators.
Объявление оператора должно включать static модификатор и не включать override модификатор.
Удаляется следующая точка маркера:
- Унарный
++или--оператор должен принимать один параметр типаTилиT?возвращать тот же тип или тип, производный от него.
Следующий абзац больше не упоминается ++ и -- маркеры операторов.
Сигнатура унарного оператора состоит из маркера оператора (
+, ,-!,~++,--trueилиfalse) и типа одного параметра. Возвращаемый тип не является частью подписи унарного оператора и не является именем параметра.
Пример в разделе следует изменить, чтобы не использовать определяемый пользователем оператор добавок.
Двоичные операторы
См. https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#15103-binary-operators.
Объявление оператора должно включать static модификатор и не включать override модификатор.
Операторы преобразования
Объявление оператора должно включать static модификатор и не включать override модификатор.
Операторы добавочного увеличения
Следующие правила применяются к объявлениям операторов статического добавочного увеличения, где T обозначает тип экземпляра класса или структуры, содержащей объявление оператора:
- Объявление оператора должно включать
staticмодификатор и не включатьoverrideмодификатор. - Оператор должен принимать один параметр типа
TилиT?возвращать тот же тип или тип, производный от него.
Сигнатура статического инкрементного оператора состоит из маркеров операторов ('checked'? ++, 'checked'? --) и типа одного параметра.
Тип возвращаемого значения не является частью подписи оператора статического добавочного значения, а также не является именем параметра.
Статические операторы добавок очень похожи на унарные операторы.
Следующие правила применяются к объявлениям операторов добавочного экземпляра:
- Объявление оператора не должно включать
staticмодификатор. - Оператор не принимает параметров.
- Оператор должен иметь
voidтип возвращаемого значения.
Фактически оператор добавочного экземпляра является пустотой возвращающий метод экземпляра, который не имеет параметров и имеет специальное имя в метаданных.
Сигнатура оператора инкремента экземпляра состоит из операторных токенов ('checked'? '++' | 'checked'? '--').
Для checked operator объявления требуется парное объявление regular operator. Ошибка во время компиляции возникает в противном случае.
См. также .. /csharp-11.0/checked-user-defined-operators.md#semantics.
Назначение метода заключается в настройке значения экземпляра в результате запрошенной операции увеличения, независимо от того, что означает в контексте декларающего типа.
Пример:
class C1
{
public int Value;
public void operator ++()
{
Value++;
}
}
Оператор добавочного увеличения экземпляра может переопределить оператор с той же сигнатурой, объявленной в базовом классе, override модификатор можно использовать для этой цели.
Для поддержки экземплярных версий операторов инкремента и декремента в ECMA-335 необходимо добавить следующие "зарезервированные" специальные имена: | Имя | Оператор | | -----| -------- | |op_DecrementAssignment| -- | |op_IncrementAssignment| ++ | |op_CheckedDecrementAssignment| проверенный -- | |op_CheckedIncrementAssignment| проверенный ++ |
Составные операторы присваивания
Следующие правила применяются к объявлениям операторов составного назначения:
- Объявление оператора не должно включать
staticмодификатор. - Оператор должен принимать один параметр.
- Оператор должен иметь
voidтип возвращаемого значения.
Фактически оператор составного назначения — это метод возвращающего экземпляра void, который принимает один параметр и имеет специальное имя в метаданных.
Сигнатура оператора составного назначения состоит из токенов оператора ('checked'? '+=', 'checked'? '-=', 'checked'? '*=', 'checked'? '/=', '%=', '&=', '|=', '^=', '<<=', right_shift_assignment, unsigned_right_shift_assignment) и типа единственного параметра. Имя параметра не является частью подписи оператора составного назначения.
Для checked operator объявления требуется парное объявление regular operator. Ошибка во время компиляции возникает в противном случае.
См. также .. /csharp-11.0/checked-user-defined-operators.md#semantics.
Назначение метода заключается в настройке значения экземпляра на результат <instance> <binary operator token> parameter.
Пример:
class C1
{
public int Value;
public void operator +=(int x)
{
Value+=x;
}
}
Оператор составного назначения может переопределить оператор с той же сигнатурой, объявленной в базовом классе, override модификатор можно использовать для этой цели.
ECMA-335 уже зарезервированы следующие специальные имена для определяемых пользователем операторов добавок: | Имя | Оператор | | -----| -------- | |op_AdditionAssignment|' +=' | |op_SubtractionAssignment|' -=' | |op_MultiplicationAssignment|' *=' | |op_DivisionAssignment|' /=' | |op_ModulusAssignment|'%=' | |op_BitwiseAndAssignment|' &=' | |op_BitwiseOrAssignment|'|=' | |op_ExclusiveOrAssignment|' ^=' | |op_LeftShiftAssignment|'<<='| |op_RightShiftAssignment| right_shift_assignment| |op_UnsignedRightShiftAssignment|unsigned_right_shift_assignment|
Однако в нем указывается, что соответствие CLS требует, чтобы методы оператора были непустыми статическими методами с двумя параметрами, т. е. соответствует тому, какие двоичные операторы C# являются. Мы должны рассмотреть вопрос о соблюдении требований к clS, чтобы позволить операторам быть пустыми возвращающими методами экземпляра с одним параметром.
Для поддержки проверенных версий операторов необходимо добавить следующие имена: | Имя | Оператор | | -----| -------- | |op_CheckedAdditionAssignment| проверенный '+=' | |op_CheckedSubtractionAssignment| проверенный '-=' | |op_CheckedMultiplicationAssignment| проверенный '*=' | |op_CheckedDivisionAssignment| проверенный '/=' |
Префиксные операторы инкремента и декремента
Если x он «op» x классифицируется как переменная, а новая языковая версия ориентирована, приоритет присваивается операторам добавочного экземпляра, как показано ниже.
Во-первых, предпринята попытка обработать операцию путем применения разрешения перегрузки оператора добавочного увеличения экземпляра. Если процесс не приводит к возникновению ошибки, операция обрабатывается путем применения разрешения перегрузки унарного оператора, как https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#1296-prefix-increment-and-decrement-operators указано в данный момент.
В противном случае операция «op»x оценивается следующим образом.
Если тип x является ссылочным типом, x он оценивается для получения экземпляра x₀, метод оператора вызывается в этом экземпляре и x₀ возвращается в результате операции.
В противном x₀ случае nullвызов метода оператора вызовет nullReferenceException.
Рассмотрим пример.
var a = ++(new C()); // error: not a variable
var b = ++a; // var temp = a; temp.op_Increment(); b = temp;
++b; // b.op_Increment();
var d = ++C.P1; // error: setter is missing
++C.P1; // error: setter is missing
var e = ++C.P2; // var temp = C.op_Increment(C.get_P2()); C.set_P2(temp); e = temp;
++C.P2; // var temp = C.op_Increment(C.get_P2()); C.set_P2(temp);
class C
{
public static C P1 { get; } = new C();
public static C P2 { get; set; } = new C();
public static C operator ++(C x) => ...;
public void operator ++() => ...;
}
Если тип не известен как ссылочный тип x :
- Если используется результат добавок,
xвычисляется получение экземпляраx₀, метод оператора вызывается в этом экземпляре,x₀назначаетсяxиx₀возвращается в результате составного назначения. - В противном случае вызывается
xметод оператора.
Обратите внимание, что побочные эффекты x оцениваются только один раз в процессе.
Рассмотрим пример.
var a = ++(new S()); // error: not a variable
var b = ++S.P2; // var temp = S.op_Increment(S.get_P2()); S.set_P2(temp); b = temp;
++S.P2; // var temp = S.op_Increment(S.get_P2()); S.set_P2(temp);
++b; // b.op_Increment();
var d = ++S.P1; // error: set is missing
++S.P1; // error: set is missing
var e = ++b; // var temp = b; temp.op_Increment(); e = (b = temp);
struct S
{
public static S P1 { get; } = new S();
public static S P2 { get; set; } = new S();
public static S operator ++(S x) => ...;
public void operator ++() => ...;
}
Постфиксный инкремент и декремент
Если результат операции используется или xx «op» не классифицируется как переменная или старая версия языка, операция обрабатывается путем применения разрешения перегрузки унарного оператора, как https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12816-postfix-increment-and-decrement-operators указано в данный момент.
Причина, по которой мы даже не пытаемся использовать операторы добавочного экземпляра при использовании результата, заключается в том, что, если мы имеем дело с ссылочным типом, невозможно создать значение x перед операцией, если она мутируется на месте.
Если мы имеем дело с типом значения, нам придется делать копии в любом случае и т. д.
В противном случае приоритет присваивается операторам добавочного увеличения экземпляра, как показано ниже.
Во-первых, предпринята попытка обработать операцию путем применения разрешения перегрузки оператора добавочного увеличения экземпляра. Если процесс не приводит к возникновению ошибки, операция обрабатывается путем применения разрешения перегрузки унарного оператора, как https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12816-postfix-increment-and-decrement-operators указано в данный момент.
В противном случае операция x«op» оценивается следующим образом.
Если тип x является ссылочным типом, метод оператора вызывается в x.
В противном x случае nullвызов метода оператора вызовет nullReferenceException.
Рассмотрим пример.
var a = (new C())++; // error: not a variable
var b = new C();
var c = b++; // var temp = b; b = C.op_Increment(temp); c = temp;
b++; // b.op_Increment();
var d = C.P1++; // error: missing setter
C.P1++; // error: missing setter
var e = C.P2++; // var temp = C.get_P2(); C.set_P2(C.op_Increment(temp)); e = temp;
C.P2++; // var temp = C.get_P2(); C.set_P2(C.op_Increment(temp));
class C
{
public static C P1 { get; } = new C();
public static C P2 { get; set; } = new C();
public static C operator ++(C x) => ...;
public void operator ++() => ...;
}
Если тип x не является ссылочным типом, вызывается xметод оператора.
Рассмотрим пример.
var a = (new S())++; // error: not a variable
var b = S.P2++; // var temp = S.get_P2(); S.set_P2(S.op_Increment(temp)); b = temp;
S.P2++; // var temp = S.get_P2(); S.set_P2(S.op_Increment(temp));
b++; // b.op_Increment();
var d = S.P1++; // error: set is missing
S.P1++; // error: missing setter
var e = b++; // var temp = b; b = S.op_Increment(temp); e = temp;
struct S
{
public static S P1 { get; } = new S();
public static S P2 { get; set; } = new S();
public static S operator ++(S x) => ...;
public void operator ++() => ...;
}
Разрешение перегрузки оператора добавочного увеличения экземпляра
Операция формы «op» x или x «op», где "op" является оператором добавочного экземпляра и x является выражением типа X, обрабатывается следующим образом:
- Набор пользовательских операторов, предоставляемых
Xоперациейoperator «op»(x), определяется с помощью правил операторов добавочного экземпляра кандидата. - Если набор операторов, определяемых пользователем, не является пустым, то он становится набором операторов-кандидатов для операции. В противном случае разрешение перегрузки не дает результата.
- Правила разрешения перегрузки применяются к набору операторов-кандидатов, чтобы выбрать лучший оператор, и этот оператор становится результатом процесса разрешения перегрузки. Если разрешению перегрузки не удаётся выбрать единственный лучший оператор, возникает ошибка времени связывания.
Операторы увеличения экземпляра-кандидата
Учитывая тип T и операцию «op», где «op» является оператором добавочного экземпляра, набор пользовательских операторов, предоставляемых T пользователем, определяется следующим образом:
- В
uncheckedконтексте оценки это группа операторов, которые будут создаваться процессом подстановки членов, когда только операторы экземпляровoperator «op»()считались соответствующими целевому имениN. - В
checkedконтексте оценки это группа операторов, которые будут создаваться процессом подстановки членов, когда рассматривались только операторы экземпляраoperator «op»()и экземпляраoperator checked «op»(), соответствующие целевому имениN. Операторыoperator «op»(), имеющие парные объявления сопоставленияoperator checked «op»(), исключаются из группы.
Комбинированное присваивание
Абзац в начале, с dynamic которым связана, по-прежнему применим.
В противном случае, если x он x «op»= y классифицируется как переменная и новая языковая версия, приоритет присваивается операторам составных назначений , как показано ниже.
Во-первых, предпринята попытка обработать операцию формы x «op»= y путем применения разрешения перегрузки оператора составного назначения.
Если процесс не приводит к возникновению ошибки, операция обрабатывается путем применения разрешения перегрузки двоичного оператора, как https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12214-compound-assignment указано в данный момент.
В противном случае операция оценивается следующим образом.
Если тип x является ссылочным типом, x он оценивается для получения экземпляра x₀, метод оператора вызывается в этом экземпляре в y качестве аргумента и x₀ возвращается в результате составного назначения.
В противном x₀ случае nullвызов метода оператора вызовет nullReferenceException.
Рассмотрим пример.
var a = (new C())+=10; // error: not a variable
var b = a += 100; // var temp = a; temp.op_AdditionAssignment(100); b = temp;
var c = b + 1000; // c = C.op_Addition(b, 1000)
c += 5; // c.op_AdditionAssignment(5);
var d = C.P1 += 11; // error: setter is missing
var e = C.P2 += 12; // var temp = C.op_Addition(C.get_P2(), 12); C.set_P2(temp); e = temp;
C.P2 += 13; // var temp = C.op_Addition(C.get_P2(), 13); C.set_P2(temp);
class C
{
public static C P1 { get; } = new C();
public static C P2 { get; set; } = new C();
// op_Addition
public static C operator +(C x, int y) => ...;
// op_AdditionAssignment
public void operator +=(int y) => ...;
}
Если тип не известен как ссылочный тип x :
- Если используется результат составного назначения,
xон оценивается для получения экземпляраx₀, метод оператора вызывается в этом экземпляре вyкачестве аргумента,x₀назначаетсяxиx₀возвращается в результате составного назначения. - В противном случае метод оператора вызывается
xвyкачестве аргумента.
Обратите внимание, что побочные эффекты x оцениваются только один раз в процессе.
Рассмотрим пример.
var a = (new S())+=10; // error: not a variable
var b = S.P2 += 100; // var temp = S.op_Addition(S.get_P2(), 100); S.set_P2(temp); b = temp;
S.P2 += 100; // var temp = S.op_Addition(S.get_P2(), 100); S.set_P2(temp);
var c = b + 1000; // c = S.op_Addition(b, 1000)
c += 5; // c.op_AdditionAssignment(5);
var d = S.P1 += 11; // error: setter is missing
var e = c += 12; // var temp = c; temp.op_AdditionAssignment(12); e = (c = temp);
struct S
{
public static S P1 { get; } = new S();
public static S P2 { get; set; } = new S();
// op_Addition
public static S operator +(S x, int y) => ...;
// op_AdditionAssignment
public void operator +=(int y) => ...;
}
Разрешение перегрузки операторов составных назначений
Операция формы x «op»= y, где «op»= является перегруженным оператором составного назначения, x является выражением типа X , как показано ниже.
- Набор пользовательских операторов, предоставляемых
Xоперациейoperator «op»=(y), определяется с помощью правил операторов составных назначений кандидатов. - Если в наборе
(y)применяется по крайней мере один определяемый пользователем оператор, то это становится набором операторов-кандидатов для операции. В противном случае разрешение перегрузки не дает результата. -
Правила разрешения перегрузки применяются к набору операторов-кандидатов, чтобы выбрать лучший оператор в отношении списка
(y)аргументов, и этот оператор становится результатом процесса разрешения перегрузки. Если разрешению перегрузки не удаётся выбрать единственный лучший оператор, возникает ошибка времени связывания.
Операторы составного назначения кандидатов
Учитывая тип T и операцию «op»=, где «op»= является перегруженным оператором составного назначения, набор пользовательских операторов, предоставляемых T пользователем, определяется следующим образом:
- В
uncheckedконтексте оценки это группа операторов, которые будут создаваться процессом подстановки членов, когда только операторы экземпляровoperator «op»=(Y)считались соответствующими целевому имениN. - В
checkedконтексте оценки это группа операторов, которые будут создаваться процессом подстановки членов, когда рассматривались только операторы экземпляраoperator «op»=(Y)и экземпляраoperator checked «op»=(Y), соответствующие целевому имениN. Операторыoperator «op»=(Y), имеющие парные объявления сопоставленияoperator checked «op»=(Y), исключаются из группы.
Открытые вопросы
[Разрешено] Должен ли readonly модификатор быть разрешен в структурах?
Он чувствует, что не будет преимуществ, позволяя пометить метод с readonly тем, когда все назначение метода заключается в изменении экземпляра.
Заключение: Мы разрешаем readonly модификаторы, но в настоящее время мы не будем ослаблять целевые требования.
[Решено] Следует ли разрешать шадоуинг?
Если производный класс объявляет оператор составного назначения/'экземпляра с той же сигнатурой, что и один в базе, следует ли требовать override модификатор?
Заключение: Тень будет разрешена с теми же правилами, что и методы.
[Разрешено] Должны ли мы иметь какое-либо принудительное применение согласованности между объявленными += и + операторами?
Во время LDM-2025-02-12 возникла озабоченность по поводу того, как авторы могут случайно подвергнуть своих пользователей необычным сценариям, где один может работать, а другой нет (или наоборот), поскольку одна форма объявляет больше операторов, чем другая.
Заключение: Проверки не будут выполняться по согласованности между различными формами операторов.
Альтернативы
Продолжайте использовать статические методы
Мы могли бы рассмотреть возможность использования методов статических операторов, в которых экземпляр передается в качестве первого параметра. В случае типа значения этот параметр должен быть параметром ref .
В противном случае метод не сможет изменить целевую переменную. В то же время в случае типа класса этот параметр не должен быть параметром ref . Так как в случае класса передаваемый экземпляр должен быть мутирован, а не расположение, в котором хранится экземпляр. Однако, если оператор объявлен в интерфейсе, он часто не знает, будет ли интерфейс реализован только классами или только структурами. Поэтому неясно, должен ли первый параметр быть параметром ref .
Совещания по проектированию
C# feature specifications