C#中的不可变数据类型介绍(不可变对象、不可变集合)

作者:junjie 时间:2022-06-13 19:08:33 

不可变对象

不可变(immutable): 即对象一旦被创建初始化后,它们的值就不能被改变,之后的每次改变都会产生一个新对象。


var str="mushroomsir";
str.Substring(0, 6)


c#中的string是不可变的,Substring(0, 6)返回的是一个新字符串值,而原字符串在共享域中是不变的。另外一个StringBuilder是可变的,这也是推荐使用StringBuilder的原因。


var age=18;


当存储值18的内存分配给age变量时,它的内存值也是不可以被修改的。


age=2;


此时会在栈中开辟新值2赋值给age变量,而不能改变18这个内存里的值,int在c#中也是不可变的。


class Contact
{
    public string Name { get;  set; }
    public string Address { get;  set; }
    public Contact(string contactName, string contactAddress)
    {
        Name = contactName;
        Address = contactAddress;              
    }
}
   var mutable = new Contact("二毛", "清华");
   mutable.Name = "大毛";
   mutable.Address = "北大";

我们实例化MutableContact赋值给mutable,随后我们可以修改MutableContact对象内部字段值,它已经不是初始后的值,可称为可变(mutable)对象。

可变对象在多线程并发中共享,是存在一些问题的。多线程下A线程赋值到 Name = "大毛" 这一步,其他的线程有可能读取到的数据就是:


  mutable.Name == "大毛";
  mutable.Address == "清华";


很明显这样数据完整性就不能保障,也有称数据撕裂。我们把可变对象更改为不可变对象如下:


public class Contact2
{
    public string Name { get; private set; }
    public string Address { get; private set; }
    private Contact2(string contactName, string contactAddress)
    {
        Name = contactName;
        Address = contactAddress;              
    }
    public static Contact2 CreateContact(string name, string address)
    {
        return new Contact2(name, address);
    }
}

使用时只能通过Contact2的构造函数来初始化Name和Address字段。Contact2此时即为不可变对象,因为对象本身是个不可变整体。通过使用不可变对象可以不用担心数据完整性,也能保证数据安全性,不会被其他线程修改。

自定义不可变集合

我们去枚举可变集合时,出于线程安全的考虑我们往往需要进行加锁处理,防止该集合在其他线程被修改,而使用不可变集合则能避免这个问题。我们平常使用的数据结构都是采用可变模式来实现的,那怎么实现一个不可变数据结构呢!以栈来示例,具体代码如下:


public interface IStack<T> : IEnumerable<T>
{
    IStack<T> Push(T value);
    IStack<T> Pop();
    T Peek();
    bool IsEmpty { get; }
}
public sealed class Stack<T> : IStack<T>
{
    private sealed class EmptyStack : IStack<T>
    {
        public bool IsEmpty { get { return true; } }
        public T Peek() { throw new Exception("Empty stack"); }
        public IStack<T> Push(T value) { return new Stack<T>(value, this); }
        public IStack<T> Pop() { throw new Exception("Empty stack"); }
        public IEnumerator<T> GetEnumerator() { yield break; }
        IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }
    }
    private static readonly EmptyStack empty = new EmptyStack();
    public static IStack<T> Empty { get { return empty; } }
    private readonly T head;
    private readonly IStack<T> tail;
    private Stack(T head, IStack<T> tail)
    {
        this.head = head;
        this.tail = tail;
    }
    public bool IsEmpty { get { return false; } }
    public T Peek() { return head; }
    public IStack<T> Pop() { return tail; }
    public IStack<T> Push(T value) { return new Stack<T>(value, this); }
    public IEnumerator<T> GetEnumerator()
    {
        for (IStack<T> stack = this; !stack.IsEmpty; stack = stack.Pop())
            yield return stack.Peek();
    }
    IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }
}

1.入栈时会实例化一个新栈对象
2.将新值通过构造函数传入,并存放在新对象Head位置,旧栈对象放在在Tail位置引用
3.出栈时返回当前栈对象的Tail引用的栈对象

使用方法如下:


IStack<int> s1 = Stack<int>.Empty;
IStack<int> s2 = s1.Push(10);
IStack<int> s3 = s2.Push(20);
IStack<int> s4 = s3.Push(30);
IStack<int> v3 = s4.Pop();
foreach (var item in s4)
{
//dosomething
}

每次Push都是一个新对象,旧对象不可修改,这样在枚举集合就不需要担心其他线程修改了。

Net提供的不可变集合

不可变队列,不可变列表等数据结构如果都自己实现工作量确实有点大。幸好的是Net在4.5版本已经提供了不可变集合的基础类库。 使用Nuget安装:


Install-Package Microsoft.Bcl.Immutable


使用如下,和上面我们自定义的几乎一样:


 ImmutableStack<int> a1 = ImmutableStack<int>.Empty;
        ImmutableStack<int> a2 = a1.Push(10);
        ImmutableStack<int> a3 = a2.Push(20);
        ImmutableStack<int> a4 = a3.Push(30);
        ImmutableStack<int> iv3 = a4.Pop();

使用Net不可变列表集合有一点要注意的是,当我们Push值时要重新赋值给原变量才正确,因为push后会生成一个新对象,原a1只是旧值:


   ImmutableStack<int> a1 = ImmutableStack<int>.Empty;
   a1.Push(10); //不正确,a1仍是空值值,push会生成新的栈。
   a1 = a1.Push(10); //需要将新栈重新赋值给a1

NET提供的常用数据结构

1.ImmutableStack
2.ImmutableQueue
3.ImmutableList
4.ImmutableHashSet
5.ImmutableSortedSet
6.ImmutableDictionary<K, V>
7.ImmutableSortedDictionary<K, V>

不可变集合和可变集合在算法复杂度上的不同:

C#中的不可变数据类型介绍(不可变对象、不可变集合)

不可变优点

1.集合共享安全,从不被改变
2.访问集合时,不需要锁集合(线程安全)
3.修改集合不担心旧集合被改变
4.书写更简洁,函数式风格。 var list = ImmutableList.Empty.Add(10).Add(20).Add(30);
5.保证数据完整性,安全性

不可变对象缺点

不可变本身的优点即是缺点,当每次对象/集合操作都会返回个新值。而旧值依旧会保留一段时间,这会使内存有极大开销,也会给GC造成回收负担,性能也比可变集合差的多。

跟string和StringBuild一样,Net提供的不可变集合也增加了批量操作的API,用来避免大量创建对象:


ImmutableList<string> immutable = ImmutableList<string>.Empty;
        //转换成可批量操作的集合
        var immutable2 = immutable.ToBuilder();
        immutable2.Add("xx");
        immutable2.Add("xxx");
        //还原成不可变集合
        immutable = immutable2.ToImmutable();

我们来对比下可变集合、不可变Builder集合、不可变集合的性能,添加新对象1000W次:

C#中的不可变数据类型介绍(不可变对象、不可变集合)

比较代码如下:


private static void List()
        {
            var list = new List<object>();
            var sp = Stopwatch.StartNew();

            for (int i = 0; i < 1000 * 10000; i++)
            {
                var obj = new object();
                list.Add(obj);
            }
            Console.WriteLine("可变列表集合:"+sp.Elapsed);
        }
     
        private static void BuilderImmutableList()
        {
            var list = ImmutableList<object>.Empty;
            var sp = Stopwatch.StartNew();
            var blist= list.ToBuilder();
            for (int i = 0; i < 1000 * 10000; i++)
            {
                var obj = new object();
                blist.Add(obj);
            }
            list=blist.ToImmutable();

            Console.WriteLine("不可变Builder列表集合:"+sp.Elapsed);
        }
        private static void ImmutableList()
        {
            var list = ImmutableList<object>.Empty;
            var sp = Stopwatch.StartNew();

            for (int i = 0; i < 1000 * 10000; i++)
            {
                var obj = new object();
                list = list.Add(obj);
            }

            Console.WriteLine("不可变列表集合:" + sp.Elapsed);
        }

另外一个缺点比较有趣,也有不少人忽略。 由于string的不可变特性,所以当我们使用string在保存敏感信息时,就需要特别注意。
比如密码 var pwd="mushroomsir",此时密码会以明文存储在内存中,也许你稍后会加密置空等,但这都是会生成新值的。而明文会长时间存储在共享域内存中,任何能拿到dump文件的人都可以看到明文,增加了密码被窃取的风险。当然这不是一个新问题,net2.0提供的有SecureString来进行安全存储,使用时进行恢复及清理。


IntPtr addr = Marshal.SecureStringToBSTR(secureString);
string temp = Marshal.PtrToStringBSTR(addr);
Marshal.ZeroFreeBSTR(addr);
WriteProcessMemory(...)

标签:C#,不可变数据类型,不可变对象,不可变集合
0
投稿

猜你喜欢

  • Spring 事务事件监控及实现原理解析

    2023-12-24 04:20:20
  • 带着问题读CLR via C#(笔记二)类型基础

    2022-01-31 07:31:08
  • java代码执行字符串中的逻辑运算方法

    2023-11-29 12:13:06
  • 解析Java内存分配和回收策略以及MinorGC、MajorGC、FullGC

    2023-02-06 08:22:19
  • maven继承父工程统一版本号的实现

    2023-01-27 09:19:49
  • Android进阶Hook拦截系统实例化View过程实现App换肤功能

    2023-12-01 21:22:12
  • java中面向对象的概念及知识点总结

    2022-07-14 03:14:01
  • JAVA实现的简单万年历代码

    2021-10-02 21:35:34
  • SpringBoot通知机制的实现方式

    2022-06-25 04:01:54
  • Android编程之ActionBar Tabs用法实例分析

    2021-11-08 08:49:26
  • 剑指Offer之Java算法习题精讲链表与字符串及数组

    2022-10-03 19:10:17
  • 关于Eureka的概念作用以及用法详解

    2023-08-23 15:17:29
  • Springboot工具类FileCopyUtils使用教程

    2023-10-15 18:56:36
  • JAVA实现单例模式的四种方法和一些特点

    2023-11-02 05:38:20
  • C语言实现超市计价收款系统

    2021-08-21 17:56:47
  • Java Swing实现简单的体重指数(BMI)计算器功能示例

    2022-12-16 09:34:56
  • SpringBoot实现PPT格式文件上传并在线预览功能

    2023-07-22 14:44:56
  • C#集合遍历时删除和增加元素的方法

    2021-12-11 18:53:24
  • Java非静态成员变量之死循环(详解)

    2022-04-15 09:18:20
  • Spring启动过程中实例化部分代码的分析之Bean的推断构造方法

    2022-08-26 02:00:07
  • asp之家 软件编程 m.aspxhome.com