如何要求一个委托或Lambda表达式必须是静态的?

编程语言 2026-07-09

在某些情形下,当我编写一个接收委托作为参数的方法时,我发现需要要求传入的委托必须是静态的。

这里有一个示例,可以作为本问题的可行场景:

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 )

为了让这一步真正线程安全,我需要确保 PR 是不可变的,(我懂的,(是的,相信我,我懂的)),然后还需要确保 asyncDelegate 不产生副作用。

为了确保 asyncDelegate 不产生副作用,我需要实现以下任一条件:

  • 要么确保 asyncDelegatestatic,要么
  • 以某种方式获取 asyncDelegate 所操作的实例。(以确保它也同样不可变。)

注:从技术上讲,即使是静态委托也可能产生副作用,通过修改静态状态,但我不在乎;我打算就让它过去。

那么,是否有人知道有什么方法可以实现上述任意一个?编译时的解决方案会很棒,运行时断言的解决方案也可以。

请注意,还有其他场景也需要强制静态委托,例如在任何可能因在创建Lambda时捕获而引发缺陷的情形中。然而,这样的场景在这里描述起来会更复杂。

补充说明

在评论中,用户 wohlstadDelegate.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);
    }
}

在此输入图片描述

实现的一些注意点:

  1. 为简化起见,我在一个特别命名的类型上使用了一个特别命名的方法。在现实场景中这可能不是最佳做法。一个可考虑的选项是引入一个像 RequireStaticDelegateAttribute 那样的特殊属性,并找到使用它的方法/参数(带属性的方法还将允许跨越函数参数实现“向上传递”)
  2. 这个分析器覆盖了一些基本场景,但在某些情况下可能无法正确处理,例如 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);
    }
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章