Customized C# Chained Calls Not Functioning as Expected

120 Views Asked by At

I've customized a C# class for chaining calls to add my styles, but when I first create an instance of the style class and then use chaining calls, it only displays the first added style in the end.

Here is the code:

MyStyleBuilder:

namespace TestStyleBuilder
{
    public struct MyStyleBuilder
    {

        private string stringBuffer;

        public MyStyleBuilder AddLineColor(string value) => this.AddStyle("color", value);

        public MyStyleBuilder AddLineDash(string value) => this.AddStyle("dash", value);

        public MyStyleBuilder AddStyle(string prop, string value)
        {
            var style = $"{prop}:{value};";
            stringBuffer += style;
            return this;
        }

        public override string ToString()
        {
            return stringBuffer != null ? stringBuffer.Trim() : string.Empty;
        }
    }
}

Test:

using TestStyleBuilder;

// Output:
// color:color;dash:dash;
MyStyleBuilder builder1 = new MyStyleBuilder().AddLineColor("color").AddLineDash("dash");
Console.WriteLine(builder1.ToString());

// Output:
// color:color;
MyStyleBuilder builder2 = new MyStyleBuilder();
builder2.AddLineColor("color").AddLineDash("dash");
Console.WriteLine(builder2.ToString());

After debugging and tracing, I found that all the styles were added in the MyStyleBuilder, but when returning to the test class, only one style was added.

My guess is that in the second example, the returned this is not equivalent to builder2, which is causing this behavior.

However, I'm not sure how to modify it to achieve the desired effect of continuous style addition in the second approach.

3

There are 3 best solutions below

0
On BEST ANSWER

When you return this from a struct method, a copy of this is returned, because structs have value semantics.

builder2.AddLineColor("color") is modifying builder2.stringBuffer, but AddLineDash("dash") is modifying the stringBuffer of the copy returned by builder2.AddLineColor("color"). That's why when you inspect builder2.stringBuffer, only the color is applied. The copy returned by AddLineDash("dash") has both styles applied, but you discarded that by not assigning to anything.

You can assign builder2 the result of the chain:

builder2 = builder2.AddLineColor("color").AddLineDash("dash");

Or just change MyStyleBuilder to a class, which will give it reference semantics.

public class MyStyleBuilder

See also What's the difference between struct and class in .NET?

0
On

To add to already given answers, here's full code snippet presenting what is wrong:

using System;
                    
public class Program
{
    public static void Main()
    {
        ChainedStructBad();
        ChainedStructGood();
        ChainedRefType();
    }
    
    public static void ChainedRefType()
    {
        var @ref = new ChainedRefType();
        @ref
            .AddInfo("INFO 5")
            .AddInfo("INFO 6");
        Console.WriteLine($"ChainedRefType: {@ref}");
    }
    
    public static void ChainedStructGood()
    {
        var @struct = new ChainedStruct();
        @struct = @struct
            .AddInfo("INFO 3")
            .AddInfo("INFO 4");
        Console.WriteLine($"Good struct method: {@struct}");
    }
    
    public static void ChainedStructBad()
    {
        var @struct = new ChainedStruct();
        @struct
            .AddInfo("INFO 1")
            .AddInfo("INFO 2");
        Console.WriteLine($"Bad struct method: {@struct}");
    }
}

public class ChainedRefType
{
    private string _gatheredInfo = string.Empty;
    
    public ChainedRefType AddInfo(string value) 
    {
        _gatheredInfo += $"#value:{value}";
        return this;
    }
    
    public override string ToString()
    {
        return _gatheredInfo;
    }
}

public struct ChainedStruct 
{
    private string _gatheredInfo = string.Empty;
    
    public ChainedStruct(){}
    
    public ChainedStruct AddInfo(string value) 
    {
        _gatheredInfo += $"#value:{value}";
        return this;
    }
    
    public override string ToString()
    {
        return _gatheredInfo;
    }
}

This produces below output, which shows exactly what's the problem (already mentioned in other answers):

Bad struct method: #value:INFO 1

Good struct method: #value:INFO 3#value:INFO 4

ChainedRefType: #value:INFO 5#value:INFO 6

0
On

This is almost certainly because you are using a struct instead of a class. Structs are passed by value, so copies will be made, like when you are returning "this". Mutable structs are discouraged because it is so easy to make mistakes, see Why are mutable structs “evil”?.

So either change to a class, or to a readonly struct:

public readonly struct MyStyleBuilder
{
    private readonly string stringBuffer ;
    private MyStyleBuilder(string str) => stringBuffer = str;
   ...
   public MyStyleBuilder AddStyle(string prop, string value)
   {
        var style = $"{prop}:{value};";
        return new MyStyleBuilder(stringBuffer  + style);
    }
}

Note that a class and a readonly struct will behave differently. The later will produce modified copies, and that may make some use cases a bit easier to do.