65.9K
CodeProject 正在变化。 阅读更多。
Home

C#11 - 不可变对象和防御性拷贝

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2023年2月7日

CPOL

5分钟阅读

viewsIcon

11821

downloadIcon

59

与不可变对象和“防御性拷贝”相关的一些问题

1. 先决条件

本文是我另一篇文章的延续

2. 不可变对象与防御性副本

当将 struct 对象传递给带有 in 参数修饰符的方法时,如果 struct 被标记为 readonly,则可以进行一些优化。因为,如果可能发生变异,编译器将创建 struct 的“防御性副本”,以防止对带有 in 修饰符的参数进行可能的变异。

2.1. 示例

让我们看下面的例子。

我们将为我们的示例创建以下 struct

  • CarStruct - 可变 struct
  • CarStructI1 - 部分可变/不可变 struct,带有一个隐藏的变异方法
  • CarStructI3 - 标记为“readonly”的不可变 struct

我们将监视在四种不同情况下传递给另一个服务方法的 struct 的地址

  • 情况 1:通过 ref(ref 修饰符)传递可变 struct
  • 情况 2:按值传递可变 struct
  • 情况 3:通过 in 修饰符传递不可变 struct,并对其应用隐藏的变异器
  • 情况 4:通过 in 修饰符传递不可变 struct,并应用 getter 方法

通过监视服务方法(TestDefenseCopy)内部和外部的对象地址,我们将看到是否以及何时创建了“防御性副本”。

//=============================================
public struct CarStruct
{
    public CarStruct(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public Char? Brand { get; set; }
    public Char? Model { get; set; }
    public int? Year { get; set; }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
    public  readonly unsafe string? GetAddress()
    {
        string? result = null;
        fixed (void* pointer1 = (&this))
        {
            result = $"0x{(long)pointer1:X}";
        }
        return result;
    }
}
//=============================================
public struct CarStructI1
{
    public CarStructI1(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public  Char? Brand { get; private set; }
    public  Char? Model { get; }
    public  int? Year { get; }
    public readonly override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }

    public (string?, string?) HiddenMutatorMethod()
    {
        Brand = 'Z';

        return (this.GetAddress(), this.ToString());
    }

    public  readonly unsafe string? GetAddress()
    {
        string? result = null;
        fixed (void* pointer1 = (&this))
        {
            result = $"0x{(long)pointer1:X}";
        }
        return result;
    }
}
//=============================================
public readonly struct CarStructI3
{
    public CarStructI3(Char brand, Char model, int year)
    {
        this.Brand = brand;
        this.Model = model;
        this.Year = year;
    }

    public Char Brand { get; }
    public Char Model { get; }
    public int Year { get; }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
    public unsafe string? GetAddress()
    {
        string? result = null;
        fixed (void* pointer1 = (&this))
        {
            result = $"0x{(long)pointer1:X}";
        }
        return result;
    }

    public (string?, string?) GetterMethod()
    {
        return (this.GetAddress(), this.ToString());
    }
}
//=============================================
//===Sample code===============================
 internal class Program
{
    private static void TestDefenseCopy(
        ref CarStruct car1, CarStruct car2,
        in CarStructI1 car3, in CarStructI3 car4,
        out string? address1, out string? address2, 
        out string? address3, out string? address4,
        out string? address3d, out string? state3d,
        out string? address4d, out string? state4d)
    {
        car1.Brand = 's';

        ( address3d, state3d) = car3.HiddenMutatorMethod(); //(*1)
        ( address4d, state4d) = car4.GetterMethod();  //(*2)

        address1 = car1.GetAddress();
        address2 = car2.GetAddress();
        address3 = car3.GetAddress();
        address4 = car4.GetAddress();
    }

    static void Main(string[] args)
    {
        CarStruct car1 = new CarStruct('T', 'C', 2022);
        CarStruct car2 = new CarStruct('T', 'C', 2022);
        CarStructI1 car3= new CarStructI1('T', 'C', 2022);
        CarStructI3 car4 = new CarStructI3('T', 'C', 2022);

        string? address_in_main_1 = car1.GetAddress(); 
        string? address_in_main_2 = car2.GetAddress(); 
        string? address_in_main_3 = car3.GetAddress(); 
        string? address_in_main_4 = car4.GetAddress(); 

        Console.WriteLine($"State of structs before method call:");
        Console.WriteLine($"car1 : before ={car1}");
        Console.WriteLine($"car2 : before ={car2}");
        Console.WriteLine($"car3 : before ={car3}");
        Console.WriteLine($"car4 : before ={car4}");
        Console.WriteLine();

        TestDefenseCopy( 
            ref car1,  car2, 
            in car3, in car4,
            out string? address_in_method_1, out string? address_in_method_2, 
            out string? address_in_method_3, out string ? address_in_method_4,
            out string? address_in_method_3d, out string? state3d,
            out string? address_in_method_4d, out string? state4d);

        Console.WriteLine($"State of struct - defense copy:");
        Console.WriteLine($"car3d: d-copy ={state3d}");
        Console.WriteLine();

        Console.WriteLine($"State of structs after method call:");
        Console.WriteLine($"car1 : after  ={car1}");
        Console.WriteLine($"car2 : after  ={car2}");
        Console.WriteLine($"car3 : after  ={car3}");
        Console.WriteLine($"car4 : after  ={car4}");
        Console.WriteLine();

        Console.WriteLine($"Case 1 : Mutable struct passed by ref:");
        Console.WriteLine($"car1 : address_in_main_1 ={address_in_main_1}, 
                          address_in_method_1 ={address_in_method_1}");
        Console.WriteLine($"Case 2 :Mutable struct passed by value:");
        Console.WriteLine($"car2 : address_in_main_2 ={address_in_main_2}, 
                          address_in_method_2 ={address_in_method_2}");
        Console.WriteLine($"Case 3 :Immutable struct passed with in modifier:");
        Console.WriteLine($"car3 : address_in_main_3 ={address_in_main_3}, 
                          address_in_method_3 ={address_in_method_3}");
        Console.WriteLine($"Case 3d:Immutable struct passed with in modifier, 
                          applying hidden mutator");
        Console.WriteLine($"car3d: address_in_main_3 ={address_in_main_3}, 
                          address_in_method_3d={address_in_method_3d}");
        Console.WriteLine($"Case 4 :Immutable struct passed with in modifier:");
        Console.WriteLine($"car4 : address_in_main_4 ={address_in_main_4}, 
                          address_in_method_4 ={address_in_method_4}");
        Console.WriteLine($"Case 4d:Immutable struct passed with in modifier, 
                          , applying getter method");
        Console.WriteLine($"car4 : address_in_main_4 ={address_in_main_4}, 
                          address_in_method_4d={address_in_method_4d}");
        Console.WriteLine();

        Console.ReadLine();
    }
}    
//=============================================
//===Result of execution=======================
/*
State of structs before method call:
car1 : before =Brand:T, Model:C, Year:2022
car2 : before =Brand:T, Model:C, Year:2022
car3 : before =Brand:T, Model:C, Year:2022
car4 : before =Brand:T, Model:C, Year:2022

State of struct - defense copy:
car3d: d-copy =Brand:Z, Model:C, Year:2022

State of structs after method call:
car1 : after  =Brand:s, Model:C, Year:2022
car2 : after  =Brand:T, Model:C, Year:2022
car3 : after  =Brand:T, Model:C, Year:2022
car4 : after  =Brand:T, Model:C, Year:2022

Case 1 : Mutable struct passed by ref:
car1 : address_in_main_1 =0x44C0D7E7D0, address_in_method_1 =0x44C0D7E7D0
Case 2 :Mutable struct passed by value:
car2 : address_in_main_2 =0x44C0D7E7C0, address_in_method_2 =0x44C0D7E698
Case 3 :Immutable struct passed with in modifier:
car3 : address_in_main_3 =0x44C0D7E7B0, address_in_method_3 =0x44C0D7E7B0
Case 3d:Immutable struct passed with in modifier, applying hidden mutator
car3d: address_in_main_3 =0x44C0D7E7B0, address_in_method_3d=0x44C0D7E5D0
Case 4 :Immutable struct passed with in modifier:
car4 : address_in_main_4 =0x44C0D7E7A8, address_in_method_4 =0x44C0D7E7A8
Case 4d:Immutable struct passed with in modifier, , applying getter method
car4 : address_in_main_4 =0x44C0D7E7A8, address_in_method_4d=0x44C0D7E7A8
*/
  • 在情况 1 中,可变 struct 是通过 ref 修饰符传递的,这意味着它是通过引用传递的,并且可以在 TestDefenseCopy 方法内部进行变异。
  • 在情况 2 中,可变 struct 在没有修饰符的情况下传递,这意味着它是按值传递的,并且副本在 TestDefenseCopy 方法内部被变异,但原始对象不受影响。
  • 在情况 3 中,不可变 struct 是通过 in 修饰符传递的,这意味着它是通过引用传递给 TestDefenseCopy 方法的。但是,当调用进行隐藏变异的方法时,编译器创建了一个“防御性副本”并变异了该副本。我们可以看到从该隐藏变异方法内部获取的 address-3d 不同于 car3 的原始地址。令人困惑的是,稍后为 car3 获取的地址再次指向 car3 的原始副本。我期望在 TestDefenseCopy 方法开始时创建一个“防御性副本”,并将其分配给 car3 局部变量。
  • 在情况 4 中,不可变 struct 是通过 in 修饰符传递的,这意味着它是通过引用传递给 TestDefenseCopy 方法的。调用 readonly 方法不会创建任何“防御性副本”,如 address-4d 所示。

2.2. 反编译示例为 IL

由于代码行(*1)中的行为有点奇怪,如果忽略的话肯定很难找到。我期望“防御性副本”会一直存在于 TestDefenseCopy 方法的整个过程中,但随后获取的地址表明它只是当场创建并丢弃的。我决定反编译程序集,查看 IL 中发生了什么。我使用 dotPeek 反编译了程序集,这是 IL 中的 TestDefenseCopy 方法。

.method /*0600001F*/ private hidebysig static void
TestDefenseCopy(
  /*08000010*/ valuetype E5_ImmutableDefensiveCopy.CarStruct/*02000007*/& car1,
  /*08000011*/ valuetype E5_ImmutableDefensiveCopy.CarStruct/*02000007*/ car2,
  /*08000012*/ [in] valuetype E5_ImmutableDefensiveCopy.CarStructI1/*02000008*/& car3,  //(*31)
  /*08000013*/ [in] valuetype E5_ImmutableDefensiveCopy.CarStructI3/*02000009*/& car4,  //(*41)
  /*08000014*/ [out] string& address1,
  /*08000015*/ [out] string& address2,
  /*08000016*/ [out] string& address3,
  /*08000017*/ [out] string& address4,
  /*08000018*/ [out] string& address3d,
  /*08000019*/ [out] string& state3d,
  /*0800001A*/ [out] string& address4d,
  /*0800001B*/ [out] string& state4d
) cil managed
{
.custom /*0C000048*/ instance void System.Runtime.CompilerServices.NullableContextAttribute/
*02000005*/::.ctor(unsigned int8)/*06000005*/
  = (01 00 02 00 00 ) // .....
  // unsigned int8(2) // 0x02
.param [3] /*08000012*/
  .custom /*0C000038*/ instance void [System.Runtime/*23000001*/]System.Runtime.
  CompilerServices.IsReadOnlyAttribute/*01000017*/::.ctor()
    = (01 00 00 00 )  //(*32)
.param [4] /*08000013*/
  .custom /*0C00003B*/ instance void [System.Runtime/*23000001*/]System.Runtime.
  CompilerServices.IsReadOnlyAttribute/*01000017*/::.ctor()
    = (01 00 00 00 )
.maxstack 2
.locals /*11000005*/ init (
  [0] valuetype [System.Runtime/*23000001*/]System.ValueTuple`2/*01000019*/<string, string> V_0,
  [1] valuetype E5_ImmutableDefensiveCopy.CarStructI1/*02000008*/ V_1  //(*33)
)

// [14 13 - 14 30]
IL_0000: ldarg.0      // car1
IL_0001: ldc.i4.s     115 // 0x73
IL_0003: newobj       instance void valuetype [System.Runtime/*23000001*/]System.Nullable
`1/*01000016*/<char>/*1B000002*/::.ctor(!0/*char*/)/*0A000018*/
IL_0008: call         instance void E5_ImmutableDefensiveCopy.CarStruct/*02000007*/::set_Brand
(valuetype [System.Runtime/*23000001*/]System.Nullable`1/*01000016*/<char>)/*06000009*/
//(*1)--------------------------------------------------------
// [16 13 - 16 64]
IL_000d: ldarg.2      // car3   //(*34)
IL_000e: ldobj        E5_ImmutableDefensiveCopy.CarStructI1/*02000008*/  //(*35) 
IL_0013: stloc.1      // V_1  //(*36)
IL_0014: ldloca.s     V_1  //(*37)
IL_0016: call         instance valuetype [System.Runtime/*23000001*/]System.ValueTuple`2
/*01000019*/<string, string> E5_ImmutableDefensiveCopy.CarStructI1/*02000008*/
::HiddenMutatorMethod()/*06000016*/ /(*38)
IL_001b: stloc.0      // V_0
IL_001c: ldarg.s      address3d
IL_001e: ldloc.0      // V_0
IL_001f: ldfld        !0/*string*/ valuetype [System.Runtime/*23000001*/]System.ValueTuple
`2/*01000019*/<string, string>/*1B000003*/::Item1/*0A00001B*/
IL_0024: stind.ref
IL_0025: ldarg.s      state3d
IL_0027: ldloc.0      // V_0
IL_0028: ldfld        !1/*string*/ valuetype [System.Runtime/*23000001*/]System.ValueTuple
`2/*01000019*/<string, string>/*1B000003*/::Item2/*0A00001C*/
IL_002d: stind.ref
//(*2)------------------------------------------------------
// [17 13 - 17 57]
IL_002e: ldarg.3      // car4  //(*42)
IL_002f: call         instance valuetype [System.Runtime/*23000001*/]System.ValueTuple
`2/*01000019*/<string, string> E5_ImmutableDefensiveCopy.CarStructI3/*02000009*/
::GetterMethod()/*0600001E*/
IL_0034: stloc.0      // V_0
IL_0035: ldarg.s      address4d
IL_0037: ldloc.0      // V_0
IL_0038: ldfld        !0/*string*/ valuetype [System.Runtime/*23000001*/]System.ValueTuple
`2/*01000019*/<string, string>/*1B000003*/::Item1/*0A00001B*/
IL_003d: stind.ref
IL_003e: ldarg.s      state4d
IL_0040: ldloc.0      // V_0
IL_0041: ldfld        !1/*string*/ valuetype [System.Runtime/*23000001*/]System.ValueTuple
`2/*01000019*/<string, string>/*1B000003*/::Item2/*0A00001C*/
IL_0046: stind.ref

// [19 13 - 19 42]
IL_0047: ldarg.s      address1
IL_0049: ldarg.0      // car1
IL_004a: call         instance string E5_ImmutableDefensiveCopy.CarStruct/*02000007*/
::GetAddress()/*0600000F*/
IL_004f: stind.ref

// [20 13 - 20 42]
IL_0050: ldarg.s      address2
IL_0052: ldarga.s     car2
IL_0054: call         instance string E5_ImmutableDefensiveCopy.CarStruct/*02000007*/
::GetAddress()/*0600000F*/
IL_0059: stind.ref

// [21 13 - 21 42]
IL_005a: ldarg.s      address3
IL_005c: ldarg.2      // car3  //(*39)
IL_005d: call         instance string E5_ImmutableDefensiveCopy.CarStructI1/*02000008*/
::GetAddress()/*06000017*/
IL_0062: stind.ref

// [22 13 - 22 42]
IL_0063: ldarg.s      address4
IL_0065: ldarg.3      // car4
IL_0066: call         instance string E5_ImmutableDefensiveCopy.CarStructI3/*02000009*/
::GetAddress()/*0600001D*/
IL_006b: stind.ref

// [23 9 - 23 10]
IL_006c: ret

} // end of method Program::TestDefenseCopy

我用(*1)和(*2)标记了 IL 中与 C# 中相同行的代码行。我用(*??)标记了处理(*1)和(*2)之间的 IL 差异。这是我从 IL 中读取到的内容:

  • 在(*31)处我们看到参数,看起来 car3 是“按引用”传递的,这没问题。
  • 在(*32)处它被标记为 readonly,所以没问题。
  • 在(*33)处,看起来创建了一个类型为 CarStructI1 的局部变量 [1]。这确实是“防御性副本”的占位符。
  • 在(*34)处,索引为 2 的参数(即 car3 的地址)被加载到评估堆栈中。
  • 在(*35)处,堆栈上的 E5_ImmutableDefensiveCopy.CarStructI1 类型的对象被加载到评估堆栈中。
  • 在(*36)处,堆栈上的对象被复制到(*33)中定义的局部变量中。因此,“防御性副本”在此局部变量中创建。
  • 在(*37)处,该局部变量(*33)的地址被推送到堆栈。
  • 在(*38)处,我们在(*33)中的局部变量上调用了 HiddenMuttatorMethod 方法。因此,原始 struct(由(*31)处的地址指向)不受影响。因此,我们可以在这里看到 HiddenMuttatorMethod 方法是在“防御性副本”上执行的。
  • 在(*39)处,当我们要获取 car3 对象的地址时,再次从(*31)处加载原始地址。这解释了为什么在这个实例中我们没有看到地址的变化。老实说,**我期望在这里得到(*33)中定义的局部变量的地址**。但我觉得正常的情况并不是实际的工作方式。因此,在这里我们获取的是原始对象 car3 的地址,而不是“防御性副本”的地址。
  • 在(*42)处,我们看到了(*1)car 3 和(*2)car4 之间的区别,即地址(*41)直接加载到堆栈,并且 GetterMethod 方法直接在 car4 的原始实例上操作。在这种情况下,没有使用“防御性副本”。

我并不完全清楚“防御性副本”为何如此工作,但这就是 IL,这就是现实世界。我期望“防御性副本”一旦创建,就会一直用于创建它的方法内部。但我刚才看到的是,有时编译器会使用“防御性副本”,有时会使用原始的只读对象引用。IL 不会撒谎。这个例子是用 .NET 7/C#11 创建的。

3. 结论

我们解释了“防御性副本”的概念,并给出了一个例子。关于“防御性副本”的行为,我个人没有看到 [5] 中描述的“确切”行为,但我确实看到了与所描述的“类似”行为。甚至有可能不同版本的 .NET 和 C# 编译器在实现细节上有所不同。

4. 参考资料

5. 历史记录

  • 2023 年 2 月 7 日:初始版本
© . All rights reserved.