﻿# What's new in C\# 15: Overview

A new version of C\# is coming soon, so we're continuing our annual tradition of reviewing its new features\. The Microsoft team has prepared several highly anticipated and exciting changes for this release\. Let's take a look at them\. 

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

## Union types

We'll start with the most straightforward change, `union` types\. They enable developers to combine types without relying on inheritance or interfaces where it's unnecessary or unavailable\. A `union` is a structure that contains the complete set of supported types\.

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

In the code above, `union` is the keyword, while `int`, `Version`, and `IEnumerable<string>` are the set of supported types\.

The `union` types come in handy when traditional inheritance isn't an option, as it requires the types to have something in common\. However, there may be cases where inheritance is impossible or makes little architectural sense\. We might need to handle a specific set of types, including primitives\. Normally, we'd have to specify a variable of the `object` type as input and then cast it to the required types within the method, including logic for handling anything unsupported\. That's error\-prone\. Using `union` in this situation would solve these problems, making the code safer and easier to read\.

Given the nature of the new structure, when we use `union`, the compiler already knows the complete list of types it can contain\. So, the `default` branch is no longer necessary\. The change protects us from a `switch` that misses some types while new subclasses keep falling into the `default` section, which can cause unpredictable program behavior\.

Let's look at a more specific example\. Before this update, we'd have had to write something like this to define a method for handling API responses:

```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"
  };
}
```

The `ApiResult` abstract class, from which specific responses inherit, is declared\. So, if a new subclass appears and the developer fails to handle it, executions falls into the `default` branch\. With a `union`, it works differently:

```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
    };
  }
}
```

As you can see, there's no need for a parent class just to group the types together\. The behavior is also more predictable because the compiler knows at compile time exactly which types `union` can contain\. So, if a `switch` doesn't handle every type in the union, the compiler issues a warning\.

Moreover, the `union` gives developers an easier way to express patterns such as monads\. It also enables implementing custom methods and properties within `union`, so working with all its types doesn't require writing `switch`\.

## Closed hierarchies

This change is somewhat related to `union` in that it also lets the compiler know exactly how many types it'll work with\. It does so by preventing inheritance from a class outside the assembly where it was declared\. This behavior resembles a mix of `internal` and `sealed`: it takes the scope from `internal` and the restriction on inheritance from `sealed`\. This can be useful, for example, in libraries where we want to restrict inheritance from certain classes without blocking it within our own assembly\. Here's an example declaration:

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

The `closed` keyword marks the class as closed to inheritance\. As a result, attempting to inherit from such a class in another assembly causes a compilation error\. Note that, like `union`, a closed hierarchy supports an exhaustive `switch`, which enables omitting the `default` branch\. 

A few important details are worth keeping in mind\. Firstly, a class marked as `closed` is always abstract\. This also helps enable an exhaustive `switch`\. Secondly, descendants don't automatically become closed\. If we need to hide them from other assemblies, we can mark them with the `internal` access modifier\. Thirdly, only classes \(`class` and `record class`\) can be closed to inheritance from another assembly\. Structs don't support inheritance, while interfaces are specifically designed to be inherited from\. So, adding the keyword to a struct or an interface will result in a compilation error\. 

## Labeled break and continue

A small but quite important addition: most of us have probably run into situations where we needed to interrupt the execution of an external structure or skip to its next iteration\. The classic approach would have looked something like this:

```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
    }
    // ....
  }
}
```

In other words, we need an additional flag so that the outer loop can exit after the inner `switch` finishes executing\. Of course, we could use `goto`, but it can make the code harder to follow because the label code execution jumps to can appear anywhere\. Labeled `break` and `continue` simplify this\. Let's see what this method would look like now\.

```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
    }
  }
  // ....
}
```

As you can see, the syntax is similar to `goto`, while the code becomes more readable\. This lets us avoid creating extra flags and checks, which can slightly reduce memory usage and also make the code more expressive\. Labels can be applied with the following constructs: `for`, `foreach`, `while`, `do`, and `switch`\.

## Extension indexers

This change enables developers to define how collection elements can be accessed by index using extension indexers\. For example, it's possible to implement element access with `Range` for `IReadOnlyList`\. Before C\# 15, we could implement this as follows:

```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;
  }
}
```

While a regular extension method already solves this problem, C\# 15 introduces the option to create extension indexers\. So, we'll get access to elements using the familiar brackets\. Let's look at how this would work in our example:

```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();
      }
    }
  }
}
```

It's worth noting that if a type has its own indexer, it takes precedence over an indexer declared as an extension\. The official order of precedence is as follows: `explicit` type indexers, `implicit` type indexers, `explicit` extension indexers, and `implicit` extension indexers\. 

## Collection expression arguments

The next feature will help shorten code and make collection initialization more convenient by passing the required arguments directly to the constructor\. For example, we can pass `capacity` and `comparer`\. In C\# 14 and earlier, we would need to write something like this:

```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");
}
```

The initializer supports only one option because `List` has just three constructors: an empty one, one that takes `capacity`, and one that takes a collection\. Let's look at the new possible implementation:

```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");

}
```

The code looks cleaner, right? Your IDE will probably even suggest this syntax and collapse everything into a single line\. However, note that `with` must always come first; otherwise, the compiler will issue an error\.

## Memory safety

The final change involves reevaluating what C\# considers unsafe\. The language team aims to flag only code that actually accesses unmanaged memory as unsafe\. In other words, using pointers will no longer automatically make code unsafe\. Before C\# 15, unsafe code was marked like this:

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

Any use of pointers used to require the `unsafe` keyword\. To better understand the change, let's take a look at what the code will look like in the new version:

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

We no longer need to mark the class as `unsafe`\. Instead, the keyword goes only where it's needed\. In this case, dereferencing the pointer is unsafe, so it should be marked as such\.

The `safe` keyword has also been added\. It should be used when a block contains unsafe operations that are safe for the caller's context, so callers don't require the `unsafe` block\. In other words, use `safe` when the compiler can't verify safety on its own\. Examples include `extern` methods and fields in structs with explicit memory layout \(`[StructLayout(LayoutKind.Explicit)]`\)\.

It's also worth mentioning `stackalloc`\. Its use is now considered unsafe when all of the following apply: it's converted to `Span<T>` or `ReadOnlySpan<T>`, it has no initializer, and it's used where `SkipLocalsInitAttribute` is present\.

The new rules introduced by this change should be enabled separately\. If you'd like to learn more, follow [this link](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/language#enable-the-updated-memory-safety-rules) for the official instructions\.

## Conclusion

This year's language release has brought developers a number of fairly important features\. Among them are useful additions such as `union` types and closed hierarchies, while several other changes simplify common coding patterns, making development little smoother once again\. Some of these features may not become as popular, but that doesn't make them any less useful to certain groups of developers\. Which of these changes did you find most useful? Share your thoughts in the comments\!

You can read the C\# 15 documentation via [this link](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-15)\. Don't forget to check out our overviews of previous versions, here's the list:

* [What's new in C\# 9: overview](https://pvs-studio.com/en/blog/posts/csharp/0860/)
* [What's new in C\# 10: overview](https://pvs-studio.com/en/blog/posts/csharp/0875/)
* [What's new in C\# 11: overview](https://pvs-studio.com/en/blog/posts/csharp/1002/)
* [What's new in C\# 12: overview](https://pvs-studio.com/en/blog/posts/csharp/1074/)
* [What's new in C\# 13: overview](https://pvs-studio.com/en/blog/posts/csharp/1173/)
* [What's new in C\# 14: overview](https://pvs-studio.com/en/blog/posts/csharp/1301/)