Почему основные конструкторы 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 имеют одинаковый порядок.

Демо @sharplab

Другие вопросы по тегам