C#9

C # 9 дает вам лучший способ создания объектов значений используя достаточно простой код. Но даже если вас не интересуют объекты значений, ключевое слово new имеет несколько интересных изменений в новой версии C#. Я не скажу, что сам по себе C # 9 стоит перехода на .NET 5 (хотя я мог бы сделать такое заявление о C # 8 и .NET Core 3.x). Новая версия C # 9 — это больше, чем просто приятная функция .NET 5, и вот мои любимые новые функции.

Неизменяемые объекты

Я большой поклонник дизайна, ориентированного на предметную область. Одной из ключевых концепций этого подхода являются так называемые объекты значений: объекты, которые считаются идентичными, потому что они имеют одинаковые значения в своих свойствах, а не потому, что у них общий первичный ключ или место в памяти. Адрес — хороший пример объекта значения: два адреса одинаковы, если у них одинаковый город / улица / и т.д. даже если они от двух разных клиентов, и один из них является адресом доставки, а другой — адресом для выставления счетов.

Структуры (и другие типы значений) работают таким же образом при сравнении, но со структурами, присвоение значения из одной структуры с другой копирует данные (с классами все по-другому: присвоение одной переменной другой просто копирует указатели вокруг, и обе переменные в конечном итоге указывают на один и тот же объект в памяти вместо получения собственных копий данных).

Проблема с созданием копии объекта-значения заключается в том, что две копии могут иметь отдельные изменения, внесенные в них — это началось с того, что два идентичных объекта разошлись. В результате объекты-значения часто становятся неизменяемыми, что требует значительного количества кода для настройки в .NET. Это фактически делает копирование данных более серьезной проблемой: теперь у вас есть данные, которые гарантированно идентичны, и занимают вдвое больше места, чем необходимо.

Но в C # 9 вы можете просто создать запись и получить объект значения, который делает все, что вы хотите. Вот неизменный объект значения Address:

public record Address
{
   public string Street { get; }
   public string City { get; }        
   public Address()
   {
      this.Street = string.Empty;
      this.City = string.Empty;
   }
   public Address(string Street, string City)
   {
      this.Street = Street;
      this.City = City;
   }
}

А вот как я могу создать два объекта Address:

Address addr1 = new Address("Ridout", "London");
Address addr2 = new Address("Ridout", "London");

Хотя это выглядит так, как будто я создаю ссылочный тип, когда вы сравниваете два объекта записи, они сравниваются как типы значений — значение имеет значение свойств (при условии, что два объекта относятся к одному типу). Итак, в этом коде тест будет пройден, потому что свойства Street и City имеют одинаковые значения в обоих объектах:

if (addr1 == addr2) { 
 //..... адреса одинаковы
}

С другой стороны, когда вы назначаете переменные, записи работают как ссылочные типы: указатели перемещаются, но данные не копируются. В этом коде переменные addr1 и addr2 указывают на один и тот же объект в памяти:

Address addr1 = new Address("Ridout", "London");
Address addr2 = addr1;

Как и классы, записи поддерживают наследование и интерфейсы. Записи также содержат полезный метод ToString. Вызов метода ToString для моего объекта Address приводит, например, к следующему:

Address { Street = Ridout , City = London }

Управление и определение свойств в записях.

Как и любое свойство, объявленное как доступное только для чтения (например, мои свойства Street и City), этот код не будет работать, потому что мои свойства, как определено, могут быть установлены только в конструкторе:

Address addr = new Address {Street = "Ridout", City = "London" }

В C # 9 теперь можно указать, что свойство readonly может быть установлено при создании экземпляра объекта, пометив свойство как «способное к инициализации», например:

public record Address
{
   public string Street { get; init; }
   public string City { get; init; }

Этот код, который устанавливает свойства при создании экземпляра объекта, теперь будет работать:

Address addr = new Address {Street = "Ridout", City = "London" }

Если я хочу, при назначении переменной записи я могу принудительно создать копию с помощью ключевого слова with. Обычно я делаю это, потому что хочу изменить значение некоторого свойства как части копии. Этот код, например, создает новый адрес, но с измененным значением в Городе:

Address newAddress = addr1 with { City = "Goderich" }

Я также могу выделить свойства моей записи в отдельные переменные. Этот код перемещает мои свойства Street и City в две строковые переменные:

var (street, city) = addr;
MessageBox.Show("The city is " + city);

Это называется «деконструкцией» записи, и если вы недовольны тем, как деконструируется ваша запись, вы можете написать свой собственный метод Deconstruct. Метод Deconstruct принимает выходной параметр для каждой отдельной переменной, которая будет возвращена, а затем в теле метода присваивает значения этим параметрам (обычно из свойств в записи, но вы можете делать все, что захотите).

Этот код, например, переопределяет метод Deconstruct по умолчанию и возвращает только свойство City:

public void Deconstruct(out string city)
{
  city = City;
}

Но я слишком много печатаю. При условии, что я готов отказаться от своих конструкторов, я могу определить свой объект значения Address, просто перечислив его свойства при его объявлении, например (обратите внимание на точку с запятой в конце оператора):

public record Address(string Street, string City);

Синтаксис выглядит как результат сворачивания объявления класса и списка параметров из конструктора записи в одну строку. Этот синтаксис не мешает вам добавлять методы, конструктор или метод Deconstruct или даже переопределять свойства вашим собственным кодом.

Этот код, например, добавляет к записи тело и делает свойство Street доступным для записи:

public record Address(string Street, string City)
{
   public string Street {get; set; }
}

 

Упрощение создания новых

Пока мы говорим о сокращении количества наборов текста, мое другое любимое изменение касается нового ключевого слова.

В моих предыдущих примерах при создании объекта Address я повторял имя записи по обе стороны от знака равенства. Это поддерживает объявление переменной как другого типа, чем класс или запись (например, объявление переменной с использованием интерфейса или базового класса). Но на самом деле в большинстве случаев имена по обе стороны от знака равенства одинаковы:

Address addr = new Address("Ridout", "London");

В этих случаях в C # 9 вы можете опустить имя класса в правой части и просто свернуть оставшуюся часть инструкции влево. Этот код делает именно то, что делал мой предыдущий оператор:

Address addr = new("Ridout", "London");

Microsoft называет это «целевой типизацией», и она работает почти везде, где вы хотите. Я могу использовать этот новый синтаксис как при создании типизированной коллекции, так и при добавлении в нее элементов, как демонстрирует этот код:

List<Address> addrs = new();
addrs.Add(new( "Ridout", "London" ), new( "Shore", "London" ), new( "St. Patrick", "Goderich" ));

Как видите, целевая типизация действительно окупается, когда вы загружаете в коллекцию несколько копий одного и того же типа.

Типизация цели также работает, если вы передаете новый объект типизированному параметру метода, как в этом примере:

PrintAddress (new("Ridout", "London"));
…
public void PrintAddress(Address addr)
{

В C # 9 есть еще о чем поговорить (включая улучшения сопоставления с образцом — это отдельная тема для публикации). Однако это те, которые я буду использовать постоянно.

Может быть, это все-таки причина для миграции.

Перевод статьи Питера Вогеля.