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