diff --git a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 7.md b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 7.md index ae5fd7949eb68..ff9d14857c787 100644 --- a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 7.md +++ b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 7.md @@ -1,139 +1,243 @@ -## This document lists known breaking changes in Roslyn after .NET 6 all the way to .NET 7. +# This document lists known breaking changes in Roslyn after .NET 6 all the way to .NET 7. -1. In Visual Studio 17.1, the contextual keyword `var` cannot be used as an explicit lambda return type. +## Foreach enumerator as a ref struct - ```csharp - using System; +***Introduced in .NET SDK 6.0.300, Visual Studio 2022 version 17.2.*** A `foreach` using a ref struct enumerator type reports an error if the language version is set to 7.3 or earlier. - F(var () => default); // error: 'var' cannot be used as an explicit lambda return type - F(@var () => default); // ok +This fixes a bug where the feature was supported in newer compilers targeting a version of C# prior to its support. - static void F(Func f) { } +Possible workarounds are: - class var { } - ``` +1. Change the `ref struct` type to a `struct` or `class` type. +1. Upgrade the `` element to 7.3 or later. -2. In Visual Studio 17.1, indexers that take an interpolated string handler and require the receiver as an input for the constructor cannot be used in an object initializer. +## Async `foreach` prefers pattern based `DisposeAsync` to an explicit interface implementation of `IAsyncDisposable.DisposeAsync()` - ```cs - using System.Runtime.CompilerServices; - _ = new C { [$""] = 1 }; // error: Interpolated string handler conversions that reference the instance being indexed cannot be used in indexer member initializers. +***Introduced in .NET SDK 6.0.300, Visual Studio 2022 version 17.2.*** An async `foreach` prefers to bind using a pattern-based `DisposeAsync()` method rather than `IAsyncDisposable.DisposeAsync()`. - class C +For instance, the `DisposeAsync()` will be picked, rather than the `IAsyncEnumerator.DisposeAsync()` method on `AsyncEnumerator`: + +```csharp +await foreach (var i in new AsyncEnumerable()) +{ +} + +struct AsyncEnumerable +{ + public AsyncEnumerator GetAsyncEnumerator() => new AsyncEnumerator(); +} + +struct AsyncEnumerator : IAsyncDisposable +{ + public int Current => 0; + public async ValueTask MoveNextAsync() { - public int this[[InterpolatedStringHandlerArgument("")] CustomHandler c] - { - get => throw null; - set => throw null; - } + await Task.Yield(); + return false; } + public async ValueTask DisposeAsync() + { + Console.WriteLine("PICKED"); + await Task.Yield(); + } + ValueTask IAsyncDisposable.DisposeAsync() => throw null; // no longer picked +} +``` + +This change fixes a spec violation where the public `DisposeAsync` method is visible on the declared type, whereas the explicit interface implementation is only visible using a reference to the interface type. + +To workaround this error, remove the pattern based `DisposeAsync` method from your type. + +## Disallow converted strings as a default argument + +***Introduced in .NET SDK 6.0.300, Visual Studio 2022 version 17.2.*** The C# compiler would accept incorrect default argument values involving a reference conversion of a string constant, and would emit `null` as the constant value instead of the default value specified in source. In Visual Studio 17.2, this becomes an error. See [roslyn#59806](https://github.com/dotnet/roslyn/pull/59806). + +This change fixes a spec violation in the compiler. Default arguments must be compile time constants. Previous versions allowed the following code: + +```csharp +void M(IEnumerable s = "hello") +``` + +The preceding declaration required a conversion from `string` to `IEnumerable`. The compiler allowed this construct, and would emit `null` as the value of the argument. The preceding code produces a compiler error starting in 17.2. + +To work around this change, you can make one of the following changes: + +1. Change the parameter type so a conversion isn't required. +1. Change the value of the default argument to `null` to restore the previous behavior. + +## The contextual keyword `var` as an explicit lambda return type + +***Introduced in .NET SDK 6.0.200, Visual Studio 2022 version 17.1.*** The contextual keyword var cannot be used as an explicit lambda return type. + +This change enables [potential future features](https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-06-02.md#lambda-return-type-parsing) by ensuring that the `var` remains the natural type for the return type of a lambda expression. + +You can encounter this error if you have a type named `var` and define a lambda expression using an explicit return type of `var` (the type). + +```csharp +using System; + +F(var () => default); // error CS8975: The contextual keyword 'var' cannot be used as an explicit lambda return type +F(@var () => default); // ok +F(() => default); // ok: return type is inferred from the parameter to F() + +static void F(Func f) { } + +public class var +{ +} +``` + +Workarounds include the following changes: + +1. Use `@var` as the return type. +1. Remove the explicit return type so that the compiler determines the return type. + +## Interpolated string handlers and indexer initialization + +***Introduced in .NET SDK 6.0.200, Visual Studio 2022 version 17.1.*** Indexers that take an interpolated string handler and require the receiver as an input for the constructor cannot be used in an object initializer. + +This change disallows an edge case scenario where indexer initializers use an interpolated string handler and that interpolated string handler takes the receiver of the indexer as a parameter of the constructor. The reason for this change is that this scenario could result in accessing variables that haven't yet been initialized. Consider this example: + +```csharp +using System.Runtime.CompilerServices; - [InterpolatedStringHandler] - class CustomHandler +// error: Interpolated string handler conversions that reference +// the instance being indexed cannot be used in indexer member initializers. +var c = new C { [$""] = 1 }; + +class C +{ + public int this[[InterpolatedStringHandlerArgument("")] CustomHandler c] { - public CustomHandler(int literalLength, int formattedCount, C c) {} + get => ...; + set => ...; } - ``` +} + +[InterpolatedStringHandler] +class CustomHandler +{ + // The constructor of the string handler takes a "C" instance: + public CustomHandler(int literalLength, int formattedCount, C c) {} +} +``` + +Workarounds include the following changes: + +1. Remove the receiver type from the interpolated string handler. +1. Change the argument to the indexer to be a `string` + +## ref, readonly ref, in, out not allowed as parameters or return on methods with Unmanaged callers only + +***Introduced in .NET SDK 6.0.200, Visual Studio 2022 version 17.1.*** `ref`/`ref readonly`/`in`/`out` are not allowed to be used on return/parameters of a method attributed with `UnmanagedCallersOnly`. + +This change is a bug fix. Return values and parameters aren't blittable. Passing arguments or return values by reference can cause undefined behavior. None of the following declarations will compile: -3. In Visual Studio 17.1, `ref`/`ref readonly`/`in`/`out` are not allowed to be used on return/parameters of a method attributed with `UnmanagedCallersOnly`. -https://github.com/dotnet/roslyn/issues/57025 +```csharp +using System.Runtime.InteropServices; +[UnmanagedCallersOnly] +static ref int M1() => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. - ```cs - using System.Runtime.InteropServices; - [UnmanagedCallersOnly] - static ref int M1() => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. +[UnmanagedCallersOnly] +static ref readonly int M2() => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. - [UnmanagedCallersOnly] - static ref readonly int M2() => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. +[UnmanagedCallersOnly] +static void M3(ref int o) => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. - [UnmanagedCallersOnly] - static void M3(ref int o) => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. +[UnmanagedCallersOnly] +static void M4(in int o) => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. - [UnmanagedCallersOnly] - static void M4(in int o) => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. +[UnmanagedCallersOnly] +static void M5(out int o) => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. +``` - [UnmanagedCallersOnly] - static void M5(out int o) => throw null; // error CS8977: Cannot use 'ref', 'in', or 'out' in a method attributed with 'UnmanagedCallersOnly'. - ``` +The workaround is to remove the by reference modifier. -4. Beginning with C# 11.0, `Length` and `Count` properties on countable and indexable types +## Length, Count assumed to be non-negative in patterns + +***Introduced in .NET SDK 6.0.200, Visual Studio 2022 version 17.1.*** `Length` and `Count` properties on countable and indexable types are assumed to be non-negative for purpose of subsumption and exhaustiveness analysis of patterns and switches. Those types can be used with implicit Index indexer and list patterns. - ```csharp - void M(int[] i) +The `Length` and `Count` properties, even though typed as `int`, are assumed to be non-negative when analyzing patterns. Consider this sample method: + +```csharp +string SampleSizeMessage(IList samples) +{ + return samples switch { - if (i is { Length: -1 }) {} // error: impossible under assumption of non-negative length - } - ``` + // This switch arm prevents a warning before 17.1, but will never happen in practice. + // Starting with 17.1, this switch arm produces a compiler error. + // Removing it won't introduce a warning. + { Count: < 0 } => throw new InvalidOperationException(), + { Count: 0 } => "Empty collection", + { Count: < 5 } => "Too small", + { Count: < 20 } => "reasonable for the first pass", + { Count: < 100 } => "reasonable", + { Count: >= 100 } => "fine", + }; +} -5. Starting with Visual Studio 17.1, format specifiers in interpolated strings can not contain curly braces (either `{` or `}`). In previous versions `{{` was interpreted as an escaped `{` and `}}` was interpreted as an escaped `}` char in the format specifier. Now the first `}` char in a format specifier ends the interpolation, and any `{` char is an error. -https://github.com/dotnet/roslyn/issues/57750 +void M(int[] i) +{ + if (i is { Length: -1 }) {} // error: impossible under assumption of non-negative length +} +``` - ```csharp - using System; +Prior to 17.1, The first switch arm, testing that `Count` is negative was necessary to avoid a warning that all possible values weren't covered. Starting with 17.1, the first switch arm generates a compiler error. The workaround is to remove the switch arms added for the invalid cases. - Console.WriteLine($"{{{12:X}}}"); +This change was made as part of adding list patterns. The processing rules are more consistent if every use of a `Length` or `Count` property on a collection are considered non-negative. You can read more details about the change in the [language design issue](https://github.com/dotnet/csharplang/issues/5226). - //prints now: "{C}" - not "{X}}" - ``` +The workaround is to remove the switch arms with unreachable conditions. -6. In Visual Studio 17.1, `struct` type declarations with field initializers must include an explicitly declared constructor. Additionally, all fields must be definitely assigned in `struct` instance constructors that do not have a `: this()` initializer so any previously unassigned fields must be assigned from the added constructor or from field initializers. See [csharplang#5552](https://github.com/dotnet/csharplang/issues/5552), [roslyn#58581](https://github.com/dotnet/roslyn/pull/58581). +## Adding field initializers to a struct requires an explicitly declared constructor - For instance, the following results in an error in 17.1: - ```csharp - struct S - { - int X = 1; // error CS8983: A 'struct' with field initializers must include an explicitly declared constructor. - int Y; - } - ``` +***Introduced in .NET SDK 6.0.200, Visual Studio 2022 version 17.1.*** `struct` type declarations with field initializers must include an explicitly declared constructor. Additionally, all fields must be definitely assigned in `struct` instance constructors that do not have a `: this()` initializer so any previously unassigned fields must be assigned from the added constructor or from field initializers. See [dotnet/csharplang#5552](https://github.com/dotnet/csharplang/issues/5552), [dotnet/roslyn#58581](https://github.com/dotnet/roslyn/pull/58581). - The error could be resolved by adding a constructor and assigning the other field. - ```csharp - struct S - { - int X = 1; - int Y; - public S() { Y = 0; } // ok - } - ``` +There are two ways to initialize a variable to its default value in C#: `new()` and `default`. For classes, the difference is evident since `new` creates a new instance and `default` returns `null`. The difference is more subtle for structs, since for `default`, structs return an instance with each field/property set to its own default. We added field initializers for structs in C# 10. Field initializers are executed only when an explicitly declared constructor runs. Significantly, they don't execute when you use `default` or create an array of any `struct` type. -7. Before Visual Studio 17.2, the C# compiler would accept incorrect default argument values involving a reference conversion of a string constant, and would emit `null` as the constant value instead of the default value specified in source. In Visual Studio 17.2, this becomes an error. See [roslyn#59806](https://github.com/dotnet/roslyn/pull/59806). +In 17.0, if there are field initializers but no declared constructors, a parameterless constructor is synthesized that runs field initializers. However, that meant adding or removing a constructor declaration may affect whether a parameterless constructor is synthesized, and as a result, may change the behavior of `new()`. - For instance, the following results in an error in 17.2: - ```csharp - void M(IEnumerable s = "hello") - ``` +To address the issue, in .NET SDK 6.0.200 (VS 17.1) the compiler no longer synthesizes a parameterless constructor. If a `struct` contains field initializers and no explicit constructors, the compiler generates an error. If a `struct` has field initializers it must declare a constructor, because otherwise the field initializers are never executed. -8. Starting with Visual Studio 17.2, an async `foreach` prefers to bind using a pattern-based `DisposeAsync()` method rather than `IAsyncDisposable.DisposeAsync()`. - For instance, the `DisposeAsync()` will be picked, rather than the `IAsyncEnumerator.DisposeAsync()` method on `AsyncEnumerator`: - ```csharp - await foreach (var i in new AsyncEnumerable()) - { - } +Additionally, all fields that do not have field initializers must be assigned in each `struct` constructor unless the constructor has a `: this()` initializer. - struct AsyncEnumerable - { - public AsyncEnumerator GetAsyncEnumerator(CancellationToken token) => new AsyncEnumerator(); - } +For instance: - struct AsyncEnumerator : IAsyncDisposable - { - public int Current => 0; - public async ValueTask MoveNextAsync() - { - await Task.Yield(); - return false; - } - public async ValueTask DisposeAsync() - { - Console.WriteLine("PICKED"); - await Task.Yield(); - } - ValueTask IAsyncDisposable.DisposeAsync() => throw null; // no longer picked - } - ``` +```csharp +struct S // error CS8983: A 'struct' with field initializers must include an explicitly declared constructor. +{ + int X = 1; + int Y; +} +``` + +The workaround is to declare a constructor. Unless fields were previously unassigned, this constructor can, and often will, be an empty parameterless constructor: + +```csharp +struct S +{ + int X = 1; + int Y; + + public S() { Y = 0; } // ok +} +``` + +## Format specifiers can't contain curly braces + +***Introduced in .NET SDK 6.0.200, Visual Studio 2022 version 17.1.*** Format specifiers in interpolated strings can not contain curly braces (either `{` or `}`). In previous versions `{{` was interpreted as an escaped `{` and `}}` was interpreted as an escaped `}` char in the format specifier. Now the first `}` char in a format specifier ends the interpolation, and any `{` char is an error. + +This makes interpolated string processing consistent with the processing for `System.String.Format`: + +```csharp +using System; +Console.WriteLine($"{{{12:X}}}"); +//prints now: "{C}" - not "{X}}" +``` + +`X` is the format for uppercase hexadecimal and `C` is the hexadecimal value for 12. -9. Starting with Visual Studio 17.2, a `foreach` using a ref struct enumerator type reports an error if the language version is set to 7.3 or earlier. +The workaround is to remove the extra braces in the format string. +You can learn more about this change in the associated [roslyn issue](https://github.com/dotnet/roslyn/issues/57750).