我正在研究一种方法,如果我覆盖基类中的特定方法但忘记在重写的方法中调用基本方法,则使Visual Studio触发警告。 E.g:
class Foo
{
[SomeAttributeToMarkTheMethodToFireTheWarning]
public virtual void A() { ... }
}
class Bar : Foo
{
public override void A()
{
// base.A(); // warning if base.A() is not called
// ...
}
}
到目前为止,我无法找到方法,可能无法让编译器直接触发此类警告。有任何想法可以做到这一点,即使它是第三方工具或使用新的Roslyn .NET编译器平台中的某些API?
更新:
例如,在AndroidStudio(IntelliJ)中,如果您在任何活动中覆盖onCreate()
但忘记调用基本方法super.onCreate()
,则会收到警告。这就是我在VS中需要的行为。
答案 0 :(得分:3)
如果您想确保运行某些代码,那么您应该更改设计:
abstract class Foo
{
protected abstract void PostA();
public void A() {
...
PostA();
}
}
class Bar : Foo
{
protected override void PostA()
{
}
}
//method signature remains the same:
Bar.A();
这样,A()
总是在被覆盖的方法
要具有多重继承并确保调用A(),您还必须使条形抽象:
abstract class Bar : Foo
{
//no need to override now
}
class Baz:Bar
{
protected override void PostA()
{
}
}
无法在C#中完成完全您想要的内容。这不是Visual Studio问题。这就是C#的工作原理。
虚拟方法签名可以被覆盖或不被覆盖,无论是否在基础中调用。您有两个虚拟或抽象选项。您使用virtual
并且我已经为您提供了abstract
解决方案。您可以选择要使用的那个。
我能想到你最想要的东西是#warning
。见this answer。但这只会在输出窗口中产生警告而不是智能感知。基本上是C# does not support custom compiler warnings。
答案 1 :(得分:3)
我终于有时间尝试使用Roslyn,看起来我找到了一个带分析仪的解决方案。这是我的解决方案。
用于标记子类中需要覆盖的方法的属性:
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class RequireBaseMethodCallAttribute : Attribute
{
public RequireBaseMethodCallAttribute() { }
}
分析仪:
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class RequiredBaseMethodCallAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "RequireBaseMethodCall";
// You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat.
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Localizing%20Analyzers.md for more on localization
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources));
private const string Category = "Usage";
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(AnalyzeMethodForBaseCall);
}
private static void AnalyzeMethodForBaseCall(CompilationStartAnalysisContext compilationStartContext)
{
compilationStartContext.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
}
private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
{
var mds = context.Node as MethodDeclarationSyntax;
if (mds == null)
{
return;
}
IMethodSymbol symbol = context.SemanticModel.GetDeclaredSymbol(mds) as IMethodSymbol;
if (symbol == null)
{
return;
}
if (!symbol.IsOverride)
{
return;
}
if (symbol.OverriddenMethod == null)
{
return;
}
var overridenMethod = symbol.OverriddenMethod;
var attrs = overridenMethod.GetAttributes();
if (!attrs.Any(ad => ad.AttributeClass.MetadataName.ToUpperInvariant()
== typeof(RequireBaseMethodCallAttribute).Name.ToUpperInvariant()))
{
return;
}
var overridenMethodName = overridenMethod.Name.ToString();
string methodName = overridenMethodName;
var invocations = mds.DescendantNodes().OfType<MemberAccessExpressionSyntax>().ToList();
foreach (var inv in invocations)
{
var expr = inv.Expression;
if ((SyntaxKind)expr.RawKind == SyntaxKind.BaseExpression)
{
var memberAccessExpr = expr.Parent as MemberAccessExpressionSyntax;
if (memberAccessExpr == null)
{
continue;
}
// compare exprSymbol and overridenMethod
var exprMethodName = memberAccessExpr.Name.ToString();
if (exprMethodName != overridenMethodName)
{
continue;
}
var invokationExpr = memberAccessExpr.Parent as InvocationExpressionSyntax;
if (invokationExpr == null)
{
continue;
}
var exprMethodArgs = invokationExpr.ArgumentList.Arguments.ToList();
var ovrMethodParams = overridenMethod.Parameters.ToList();
if (exprMethodArgs.Count != ovrMethodParams.Count)
{
continue;
}
var paramMismatch = false;
for (int i = 0; i < exprMethodArgs.Count; i++)
{
var arg = exprMethodArgs[i];
var argType = context.SemanticModel.GetTypeInfo(arg.Expression);
var param = arg.NameColon != null ?
ovrMethodParams.FirstOrDefault(p => p.Name.ToString() == arg.NameColon.Name.ToString()) :
ovrMethodParams[i];
if (param == null || argType.Type != param.Type)
{
paramMismatch = true;
break;
}
exprMethodArgs.Remove(arg);
ovrMethodParams.Remove(param);
i--;
}
// If there are any parameters left without default value
// then it is not the base method overload we are looking for
if (ovrMethodParams.Any(p => p.HasExplicitDefaultValue))
{
continue;
}
if (!paramMismatch)
{
// If the actual arguments match with the method params
// then the base method invokation was found
// and there is no need to continue the search
return;
}
}
}
var diag = Diagnostic.Create(Rule, mds.GetLocation(), methodName);
context.ReportDiagnostic(diag);
}
}
CodeFix提供商:
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(BaseMethodCallCodeFixProvider)), Shared]
public class BaseMethodCallCodeFixProvider : CodeFixProvider
{
private const string title = "Add base method invocation";
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(RequiredBaseMethodCallAnalyzer.DiagnosticId); }
}
public sealed override FixAllProvider GetFixAllProvider()
{
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
return WellKnownFixAllProviders.BatchFixer;
}
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
// Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument: c => AddBaseMethodCallAsync(context.Document, diagnosticSpan, c),
equivalenceKey: title),
diagnostic);
}
private async Task<Document> AddBaseMethodCallAsync(Document document, TextSpan diagnosticSpan, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken);
var node = root.FindNode(diagnosticSpan) as MethodDeclarationSyntax;
var args = new List<ArgumentSyntax>();
foreach (var param in node.ParameterList.Parameters)
{
args.Add(SyntaxFactory.Argument(SyntaxFactory.ParseExpression(param.Identifier.ValueText)));
}
var argsList = SyntaxFactory.SeparatedList(args);
var exprStatement = SyntaxFactory.ExpressionStatement(
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.BaseExpression(),
SyntaxFactory.Token(SyntaxKind.DotToken),
SyntaxFactory.IdentifierName(node.Identifier.ToString())
),
SyntaxFactory.ArgumentList(argsList)
),
SyntaxFactory.Token(SyntaxKind.SemicolonToken)
);
var newBodyStatements = SyntaxFactory.Block(node.Body.Statements.Insert(0, exprStatement));
var newRoot = root.ReplaceNode(node.Body, newBodyStatements).WithAdditionalAnnotations(Simplifier.Annotation);
return document.WithSyntaxRoot(newRoot);
}
}
演示如何运作:http://screencast.com/t/4Jgm989TI
由于我是.NET编译器平台的新手,我很乐意就如何改进我的解决方案提出任何反馈和建议。提前谢谢!
答案 2 :(得分:0)
我已经通过运行时检查解决了这个问题。
我创建了一个对象,该对象能够断言最基本的方法已为其任何可覆盖的方法调用。
这当然不是理想的;如Borislav的解决方案所示,理想的方法是使它成为编译时检查。但是Borislav的解决方案代表了大量的知识,大量的工作,对构建系统的干预,对编辑器的干预aaaargh! Roslyn Analyzers,CodeFix提供程序,这些都是奇特的东西。我什至不知道从哪里开始实施这样的事情。
因此,这是我相当简单的运行时检查方法:
/// <summary>
/// Base class for making sure that descendants always invoke overridable
/// methods of base.
/// </summary>
public abstract class Overridable
{
private sealed class InvocationGuard : IDisposable
{
private readonly Overridable overridable;
public readonly string MethodName;
public bool Invoked;
public InvocationGuard( Overridable overridable, string methodName )
{
this.overridable = overridable;
MethodName = methodName;
}
public void Dispose()
{
Assert( ReferenceEquals( overridable.invocationStack.Peek(), this ) );
Assert( Invoked );
overridable.invocationStack.Pop();
}
}
private readonly Stack<InvocationGuard> invocationStack = new Stack<InvocationGuard>();
public IDisposable NewOverridableGuard( string methodName )
{
Assert( ReflectionHelpers.MethodExistsAssertion( GetType(), methodName ) );
var invocationGuard = new InvocationGuard( this, methodName );
invocationStack.Push( invocationGuard );
return invocationGuard;
}
public void OverridableWasInvoked( [CanBeNull][CallerMemberName] string methodName = null )
{
Assert( methodName != null );
Assert( ReflectionHelpers.MethodExistsAssertion( GetType(), methodName ) );
InvocationGuard invocationGuard = invocationStack.Peek();
Assert( invocationGuard.MethodName == methodName );
Assert( !invocationGuard.Invoked );
invocationGuard.Invoked = true;
}
}
为简洁起见,ReflectionHelpers.MethodExistsAssertion()
的实现留给读者练习;无论如何,这是一个可选的断言。
按以下方式使用它:
(我选择OnPropertyChanged( string propertyName )
作为示例,因为许多开发人员可能已经熟悉它,以及使用它时忘记调用base
的问题。)
protected internal void RaisePropertyChanged( [CallerMemberName] string propertyName = null )
{
using( NewOverridableGuard( nameof(OnPropertyChanged) ) ) //Add this to your existing code
OnPropertyChanged( propertyName );
}
protected virtual void OnPropertyChanged( string propertyName )
{
OverridableWasInvoked(); //Add this to your existing code
PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
}
注意:Stack<>
是处理递归调用所必需的。
注意:检查仅考虑可重写方法的名称,而不考虑其参数,因此,为获得最佳结果,最好不要重载可重写对象。