Начиная с версии C# 14 можно для своих типов настраивать поведение составных операторов присваивания таким образом, чтобы цель присваивания изменялась на месте.
При этом язык C# уже в более ранних версиях поддерживал определение составных операторов присваивания операторов для кастомных типов, которые определяет сам разработчик. Например:
Counter counter1 = new Counter { Value = 23 };
Counter counter2 = counter1 + 45;
Console.WriteLine(counter2.Value); // 23 + 45 = 68
class Counter
{
static int count;
public int Id {get;}
public int Value { get; set; }
public Counter()
{
Id = ++count;
}
public static Counter operator +(Counter c, int n)
{
return new Counter { Value = c.Value + n };
}
}
В данном случае определяем для типа Counter оператор сложения , который "складывает" объект Counter и число и возвращает новый объект Counter. И автоматически этот же оператор применяется в составных операторах присваивания. То есть мы можем написать следующим образом:
Counter counter1 = new Counter { Value = 23 };
counter1 += 45;
Console.WriteLine($"counter1.Value: {counter1.Value}"); // 68
Таким образом, хотя мы явным образом НЕ определяли составной оператор присваивания += для типа Counter, но благодаря оператору сложения + мы можем выполнять сложение с присвоением.
И по консольному выводу мы увидим, что все работает правильно. Подобным образом мы можем определять другие операторы без присвоения, и они будут не явно использоваться там, где требуется аналогичный оператор с присваиванием.
Например, определить оператор - и выполнять операцию -=.
Однако все таки здесь есть проблема. Так, инструкция
counter1 += 45;Фактически аналогична следующей:
counter1 = counter1 + 45;То есть при сложении будет создаваться новый объект Counter. И я не случайно добавил свойство Id, которое и поможет нам это увидеть:
Counter counter1 = new Counter { Value = 23 };
Console.WriteLine($"counter1.Id: {counter1.Id}"); // counter1.Id: 1
counter1 += 45; // Здесь создается новый объект Counter
Console.WriteLine($"counter1.Value: {counter1.Value}"); // 68
Console.WriteLine($"counter1.Id: {counter1.Id}"); // counter1.Id: 2
Консольный вывод:
counter1.Id: 1
counter1.Value: 68
counter1.Id: 2
Мы видим, что Id у counter1 изменился, то есть конструктор сработал два раза. А если у нас несколько подобных операций:
Counter counter1 = new Counter { Value = 0 };
counter1 += 1; // 2-й вызов конструктора
counter1 += 1; // 3-й вызов конструктора
counter1 += 1; // 4-й вызов конструктора
Console.WriteLine($"counter1.Value: {counter1.Value}"); // 3
Очевидно, что такое поведение не очень эффективно. И чтобы решить подобные проблемы в версию языка C# 14 (.NET 10) и была добавлена возможность отдельно определеить составные операции присваивания, которы вместо создания нового объекта позволяют изменить текущий объект.
Фактически, составной оператор присваивания - это метод объекта, который возвращает void (то есть фактически ничего не возвращает), который принимает один параметр и который
имеет специальное имя название оператора. К объявлениям составных операторов присваивания применяются следующие правила:
Объявление оператора не должно содержать модификатор
staticОператор должен принимать один параметр
В качестве возвращаемого типа для оператора применяется
void.Имя метода оператора совпадает с названием оператора (
+=,-=,*=,/=,%=,&=,|=,^=,,>>=или>>>=)
Определим оператор += для типа Counter:
Counter counter1 = new Counter { Value = 0 };
Console.WriteLine($"counter1.Id: {counter1.Id}"); // counter1.Id: 1
counter1 += 1;
counter1 += 1;
counter1 += 1;
Console.WriteLine($"counter1.Value: {counter1.Value}"); // 3
Console.WriteLine($"counter1.Id: {counter1.Id}"); // counter1.Id: 1
class Counter
{
static int count;
public int Id {get;}
public int Value { get; set; }
public Counter()
{
Id = ++count;
}
public void operator +=(int n)
{
Value += n;
}
}
И по консольному выводы мы увидим, что при работе оператора конструктор не вызывается и новых объектов не создается, изменяется только текущий объект:
counter1.Id: 1
counter1.Value: 3
counter1.Id: 1
Подобным образом можно определить несколько одноименных операторов:
Counter counter1 = new Counter { Value = 0 };
counter1 += 1;
counter1 += new Counter { Value = 5 };
counter1 += 2;
Console.WriteLine(counter1.Value); // 8
class Counter
{
public int Value { get; set; }
public void operator +=(int n)
{
Value += n;
}
public void operator +=(Counter c)
{
Value += c.Value;
}
}
Операции инкремента и декремента фактически созвучны операциям += и -=, и их также можно определить подобным образом:
Counter counter1 = new Counter { Value = 1 };
counter1++;
Console.WriteLine(counter1.Value); // 2
++counter1;
Console.WriteLine(counter1.Value); // 3
class Counter
{
public int Value { get; set; }
public void operator ++() => ++Value;
}
Однако в данном случае есть проблема - мы не можем написать следующим образом:
Counter counter2 = ++counter1;
// или так
Counter counter2 = counter1++;
В этом случае мы можем комбинировать два определения оператора - статическое и нестатическое:
Counter counter1 = new Counter { Value = 1 };
Counter counter2 = ++counter1;
Console.WriteLine(counter1.Value); // 2
Console.WriteLine(counter2.Value); // 2
class Counter
{
public int Value { get; set; }
// два определения оператора
public static Counter operator ++(Counter c) => new Counter{Value = c.Value++};
public void operator ++() => ++Value;
}
Производные классы могут переопределять операторы присваивания базовых классов:
var dc = new DoubleCounter { Value = 0 };
dc += 3;
Console.WriteLine(dc.Value); // 6
class Counter
{
public int Value { get; set; }
public virtual void operator +=(int n) => Value += n;
}
class DoubleCounter : Counter
{
public new int Value { get; set; }
// переопределяем оператор
public override void operator +=(int n) => Value += 2*n;
}