Fix: The call is ambiguous between the following methods or properties после перехода на члены расширения C# 14
CS0121 после переноса метода расширения в блок extension C# 14: компилятор по-прежнему генерирует старую статическую форму. Удалите дубликат или уточните вызов.
Вы перенесли метод расширения с параметром this в блок extension C# 14, оставили оригинал “на всякий случай”, и теперь каждое место вызова падает с CS0121. Исправление состоит в том, чтобы удалить одно из двух объявлений, потому что это не две разные сущности: компилятор понижает метод из блока расширения ровно до того же статического метода с параметром this, который у вас уже был. Если удалить ни одно из них нельзя (второе лежит в пакете NuGet), уточните вызов именем содержащего статического класса: MyExtensions.WordCount(s) вместо s.WordCount().
error CS0121: The call is ambiguous between the following methods or properties:
'New.StringExtensions2.extension(string).WordCount()' and 'Old.StringExtensions.WordCount(string)'
Обратите внимание на форму сообщения. Один кандидат печатается как extension(string).WordCount(), а другой как WordCount(string). Эта асимметрия и есть весь диагноз: Roslyn сообщает, что один кандидат пришёл из блока расширения, а другой из классического метода с параметром this, и выбрать между ними он не может. Всё изложенное ниже проверено на .NET SDK 10.0.201 с <LangVersion>14.0</LangVersion>.
Почему CS0121 срабатывает, когда обе синтаксические формы в области видимости?
C# 14 не вводил второй, отдельный механизм поиска для членов расширения. Блок расширения это синтаксис объявления, и компилятор понижает его до члена статического класса, неотличимого от того, что порождает this string s. Когда две директивы using приносят в область видимости по классу и оба класса дают кандидата WordCount(string) с одинаковой применимостью, у разрешения перегрузки не остаётся критерия выбора, поэтому оно сообщает CS0121.
Это не новое правило. Та же ошибка всегда возникала, когда две библиотеки определяют один и тот же метод расширения для одного типа. Новым стало то, что миграция собственного кода теперь сама создаёт коллизию, ведь недоделанная миграция оставляет обе формы живыми одновременно.
Что компилятор на самом деле генерирует для блока расширения?
Это та часть, которую стоит усвоить, потому что она объясняет все симптомы на этой странице. Возьмём один блок с методом и свойством:
// .NET 10.0.201, C# 14
namespace Lib;
public static class StringExtensions
{
extension(string s)
{
public int WordCount() => s.Split(' ').Length;
public bool IsBlank => string.IsNullOrWhiteSpace(s);
}
}
Рефлексия по скомпилированному Lib.StringExtensions в том же решении печатает:
METHOD Int32 WordCount(String s) [Extension]
METHOD Boolean get_IsBlank(String s)
NESTED <G>$34505F560D9EACF86A87F3ED1F85E448 ext-attr=True
CLASS ext-attr=True
Из этого дампа следуют три вещи:
WordCountгенерируется как публичный статический метод, принимающий приёмник первым параметром и несущий[ExtensionAttribute]. В метаданных это и есть классический метод расширения. Именно поэтому он конфликтует с написанным вручную методомthis, и именно поэтому написать оба это дубликат, а не слой совместимости.- Свойство понижается до
get_IsBlank(String s), публичного статического метода без[ExtensionAttribute]. Свойства не являются классическими методами расширения, поэтому их находит другой путь поиска и они падают с другой диагностикой (смотрите ниже). - Вложенный тип
<G>$<hash>это маркерный тип на основе содержимого, который компилятор генерирует для каждого блока расширения. Хеш выводится из содержимого блока, поэтому два блока с одинаковыми приёмниками и членами в одном классе конфликтуют с CS9329.
Поскольку пониженный метод действительно является обычным методом расширения, проект, зафиксированный на <LangVersion>13.0</LangVersion>, всё ещё может его потреблять. Я проверил это ссылкой на проект из приложения на C# 13 на библиотеку на C# 14: "a b c".WordCount() и StringExtensions.WordCount("a b c") компилируются и печатают 3. Добавление "a b c".IsBlank в тот же файл падает с error CS9260: Feature 'extensions' is not available in C# 13.0. Методы расширения, объявленные в блоке, потребляются из старых версий языка, а свойства расширения нет.
Минимальное воспроизведение: два статических класса, одно имя метода
// Old.cs -- .NET 10.0.201, C# 14
namespace Old;
public static class StringExtensions
{
public static int WordCount(this string s) => s.Split(' ').Length;
}
// New.cs -- .NET 10.0.201, C# 14
namespace New;
public static class StringExtensions2
{
extension(string s)
{
public int WordCount() => s.Split(' ').Length;
}
}
// Use.cs -- .NET 10.0.201, C# 14
using Old;
using New;
System.Console.WriteLine("a b c".WordCount()); // CS0121
dotnet build падает на месте вызова, а не на одном из объявлений. Это важно: по отдельности объявления законны, поэтому ошибка появляется только в файлах, где импортированы оба пространства имён. Частично мигрированное решение поэтому собирается в одних проектах и падает в других, что выглядит как нестабильная сборка, пока вы не посмотрите на списки using.
То же самое происходит между сборками, и именно эту версию большинство людей встречает на практике. Библиотека поставляет блоки расширения, вы держите локальную прослойку с методом this, написанную до обновления, и любой файл, импортирующий оба пространства имён, ломается:
error CS0121: The call is ambiguous between the following methods or properties:
'Lib.StringExtensions.extension(string).WordCount()' and 'App.Compat.MyStringExtensions.WordCount(string)'
Как исправить CS0121, если оба объявления принадлежат мне?
Удалите версию с параметром this. Это всё исправление, и это не компромисс: как показано выше, блок расширения всё равно генерирует помеченный [ExtensionAttribute] статический метод с идентичной сигнатурой, так что каждое существующее место вызова продолжает работать, включая полностью уточнённую форму MyExtensions.WordCount(s) и вызывающих на старых версиях языка.
// .NET 10.0.201, C# 14 -- one declaration, both call shapes still work
namespace Lib;
public static class StringExtensions
{
extension(string s)
{
public int WordCount() => s.Split(' ').Length;
}
}
// both of these compile:
// "a b c".WordCount()
// StringExtensions.WordCount("a b c")
Правило миграции, которое стоит написать на доске: блок расширения заменяет старый метод, а не соседствует с ним. Всякий инстинкт “оставить старый ради совместимости” здесь ошибочен, ведь двоичная и исходная совместимость уже сохраняются самим понижением.
Как разрешить неоднозначность, если дубликат лежит в пакете NuGet?
Удалить чужое объявление вы не можете, поэтому выберите один из вариантов, в порядке предпочтения.
Вызовите статический метод напрямую. Оба кандидата предоставляют статическую форму, поэтому назовите нужный класс:
// .NET 10.0.201, C# 14
System.Console.WriteLine(New.StringExtensions2.WordCount("a b c")); // extension block version
System.Console.WriteLine(Old.StringExtensions.WordCount("a b c")); // this-parameter version
Это компилируется чисто. В месте вызова получается многословно, зато однозначно, легко ищется через grep и переживает будущие обновления пакетов.
Уберите using и перейдите на псевдоним пространства имён. Члены расширения попадают в область видимости только через простой using пространства имён. Псевдоним пространства имён импортирует имена, не добавляя кандидатов расширения:
// .NET 10.0.201, C# 14
using OldAlias = Old; // types reachable as OldAlias.StringExtensions, but no extension candidates
using New;
System.Console.WriteLine("x".WordCount()); // binds to New, prints 2
Я запустил именно этот файл, он печатает 2. Это самый чистый вариант, когда файлу нужны типы из пространства имён, но не его расширения. Следите за директивами global using в GlobalUsings.cs и элементами <Using Include="..."/> в csproj, потому что они импортируют расширения в каждый файл проекта и обычно именно из-за них неоднозначность возникает в файле, чей собственный список using выглядит безобидно.
Дайте двум членам разные имена. Если новый принадлежит вам и ещё не опубликован, переименование обойдётся дешевле, чем обучение всей команды правилу разрешения неоднозначности.
Можно ли пометить старый метод атрибутом [Obsolete], чтобы разрешить конфликт?
Нет. Устаревание не является критерием выбора при разрешении перегрузки. Кандидат остаётся применимым, а ошибка идентична:
// .NET 10.0.201, C# 14 -- still CS0121
[System.Obsolete("Use Lib")]
public static int WordCount(this string s) => 1;
[Obsolete] полезен, чтобы сказать потребителям перестать что-то вызывать, но на множество кандидатов компилятора он не влияет. То же касается [EditorBrowsable(EditorBrowsableState.Never)], который лишь прячет члены из IntelliSense.
Когда я получаю CS0111 вместо CS0121?
Потому что оба объявления находятся в одном статическом классе. Тогда это не неоднозначный вызов, а дублирующийся член:
// .NET 10.0.201, C# 14
namespace A;
public static class E1
{
public static int WordCount(this string s) => 1;
extension(string s)
{
public int WordCount() => 2; // CS0111
}
}
error CS0111: Type 'E1' already defines a member called 'WordCount' with the same parameter types
CS0111 сообщается на объявлении, ещё до того как появится хоть одно место вызова. Из двух ошибок она дружелюбнее, потому что доказывает эквивалентность напрямую: компилятор считает, что у WordCount(this string) и у WordCount() из блока одинаковые типы параметров. Если вы мигрируете класс по одному методу, эту ошибку вы увидите первой.
Что делать, если неоднозначность в свойстве расширения (CS9339)?
У свойств расширения своя диагностика, потому что в метаданных они не являются методами с [ExtensionAttribute] и разрешаются через поиск членов расширения, а не через обычное разрешение перегрузки:
// N1.cs -- .NET 10.0.201, C# 14
namespace N1;
public static class E
{
extension(System.Text.StringBuilder b)
{
public int Cap { get => b.Capacity; set => b.Capacity = value; }
}
}
// N2.cs -- .NET 10.0.201, C# 14
namespace N2;
public static class E
{
extension(System.Text.StringBuilder b)
{
public int Cap { get => b.Capacity; set => b.Capacity = value; }
}
}
// Use.cs -- .NET 10.0.201, C# 14
using N1;
using N2;
var sb = new System.Text.StringBuilder();
sb.Cap = 64; // CS9339
error CS9339: The extension resolution is ambiguous between the following members:
'N1.E.extension(System.Text.StringBuilder).Cap' and 'N2.E.extension(System.Text.StringBuilder).Cap'
Исправление той же формы, но вам придётся назвать метод доступа, поскольку синтаксиса свойства, несущего имя класса, не существует:
// .NET 10.0.201, C# 14 -- disambiguated, prints 64
N1.E.set_Cap(sb, 64);
System.Console.WriteLine(N1.E.get_Cap(sb));
Методы доступа get_ и set_ это ровно то, во что понижается блок, поэтому их вызов не хак, а вызов настоящего члена. Он достаточно уродлив, чтобы считать его временной разблокировкой на время, пока вы удаляете один из дубликатов. Если вы ещё решаете, какую форму придать этим объявлениям, правила объявления свойств расширения в C# 14 объясняют, почему автосвойства отклоняются и что могут делать методы доступа.
Разрешает ли конфликт более специфичный тип приёмника?
Да, и именно поэтому ломается лишь часть ваших мест вызова. Разрешение перегрузки по-прежнему предпочитает лучшее преобразование от приёмника, и это сравнение происходит поверх обеих синтаксических форм. Блок расширения на string побеждает метод с параметром this на IEnumerable<char>:
// Old.cs -- .NET 10.0.201, C# 14
namespace Old;
public static class E
{
public static string Describe(this System.Collections.Generic.IEnumerable<char> s) => "IEnumerable<char>";
}
// New.cs -- .NET 10.0.201, C# 14
namespace New;
public static class E
{
extension(string s)
{
public string Describe() => "string";
}
}
// Use.cs -- .NET 10.0.201, C# 14
using Old;
using New;
System.Console.WriteLine("x".Describe()); // prints: string
Обобщённый метод с параметром this проигрывает конкретному блоку расширения на том же приёмнике и по-прежнему побеждает для любого другого типа приёмника:
// .NET 10.0.201, C# 14
// G1.E: public static string Kind<T>(this T value) => "generic this-method";
// G2.E: extension(string s) { public string Kind() => "extension block on string"; }
System.Console.WriteLine("x".Kind()); // extension block on string
System.Console.WriteLine(42.Kind()); // generic this-method
Значит миграция, меняющая приёмник с IEnumerable<T> на конкретный тип, молча переведёт часть мест вызова на новую реализацию вообще без ошибки. Это изменение поведения, спрятанное внутри того, что выглядит как рефакторинг синтаксиса, и оно заслуживает теста, а не компиляции.
Разрешает ли конфликт метод экземпляра?
Член экземпляра всегда побеждает любой член расширения, в любой из двух синтаксических форм, без диагностики. Если тип получает метод экземпляра с подходящей сигнатурой в более поздней версии зависимости, оба ваших объявления расширения становятся недостижимыми и ничто вас не предупредит:
// .NET 10.0.201, C# 14
public class Order { public decimal Total() => 10m; }
public static class E1 { public static decimal Total(this Order o) => 20m; }
public static class E2 { extension(Order o) { public decimal Total() => 30m; } }
// new Order().Total() prints 10
Эта программа компилируется без предупреждений и печатает 10. Это зеркальное отражение CS0121: два неоднозначных члена расширения шумят, два затенённых молчат. Это тот же класс риска при обновлении, что и ломающее изменение разрешения перегрузки в C# 14 со span, где новое неявное преобразование тихо перепривязывает существующие вызовы.
Какой порядок миграции полностью исключает эту ошибку?
- Переносите объявления, а не копируйте их. Вырежьте метод
thisиз статического класса и вставьте тело в блокextensionтого же класса. CS0111 поймает вас сразу, если вы оступитесь на этом шаге, и именно поэтому миграция внутри одного класса безопаснее, чем заведение нового. - Мигрируйте по целому статическому классу за раз. Наполовину мигрированные классы это нормально, а вот наполовину мигрированные пространства имён с параллельным классом “V2” и есть источник CS0121.
- Никогда не создавайте класс расширений
NewилиV2рядом со старым. Поддерживать совместимость не с чем, поэтому параллельный класс покупает вам только неоднозначность. - После переноса соберите решение через
dotnet build, прежде чем трогать места вызова. Каждое место вызова, которое всё ещё компилируется, доказывает, что понижение совпало. - Запускайте тесты, а не только компилятор. Правила специфичности приёмника выше означают, что миграция может изменить, какая реализация выполняется, не сломав сборку.
Если вы делаете это в рамках более крупного скачка, чеклист миграции с .NET 8 на .NET 11 выстраивает подъём версии языка относительно обновлений среды выполнения и пакетов, и именно этот порядок не даёт этой ошибке прийти вместе с двумя десятками других.
Связанное
- Члены расширения C# 14: свойства расширения, операторы и статические расширения для полной поверхности возможности, включая формы операторов и статических членов, которые эта статья не покрывает.
- Как объявлять свойства расширения в C# 14 для правил методов доступа, стоящих за приёмом с
get_иset_. - Индексаторы расширения C# 15 в .NET 11 Preview 6 о том, куда движется синтаксис блока расширения.
- Fix: ломающее изменение разрешения перегрузки в C# 14 со Span и ReadOnlySpan о другом изменении C# 14, перепривязывающем существующие места вызова.
- Миграция с .NET 8 на .NET 11: полный чеклист для выстраивания подъёма версии языка.
Источники
- Resolve errors and warnings related to extension declarations на MS Learn, где перечислены CS9339 и семейство CS93xx диагностик блоков расширения.
- Extension methods на MS Learn, о двух синтаксисах объявления и рекомендациях по разрешению неоднозначности.
- C# 14: exploring extension members в .NET Blog, где задокументировано понижение до статических методов с префиксом
get_и подтверждена цель дизайна: преобразование метода расширения в новый синтаксис не ломает его потребителей. - Extensions discussion в dotnet/csharplang, тред проектирования возможности.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.