C#9

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

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

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

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

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

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

[pastacode lang=»c» manual=»public%20record%20Address%0A%7B%0A%20%20%20public%20string%20Street%20%7B%20get%3B%20%7D%0A%20%20%20public%20string%20City%20%7B%20get%3B%20%7D%20%20%20%20%20%20%20%20%0A%20%20%20public%20Address()%0A%20%20%20%7B%0A%20%20%20%20%20%20this.Street%20%3D%20string.Empty%3B%0A%20%20%20%20%20%20this.City%20%3D%20string.Empty%3B%0A%20%20%20%7D%0A%20%20%20public%20Address(string%20Street%2C%20string%20City)%0A%20%20%20%7B%0A%20%20%20%20%20%20this.Street%20%3D%20Street%3B%0A%20%20%20%20%20%20this.City%20%3D%20City%3B%0A%20%20%20%7D%0A%7D» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»Address%20addr1%20%3D%20new%20Address(%22Ridout%22%2C%20%22London%22)%3B%0AAddress%20addr2%20%3D%20new%20Address(%22Ridout%22%2C%20%22London%22)%3B» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»if%20(addr1%20%3D%3D%20addr2)%20%7B%20%0A%20%2F%2F…..%20%D0%B0%D0%B4%D1%80%D0%B5%D1%81%D0%B0%20%D0%BE%D0%B4%D0%B8%D0%BD%D0%B0%D0%BA%D0%BE%D0%B2%D1%8B%0A%7D» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»Address%20addr1%20%3D%20new%20Address(%22Ridout%22%2C%20%22London%22)%3B%0AAddress%20addr2%20%3D%20addr1%3B» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»Address%20%7B%20Street%20%3D%20Ridout%20%2C%20City%20%3D%20London%20%7D» message=»» highlight=»» provider=»manual»/]

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

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

[pastacode lang=»c» manual=»Address%20addr%20%3D%20new%20Address%20%7BStreet%20%3D%20%22Ridout%22%2C%20City%20%3D%20%22London%22%20%7D» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»public%20record%20Address%0A%7B%0A%20%20%20public%20string%20Street%20%7B%20get%3B%20init%3B%20%7D%0A%20%20%20public%20string%20City%20%7B%20get%3B%20init%3B%20%7D» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»Address%20addr%20%3D%20new%20Address%20%7BStreet%20%3D%20%22Ridout%22%2C%20City%20%3D%20%22London%22%20%7D» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»Address%20newAddress%20%3D%20addr1%20with%20%7B%20City%20%3D%20%22Goderich%22%20%7D» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»var%20(street%2C%20city)%20%3D%20addr%3B%0AMessageBox.Show(%22The%20city%20is%20%22%20%2B%20city)%3B» message=»» highlight=»» provider=»manual»/]

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

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

[pastacode lang=»c» manual=»public%20void%20Deconstruct(out%20string%20city)%0A%7B%0A%20%20city%20%3D%20City%3B%0A%7D» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»public%20record%20Address(string%20Street%2C%20string%20City)%3B» message=»» highlight=»» provider=»manual»/]

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

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

[pastacode lang=»c» manual=»public%20record%20Address(string%20Street%2C%20string%20City)%0A%7B%0A%20%20%20public%20string%20Street%20%7Bget%3B%20set%3B%20%7D%0A%7D» message=»» highlight=»» provider=»manual»/]

 

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

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

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

[pastacode lang=»c» manual=»Address%20addr%20%3D%20new%20Address(%22Ridout%22%2C%20%22London%22)%3B» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»Address%20addr%20%3D%20new(%22Ridout%22%2C%20%22London%22)%3B» message=»» highlight=»» provider=»manual»/]

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

[pastacode lang=»c» manual=»List%3CAddress%3E%20addrs%20%3D%20new()%3B%0Aaddrs.Add(new(%20%22Ridout%22%2C%20%22London%22%20)%2C%20new(%20%22Shore%22%2C%20%22London%22%20)%2C%20new(%20%22St.%20Patrick%22%2C%20%22Goderich%22%20))%3B» message=»» highlight=»» provider=»manual»/]

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

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

[pastacode lang=»c» manual=»PrintAddress%20(new(%22Ridout%22%2C%20%22London%22))%3B%0A%E2%80%A6%0Apublic%20void%20PrintAddress(Address%20addr)%0A%7B» message=»» highlight=»» provider=»manual»/]

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

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

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