Любопытная неоднозначность в спецификации атрибутов (две с использованием директив)
Справочная информация: В спецификации атрибута attribute specification иногда есть два допустимых способа написания примененного атрибута. Например, если класс атрибута имеет имя HorseAttribute
, вы можете применить атрибут как [HorseAttribute]
или просто [Horse]
. Неопределенности можно устранить с помощью @
, например [@Horse]
.
Ниже приведена действительная программа:
using System;
using Alpha;
using Beta;
namespace N
{
[Horse]
class C
{
}
}
namespace Alpha
{
// valid non-abstract attribute type with accessible constructor
class HorseAttribute : Attribute
{
}
}
namespace Beta
{
// any non-attribute type with that name
enum Horse
{
}
}
Компилятор С# может выбрать Alpha.HorseAttribute
, когда я пишу только [Horse]
. В конце концов, тип Beta.Horse
совершенно не подходит для использования в спецификации атрибутов.
Даже если я поменяю имена, компилятор С# будет знать, что делать:
using System;
using Alpha;
using Beta;
namespace N
{
[Horse]
class C
{
}
}
namespace Alpha
{
// valid non-abstract attribute type with accessible constructor
class Horse : Attribute
{
}
}
namespace Beta
{
// any non-attribute type with that name
enum HorseAttribute
{
}
}
Опять же, компилятор знает, что я хочу Alpha.Horse
.
А теперь код, о котором я хочу спросить. Оно идентично приведенному выше, за исключением того, что два типа теперь имеют одинаковое имя:
using System;
using Alpha;
using Beta;
namespace N
{
[Horse]
class C
{
}
}
namespace Alpha
{
// valid non-abstract attribute type with accessible constructor
class Horse : Attribute
{
}
}
namespace Beta
{
// any non-attribute type with that name
enum Horse
{
}
}
Теперь С# -compiler отказывается строить, говоря:
Ошибка CS0104: "Лошадь" является неоднозначной ссылкой между "Alpha.Horse" и "Beta.Horse"
Попробуйте онлайн!
У меня вопрос: почему компилятор не может выбрать правильный в этом случае, если он хорошо это сделал в двух предыдущих примерах?
Соответствует ли это поведение спецификации языка С#? Действительно ли требуется, чтобы компилятор С# выдавал ошибку здесь?
(Конечно, я знаю, что могу решить эту проблему, сказав [Alpha.Horse]
в явном виде, поэтому я не прошу этого "решения".)
Ответы
Ответ 1
То, что мы имеем здесь, это две взаимосвязанные концепции.
1. Как компилятор знает, какой класс реализует атрибут
Существует простое соглашение о том, что на атрибуты можно ссылаться либо по имени класса, либо по имени класса без суффикса атрибута. Поэтому, когда вы добавляете аннотацию [Horse]
к someIdentifier
следующим образом,
[Horse]
someIdentifier
реализация [Horse]
должна быть классом, который наследует Attribute
, который называется либо HorseAttribute
, либо Horse
.
Примечание. Существует широко распространенное соглашение, согласно которому все классы, реализующие атрибуты, должны иметь суффикс "Атрибут" к имени типа.
2. Как компилятор узнает, какой код типа ссылается на
Когда мы ссылаемся на тип, в коде компилятор ищет определение этого типа, которое было загружено в пространство имен. Если для этого типа в пространстве имен существует несколько определений, компилятор ничего не делает для устранения этой неоднозначности, разработчик должен улучшить код. Компилятор не может выбрать, поэтому возникает ошибка CS1040.
Компилятор не выполняет никакого семантического или статического анализа, чтобы определить намерение кодеров. Это было бы трудно определить, дорого обходиться и быть подверженным ошибкам.
Эта ошибка возникает не только при поиске реализаций для атрибутов.
В ваших примерах компиляции нет никакой двусмысленности вокруг пункта 2, поэтому код компилируется.
Если разрешение точки 1 приводит к неоднозначному имени типа, будь то Horse
или HorseAttribute
, то ошибки будут исходить из пункта 2.
Компилятор не делает специальных допусков, например, я выполняю пункт 2 в ответ на пункт 1, поэтому, если у меня есть неоднозначность в этом случае, есть ли специальная запасная позиция для точки 2, выполненной для точки 1 с?
Если вы учитываете уровень дополнительной сложности и время, которое предусматривают специальные положения, вы можете согласиться с тем, что было бы лучше требовать определенного уровня строгости от авторов кода.
По моему мнению, и, как и у других, требование кода, который избегает такой двусмысленности, приводит к коду, который будет легче понять другим и будущему самому себе. Это делает дискуссию о том, почему спорным, как мы могли бы утверждать, усилия, приложенные здесь командой компиляторов, сделали бы более "грязный", труднее поддерживать код.
ПРИМЕЧАНИЕ. В дополнение к ответу
Если учесть поведение, демонстрируемое примером из спецификации Langauge
using System;
[AttributeUsage(AttributeTargets.All)]
public class X: Attribute
{}
[AttributeUsage(AttributeTargets.All)]
public class XAttribute: Attribute
{}
[X] // Error: ambiguity
class Class1 {}
[XAttribute] // Refers to XAttribute
class Class2 {}
[@X] // Refers to X
class Class3 {}
[@XAttribute] // Refers to XAttribute
class Class4 {}
Попробуйте здесь
Я бы согласился, что существует путаница и, действительно, несоответствие в том, как компилятор обрабатывает определения из одного пространства имен и импортированные из разных пространств имен.
Ответ 2
Это противоречивое поведение компилятора, даже если оно может соответствовать спецификациям. ИМХО в первом сценарии тоже должна быть ошибка двусмысленности; не совсем ясно, что тот, кто написал этот код, знал, что enum
не может быть использован в этом контексте и может попробовать что-то совершенно другое (и, очевидно, неправильно). Синтаксический сахар, который позволяет вам элиминировать Attribute
, может столкнуться с проблемами в подобных ситуациях.
Я не эксперт в том, как работает компилятор, но я думаю, что здесь происходит то, что на одном из самых первых проходов компилятор выполняет над исходным кодом, он должен искать все атрибуты elided и заменять их типом full имя, и вау, двусмысленность ушла навсегда. Этого не происходит, когда имя атрибута не удаляется, так как имя не заменяется, а затем ошибка неоднозначности возникает на более позднем этапе.
Ответ 3
Attribute
в основном тип, а также enum
. Эта двусмысленность является стандартным поведением. Просто освободите свой разум от наследования class Horse
Attribute
. В этом смысле трактуйте это как тип. Разрешение имен типов - это первое, что делает компилятор. После этого выполняется проверка атрибутов (которые вы пытаетесь использовать в качестве атрибута) на предмет совместимости с Attribute
. Ваше решение, в котором указано полное имя, является единственным правильным.
Обновление:
Похоже, вы ожидаете, что компилятор CS будет различать семантику использования атрибутов наряду с разрешением имен типов. Это можно реализовать вручную с помощью специального анализатора кода, например:
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public partial class AmbiguityAnalysisAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "AA0001";
private static readonly DiagnosticDescriptor Rule =
new DiagnosticDescriptor(id: DiagnosticId,
title: "Specify the attribute.",
messageFormat: "Possible attribute '{0}' is ambiguous between {1}",
category: "Attribute Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Ambiguous attribute.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context) =>
context.RegisterSemanticModelAction(SemanticModelAction);
private void SemanticModelAction(SemanticModelAnalysisContext context)
{
var types = GetAllTypes(context.SemanticModel.Compilation).ToArray();
var attributes = GetAllAttribute(context);
var ambiguities = GetAmbiguities(types, attributes);
foreach (var ambiguity in ambiguities)
context.ReportDiagnostic(ambiguity);
}
}
С
public partial class AmbiguityAnalysisAnalyzer
{
private static IEnumerable<INamedTypeSymbol> GetAllTypes(Compilation compilation) =>
GetAllTypes(compilation.GlobalNamespace);
private static IEnumerable<INamedTypeSymbol> GetAllTypes(INamespaceSymbol @namespace)
{
foreach (var type in @namespace.GetTypeMembers())
foreach (var nestedType in GetNestedTypes(type))
yield return nestedType;
foreach (var nestedNamespace in @namespace.GetNamespaceMembers())
foreach (var type in GetAllTypes(nestedNamespace))
yield return type;
}
private static IEnumerable<INamedTypeSymbol> GetNestedTypes(INamedTypeSymbol type)
{
yield return type;
foreach (var nestedType in type.GetTypeMembers()
.SelectMany(nestedType => GetNestedTypes(nestedType)))
yield return nestedType;
}
private static AttributeSyntax[] GetAllAttribute(SemanticModelAnalysisContext context) =>
context
.SemanticModel
.SyntaxTree
.GetRoot()
.DescendantNodes()
.OfType<AttributeSyntax>()
.ToArray();
private static IEnumerable<Diagnostic> GetAmbiguities(INamedTypeSymbol[] types, AttributeSyntax[] attributes)
{
foreach (var attribute in attributes)
{
var usings = GetUsings(attribute.SyntaxTree);
var ambiguities = GetAmbiguities(usings, types, attribute);
if (ambiguities.Length < 2)
continue;
var suggestedAttributes = GetAttributes(ambiguities);
var suggestedNonAttributes = GetNonAttributes(ambiguities);
var parts =
new[]
{
GetPart("attributes", suggestedAttributes),
GetPart("non attributes", suggestedNonAttributes)
}
.Where(part => !part.Equals(string.Empty));
var name = (attribute.Name as IdentifierNameSyntax)?.Identifier.ValueText;
var suggestions =
name == null ?
ImmutableDictionary<string, string>.Empty :
suggestedAttributes.Select(type => GetFullyQualifiedName(type))
.ToImmutableDictionary(type => type, type => name);
var message = string.Join(" and ", parts);
yield return Diagnostic.Create(Rule, attribute.GetLocation(), suggestions, attribute.Name, message);
}
}
}
И другие вспомогательные методы
public partial class AmbiguityAnalysisAnalyzer
{
private static string GetFullyQualifiedName(INamedTypeSymbol type)
{
var @namespace = GetFullName(type.ContainingNamespace, n => !n.IsGlobalNamespace, n => n.ContainingNamespace);
var name = GetFullName(type, t => t != null, t => t.ContainingType);
if ([email protected](string.Empty, StringComparison.Ordinal))
return $"{@namespace}.{name}";
return name;
}
private static string[] GetUsings(SyntaxTree syntaxTree) =>
syntaxTree
.GetCompilationUnitRoot()
.Usings.Select(GetUsingString)
.Concat(new[] { string.Empty })
.ToArray();
private static string GetUsingString(UsingDirectiveSyntax @using) =>
GetUsingStringFromName(@using.Name);
private static string GetUsingStringFromName(NameSyntax name)
{
if (name is IdentifierNameSyntax identifierName)
return identifierName.Identifier.ValueText;
if (name is QualifiedNameSyntax qualifiedName)
return $"{GetUsingStringFromName(qualifiedName.Left)}.{GetUsingStringFromName(qualifiedName.Right)}";
throw new ArgumentException($"Argument '{nameof(name)}' was of unexpected type.");
}
private static INamedTypeSymbol[] GetAmbiguities(IEnumerable<string> usings, IEnumerable<INamedTypeSymbol> types, AttributeSyntax attribute) =>
types
.Where(t => attribute.Name is IdentifierNameSyntax name &&
NameMatches(t, name) &&
NamespaceInUsings(usings, t))
.ToArray();
private static bool NamespaceInUsings(IEnumerable<string> usings, INamedTypeSymbol type) =>
usings.Contains(GetFullName(type.ContainingNamespace, n => !n.IsGlobalNamespace, n => n.ContainingNamespace));
private static bool NameMatches(INamedTypeSymbol type, IdentifierNameSyntax nameSyntax)
{
var isVerbatim = nameSyntax.Identifier.Text.StartsWith("@");
var name = nameSyntax.Identifier.ValueText;
var names = isVerbatim ? new[] { name } : new[] { name, name + "Attribute" };
var fullName = GetFullName(type, t => t != null, t => t.ContainingType);
var res = names.Contains(fullName, StringComparer.Ordinal);
return res;
}
private static string GetFullName<TSymbol>(TSymbol symbol, Func<TSymbol, bool> condition, Func<TSymbol, TSymbol> transition) where TSymbol : ISymbol
{
var values = new List<string>();
while (condition(symbol))
{
values.Add(symbol.Name);
symbol = transition(symbol);
}
values.Reverse();
return string.Join(".", values);
}
private static IEnumerable<INamedTypeSymbol> GetAttributes(IEnumerable<INamedTypeSymbol> types) =>
types.Where(type => IsAttribute(type));
private static IEnumerable<INamedTypeSymbol> GetNonAttributes(IEnumerable<INamedTypeSymbol> types) =>
types.Where(type => !IsAttribute(type));
private static bool IsAttribute(INamedTypeSymbol type) =>
type == null ?
false :
type.ContainingNamespace.Name.Equals("System", StringComparison.Ordinal) &&
type.Name.Equals("Attribute", StringComparison.Ordinal) ||
IsAttribute(type.BaseType);
private static string GetPart(string description, IEnumerable<INamedTypeSymbol> types)
{
var part = string.Join(", ", types.Select(type => $"'{type}'"));
if (!part.Equals(string.Empty))
part = $"{description} {part}";
return part;
}
}
Поставщик исправления кода может быть следующим:
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AmbiguityAnalysisCodeFixProvider)), Shared]
public class AmbiguityAnalysisCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(AmbiguityAnalysisAnalyzer.DiagnosticId);
public sealed override FixAllProvider GetFixAllProvider() =>
WellKnownFixAllProviders.BatchFixer;
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.First();
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var attribute =
root
.FindToken(diagnostic.Location.SourceSpan.Start)
.Parent
.AncestorsAndSelf()
.OfType<AttributeSyntax>()
.First();
foreach(var suggestion in diagnostic.Properties)
{
var title = $"'{suggestion.Value}' to '{suggestion.Key}'";
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedSolution: c => ReplaceAttributeAsync(context.Document, attribute, suggestion.Key, c),
equivalenceKey: title),
diagnostic);
}
}
private static async Task<Solution> ReplaceAttributeAsync(Document document, AttributeSyntax oldAttribute, string suggestion, CancellationToken cancellationToken)
{
var name = SyntaxFactory.ParseName(suggestion);
var newAttribute = SyntaxFactory.Attribute(name);
var root = await document.GetSyntaxRootAsync().ConfigureAwait(false);
root = root.ReplaceNode(oldAttribute, newAttribute);
return document.Project.Solution.WithDocumentSyntaxRoot(document.Id, root);
}
}
Попробуйте выполнить анализ с помощью следующего кода:
using System;
using Alpha;
using Alpha.Middle;
using Alpha.Middle.Omega;
using Beta;
public class Horse { }
namespace N
{
[Horse]
class C { }
}
namespace Alpha
{
public class Horse : Attribute { }
namespace Middle
{
public class Horse { }
namespace Omega
{
public class Horse : Attribute { }
}
}
}
namespace Beta
{
public enum Horse { }
public class Foo
{
public class Horse : Attribute { }
}
}
выдает ошибки:
CS0616 "Лошадь" не является классом атрибутов
AA0001 Возможный атрибут "Лошадь" неоднозначен между атрибутами "Alpha.Horse", "Alpha.Middle.Omega.Horse" и без атрибутов "Horse", "Alpha.Middle.Horse", "Beta.Horse"
Предлагаемые исправления:
"Лошадь" - "Альфа. Лошадь"
"Лошадь" - "Альфа. Середина. Омега. Лошадь"