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

Совсем скоро нам станет полноценно доступна новая версия C\#, и мы продолжаем наш ежегодный цикл обзора нововведений\. В этот раз команда Microsoft приготовила нам ряд очень интересных и ожидаемых изменений\. Предлагаю рассмотреть их подробно\. 

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

## Union\-типы

Для начала рассмотрим наиболее понятное нововведение — `union`\-типы\. Данная фича позволит разработчикам объединять типы, не прибегая к наследованию и интерфейсам, где это не нужно или невозможно\. `Union` — это структура, которая содержит в себе полный набор поддерживаемых типов\.

```cpp
public union TestUnion(int, Version, IEnumerable<string>);
```

В коде выше `union` — ключевое слово, а `int`, `Version`, `IEnumerable<string>` — набор поддерживаемых типов\.

Примером использования новой структуры может служить ситуация, где традиционное наследование не всегда может подойти, так как оно обязывает объекты иметь что\-то общее\. Но могут быть случаи, когда наследование невозможно или бессмысленно с точки зрения архитектуры\. Допустим, нам нужно обработать определённый ряд типов, включая примитивы\. В обычном случае нам пришлось бы указать на входе переменную типа `object`, которую внутри необходимо было кастить к нужным типам, включая описание логики для неподходящих вариантов\. Это звучит ненадёжно\. Использование `union` в данном случае исправило бы эти проблемы, делая код надёжнее и читабельнее\.

Исходя из сущности новой структуры, при использовании `union` компилятору изначально известен полный список всех типов, которые находятся внутри него\. Это убирает необходимость в ветке `default`, что защищает программиста от ситуации, когда в `switch` были обработаны не все типы, а новые наследники постоянно попадают в `default`\-секцию, что может вызывать непредсказуемое поведение программы\.

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

```cpp
public abstract record ApiResult;

public record ErrorData(string Message);
public record Success(string Data) : ApiResult;
public record ValidationError(ErrorData Data) : ApiResult;

public string Process(ApiResult result)
{
  return result switch
  {
    Success s => s.Data,
    ValidationError s => s.Data.Message,
    _ => "Unknown result"
  };
}
```

Объявляется абстрактный класс `ApiResult`, от которого наследуются конкретные ответы\. Потому, если появится новый наследник, а разработчик не предусмотрит его обработку, то выполнение кода приведёт в ветку `default`\. `Union` помогает реализовать это иначе:

```cpp
public record ErrorData(string Message);
public record Success(string Data);
public record ValidationError(ErrorData Data);

public union ApiResult(Success, ValidationError)
{ 
  public string GetData()
  {
    return this switch
    {
      Success s => s.Data,
      ValidationError s => s.Data.Message
    };
  }
}
```

Как видно, здесь не нужно создавать родительский класс, чтобы объединить их\. Также мы получаем более стабильное поведение за счёт того, что ещё во время компиляции известно, какие именно типы может содержать данный `union`\. Это значит, что при попытке реализовать `switch` без обработки всех возможных типов объединения, компилятор выдаст предупреждение об этом\.

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

## Закрытые иерархии

Данное изменение в некоторой степени связано с `union` в том плане, что тоже позволяет компилятору знать, с каким числом типов он в итоге будет работать\. В данном случае это достигается с помощью того, что закрывается возможность наследования от класса за пределами сборки, где он был объявлен\. Это поведение напоминает некоторую смесь `internal` и `sealed`, где от `internal` берётся область действия, а от `sealed` — запрет наследования\. Это может быть полезно, например, в библиотеках, где мы хотим ограничить наследование от определённых классов, не мешая своей же сборке\. Предлагаю сразу рассмотреть пример объявления:

```cpp
public closed record ClosedRecordClass;
```

Здесь ключевое слово `closed` обозначает, что класс закрыт для наследования\. Соответственно, попытка наследоваться от такого класса из другой сборки вызовет ошибку компиляции\. И стоит отметить, что закрытая иерархия так же, как и `union`, позволяет использовать так называемый exhaustive `switch`, который разрешает не описывать `default`\-ветку\. 

Также есть важные уточнения\. Во\-первых, класс, помеченный как `closed`, всегда будет являться абстрактным\. Это в том числе позволяет обеспечить exhaustive `switch`\. Во\-вторых, наследники не становятся закрытыми автоматически\. Если потребуется сокрыть их от других сборок, то им можно указать модификатор доступа `internal`\. И в\-третьих, закрыть для наследования из другой сборки можно только классы \(`class` и `record class`\), так как для структур в целом нет наследования, а интерфейсы и нужны для того, чтобы от них наследоваться\. Таким образом, при попытке добавить ключевое слово к структуре или интерфейсу будет выдана ошибка\. 

## Именованные break и continue

Небольшое, но тоже довольно важное нововведение\. У каждого иногда возникали ситуации, когда нужно было прервать выполнение внешней конструкции, либо перейти на следующую её итерацию\. С классическим подходом пришлось бы реализовать нечто такое:

```cpp
public enum WorkItemType
{
  Process,
  StopAll
}
public class WorkItem
{
  // ....
  public WorkItemType Type { get; set; }
}

public class OldLabeledBreak
{
  public void PerformAction(IEnumerable<WorkItem> workList)
  {
    bool stopProcessing = false;

    foreach (var item in workList) // #1
    {
      switch (item.Type) // #2
      {
        case WorkItemType.Process:
          // ....
          break; // break #2
        case WorkItemType.StopAll:
          stopProcessing = true;
          break; // break #2
      }

      if (stopProcessing)
        break; // break #1
    }
    // ....
  }
}
```

То есть необходимо использовать дополнительный флаг, чтобы внешний цикл мог выйти после завершения выполнения внутреннего `switch`\. Конечно, есть вариант с использованием `goto`, но он может внести в код некоторую путаницу, так как маркер, к которому перейдёт выполнение кода, может располагаться в любом месте\. Именованные `break` и `continue` позволяют упростить код\. Давайте посмотрим, как бы выглядел данный метод с применением новых возможностей\.

```cpp
public void PerformAction(IEnumerable<WorkItem> workList)
{
  outer:  foreach (var item in workList) // #1
  {
    switch (item.Type) // #2
    {
      case WorkItemType.Process:
        // ....
        break; // break #2
      case WorkItemType.StopAll:
        break outer; // break #1
    }
  }
  // ....
}
```

Как видно, синтаксис походит на `goto`, но при этом код выглядит более читабельным\. То есть, таким образом можно избежать создания лишних флагов и проверок, что позволит немного сэкономить память и сделать код более выразительным\. Стоит уточнить, что данное нововведение можно применить для следующих конструкций: `for`, `foreach`, `while`, `do` и `switch`\.

## Индексаторы расширений

Это изменение позволяет разработчикам описать способы доступа к элементам коллекции по индексам с помощью индексаторов расширения\. Например, можно реализовать получение элементов через `Range` в `IReadOnlyList`\. До грядущей версии это можно было реализовать подобным образом:

```cpp
public class OldExtensionIndexers
{
  public void Process()
  {
    IReadOnlyList<string> readOnlyList = ["val1", "val2", "val3"];
    var elem = readOnlyList.Slice(..2);
  }
}
public static class OldReadOnlyListExtensions
{
  public static IReadOnlyList<T> Slice<T>(this IReadOnlyList<T> list, 
                                          Range range)
  {
    var (offset, length) = range.GetOffsetAndLength(list.Count);
    var result = new List<T>(length);
    for (int i = 0; i < length; i++)
      result.Add(list[offset + i]);
    
    return result;
  }
}
```

То есть проблема решается созданием обычного метода расширения, но C\# 15 приносит нам возможность создать расширения для индексаторов\. Следовательно, можно будет получать элементы через привычные квадратные скобки\. Предлагаю рассмотреть, как бы это выглядело в нашем примере:

```cpp
public class NewExtensionIndexers
{
  public void Process()
  {
    IReadOnlyList<string> readOnlyList = ["val1", "val2", "val3"];
    var elem = readOnlyList[..2];
  }
}
public static class NewReadOnlyListExtensions
{
  extension<T>(IReadOnlyList<T> list)
  {
    public IReadOnlyList<T> this[Range range]
    {
      get
      {
        var (offset, length) = range.GetOffsetAndLength(list.Count);
        return list.Skip(offset).Take(length).ToList();
      }
    }
  }
}
```

Стоит сразу учесть, что если у типа есть свой индексатор, то он будет приоритетнее, чем тот, который разработчик объявит в качестве расширения\. Официальный порядок таков: `explicit` индексаторы типа, `implicit` индексаторы типа, `explicit` индексаторы расширений, `implicit` индексаторы расширений\. 

## Аргументы выражения коллекции

Следующая фича позволит сократить количество строк кода и удобнее инициализировать коллекции, передавая напрямую в конструктор необходимые аргументы\. Таким образом можно передать, например, `capacity` и `comparer`\. Соответственно, в версии языка C\# 14 и ранее нужно писать подобные конструкции:

```cpp
public void ProcessList()
{
  List<string> loginList = ["login1", "login2"];

  List<string> newLoginList = new List<string>(loginList.Count);
  newLoginList.AddRange(loginList);
}
public void ProcessHashSet()
{
  HashSet<string> hashSet = new HashSet<string>
(StringComparer.OrdinalIgnoreCase);
  hashSet.Add("value 1");
  hashSet.Add("value 2");
}
```

Инициализатор предусматривает что\-то одно, так как тот же `List` содержит всего лишь 3 конструктора: пустой, с указанием `capacity`, либо просто с коллекцией\. Предлагаю посмотреть новую возможную реализацию:

```cpp
public void ProcessList()
{
  List<string> loginList = ["login1", "login2"];

  List<string> newLoginList = [with(loginList.Count), ..loginList];
  // newLoginList.AddRange(loginList);
}
public void ProcessHashSet()
{
  HashSet<string> hashSet = [with(StringComparer.OrdinalIgnoreCase), 
    "value 1", "value 2"];
  // hashSet.Add("value 1");
  // hashSet.Add("value 2");

}
```

Выглядит чище, не правда ли? Вероятно, IDE даже сама предложит вам такой вариант написания и свернёт всё в одну строчку\. Но обратите внимание, что `with` всегда должен идти первым элементом, иначе компилятор выдаст ошибку\.

## Безопасность памяти

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

```cpp
public unsafe class OldMemorySafety
{
  public int ReadMemory(byte* ptr)
  {
    return *ptr;
  }
  public int Process()
  {
    byte b = 42;
    byte* newPtr = &b;
    return ReadMemory(newPtr);
  }
}
```

Любое использование указателей влекло за собой необходимость ставить ключевое слово `unsafe`\. Для более ясного представления изменения сразу посмотрим на то, как будет выглядеть этот же код в новой версии:

```cpp
public class NewMemorySafety
{
  public int ReadMemory(byte* ptr)
  {
    unsafe
    {
      return *ptr;
    }
  }
  public int Process()
  {
    byte b = 42;
    byte* newPtr = &b;
    return ReadMemory(newPtr);
  }
}
```

Видно, что теперь класс не помечается как `unsafe`\. Вместо этого ключевое слово ставится более локально — непосредственно в месте применения\. В данном случае небезопасным является именно разыменование указателя, потому его необходимо обозначить небезопасным\.

Кроме того, добавлено и ключевое слово `safe`, которое следует использовать в случаях, когда внутри содержатся небезопасные операции, но они безопасны для вызывающего контекста и не потребуют объявления `unsafe`\-блока\. Таким образом, `safe` нужно использовать, когда компилятор не может сам проверить безопасность\. Например, для `extern`\-методов либо в полях структур с явным размещением памяти \(`[StructLayout(LayoutKind.Explicit)]`\)\.

Отдельно можно сказать про `stackalloc`\. Теперь его применение считается небезопасным, если выполняются все следующие условия одновременно: конвертация в `Span<T>` или `ReadOnlySpan<T>`, отсутствие инициализатора, применение там, где есть ` SkipLocalsInitAttribute`\.

Добавлю, что новые правила этого изменения включаются отдельно\. Для заинтересовавшихся есть официальная инструкция по данной [ссылке](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/language#enable-the-updated-memory-safety-rules)\.

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

В этом году новая версия языка принесла разработчикам много довольно важных возможностей\. Например, были реализованы полезные фичи в виде `union`\-типов и закрытых иерархий; ряд изменений упростил написание типичных конструкций, что в очередной раз сделает работу приятнее\. Пусть некоторые, вероятно, будут не настолько популярны, но это не отменяет их полезности определённым группам разработчиков\. А какое из этих нововведений показалось наиболее полезным для вас? Напишите в комментариях\!

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

* [Обзор нововведений в 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/)
* [Обзор нововведение в C\# 14](https://pvs-studio.ru/ru/blog/posts/csharp/1301/)