如何要求一个委托或Lambda表达式必须是静态的?
在某些情形下,当我编写一个接收委托作为参数的方法时,我发现需要要求传入的委托必须是静态的。
这里有一个示例,可以作为本问题的可行场景:
Say, I want to write a truly thread-safe replacement for RunTask( action ).
替代实现可能看起来像这样:
//executes asyncDelegate in a different thread to process an instance of P and
//produce an instance of R, then executes resultConsumer in the original thread,
//to process R.
RunTask<P,R>( P parameter, System.Func<P,R> asyncDelegate,
System.Action<R> resultConsumer )
为了让这一步真正线程安全,我需要确保 P 和 R 是不可变的,(我懂的,(是的,相信我,我懂的)),然后还需要确保 asyncDelegate 不产生副作用。
为了确保 asyncDelegate 不产生副作用,我需要实现以下任一条件:
- 要么确保
asyncDelegate是static,要么 - 以某种方式获取
asyncDelegate所操作的实例。(以确保它也同样不可变。)
注:从技术上讲,即使是静态委托也可能产生副作用,通过修改静态状态,但我不在乎;我打算就让它过去。
那么,是否有人知道有什么方法可以实现上述任意一个?编译时的解决方案会很棒,运行时断言的解决方案也可以。
请注意,还有其他场景也需要强制静态委托,例如在任何可能因在创建Lambda时捕获而引发缺陷的情形中。然而,这样的场景在这里描述起来会更复杂。
补充说明
在评论中,用户 wohlstad 说 Delegate.Target 应该能解决我的问题,用户 Sweeper 补充说我还可以确保 Delegate.GetInvocationList().Length == 1,因为 Target 只返回第一个元素。太好了;谢谢;不幸的是,这让我意识到我并未问完整的问题;我忘了提及一个非常重要的部分:
检查不仅在传入方法时起作用,在传入Lambda时也应起作用!
因为你看,当传入一个Lambda时,一切都会不同。毕竟如此。
以下代码演示了这个问题:
public static class RunTaskTest
{
public static void Run()
{
int fortyTwo = 42;
assertStatic( true, staticFunction ); //works (trivial)
assertStatic( false, nonStaticFunction ); //works (trivial)
assertStatic( false, () => fortyTwo ); //works (happy path)
assertStatic( true, () => 42 ); //fails
assertStatic( true, static () => 42 ); //fails too
return;
static int staticFunction() => 42;
int nonStaticFunction() => fortyTwo;
}
static void assertStatic<T>( bool shouldBeStatic, System.Func<T> action )
{
Assert( IsStatic( action ) == shouldBeStatic );
}
public static bool IsStatic( System.Delegate @delegate )
{
bool hasTarget = @delegate.Target != null;
Assert( @delegate.Method.IsStatic == !hasTarget );
return !hasTarget;
}
}
解决方案
然后我还需要确保
asyncDelegate不产生副作用。
在一般情况下你做不到这一点,因为即使委托是静态的、并且它作用于真正不可变的数据,它仍然可能做任何事情,例如修改全局状态。除非你愿意走分析表达式树的路(即接受 Expression<Action<P, R>> 并在其中搜索副作用),否则你对以下东西就无能为力:
static class MyActionHolder
{
private static int Counter;
public static void Act<P, R>(P p, R r)
{
Counter++; // modify global state in non-thread-safe manner
// or any other side-effect like
// Console.WriteLine($"{p}: {r}");
}
}
Rider曾有一个将方法标记为Pure的属性 - PureAttribute,但不确定它是如何强制执行的,以及你如何在委托上强制它。
to process an instance of P and produce an instance of R
那么应该是 Func<P, R> 吗,而不是 Action?
A compile-time solution would be great
如果你想只确保 static 部分,那么可以考虑编写你自己的 Roslyn代码分析器 并在其中检查传入的委托是否带有 static 标记,或者是调用了一个 static 方法(我会在周末尝试做一个演示)。
更新
创建了一个分析器的演示版本,能够处理问题中的示例:
namespace StaticLambdaAnalyzer.Analyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AvoidNonstaticLabmdasAnalyzer : DiagnosticAnalyzer
{
public const string AvoidNonstaticLabmdasAnalyzerId = "STLA0001";
private static readonly DiagnosticDescriptor Rule = new(
AvoidNonstaticLabmdasAnalyzerId,
"Avoid Nonstatic Labmdas",
"Use static methods/lamdas instead of {0}",
"Performance",
DiagnosticSeverity.Warning,
true,
"",
"");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
/// <inheritdoc />
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(ctx =>
{
var analyzerContext = new AnalyzerContext(ctx.Compilation);
// if (analyzerContext.ConcurrentDictionarySymbol is null)
// return;
ctx.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
});
}
private sealed class AnalyzerContext
{
public AnalyzerContext(Compilation compilation)
{
var symbol = compilation.GetTypesByMetadataName("MyAnalyzedClass");
AnalyzedType = symbol.FirstOrDefault();
if (AnalyzedType is null) return;
// TODO: change to correct name and add validations
AnalyzedMethod = AnalyzedType.GetMembers("assertStatic").OfType<IMethodSymbol>().SingleOrDefault();
}
public INamedTypeSymbol AnalyzedType { get; }
public IMethodSymbol AnalyzedMethod { get; }
public void AnalyzeInvocation(OperationAnalysisContext context)
{
var op = (IInvocationOperation)context.Operation;
// if (!op.TargetMethod.ContainingSymbol.OriginalDefinition.IsEqualTo(AnalyzedType))
// {
// return;
// }
if (!op.TargetMethod.OriginalDefinition.IsEqualTo(AnalyzedMethod)) return;
// TODO: change + validations
var argumentOperation = op.Arguments[1];
if (argumentOperation.Value is IDelegateCreationOperation delegateCreationOperation)
{
var delegateTarget = delegateCreationOperation.Target;
if (delegateTarget is IMethodReferenceOperation methodReferenceOperation)
{
if (methodReferenceOperation.Method.IsStatic) return;
context.ReportDiagnostic(
Diagnostic.Create(
Rule,
methodReferenceOperation.Syntax.GetLocation(),
methodReferenceOperation.Method.Name));
}
else if (delegateCreationOperation.Target is IAnonymousFunctionOperation anonymousFunctionOperation)
{
// check that lambda marked as static
if (anonymousFunctionOperation.Symbol.IsStatic) return;
var syntax = GetDataFlowArgument(anonymousFunctionOperation.Body.Syntax);
var semanticModel = context.Operation.SemanticModel!;
var dataFlow = semanticModel.AnalyzeDataFlow(syntax);
if (dataFlow.CapturedInside.Length > 0)
context.ReportDiagnostic(Diagnostic.Create(
Rule,
anonymousFunctionOperation.Syntax.GetLocation(),
string.Join(", ", dataFlow.Captured.Select(symbol => symbol.Name))));
}
}
}
private static SyntaxNode GetDataFlowArgument(SyntaxNode node)
{
if (node is null)
return null;
if (node is ArrowExpressionClauseSyntax expression) return expression.Expression;
return node;
}
}
}
}
internal static class SymbolExtensions
{
public static bool IsEqualTo(this ISymbol symbol, ISymbol expectedType)
{
if (symbol is null || expectedType is null)
return false;
return SymbolEqualityComparer.Default.Equals(expectedType, symbol);
}
}
实现的一些注意点:
- 为简化起见,我在一个特别命名的类型上使用了一个特别命名的方法。在现实场景中这可能不是最佳做法。一个可考虑的选项是引入一个像
RequireStaticDelegateAttribute那样的特殊属性,并找到使用它的方法/参数(带属性的方法还将允许跨越函数参数实现“向上传递”) - 这个分析器覆盖了一些基本场景,但在某些情况下可能无法正确处理,例如
Func在被实例化后就传入调用中,像下面这样的情况:
cs
Func<int> nonStaticAsParam = nonStaticFunction;
assertStatic(false, nonStaticAsParam);
或者非静态函数在匿名lambda中被调用的情况:
cs
assertStatic(false, () => nonStaticFunction());
3.也许最好的/最简单的方法是对Lambdas强制使用 static 修饰符(尽管它仍会漏掉诸如 static () => new Foo().NonStaticFunction() 和委托创建等内容)
4.实现很基础,仅覆盖了一些基本内容,但可以说分析器方法尽管复杂,却为更广泛的分析提供了能力。
完整代码、测试和示例应用可以在 @github 找到
namespace StaticLambdaAnalyzer.Analyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AvoidNonstaticLabmdasAnalyzer : DiagnosticAnalyzer
{
public const string AvoidNonstaticLabmdasAnalyzerId = "STLA0001";
private static readonly DiagnosticDescriptor Rule = new(
AvoidNonstaticLabmdasAnalyzerId,
"Avoid Nonstatic Labmdas",
"Use static methods/lamdas instead of {0}",
"Performance",
DiagnosticSeverity.Warning,
true,
"",
"");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
/// <inheritdoc />
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(ctx =>
{
var analyzerContext = new AnalyzerContext(ctx.Compilation);
// if (analyzerContext.ConcurrentDictionarySymbol is null)
// return;
ctx.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation);
});
}
private sealed class AnalyzerContext
{
public AnalyzerContext(Compilation compilation)
{
var symbol = compilation.GetTypesByMetadataName("MyAnalyzedClass");
AnalyzedType = symbol.FirstOrDefault();
if (AnalyzedType is null) return;
// TODO: change to correct name and add validations
AnalyzedMethod = AnalyzedType.GetMembers("assertStatic").OfType<IMethodSymbol>().SingleOrDefault();
}
public INamedTypeSymbol AnalyzedType { get; }
public IMethodSymbol AnalyzedMethod { get; }
public void AnalyzeInvocation(OperationAnalysisContext context)
{
var op = (IInvocationOperation)context.Operation;
// if (!op.TargetMethod.ContainingSymbol.OriginalDefinition.IsEqualTo(AnalyzedType))
// {
// return;
// }
if (!op.TargetMethod.OriginalDefinition.IsEqualTo(AnalyzedMethod)) return;
// TODO: change + validations
var argumentOperation = op.Arguments[1];
if (argumentOperation.Value is IDelegateCreationOperation delegateCreationOperation)
{
var delegateTarget = delegateCreationOperation.Target;
if (delegateTarget is IMethodReferenceOperation methodReferenceOperation)
{
if (methodReferenceOperation.Method.IsStatic) return;
context.ReportDiagnostic(
Diagnostic.Create(
Rule,
methodReferenceOperation.Syntax.GetLocation(),
methodReferenceOperation.Method.Name));
}
else if (delegateCreationOperation.Target is IAnonymousFunctionOperation anonymousFunctionOperation)
{
// check that lambda marked as static
if (anonymousFunctionOperation.Symbol.IsStatic) return;
var syntax = GetDataFlowArgument(anonymousFunctionOperation.Body.Syntax);
var semanticModel = context.Operation.SemanticModel!;
var dataFlow = semanticModel.AnalyzeDataFlow(syntax);
if (dataFlow.CapturedInside.Length > 0)
context.ReportDiagnostic(Diagnostic.Create(
Rule,
anonymousFunctionOperation.Syntax.GetLocation(),
string.Join(", ", dataFlow.Captured.Select(symbol => symbol.Name))));
}
}
}
private static SyntaxNode GetDataFlowArgument(SyntaxNode node)
{
if (node is null)
return null;
if (node is ArrowExpressionClauseSyntax expression) return expression.Expression;
return node;
}
}
}
}
internal static class SymbolExtensions
{
public static bool IsEqualTo(this ISymbol symbol, ISymbol expectedType)
{
if (symbol is null || expectedType is null)
return false;
return SymbolEqualityComparer.Default.Equals(expectedType, symbol);
}
}
