C#11——不可变对象模式

目录

1. 不可变对象定义

2. 查找对象地址的实用程序

3. 一个可变对象的例子(基于类)

4. 可变对象示例(基于结构)

5. 不可变对象示例(基于结构)

5.1. 方法1 –只读属性

5.2. 方法2 –初始化器属性

5.3. 方法3 –只读结构

6. 不可变对象示例(基于类)

6.1. 方法1 –只读属性

6.2. 方法2 –初始化器属性

7. 内部不变性与观察不变性

8. 线程安全性和不变性

9. 不可变对象(基于结构)和非破坏性突变

10. 不可变对象(基于类)和非破坏性突变

11. 结论

12. 参考资料


1. 不可变对象定义

C#中的不可变对象(内部不可变性)是创建其内部状态后无法更改的对象。这与普通对象(可变对象)不同,后者的内部状态通常在创建后可以更改。C#对象的不可变性在编译时强制执行。不可变性是一种编译时约束,它指示程序员可以通过对象的正常接口执行哪些操作。

有一个小的混淆,因为有时在不可变对象下,假设以下定义:

C#中的不可变对象(观察不可变性)([2])是创建后无法更改public状态的对象。在这种情况下,我们不关心对象的内部状态是否随时间变化,如果public,可观察状态总是相同的。对于代码的其余部分,它始终显示为同一对象,因为这就是它的显示方式。

2. 查找对象地址的实用程序

由于我们将继续在示例中显示栈和堆上的对象,为了更好地显示行为差异,我们开发了一个小实用程序,它将为我们提供相关对象的地址,因此通过比较地址,很容易看到我们是否在谈论相同或不同的对象。唯一的问题是我们的地址查找实用程序有一个限制,也就是说,它仅适用于堆上不包含堆上其他对象(引用)的对象。因此,我们被迫在对象中仅使用基元值,这就是我需要避免使用C# string而仅使用char类型的原因。

这是地址查找实用程序。我们创建了其中两个,一个用于基于class对象,另一个用于基于struct对象。问题是我们希望避免对基于-struct的对象进行装箱,因为这会在装箱对象的堆上为我们提供一个地址,而不是在原始对象的堆栈上。我们使用泛型来阻止实用程序的错误使用。

public static Tuple<string?, string?>
    GetMemoryAddressOfClass<T1, T2>(T1 o1, T2 o2)
    where T1 : class
    where T2 : class
{
    //using generics to block structs, that would be boxed
    //so we would get address of a boxed object, not struct
    //works only for objects that do not contain references
    // to other objects
    string? address1 = null;
    string? address2 = null;

    GCHandle? handleO1 = null;
    GCHandle? handleO2 = null;

    if (o1 != null)
    {
        handleO1 = GCHandle.Alloc(o1, GCHandleType.Pinned);
    }

    if (o2 != null)
    {
        handleO2 = GCHandle.Alloc(o2, GCHandleType.Pinned);
    }

    if (handleO1 != null)
    {
        IntPtr pointer1 = handleO1.Value.AddrOfPinnedObject();
        address1 = "0x" + pointer1.ToString("X");
    }

    if (handleO2 != null)
    {
        IntPtr pointer2 = handleO2.Value.AddrOfPinnedObject();
        address2 = "0x" + pointer2.ToString("X");
    }

    if (handleO1 != null)
    {
        handleO1.Value.Free();
    }

    if (handleO2 != null)
    {
        handleO2.Value.Free();
    }

    Tuple<string?, string?> result = 
        new Tuple<string?, string?>(address1, address2);

    return result;
}

public static unsafe string? 
    GetMemoryAddressOfStruct<T1>(ref T1 o1)
    where T1 : unmanaged
{
    //In order to satisfy this constraint "unmanaged" a type must be a struct
    //and all the fields of the type must be unmanaged
    //using ref, so I would not get a value copy
    string? result = null;
    fixed (void* pointer1 = (&o1))
    {
        result = $"0x{(long)pointer1:X}";
    }

    return result;
}

3. 一个可变对象的例子(基于类)

下面是一个基于类的可变对象的示例,这意味着它位于托管堆上。这是一个执行和变异的例子。然后是执行结果:

public class CarClass
{
    public CarClass(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}";
    }
}    
//============================================
//===Sample code==============================
//class based objects
Console.WriteLine("-----");
Console.WriteLine("Mutation of mutable class object");
CarClass car1 = new CarClass('T', 'C', 2022);
Console.WriteLine($"Before mutation: car1={car1}");
car1.Model = 'A';
Console.WriteLine($"After  mutation: car1={car1}");
Console.WriteLine();

//--assigning class based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of mutable class object");
Console.WriteLine("From addresses you can see that assignment created ");
Console.WriteLine("two references pointing to the same object on heap ");
CarClass car3 = new CarClass('T', 'C', 1991);
CarClass car4 = car3;

Tuple<string?, string?> addresses1 = Util.GetMemoryAddressOfClass(car3, car4);
Console.WriteLine($"Address car3={addresses1.Item1}, Address car4={addresses1.Item2}");

Console.WriteLine($"Before mutation: car3={car3}");
Console.WriteLine($"Before mutation: car4={car4}");
car4.Model = 'Y';
Console.WriteLine($"After  mutation: car3={car3}");
Console.WriteLine($"After  mutation: car4={car4}");
Console.WriteLine();
//============================================
//===Result of execution======================
/*
-----
Mutation of mutable class object
Before mutation: car1=Brand:T, Model:C, Year:2022
After  mutation: car1=Brand:T, Model:A, Year:2022

-----
Assignment of mutable class object
From addresses you can see that assignment created
two references pointing to the same object on heap
Address car3=0x21E4F160280, Address car4=0x21E4F160280
Before mutation: car3=Brand:T, Model:C, Year:1991
Before mutation: car4=Brand:T, Model:C, Year:1991
After  mutation: car3=Brand:T, Model:Y, Year:1991
After  mutation: car4=Brand:T, Model:Y, Year:1991
*/

众所周知,类类型具有引用语义[3]),赋值只是指向同一对象的引用赋值。因此,赋值只是复制了一个引用,并且我们有两个引用指向堆上的一个对象的情况,我们使用哪个引用并不重要,那个对象发生了变化。

4. 可变对象示例(基于结构)

下面是一个基于struct可变对象的示例,这意味着它位于堆栈上。并且有一个样本执行和突变。然后,是执行结果:

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}";
    }
}

//=============================================
//===Sample code===============================
//struct based objects
Console.WriteLine("-----");
Console.WriteLine("Mutation of mutable struct object");
CarStruct car5 = new CarStruct('T', 'C', 2022);
Console.WriteLine($"Before mutation: car5={car5}");
car5.Model = 'Y';
Console.WriteLine($"After  mutation: car5={car5}");
Console.WriteLine();

//--assigning struct based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of mutable struct object");
Console.WriteLine("From addresses you can see that assignment created ");
Console.WriteLine("two different object on the stack ");
CarStruct car7 = new CarStruct('T', 'C', 1991);
CarStruct car8 = car7;

string? address7 = Util.GetMemoryAddressOfStruct(ref car7);
string? address8 = Util.GetMemoryAddressOfStruct(ref car8);
Console.WriteLine($"Address car7={address7}, Address car8={address8}");

Console.WriteLine($"Before mutation: car7={car7}");
Console.WriteLine($"Before mutation: car8={car8}");
car8.Model = 'M';
Console.WriteLine($"After  mutation: car7={car7}");
Console.WriteLine($"After  mutation: car8={car8}");
Console.WriteLine();
//=============================================
//===Result of execution=======================
/*
Mutation of mutable struct object
Before mutation: car5=Brand:T, Model:C, Year:2022
After  mutation: car5=Brand:T, Model:Y, Year:2022

-----
Assignment of mutable struct object
From addresses you can see that assignment created
two different object on the stack
Address car7=0x2A7F79E570, Address car8=0x2A7F79E560
Before mutation: car7=Brand:T, Model:C, Year:1991
Before mutation: car8=Brand:T, Model:C, Year:1991
After  mutation: car7=Brand:T, Model:C, Year:1991
After  mutation: car8=Brand:T, Model:M, Year:1991
*/

众所周知,struct具有值语义([3]),并且在赋时,会复制该类型的实例。这与上面显示的基于类的对象(即引用类型)的行为不同。如我们所见,赋值创建了一个对象的新实例,因此突变仅影响新实例。

5. 不可变对象示例(基于结构)

5.1. 方法1 –只读属性

可以通过用readonly关键字标记所有public属性来创建基于-struct类型的不可变对象。这些属性只能在对象的构造阶段发生突变,之后是不可变的。在这种情况下,无法在对象的初始化阶段设置属性。

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

    public readonly Char? Brand { get;  }
    public readonly Char? Model { get;  }
    public readonly int? Year { get;  }
    public override readonly string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}
    
//------------------------------------
CarStructI1 car10 = new CarStructI1('T', 'C', 2022);
//next line will not compile, since is readonly property
//car10.Model = 'Y';
//next line will not compile, can not initialize readonly property
//CarStructI1 car11 = new CarStructI1() { Brand = 'A', Model = 'A', Year = 2000 };    

5.2. 方法2 –初始化器属性

可以通过使用资源库的init关键字标记所有public属性来创建基于-struct类型的不可变对象。这些属性只能在对象的构造阶段和对象的初始化阶段发生变化,之后是不可变的。在这种情况下,可以在对象的初始化阶段设置属性。

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

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

//---------------------------------------
CarStructI2 car20 = new CarStructI2('T', 'C', 2022);
//next line will not compile, since is readonly property
//car20.Model = 'Y';
//this works now
CarStructI2 car21 = new CarStructI2() { Brand = 'A', Model = 'A', Year = 2000 };

5.3. 方法3 –只读结构

通过使用readonly关键字标记struct,可以创建基于struct的不可变对象。在这样的struct中,所有属性都必须标记为readonly并且只能在对象的构造阶段进行突变,之后是不可变的。在这种情况下,无法在对象的初始化阶段设置属性。我认为在这种情况下与上面的方法1没有区别,当所有属性/方法都被标记为readonly除了在struct级别定义上很容易看到struct的意图是什么,即struct创建者从一开始就计划它是不可变的。

ublic readonly struct CarStructI3
{
    public CarStructI3(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        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}";
    }
}

//--------------------------------------
CarStructI3 car30= new CarStructI3('T', 'C', 2022);
//next line will not compile, since is readonly property
//car30.Model = 'Y';
//next line will not compile, can not initialize readonly property
//CarStructI3 car31 = new CarStructI1() { Brand = 'A', Model = 'A', Year = 2000 };

6. 不可变对象示例(基于类)

6.1. 方法1 –只读属性

通过移除setter,将所有public属性设置为只读,可以创建基于类类型的Immutable对象。此类属性只能由类的private成员改变。在这种情况下,无法在对象的初始化阶段设置属性。

public class CarClassI1
{
    public CarClassI1(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public CarClassI1()
    { }

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

//----------------------------------
CarClassI1 car50 = new CarClassI1('T', 'C', 2022);
//next line will not compile, since is readonly property
//car50.Model = 'Y';
//next line will not compile, can not initialize readonly property
//CarClassI1 car51 = new CarClassI1() { Brand = 'A', Model = 'A', Year = 2000 };

6.2. 方法2 –初始化器属性

可以通过使用资源库的init关键字标记所有public属性来创建基于-class类型的不可变对象。这些属性只能在对象的构造阶段和对象的初始化阶段发生变化,之后是不可变的。在这种情况下,可以在对象的初始化阶段设置属性。

public class CarClassI2
{
    public CarClassI2(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public CarClassI2()
    { }

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

//------------------------------------------
CarClassI2 car60 = new CarClassI2('T', 'C', 2022);
//next line will not compile, since is readonly property
//car60.Model = 'Y';
//this works now
CarClassI2 car61 = new CarClassI2() { Brand = 'A', Model = 'A', Year = 2000 };

7. 内部不变性与观察不变性

上述情况都是内部不可变性不可变对象的情况。让我们举一个观察不变性不可变对象的例子。下面是这样一个示例。我们基本上缓存长期价格计算的结果。对象始终报告相同的状态,因此它满足观察不变性,但其内部状态发生变化,因此它不满足内部不变性

public class CarClassI1
{
    public CarClassI1(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }
    public Char? Brand { get; }
    public Char? Model { get; }
    public int? Year { get; }
    public int? Price
    {
        get
        {
            // not thread safe
            if (_price== null)
            {
                LongPriceCalcualtion();
            }
            return _price;  
        }
    }

    private int? _price = null;

    private void LongPriceCalcualtion()
    {
        _price = 0;
        Thread.Sleep(1000); //long features calculation
        _price += 10_000;
        Thread.Sleep(1000); //long engine price calculation
        _price += 10_000;
        Thread.Sleep(1000); //long tax calculation
        _price += 10_000;
    }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}, Price:{Price}";
    }
}

//=============================================
//===Sample code===============================
CarClassI1 car50 = new CarClassI1('T', 'C', 2022);

Console.WriteLine($"The 1st object state: car50={car50}");
Console.WriteLine($"The 2nd object state: car50={car50}");

//=============================================
//===Result of execution=======================
/*
The 1st object state: car50=Brand:T, Model:C, Year:2022, Price:30000
The 2nd object state: car50=Brand:T, Model:C, Year:2022, Price:30000
*/

8. 线程安全性和不变性

内部不变性不可变对象是线程安全的。这源于一个简单的逻辑,即所有共享资源都是只读的,因此线程不可能相互干扰。

观测不变性不可变对象不一定是线程安全的,上面的示例显示了这一点。获取状态会调用一些private线程不安全的方法,最终结果不是线程安全的。如果从两个不同的线程进行访问,则上述对象可能会报告不同的状态。

9. 不可变对象(基于结构)和非破坏性突变

如果要重用不可变对象,可以根据需要多次引用它,因为它可以保证不会更改。但是,如果您想重用不可变对象的某些数据,但对其进行一些修改,该怎么办?这就是他们发明无损突变的原因。在C#语言中,现在您可以使用with关键字来执行此操作。通常,您可能希望保留不可变对象的大部分状态,但仅更改某些属性。以下是在C#10及之后完成的方法。

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

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

//=============================================
//===Sample code===============================
//struct based objects
Console.WriteLine("-----");
Console.WriteLine("Nondestructive Mutation of immutable struct object");
CarStructI2 car7 = new CarStructI2('T', 'C', 1991);
CarStructI2 car8 = car7 with { Brand = 'A' };

string? address1 = Util.GetMemoryAddressOfStruct(ref car7);
string? address2 = Util.GetMemoryAddressOfStruct(ref car8);
Console.WriteLine($"Address car7={address1}, Address car8={address2}");

Console.WriteLine($"State: car7={car7}");
Console.WriteLine($"State: car8={car8}");
Console.WriteLine();

//=============================================
//===Result of execution=======================
/*
-----
Nondestructive Mutation of immutable struct object
Address car7=0xC4A4FCE420, Address car8=0xC4A4FCE410
State: car7=Brand:T, Model:C, Year:1991
State: car8=Brand:A, Model:C, Year:1991
*/

10. 不可变对象(基于类)和非破坏性突变

对于基于类的不可变对象,他们没有使用new with关键字扩展C#语言,但仍然可以轻松自定义编程相同的功能。下面是一个示例:

public class CarClassI4
{
    public CarClassI4(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public Char? Brand { get;  }
    public Char? Model { get;  }
    public int? Year { get;  }

    public CarClassI4 NondestructiveMutation 
        ( Char? Brand=null,  Char? Model = null, int? Year=null)
    {
        return new CarClassI4(
            Brand ?? this.Brand, Model ?? this.Model, Year ?? this.Year);
    }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}

//=============================================
//===Sample code===============================
//class based objects
Console.WriteLine("-----");
Console.WriteLine("Nondestructive Mutation of immutable class object");
CarClassI4 car1 = new CarClassI4('T', 'C', 1991);
CarClassI4 car2 = car1.NondestructiveMutation(Model:'M');


Tuple<string?, string?> addresses2 = Util.GetMemoryAddressOfClass(car1, car2);
Console.WriteLine($"Address car1={addresses2.Item1}, Address car2={addresses2.Item2}");

Console.WriteLine($"State: car1={car1}");
Console.WriteLine($"State: car2={car2}");
Console.WriteLine();
//=============================================
//===Result of execution=======================
/*
-----
Nondestructive Mutation of immutable class object
Address car1=0x238EED63FA8, Address car2=0x238EED63FC8
State: car1=Brand:T, Model:C, Year:1991
State: car2=Brand:T, Model:M, Year:1991
*/

11. 结论

不可变对象模式非常流行,并且经常使用。在这里,我们介绍了在C#中创建不可变的structclass以及一些有趣的示例。

我们讨论了内部不变性与观察不变性,并讨论了线程安全问题。

建议读者感兴趣的相关概念是 C#中的值对象和记录。

12. 参考资料

https://www.codeproject.com/Articles/5353999/Csharp11-Immutable-Object-Pattern

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值