Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

C#11 - Immutable Object and Defensive Copy

5.00/5 (4 votes)
6 Feb 2023CPOL5 min read 11.4K   28  
Some issues related to Immutable Object and “defense copy”
This is a continuation of the article on Immutable Object Pattern. We discuss some issues related to the creation of “defense copy”.

1. Prerequisite

This article is a continuation of another article of mine:

2. Immutable Object and Defensive Copies

When passing a struct object to a method with an in parameter modifier, some optimizations are possible if the struct is marked with readonly. Because, if a mutation is possibly going to happen, the compiler will create the “defense copy” of the struct, to prevent possible mutation of the parameter marked with in modifier.

2.1. Example

Let us look at the following example.

We will create the following structs for our example:

  • CarStruct - Mutable struct
  • CarStructI1 - Partly Mutable/Immutable struct that has a hidden mutator method
  • CarStructI3 - Immutable struct marked "readonly"

We are going to monitor the addresses of structs passed to another service method in four different cases:

  • Case 1: Mutable struct passed by ref (ref modifier)
  • Case 2: Mutable struct passed by value
  • Case 3: Immutable struct passed with in modifier, applying hidden mutator on it
  • Case 4: Immutable struct passed with in modifier, applying getter method

By monitoring object addresses, outside and inside service method (TestDefenseCopy), we will see if and when “defense-copy” has been created.

C#
//=============================================
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
*/
  • In Case-1, the mutable struct is passed with ref modifier, meaning it is passed by reference and can be mutated inside the method TestDefenseCopy
  • In Case-2, the mutable struct is passed without a modifier, meaning it is passed by value and the copy is mutated inside the method TestDefenseCopy, but original is not affected.
  • In Case-3, the immutable struct is passed with in modifier, meaning it is passed by reference to the method TestDefenseCopy. But when the method making hidden mutations is invoked, the compiler created a “defense copy” and mutated that copy. We can see that address-3d taken from inside that hidden mutator method is different from the original address of car3. The confusing part is that address taken later for car3 again points to the original copy of car3. I expected that a “defensive copy” will be created once at the beginning of the method TestDefenseCopy, and assigned to car3 local variable.
  • In Case-4, the immutable struct is passed with in modifier, meaning it is passed by reference to the method TestDefenseCopy. Invoking readonly method does not create any kind of “defense copy”, as can be seen from address-4d.

2.2. Decompiling Example into IL

Since behavior in line of code (*1) looks weird a bit and would be definitely hard to find if overlooked. I expected that “defense copy” will exist through the whole method TestDefenseCopy, but later address taken says it is just created on the spot and abandoned. I decided to decompile the assembly and look into IL what is happening here. I used dotPeek to decompile the assembly and here is the TestDefenseCopy method in IL:

MSIL
.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

I marked with (*1) and (*2) lines of code in IL corresponding to the same lines in C#. I marked the difference in IL between handling (*1) and (*2) with (*??). This is what I can read from IL:

  • At (*31) we see parameters, it looks like car3 is passed “by ref” and that is fine
  • At (*32) looks like it is marked as readonly, so that is fine,
  • At (*33) looks like a local variable of type CarStructI1 is created as a local variable [1]. That is really a placeholder for that “defense copy” to be.
  • At (*34) argument at index 2 (that is the address of car3) is loaded into the Evaluation stack
  • At (*35) object of type E5_ImmutableDefensiveCopy.CarStructI1 whose address is on the stack is loaded into the Evaluation stack
  • At (*36) object from the stack is copied into the local variable defined at (*33). So here is “defense-copy” created in local variable.
  • At (*37) address of that local variable from (*33) is pushed to the stack
  • At (*38) we have the invocation of the method HiddenMuttatorMethod over the local variable in (*33). So, the original struct pointed by the address at (*31) is not affected. So here, we can see that method HiddenMuttatorMethod is executed on “defense-copy”
  • At (*39) again original address from (*31) is loaded for the call when we take the address of the car3 object. That explains why we do not see the change of address in this instance. Honestly, I expected that here we would get the address of the local variable defined at (*33) here. But what I consider normal is not how it actually works. So, here we do not take the address of “defense-copy” but of original object car3.
  • At (*42) we see the difference between (*1) car 3 and (*2) car4 , that is the address from (*41) is directly loaded to the stack, and the method GetterMethod directly operates on that original instance of car4. In this case, no “defense-copy” is used.

It is not completely obvious to me in all details why “defense copy” works like that, but that is IL, so that is the real world. I expected that “defense-copy”, once created, would be used all the time inside the method for which is created. But what I just saw is that sometimes the compiler uses “defense-copy” and sometimes the original reference to the read-only object. IL does not lie. This example was made with .NET 7/C#11.

3. Conclusion

We explained the concept of “defense copy” and gave an example of it. Regarding “defense copy” behavior, I did not personally see the “exact” behavior described in [5], but I did see “similar” behavior to the one described. It is even possible that details regarding implementation change between different versions of .NET and C# compiler.

4. References

5. History

  • 7th February, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)