Skip to content

DelegateTransform.With(5, (ref int x) => x++) returns 5: expression-bodied ref lambdas bind to the FuncRef overload, which discards the in-place change #79

Description

@matt-edmondson

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:

  1. Give the ref-based overloads distinct names, for example WithRef(T, ActionRef<T>), so an expression lambda can't silently switch overloads.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions