--- name: ref-struct-delegates description: > Replace Func<> and Action<> with custom delegate types when any parameter or return type is a ref struct (Span, ReadOnlySpan, etc.). Ref structs cannot appear as generic type arguments, so Func, bool> is a compile error. The fix is a named delegate. USE FOR: writing public APIs that accept callbacks involving spans, storing span-accepting lambdas in fields, wrapping IJsonPathFunction or similar interfaces with delegate-based factories. DO NOT USE FOR: APIs where all parameters are regular (non-ref-struct) types — plain Func<>/Action<> is fine there. --- # Ref Struct Delegate Translation ## The Problem `Func<>`, `Action<>`, and `Predicate<>` are generic types. C# forbids ref structs (`Span`, `ReadOnlySpan`, `ReadOnlyMemory.Span`, etc.) as generic type arguments. This means: ```csharp // ❌ COMPILE ERROR — ReadOnlySpan is a ref struct Func, bool> predicate; Action> writer; Func, JsonWorkspace, JsonPathFunctionResult> evaluator; ``` ## The Fix: Named Delegates Define a named delegate type with the ref struct parameter directly: ```csharp // ✅ COMPILES — named delegates can have ref struct parameters public delegate bool SpanPredicate(ReadOnlySpan data); public delegate void SpanWriter(Span buffer); public delegate JsonPathFunctionResult JsonPathFunctionEvaluator( ReadOnlySpan arguments, JsonWorkspace workspace); ``` ## Translation Rules ### 1. Identify the offending Func/Action Find every `Func<>` or `Action<>` where **any** type argument is a ref struct. ### 2. Create a named delegate with the same shape | Original (broken) | Replacement | |---|---| | `Func, TResult>` | `delegate TResult MyDelegate(ReadOnlySpan items);` | | `Func, TOther, TResult>` | `delegate TResult MyDelegate(ReadOnlySpan items, TOther other);` | | `Action>` | `delegate void MyDelegate(Span buffer);` | | `Predicate>` | `delegate bool MyDelegate(ReadOnlySpan items);` | ### 3. Replace all usages Fields, parameters, locals, and lambda assignments all change from the `Func<>`/`Action<>` type to the named delegate. Lambda syntax remains identical — only the declared type changes: ```csharp // Before (broken): Func, int, bool> check = (span, len) => span.Length >= len; // After (works): public delegate bool SpanCheck(ReadOnlySpan span, int minLength); SpanCheck check = (span, len) => span.Length >= len; ``` ## Convenience Factory Pattern When building factory methods that wrap lambdas into an interface (e.g. `IJsonPathFunction`), split the API into two tiers: 1. **Convenience overloads** — use `Func<>` with non-ref-struct projections of the data (e.g. copy `ReadOnlySpan` to `T[]` before passing to the delegate): ```csharp // Nodes are copied to an array so Func<> works public static IJsonPathFunction NodesValue( Func func) => new DelegateFunction( JsonPathFunctionType.ValueType, [JsonPathFunctionType.NodesType], (args, ws) => JsonPathFunctionResult.FromValue( func(args[0].Nodes.ToArray(), ws))); ``` 2. **Full-control overload** — use the named delegate for zero-copy access: ```csharp public delegate JsonPathFunctionResult JsonPathFunctionEvaluator( ReadOnlySpan arguments, JsonWorkspace workspace); public static IJsonPathFunction Create( JsonPathFunctionType returnType, JsonPathFunctionType[] parameterTypes, JsonPathFunctionEvaluator evaluate) => new DelegateFunction(returnType, parameterTypes, evaluate); ``` This gives callers a choice: simple lambda with a small allocation (`Func`) or zero-allocation with the named delegate. ## Storing the Delegate in a Field Named delegates are regular reference types and can be stored in fields, unlike the ref structs they accept: ```csharp private sealed class DelegateFunction : IJsonPathFunction { private readonly JsonPathFunctionEvaluator evaluate; // ✅ field storage works public DelegateFunction(JsonPathFunctionEvaluator evaluate) { this.evaluate = evaluate; } public JsonPathFunctionResult Evaluate( ReadOnlySpan arguments, JsonWorkspace workspace) => this.evaluate(arguments, workspace); } ``` ## Naming Convention Name the delegate after its purpose, not its shape. Good names describe *what* the callback does: | ✅ Good | ❌ Bad | |---|---| | `JsonPathFunctionEvaluator` | `SpanFuncDelegate` | | `SpanPredicate` | `ReadOnlySpanBoolFunc` | | `BufferWriter` | `SpanAction` | ## Scope This applies to **all** ref struct types, not just spans: - `Span`, `ReadOnlySpan` - Custom `ref struct` types in the codebase - Any future `ref struct` added by .NET ## Cross-References - For the stackalloc/ArrayPool rent pattern that produces spans, see `corvus-buffer-and-pooling` - For ref-struct collections in this codebase, see `corvus-low-alloc-data-structures`