Поделиться через


Определяемые пользователем операторы составных назначений

Замечание

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Этот документ включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию 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 модификатор.

Операторы преобразования

См. https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#15104-conversion-operators.

Объявление оператора должно включать 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| проверенный '/=' |

Префиксные операторы инкремента и декремента

См. https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#1296-prefix-increment-and-decrement-operators

Если 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 ++() => ...;
}

Постфиксный инкремент и декремент

См. https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12816-postfix-increment-and-decrement-operators

Если результат операции используется или 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»() , исключаются из группы.

Комбинированное присваивание

См. https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12214-compound-assignment

Абзац в начале, с 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 .

Совещания по проектированию