What's wrong
A lambda with a ref T parameter whose expression body has type T converts to both ActionRef<T> and FuncRef<T>. C#'s better-conversion rule prefers the delegate that returns a value, so overload resolution picks With<T>(T input, FuncRef<T>) (DelegateTransform/DelegateTransform.cs:65) over With<T>(T input, ActionRef<T>) (:33).
The FuncRef overload returns the lambda's result. It takes input by value, so a mutation made through ref only changes a local copy and is thrown away.
Observed at c0ba286 with a net10.0 build:
(ref int x) => x++ : 5 (ActionRef intent expects 6)
(ref int x) => { x++; } : 6
(ref int x) => Interlocked.Exchange(ref x, 7) : 5 (expects 7)
FuncRef mutating x to 99, returning 1 : 1 (caller's input still 5)
The only difference between the first two lines is the braces.
Why it matters
- Wrong values, silently. A caller who writes an expression-bodied lambda to "modify by reference" (the README's framing of
ActionRef) gets the old value back, with no warning.
- Mislabelled README example. The "With ActionRef" example,
(ref int x) => x *= 2, actually binds to FuncRef. It only returns the expected 10 because a compound assignment happens to evaluate to the new value.
- Unfounded copy-avoidance advice. README.md:177 says to use
FuncRef<T> for large structs "where you want to avoid copying". input is a by-value parameter, so it is copied anyway, and a ref mutation never reaches the caller. As implemented, FuncRef behaves exactly like Func<T, T>.
Suggested fix
Either of these works:
- Give the ref-based overloads distinct names, for example
WithRef(T, ActionRef<T>), so an expression lambda can't silently switch overloads.
- Make
FuncRef genuinely by reference, as With<T>(ref T input, FuncRef<T>), or drop it as redundant with Func<T, T>.
With either option:
- Change the README's ActionRef example to a block body,
(ref int x) => { x *= 2; }.
- Correct the "use FuncRef to avoid copying" guidance.
Acceptance criteria
(ref int x) => x++ used with ActionRef intent either returns 6 or fails to compile.
- The README examples bind to the overloads they are labelled with.
- A regression test covers the expression-bodied case.
What's wrong
A lambda with a
ref Tparameter whose expression body has typeTconverts to bothActionRef<T>andFuncRef<T>. C#'s better-conversion rule prefers the delegate that returns a value, so overload resolution picksWith<T>(T input, FuncRef<T>)(DelegateTransform/DelegateTransform.cs:65) overWith<T>(T input, ActionRef<T>)(:33).The
FuncRefoverload returns the lambda's result. It takesinputby value, so a mutation made throughrefonly changes a local copy and is thrown away.Observed at
c0ba286with a net10.0 build:The only difference between the first two lines is the braces.
Why it matters
ActionRef) gets the old value back, with no warning.(ref int x) => x *= 2, actually binds toFuncRef. It only returns the expected 10 because a compound assignment happens to evaluate to the new value.FuncRef<T>for large structs "where you want to avoid copying".inputis a by-value parameter, so it is copied anyway, and arefmutation never reaches the caller. As implemented,FuncRefbehaves exactly likeFunc<T, T>.Suggested fix
Either of these works:
WithRef(T, ActionRef<T>), so an expression lambda can't silently switch overloads.FuncRefgenuinely by reference, asWith<T>(ref T input, FuncRef<T>), or drop it as redundant withFunc<T, T>.With either option:
(ref int x) => { x *= 2; }.Acceptance criteria
(ref int x) => x++used with ActionRef intent either returns 6 or fails to compile.