Почему основные конструкторы C# 12 выполняются в противоположном порядке?
Почему основные конструкторы в C# 12 выполняются в противоположном порядке?
Это своего рода переломное изменение , если не сказать больше...
Пример:
namespace Whatever;
[TestClass]
public class UnitTestTemp
{
[TestMethod]
public void TestMethod1() // PASS // is expected, 1st is 1, 2nd is 2
{
using var stream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
var classicDerived = new ClassicDerived(stream);
Console.WriteLine(classicDerived.Value1);
Console.WriteLine(classicDerived.Value2);
Assert.AreEqual(1, classicDerived.Value1);
Assert.AreEqual(2, classicDerived.Value2);
}
[TestMethod]
public void TestMethod2() // FAIL // is opposite, 1st is 2, 2nd is 1
{
using var stream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
var primaryDerived = new PrimaryDerived(stream);
Console.WriteLine(primaryDerived.Value1);
Console.WriteLine(primaryDerived.Value2);
Assert.AreEqual(1, primaryDerived.Value1);
Assert.AreEqual(2, primaryDerived.Value2);
}
}
Классический конструктор:
public class ClassicBase
{
public readonly int Value1;
protected ClassicBase(Stream stream)
{
Value1 = stream.ReadByte();
}
}
public class ClassicDerived : ClassicBase
{
public readonly int Value2;
public ClassicDerived(Stream stream) : base(stream)
{
Value2 = stream.ReadByte();
}
}
Первичный конструктор:
public class PrimaryBase(Stream stream)
{
public readonly int Value1 = stream.ReadByte();
}
public class PrimaryDerived(Stream stream) : PrimaryBase(stream)
{
public readonly int Value2 = stream.ReadByte();
}
Результат первого теста:
TestMethod1
Source: UnitTestTemp.cs line 7
Duration: 4 ms
Standard Output:
1
2
Результат второго теста:
TestMethod2
Source: UnitTestTemp.cs line 21
Duration: 26 ms
Message:
Assert.AreEqual failed. Expected:<1>. Actual:<2>.
Stack Trace:
UnitTestTemp.TestMethod2() line 30
RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Standard Output:
2
1
Как видите, это немного проблематично, если вы, например, используете поток из конструкторов.
Вопрос:
Есть ли другой способ решить эту проблему, кроме возврата к классическим конструкторам?
(думал о чем-то вроде SetsRequiredMembers для нового обязательного модификатора)
1 ответ
ТЛ;ДР
В вашем основном случае используется инициализация полей и поля (Руководство по программированию на C#):
Поля инициализируются непосредственно перед вызовом конструктора экземпляра объекта . Если конструктор присваивает значение полю, он перезаписывает любое значение, указанное во время объявления поля.
Таким образом, инициализация всех полей происходит до вызова конструкторов, в результате чего сначала инициализируются поля производного класса, а затем база (я предполагаю, что это происходит из-за возможности вызывать перегруженные методы в базовом векторе, и эти перегруженные методы могут использовать некоторые инициализированные поля - см., например, этот ).
И из спецификации основных актеров :
Первичный конструктор выполнит следующую последовательность операций:
- Значения параметров сохраняются в полях захвата, если таковые имеются.
- Инициализаторы экземпляров выполняются
- Инициализатор базового конструктора вызывается
Подробности
Если вы проверите декомпиляцию своих классов с первичными векторами, вы увидите что-то вроде следующего:
public class PrimaryBase
{
public readonly int Value1 = stream.ReadByte();
public PrimaryBase(Stream stream)
{
}
}
public class PrimaryDerived : PrimaryBase
{
public readonly int Value2 = stream.ReadByte();
public PrimaryDerived(Stream stream)
: base(stream)
{
}
}
Следовательно, поля будут инициализированы перед вызовом ctor и впоследствии перед инициализацией базового класса.
И следующий IL для кторов:
Полученный:
.method public hidebysig specialname rtspecialname
instance void .ctor (
class [System.Runtime]System.IO.Stream 'stream'
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
01 00 01 00 00
)
// Method begins at RVA 0x20b1
// Code size 20 (0x14)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: callvirt instance int32 [System.Runtime]System.IO.Stream::ReadByte()
IL_0007: stfld int32 PrimaryDerived::Value2
IL_000c: ldarg.0
IL_000d: ldarg.1
IL_000e: call instance void PrimaryBase::.ctor(class [System.Runtime]System.IO.Stream)
IL_0013: ret
} // end of method PrimaryDerived::.ctor
База:
.method public hidebysig specialname rtspecialname
instance void .ctor (
class [System.Runtime]System.IO.Stream 'stream'
) cil managed
{
// Method begins at RVA 0x209d
// Code size 19 (0x13)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: callvirt instance int32 [System.Runtime]System.IO.Stream::ReadByte()
IL_0007: stfld int32 PrimaryBase::Value1
IL_000c: ldarg.0
IL_000d: call instance void [System.Runtime]System.Object::.ctor()
IL_0012: ret
} // end of method PrimaryBase::.ctor
Таким образом, в основном с точки зрения IL(и «нижнего») ctor производного класса фактически всегда будет вызываться первым, он будет инициализировать производные поля, вызывать базовый ctor (который, в свою очередь, будет инициализировать поля и вызывать код, определенный в базе C#). class ctor), а затем вызвать код, определенный в производном классе C# ctor.
Минимальный воспроизводимый пример для сравнения поведения можно упростить до следующего:
public static class Helper
{
private static int Counter = 0;
public static int GetValue(string callerName)
{
Console.WriteLine($"{callerName}: {Counter}");
return Counter++;
}
}
// "Classic"
public class ClassicCtorBase
{
public readonly int Value1;
protected ClassicCtorBase() => Value1 = Helper.GetValue(nameof(ClassicCtorBase));
}
public class ClassicCtorDerived : ClassicCtorBase
{
public readonly int Value2;
public ClassicCtorDerived() => Value2 = Helper.GetValue(nameof(ClassicCtorDerived));
}
// Classic witout ctor but with fields initialization
public class ClassicFieldInitBase
{
public readonly int Value1 = Helper.GetValue(nameof(ClassicFieldInitBase));
}
public class ClassicFieldInitDerived : ClassicFieldInitBase
{
public readonly int Value2 = Helper.GetValue(nameof(ClassicFieldInitDerived));
}
// Primary ctor
public class PrimaryBase(int i = 1)
{
public readonly int Value1 = Helper.GetValue(nameof(PrimaryBase));
}
public class PrimaryDerived(int i = 2) : PrimaryBase(i)
{
public readonly int Value2 = Helper.GetValue(nameof(PrimaryDerived));
}
И исполнение:
new ClassicCtorDerived();
new ClassicFieldInitDerived();
new PrimaryDerived();
Что приводит к следующему выводу:
ClassicCtorBase: 0
ClassicCtorDerived: 1
ClassicFieldInitDerived: 2
ClassicFieldInitBase: 3
PrimaryDerived: 4
PrimaryBase: 5
Как вы можете видеть, оба «классических» класса с полями init и первичные классы ctor имеют одинаковый порядок.