Search code examples
c#recordc#-9.0

C# records with delegate property


While experimenting with C# 9 record, I came across a rather strange behavior. My record looks like this:

record MyRecord(Func<MyRecord> SomeAction, string Name)
{
    public MyRecord(string name) : this(null, name) 
    {
        SomeAction = Foo;
    }

    // Foo returns 'this' with SomeAction changed to Bar
    MyRecord Foo()
    {
        Console.WriteLine("Foo: " + SomeAction.Method.Name);
        return this with { SomeAction = Bar };
    }

    MyRecord Bar()
    {
        Console.WriteLine("Bar: " + SomeAction.Method.Name);
        return this;
    }
}

And I use it like this:

class Program
{
    static void Main(string[] args)
    {
        var r = new MyRecord("Foo");
        Console.WriteLine(r.ToString());
        r = r.SomeAction();
        r = r.SomeAction();
        r = r.SomeAction();
    }
}

The output I expected would be

Foo: Foo
Bar: Bar
Bar: Bar

However, the actual output I got:

Foo: Foo
Bar: Foo 
Foo: Foo

Is this a bug or am I missing something?


Solution

  • return this with { SomeAction = Bar };
    

    Captures Bar on the original record, not the updated record. And on the original record, SomeAction.Method.Name is Foo. Since Bar returns this, and Bar is the Bar of the original record, the second line returns the original record, which explains why the third line is the same as the first.

    It will be easier to understnd if we rewrite it like this:

    class Program
    {
        static void Main(string[] args)
        {
            var r = new MyRecord("Foo");
            Console.WriteLine(r.ToString());
            var r1 = r.SomeAction(); // calls r.Foo, returns new instance of record, capturing r.Bar
            var r2 = r1.SomeAction(); // calls r.Bar, and returns r.
            var r3 = r2.SomeAction(); // calls r.Foo, returns new instance of record, capturing r.Bar
        }
    }
    

    To get your expected behavior, you would have to do something like this:

    record MyRecord(Func<MyRecord> SomeAction, string Name)
    {
        public MyRecord(string name) : this(null, name) 
        {
            SomeAction = Foo;
        }
    
        // Foo returns 'this' with SomeAction changed to Bar
        MyRecord Foo()
        {
            Console.WriteLine("Foo: " + SomeAction.Method.Name);
            MyRecord updated = null;
            updated = this with { SomeAction = () => updated.Bar() };
            return updated;
        }
    
        MyRecord Bar()
        {
            Console.WriteLine("Bar: " + SomeAction.Method.Name);
            return this;
        }
    }
    

    However I wouldn't recommend this approach - it is very difficult to follow.