属性规范中奇怪的歧义(两个 using 指令)

Curious ambiguity in attribute specification (two using directives)

背景:attribute specification中,有时有两种有效的方法来编写应用属性。例如,如果属性 class 的名称为 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
  {
  }
}

当我只写 [Horse] 时,C# 编译器能够选择 Alpha.HorseAttribute。毕竟,类型 Beta.Horse 完全不适合在属性规范中使用。

即使我交换名称,C# 编译器也会知道该怎么做:

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
  {
  }
}

现在,C# 编译器拒绝构建,说:

error CS0104: 'Horse' is an ambiguous reference between 'Alpha.Horse' and 'Beta.Horse'

Try it online!

我的问题是,为什么编译器在这种情况下不能选择正确的,而它在前面的两个示例中做得很好?

此行为是否符合 C# 语言规范?这里真的需要C#编译器报错吗?

(我当然知道我可以通过明确地说 [Alpha.Horse] 来解决它,所以我不是要求那个 "solution"。)

这是编译器的不一致行为,尽管它可能符合规范。恕我直言,第一种情况也应该是歧义错误;尚不清楚编写该代码的人是否知道 enum 不能在该上下文中使用,并且可能正在尝试完全不同的东西(显然是错误的)。允许您省略 Attribute 的语法糖可能 运行 在此类情况下会遇到麻烦。

我不是编译器如何工作的专家,但我认为这里发生的是,在编译器对源代码进行的第一遍中,它必须寻找所有省略的属性并替换它们带有类型的全名,并且 woosh,歧义永远消失了。如果不省略属性名称,则不会发生这种情况,因为名称不会被替换,然后歧义错误会在后面的步骤中突然出现。

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 (!@namespace.Equals(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 'Horse' is not an attribute class

AA0001 Possible attribute 'Horse' is ambiguous between attributes 'Alpha.Horse', 'Alpha.Middle.Omega.Horse' and non attributes 'Horse', 'Alpha.Middle.Horse', 'Beta.Horse'

建议的修复是:

'Horse' to 'Alpha.Horse'

'Horse' to 'Alpha.Middle.Omega.Horse'

我们这里有两个概念混为一谈。


1.编译器如何知道 class 实现了一个属性

有一个简单的约定,可以通过 class 名称或 class 名称减去属性后缀来引用属性。因此,当您像这样将 [Horse] 注释添加到 someIdentifier 时,

[Horse]
someIdentifier

[Horse] 的实现必须是继承 Attribute 的 class,称为 HorseAttributeHorse

注意: 有一个被广泛接受的约定,即所有实现属性的 class 都应该在类型名称后加上 "Attribute" 后缀。

2。编译器如何知道引用的是哪种类型的代码

当我们在代码中引用一个类型时,编译器会查找已加载到命名空间中的该类型的定义。如果命名空间中该类型有多个定义,则编译器不会采取任何措施来解决这种歧义,这取决于开发人员改进代码。编译器不能选择所以引发 error CS1040.

编译器不做任何语义或静态分析来判断编码器的意图。很难定义,执行成本高且容易出错。

此错误不会仅在查找属性的实现时抛出。


在您的编译示例中,第 2 点没有歧义,因此代码可以编译。

如果第 1 点的解析导致类型名称不明确,HorseHorseAttribute,则错误将来自第 2 点。

编译器没有特别允许,例如我正在执行第 2 点以响应第 1 点,所以,如果我在这种情况下有歧义,是否有针对第 1 点执行的第 2 点的特殊回退位置?

如果考虑到特殊规定引入的额外复杂性和时间,您可能会接受最好要求代码作者的严格程度。

在我和其他人看来,要求代码避免这种歧义会导致代码更容易被他人和未来的自己理解。这使得关于 为什么 的讨论有些没有实际意义,因为我们可以争辩说,编译器团队在这里所做的努力会使 "smellier" 更难维护代码。


注意:进一步回答

当您考虑 Langauge specification

中的示例所展示的行为时
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 {}

Try here

我同意编译器处理来自一个命名空间的定义和从不同命名空间导入的定义的方式存在混淆,实际上是不一致。