Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Замечание
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Этот документ включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию 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
. Ошибка во время компиляции возникает в противном случае.
См. также https://github.com/dotnet/csharplang/blob/main/proposals/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
. Ошибка во время компиляции возникает в противном случае.
См. также https://github.com/dotnet/csharplang/blob/main/proposals/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 ++() => ...;
}
Постфиксный инкремент и декремент
Если результат операции используется или x
x «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