﻿# Обзор нововведений в C\# 14

Грядёт новая версия C\#, а это значит, что мы вновь выпускаем наш ежегодный обзор нововведений\. Этот год принёс нам не так много изменений, как прошлый\. Возможно, некоторым они покажутся совсем незначительными, но так ли это на самом деле? Давайте же взглянем на них\.

![1301_csharp14_ru/image1.png](https://import.viva64.com/docx/blog/1301_csharp14_ru/image1.png)

## Ключевое слово field

Да, теперь действительно можно не писать поле для свойства\. Кто\-то скажет, что и раньше можно было написать свойство такого вида:

```cpp
public string Name { get; set; }
```

Это автоматически реализуемое свойство, которое создаёт внутри себя скрытое поле\. Но проблема в том, что применять его можно только в случаях, когда не требуется дополнительная логика\. Если же это необходимо, то нам приходится создавать поле, которое будет просто хранить данные, чтобы мы могли обработать их в сеттере свойства\.

Например, когда необходимо убрать лишние пробелы по краям в электронной почте при её записи, обычно пишут что\-то вроде этого:

```cpp
public class User
{
  private string _oldEmail;
  public string OldEmail 
  { 
    get => _oldEmail; 
    set => _oldEmail = value.Trim(); 
  }
}
```

И мы привыкли такое видеть\. Но теперь можно обойтись без явного объявления поля:

```cpp
public class User
{
  public string Email 
  { 
    get; // Здесь также происходит использование field
    set => field = value.Trim(); 
  }
}
```

Если вам необходимо провести дополнительные действия со значением, теперь можно пользоваться ключевым словом `field` и не прописывать поле самостоятельно\. Пусть и спустя несколько лет, но мы дождались этого\! В очередной раз повышаем читабельность и снижаем количество строк кода\.

## Лямбда\-параметры с модификаторами без указания типа

Помните, как раньше при использовании внутри лямбды модификаторов `scoped`, `ref`, `in`, `out`, `ref readonly` нужно было обязательно указывать тип значения? Для примера рассмотрим такой код:

```cpp
delegate bool ValidatorHandler(object value, out string errorMessage);
public void Validate(object objectValue)
{
  ValidatorHandler Validator = (object value, out string error) =>
  {
    // ....
  };
  // ....
}
```

Теперь разрешается использовать модификаторы и без указания типа параметра:

```cpp
ValidatorHandler Validator = (value, out error) =>
{
  // ....
};
```

Это нововведение позволит нам сосредоточиться на написании кода, а не описании шаблонных моментов\. Кажется, чаще всего с этим будут сталкиваться именно в контексте out\-параметров\.

## Перегрузка операторов составного присваивания

Оптимизационная фича, которая позволит перегружать не только унарные или бинарные операторы, но и составные по типу `+=`, `*=` и т\. п\. Так как эти операторы по своей реализации очень схожи, далее по тексту будет рассматриваться оператор `+=`, но все выводы будут также применимы и к схожим операторам\. 

Стоит сразу отметить, что при использовании оператора `+=` в C\# 13 сначала вызывается перегруженный оператор `+`, а лишь потом применяется присваивание\.

При работе со значимыми типами перегрузка оператора `+` лишний раз копирует оба операнда и создаёт новый экземпляр в качестве результата\. Поведение ожидаемое, но может привести к неоправданным накладным расходам на копирование и обработку значений, тем более, если работать с крупными типами \(математические векторы, тензоры и т\.д\.\)\. 

Можно написать метод, работающий аналогично оператору `+=`, который бы сводил копирование к минимуму и позволил модифицировать первый операнд:

```cpp
public struct Vector3
{
  public double X, Y, Z;

  public Vector3(double x, double y, double z)
  {
    X = x;
    Y = y;
    Z = z;
  }

  public void Add(Vector3 vector)
  {
    X += vector.X;
    Y += vector.Y;
    Z += vector.Z;
  }
}
```

Хотя метод `Add` избавляет от большинства минусов, он выглядит менее нативно, чем оператор `+=`\.

Перегрузка оператора `+=` будет выглядеть так:

```cpp
public void operator +=(Vector3 right)
{
  X += right.X;
  Y += right.Y;
  Z += right.Z;
}
```

Результат получается одинаковым, но улучшается читабельность кода\. К тому же теперь не будет ситуаций, когда разработчик, оперирующий крупными типами, обнаружит, что производительность его кода резко упала\.

Пара слов про ссылочные типы\. Если **не** перегрузить оператор `+=` для ссылочного типа, то при его использовании так же будет отрабатывать оператор `+`, после чего произойдёт присваивание\. Проблема копирования операндов при этом не возникает\. Однако в реализации `+` может создаваться новый объект, что может быть нежелательными в контексте выполнения `+=`\.

## Больше partial\-элементов

Ранее C\# 13 уже разрешил применение [`partial` для свойств и индексаторов](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#more-partial-members)\. В новой же версии `partial` можно использовать для разделения конструкторов и ивентов\.

Как и всегда с `partial`, наиболее полезной эта возможность станет для разработчиков библиотек и генераторов кода\. Так, `partial` события полезны для библиотек для слабых событий, а конструкторы для платформ с генерацией совместимого кода, например, Xamarin\.

Единственное отличие в том, частичные конструкторы и ивенты обязательно должны иметь реализацию, в то время как методы могут оставаться лишь объявленными\.

## Null\-условное присваивание

Продолжается традиция улучшения читабельности\. На этот раз добавлена возможность проверять переменную на `null` не только в правой стороне выражения, но и в левой\.


> \*\*Важно\\\!\*\* Стоит учитывать, что инкремент и декремент в этом случае нельзя использовать\\\.



Другими словами, при присваивании с возможным `null`\-разыменованием ранее была необходима классическая проверка на `null` в виде `if`\.

```cpp
public class User
{
  public DateTime LastActive { get; set; }
  public bool LoggedIn { get; set; }

  // ....

  public void UpdateLastActivity(User user)
  {
    if (user is not null)
      user.LastActive = DateTime.UtcNow;
    // ....
  }
}
```

В C\# 14 эту запись можно сократить всего до одной строки:

```cpp
public void UpdateLastActivity(User user)
{
  user?.LastActive = DateTime.UtcNow;
  // ....
}
```

Хочется уточнить, что ранее `null`\-условие можно было применять при проверках, однако присваивание с помощью него было запрещено:

```cpp
if (user?.LoggedIn == true) // OK
{
  user?.LoggedIn = false; // Error
}
```

Каков итог нововведения? Поведение то же: правая часть выражения не будет вычисляться, если проверка не пройдёт, но выглядит гораздо компактнее\. 

## Элементы расширения

Теперь можно писать не только методы расширения, но и свойства\. Для выделения блоков расширения создали новое ключевое слово — `extension`\. 

Как можно было имитировать это раньше? Неудобно, массивно и с костылями\. Но код скажет лучше слов\. Вот пример реализации:

```cpp
public static class ExtensionMembers
{
  public static EnumerableWrapper<TSource> AsExtended<TSource>
  (this IEnumerable<TSource> source) =>
    new(source);
}

public class EnumerableWrapper<T> : IEnumerable<T>
{
  private readonly IEnumerable<T> _source;

  public EnumerableWrapper(IEnumerable<T> source)
  {
    _source = source;
  }

  public IEnumerator<T> GetEnumerator() => _source.GetEnumerator();
  IEnumerator IEnumerable.GetEnumerator() => _source.GetEnumerator();

  public bool IsEmpty => !_source.Any();
}

public class TestClass
{
  public void OldExtensions()
  {
    var enumerable = new List<int>();

    bool isEmpty = enumerable.AsExtended().IsEmpty;
  }
}
```

То есть, чтобы `IEnumerable` смог получить, например, свойство `IsEmpty`, ему приходилось стать обёрткой\. Всё это очень громоздко, согласны? А если вам всё ещё кажется иначе, то просто посмотрите, как подобное можно реализовать теперь:

```cpp
public static class ExtensionMembers
{
  extension<TSource>(IEnumerable<TSource> source)
  {
    public bool IsEmpty => !source.Any();
  }
}

public class TestClass
{
  public void NewExtensions()
  {
    var enumerable = new List<int>();

    bool isEmpty = enumerable.IsEmpty;
  }
}
```

В результате код стало проще и приятнее читать, а подобные расширения выглядят куда естественнее прежних\. Уточню, что в этом случае `IsEmpty` вызывается как член экземпляра `IEnumerable<TSource>`, но мы можем создать и его статические элементы, если объявим блок расширений вот так:

```cpp
extension<TSource>(IEnumerable<TSource>)
{
  // ....
}
```

Однако можно использовать новые расширения и более интересно\. Например, чтобы упростить извечную проверку пустоты строки или сделать геттер для получения строки, написанной с заглавной буквы:

```cpp
public static class StringExtensions
{
  extension(string str)
  {
    public bool IsNullOrEmpty => string.IsNullOrEmpty(str);
    public bool IsNullOrWhiteSpace => string.IsNullOrWhiteSpace(str);

    public string ToTitleCase => 
      System.Globalization.CultureInfo
      .CurrentCulture.TextInfo
      .ToTitleCase(str.ToLower());  
  }
}
```

В общем, нововведение позволяет более естественно расширять нужные типы не только методами, но и свойствами без создания классов\-обёрток\.

## Неявные преобразования Span

Изменение поможет сделать код более оптимизированным с меньшими усилиями\. C\# научился проводить больше неявных преобразований, чтобы нам было приятнее использовать `Span` в работе\. Для начала предложу разобраться, почему вообще стоит рассмотреть `Span` как один из способов сделать свой код чуть лучше\.


> \*\*Примечание:\*\* Использование \`Span\` критично в случаях, когда мы работаем в высокопроизводительных сценариях, где важна каждая миллисекунда\\\.



`Span` — это тип, который позволяет эффективно управлять памятью\. Мы как бы создаём "окно", через которое взаимодействуем с памятью\. Например, если мы хотим провести операцию над строкой, то можем просто передать её в метод\. Но если там нам необходима будет лишь часть строки? Методу `Substring` придётся создавать новую строку, что повлечёт за собой лишние расходы\.

Избежать этого можно с помощью использования `Span`\. Например, чтобы получить подстроку без помощи `Span`, нужно писать так:

```cpp
string str = "test string";
string testString = str.Substring(startIndex: 0, length: 4);
```

И таким способом выделяется память под новую подстроку, что довольно неэффективно, так как требует времени и ресурсов\. Но через `Span` это делается так:

```cpp
ReadOnlySpan<char> testSpan = str.AsSpan().Slice(start: 0, length: 4);
```

Здесь, соответственно, не выделяется память для подстроки, потому что мы обращаемся напрямую к памяти, содержащей переданную строку, что быстрее и экономичнее\.

В целом, можно представить `Span` как поведение любого человека, которому дали задачу найти, скажем, главу в книге\. Ведь никто из нас не станет переписывать её без необходимости\. Мы просто узнаем, где начало главы, а где конец\.

К тому же добавлю, что `Span`, как и массивы, не допустит выхода за границы выделенного "окна" памяти, как это могло бы произойти при небезопасном обращении к ней\. Он сам проверит и выдаст ошибку, если индекс выходит за границы\. 

Теперь, имея общее понимание того, зачем нам нужна эта структура, можем рассмотреть и само изменение\. Напомню, что неявные преобразования к `Span` могли быть и раньше, но сейчас этот список расширили\.

Предположим, что у нас есть некоторый метод для обработки `ReadOnlySpan`:

```cpp
public static void ProcessSpan<T>(ReadOnlySpan<T> span)
{
  // ....
}
```

И мы пытаемся вызвать его, передавая внутрь разные данные:

```cpp
var intArray = new int[10];
string str = "test";
Span<char> charSpan = new();
ReadOnlySpan<char> charReadOnlySpan = new();

ProcessSpan(str);              // Error
ProcessSpan(intArray);         // Error
ProcessSpan(charSpan);         // Error
ProcessSpan(charReadOnlySpan); // OK
```

В этом случае никаких неявных преобразований не происходит\. А чтобы всё сработало, нам необходимо сделать примерно следующее:

```cpp
ProcessSpan(str.AsSpan());
ProcessSpan<int>(intArray.AsSpan());
ProcessSpan<char>(charSpan);
ProcessSpan(charReadOnlySpan);
```

Но с новыми неявными преобразованиями всё становится гораздо проще\. Остаётся лишь единственный момент: для строк всё ещё нужно указать generic\-параметр `char`\.

```cpp
ProcessSpan<char>(str);
ProcessSpan(intArray);
ProcessSpan(charSpan);
ProcessSpan(charReadOnlySpan);
```

Теперь можно гораздо проще писать более производительный код и тратить на рефакторинг меньше времени\.

## Несвязанные обобщённые типы и nameof

Раньше получить имя типа можно было только при вызове с указанием параметра:

```cpp
Console.WriteLine(nameof(List<int>));
```

Сейчас же разрешены обобщённые реализации:

```cpp
Console.WriteLine(nameof(List<>));
```

Да, результат вывода у обоих способов будет одинаковым, но суть в том, что так мы не вводим никого в заблуждение, а чётко выражаем намерение получить имя открытого типа\. Поэтому рефакторить и понимать код, когда не придётся разбираться, почему вдруг в параметры попал `int`, будет проще\.

## Заключение

Нововведения C\# 14 принесли ещё больше удобств и расширили возможности разработчика, хотя и без изменений жанра "синтаксический сахар" тоже не обошлось\. Заглядывая в будущее, можно предположить, что наибольшим спросом будут пользоваться новые расширения, ключевое слово `field` и `null`\-условное присваивание\. А как вам эти изменения? Напишите в комментариях\.

Можете ознакомиться с документацией по C\# 14 самостоятельно по [ссылке](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14)\. Если хотите изучить наши разборы предыдущих версий, вот их список:

* [Обзор нововведений в C\# 9](https://pvs-studio.ru/ru/blog/posts/csharp/0860/)
* [Обзор нововведений в C\# 10](https://pvs-studio.ru/ru/blog/posts/csharp/0875/)
* [Обзор нововведений в C\# 11](https://pvs-studio.ru/ru/blog/posts/csharp/1002/)
* [Обзор нововведений в C\# 12](https://pvs-studio.ru/ru/blog/posts/csharp/1074/)
* [Обзор нововведений в C\# 13](https://pvs-studio.ru/ru/blog/posts/csharp/1173/)