目录
1. 先决条件
这篇文章是我另一篇文章的延续:
2. 不可变对象和防御性副本
当将struct对象传递给带有in形参修饰符的方法时,如果struct标记为readonly,则可能进行一些优化。因为,如果可能发生突变,编译器将创建struct的“防御副本”,以防止用in修饰符标记的参数可能发生突变。
2.1. 例子
让我们看下面的例子。
我们将为我们的示例创建以下struct:
- CarStruct——可变struct
- CarStructI1——部分可变/不可变struct,具有隐藏的突变器方法
- CarStructI3——不可变struct标记”readonly"
我们将在四种不同的情况下监视传递给另一个服务方法的struct地址:
- 情况 1:可变struct可由ref传递(ref修饰符)
- 情况 2:按值传递的可变struct
- 情况 3:不可变struct传递in修饰符,在其上应用隐藏突变器
- 案例 4:不可变struct地传递in修饰符,应用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
*/
- 在Case-1中,可变对象struct使用ref修饰符传递,这意味着它是通过引用传递的,并且可以在TestDefenseCopy方法内部进行突变
- 在Case-2中,可变对象struct在没有修饰符的情况下传递,这意味着它是按值传递的,并且副本在TestDefenseCopy方法内部发生了变化,但原始不受影响。
- 在Case-3中,不可变对象struct与in修饰符一起传递,这意味着它是通过引用TestDefenseCopy方法传递的。但是,当调用进行隐藏突变的方法时,编译器创建了一个“防御副本”并更改了该副本。我们可以看到,address-3d从内部获取的隐藏突变器方法与car3的原始地址不同。令人困惑的部分是,稍后获取的地址car3再次指向car3的原始副本。我预计在TestDefenseCopy方法开始时会创建一个“防御性副本”,并分配给局部变量car3。
- 在Case-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
我在IL中用(*1)和(*2)行代码标记,对应于C#中的相同行。我用(*??)标记了处理(*1)和(*2)之间的IL差异。这是我可以从IL读到的:
- 在(*31)处,我们看到参数,看起来car3是“通过ref”传递的,这很好
- 在(*32)看起来像它被标记为readonly,所以这很好,
- 在(*33)看起来像是将CarStructI1类型的局部变量创建为局部变量[1]。这实际上是“防御副本”的占位符。
- 在(*34)索引2处的参数(即car3的地址)加载到评估堆栈中
- 地址在堆栈上的E5_ImmutableDefensiveCopy.CarStructI1类型的对象(*35)将加载到求值堆栈中
- 堆栈中的(*36)对象被复制到(*33)定义的局部变量中。所以这是在局部变量中创建的“防御副本”。
- 在(*37)处,将(*33)中的局部变量的地址推送到堆栈
- 在(*38)处,我们在(*33)中的局部变量上调用该HiddenMuttatorMethod方法。因此,地址(*31)的原始struct指向不受影响。所以在这里,我们可以看到该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. 参考资料
- [1] https://en.wikipedia.org/wiki/Immutable_object
- [2] Immutability in C# Part One: Kinds of Immutability | Fabulous adventures in coding
- [3] Structure types - C# reference - C# | Microsoft Learn
- [4] with expression - create new objects that are modified copies of existing objects - C# | Microsoft Learn
- [5] Avoid memory allocations and data copies - C# | Microsoft Learn
- [6] c# - Is this a defensive copy of readonly struct passed to a method with in keyword - Stack Overflow
- [7] https://levelup.gitconnected.com/defensive-copy-in-net-c-38ae28b828
- [8] https://blog.paranoidcoding.com/2019/03/27/readonly-struct-breaking-change.html
- [9] C#11 - Immutable Object Pattern - CodeProject
https://www.codeproject.com/Articles/5354013/Csharp11-Immutable-Object-and-Defensive-Copy