Difference in struct initialization is causing assertion failure

42 Views Asked by At

The below code snippet fails with this exception and I'm not able to explain why.

0 == subjectB.ValueB
Expected: 0, Actual: 5
   at Program.<<Main>$>g__FailsWithSingleInit|0_0() in C:\repos\Program.cs:line 24
   at Program.<Main>$(String[] args) in C:\repos\Program.cs:line 9

In the example, I have several tests doing the same task. The only difference is the struct being used. I create 2 structs, test their values, replace one of the structs with another instance, and compare again.

It seems that the addition of a string to the struct, and adding an initializer value, causes the previous struct to be reused.

I've had a look at the IL code to see what is going on, and I believe it is a lack of initobj.

Can anyone please help explain this?

using System.Diagnostics;

PassesNormally();
PassesWithStringAdded();
PassesWhenInitAll();
PassesWhenRunAllViaLocalStaticInit();

// This will fail.
FailsWithSingleInit();



static void FailsWithSingleInit()
{
    // Setup 2 items.
    var subjectA = new ExampleWithStringInitOne();
    var subjectB = new ExampleWithStringInitOne { ValueB = 5, ValueA = 10u };
    Debug.Assert(subjectA.ValueA != subjectB.ValueA, "subjectA.ValueA != subjectB.ValueA", "Expected: {0}, Actual: {1}", subjectA.ValueA, subjectB.ValueA);
    Debug.Assert(subjectA.ValueB != subjectB.ValueB, "subjectA.ValueB != subjectB.ValueB", "Expected: {0}, Actual: {1}", subjectA.ValueB, subjectB.ValueB);

    // Re-assign one.
    subjectB = new ExampleWithStringInitOne { ValueA = 10u };
    Debug.Assert(10u == subjectB.ValueA, "10u == subjectB.ValueA", "Expected: {0}, Actual: {1}", subjectB.ValueA, 10u);
    Debug.Assert(0 == subjectB.ValueB, "0 == subjectB.ValueB", "Expected: {0}, Actual: {1}", 0, subjectB.ValueB);
}

static void PassesNormally()
{
    // Setup 2 items.
    var subjectA = new ExampleWithoutString();
    var subjectB = new ExampleWithoutString { ValueB = 5, ValueA = 10u };
    Debug.Assert(subjectA.ValueA != subjectB.ValueA, "subjectA.ValueA != subjectB.ValueA",
        "Expected: {0}, Actual: {1}", subjectA.ValueA, subjectB.ValueA);
    Debug.Assert(subjectA.ValueB != subjectB.ValueB, "subjectA.ValueB != subjectB.ValueB", "Expected: {0}, Actual: {1}",
        subjectA.ValueB, subjectB.ValueB);

    // Re-assign one.
    subjectB = new ExampleWithoutString { ValueA = 10u };
    Debug.Assert(10u == subjectB.ValueA, "10u == subjectB.ValueA", "Expected: {0}, Actual: {1}", subjectB.ValueA,
        10u);
    Debug.Assert(0 == subjectB.ValueB, "0 == subjectB.ValueB", "Expected: {0}, Actual: {1}", 0, subjectB.ValueB);
}

static void PassesWithStringAdded()
{
    // Setup 2 items.
    var subjectA = new ExampleWithString();
    var subjectB = new ExampleWithString { ValueB = 5, ValueA = 10u };
    Debug.Assert(subjectA.ValueA != subjectB.ValueA, "subjectA.ValueA != subjectB.ValueA", "Expected: {0}, Actual: {1}", subjectA.ValueA, subjectB.ValueA);
    Debug.Assert(subjectA.ValueB != subjectB.ValueB, "subjectA.ValueB != subjectB.ValueB", "Expected: {0}, Actual: {1}", subjectA.ValueB, subjectB.ValueB);

    // Re-assign one.
    subjectB = new ExampleWithString { ValueA = 10u };
    Debug.Assert(10u == subjectB.ValueA, "10u == subjectB.ValueA", "Expected: {0}, Actual: {1}", subjectB.ValueA, 10u);
    Debug.Assert(0 == subjectB.ValueB, "0 == subjectB.ValueB", "Expected: {0}, Actual: {1}", 0, subjectB.ValueB);
}

static void PassesWhenInitAll()
{
    // Setup 2 items.
    var subjectA = new ExampleWithStringInitAll();
    var subjectB = new ExampleWithStringInitAll { ValueB = 5, ValueA = 10u };
    Debug.Assert(subjectA.ValueA != subjectB.ValueA, "subjectA.ValueA != subjectB.ValueA", "Expected: {0}, Actual: {1}", subjectA.ValueA, subjectB.ValueA);
    Debug.Assert(subjectA.ValueB != subjectB.ValueB, "subjectA.ValueB != subjectB.ValueB", "Expected: {0}, Actual: {1}", subjectA.ValueB, subjectB.ValueB);

    // Re-assign one.
    subjectB = new ExampleWithStringInitAll { ValueA = 10u };
    Debug.Assert(10u == subjectB.ValueA, "10u == subjectB.ValueA", "Expected: {0}, Actual: {1}", subjectB.ValueA, 10u);
    Debug.Assert(0 == subjectB.ValueB, "0 == subjectB.ValueB", "Expected: {0}, Actual: {1}", 0, subjectB.ValueB);
}

static void PassesWhenRunAllViaLocalStaticInit()
{
    RunTest<ExampleWithoutString>();
    RunTest<ExampleWithString>();
    RunTest<ExampleWithStringInitOne>();

    static void RunTest<T>() where T : IExample, new()
    {
        
        // Setup 2 items.
        var subjectA = new T();
        var subjectB = new T { ValueB = 5, ValueA = 10u };
        Debug.Assert(subjectA.ValueA != subjectB.ValueA, "subjectA.ValueA != subjectB.ValueA", "Expected: {0}, Actual: {1}", subjectA.ValueA, subjectB.ValueA);
        Debug.Assert(subjectA.ValueB != subjectB.ValueB, "subjectA.ValueB != subjectB.ValueB", "Expected: {0}, Actual: {1}", subjectA.ValueB, subjectB.ValueB);

        // Re-assign one.
        subjectB = new T { ValueA = 10u };
        Debug.Assert(10u == subjectB.ValueA, "10u == subjectB.ValueA", "Expected: {0}, Actual: {1}", subjectB.ValueA, 10u);
        Debug.Assert(0 == subjectB.ValueB, "0 == subjectB.ValueB", "Expected: {0}, Actual: {1}", 0, subjectB.ValueB);
    }
}

internal interface IExample
{
    public uint ValueA { get; init; }
    public int ValueB { get; init; }
}

internal struct ExampleWithoutString : IExample
{
    public uint ValueA { get; init; }
    public int ValueB { get; init; }
}

internal struct ExampleWithString : IExample
{
    public uint ValueA { get; init; }
    public int ValueB { get; init; }
    public string ValueC { get; init; }
}

internal struct ExampleWithStringInitOne : IExample
{
    public uint ValueA { get; init; }
    public int ValueB { get; init; }
    public string ValueC { get; init; } = "x";
}

#pragma warning disable CA1805
internal struct ExampleWithStringInitAll : IExample
{
    public uint ValueA { get; init; } = 0;
    public int ValueB { get; init; } = 0;
    public string ValueC { get; init; } = "x";
}
#pragma warning restore CA1805
0

There are 0 best solutions below