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

不可变对象

不可变(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>

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

不可变优点

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次:

比较代码如下:

代码如下:

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(...)

(0)

相关推荐

  • C#写入对象或集合类型数据到xml文件的方法

    本文实例讲述了C#写入对象或集合类型数据到xml文件的方法.分享给大家供大家参考.具体实现方法如下: public static string SerializeToXmlString(object objectToSerialize) { MemoryStream memoryStream = new MemoryStream(); System.Xml.Serialization.XmlSerializer xmlSerializer = new System.Xml.Serializati

  • 浅谈c# 面向对象之类与对象

    类与对象 1.了解类:从这里开始,学习C#面向对象编程的基本内容,使用C#编程,所有的程序代码都放在类中,结构体是一个用户自定义的类型,是由其他类型组成的变量组,不存在独立于类之外的函数:因此,在面向对象中,类是面向对象编程的基本单元.类与类之间交互. 2.类的定义:一个类都可以包含2种成员:字段和方法. 1)类的字段一般代表类中被处理的数据(变量): 2)类的方法大多数代表对这些数据的处理过程或用于实现某种特定的功能,方法中的代码往往需要访问字段保存的数据. 简单的说,字段即变量,方法即函数.

  • 讲解C#面相对象编程中的类与对象的特性与概念

    类 "类"是一种构造,通过使用该构造,您可以将其他类型的变量.方法和事件组合在一起,从而创建自己的自定义类型.类就像一个蓝图,它定义类型的数据和行为.如果类没有声明为静态类,客户端代码就可以创建赋给变量的"对象"或"实例",从而使用该类.在对变量的所有引用都超出范围之前,该变量始终保持在内存中.所有引用都超出范围时,CLR 将标记该变量以供垃圾回收.如果类声明为静态类,则内存中只存在一个副本,并且客户端代码只能通过该类自身而不是"实例变

  • C# DataTable 转换为 实体类对象实例

    复制代码 代码如下: public class User {         public int ID { get; set; }         public string Name { get; set; } } //对应数据库表: //User //字段:ID.Name 那么你也许需要编写将DataTable 转换为实体对象的方法,便利DataTable.Rows 获得并填充.. 下面是我写的一个通用方法,分享+记录,便于日后直接Copy ~ 复制代码 代码如下: private sta

  • C#编程自学之类和对象

    在之前的文章中介绍了C#是一种完全面向对象的语言,既然是完全面向对象的语言就应该用面向对象的模式去学习C#,不了解面向对象?不必担心.这篇文章将介绍学习C#基础部分所要用到的面向对象的知识,后面文章我们将系统的介绍C#面向对象编程.接下来就开始我们C#旅程的起点! 一.类与对象 1 什么是类? 在我们现实世界中,我们经常会把具有一系列相同行为和属性的事物归类,例如:人类,人类就是我们所说的类.         例如 所有的人都有五官,内脏等等(除其他特殊原因),这些组成部分我们称之为人类的属性.

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

    不可变对象 不可变(immutable): 即对象一旦被创建初始化后,它们的值就不能被改变,之后的每次改变都会产生一个新对象. 复制代码 代码如下: var str="mushroomsir"; str.Substring(0, 6) c#中的string是不可变的,Substring(0, 6)返回的是一个新字符串值,而原字符串在共享域中是不变的.另外一个StringBuilder是可变的,这也是推荐使用StringBuilder的原因. 复制代码 代码如下: var age=18;

  • python中的不可变数据类型与可变数据类型详解

    前言 python与C/C++不一样,它的变量使用有自己的特点,当初学python的时候,一定要记住"一切皆为对象,一切皆为对象的引用"这句话,其实这个特点类似于JAVA,所以在python里面大家也不用担心类似于C/C++中的指针的复杂问题, 在python中数据分为可变数据类型,不可变数据类型. 所以在学习python过程中我们一定会遇到不可变数据类型和可变数据类型.下面话不多说了,来一起看看详细的介绍吧 1.名词解释 以下所有的内容都是基于内存地址来说的. 不可变数据类型: 当该

  • C++中const的实现细节介绍(C,C#同理)

    1.什么是const?  常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的.(当然,我们可以偷梁换柱进行更新:) 2.为什么引入const?  const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点. 3.cons有什么主要的作用? (1)可以定义const常量,具有不可变性. 例如:  const int Max=100; int Array[Max]; (2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患.例如:

  • Python中的引用与copy介绍

    目录 Python中的引用和copy 1.引用整型数据及列表 2.传递引用 3.copy模块中的copy()和deepcopy() Python中的引用和copy 1.引用整型数据及列表 这里以整型数据类型及列表为例 对于赋值字符串.整型.元组等不可更改数据的变量,其保存的仅是值,改变新变量中的值并不会影响原来变量中的值 origin = 1 new = origin print("new = ",new) new = 2 print("origin = ",ori

  • Python中缓存lru_cache的基本介绍和讲解

    目录 一.前言 二.举例说明 三.lru_cache 用法 1.参数详解 2. lru_cache不支持可变参数 四.lru_cache 与redis的区别 五.总结 一.前言 我们经常谈论的缓存一词,更多的类似于将硬盘中的数据存放到内存中以至于提高读取速度,比如常说的redis,就经常用来做数据的缓存.Python的缓存(lru_cache)是一种装饰在被执行的函数上,将其执行的结果缓存起来,当下次请求的时候,如果请求该函数的传参未变则直接返回缓存起来的结果而不再执行函数的一种缓存装饰器. 那

  • Python中的变量和数据类型详情

    python是一门弱数据类型的语言,变量不需要声明即可使用,向变量赋值即定义变量,赋予的值的类型就是变量的类型,但变量也是有数据类型的,字符串'1'如果想参与数据计算,则需要使用int()函数来进行转换,使用type()函数可以查看变量的数据类型. 变量保存的是数据的内存地址的引用,python中变量分为不可修改变量和可修改变量,不可修改变量有int,float,str字符串,tuple元组等,可修改变量有list列表,dict字典,set集合等.当向函数中传递参数时,python一律传递值的引

  • Java中ArrayList的使用详细介绍

    目录 1.ArrayList类 1.1ArrayList类概述 1.2ArrayList类常用方法 1.2.1构造方法 1.2.2成员方法 1.2.3示例代码 1.3ArrayList存储字符串并遍历 1.3.1案例需求 1.3.2代码实现 1.4ArrayList存储学生对象并遍历 1.4.1案例需求 1.4.2代码实现 1.5ArrayList存储学生对象并遍历升级版 1.5.1案例需求 1.5.2代码实现 总结 1.ArrayList类 1.1ArrayList类概述 在java中,我们会

  • Java中八种基本数据类型的默认值

    通过一段代码来测试一下 8种基本数据类型的默认值 package dierge; public class Ceshi { int a; double b; boolean c; char d; float f; byte e; long h; short j; public static void main(String args[]){ Ceshi a=new Ceshi(); System.out.println("整型的默认值是:"+a.a); System.out.print

  • C及C++中typedef的简单使用介绍

    typedef 声明,简称 typedef,为现有类型创建一个新的名字.比如人们常常使用 typedef 来编写更美观和可读的代码.所谓美观,意指 typedef 能隐藏笨拙的语法构造以及平台相关的数据类型,从而增强可移植性和以及未来的可维护性. 在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明. 又是在学数据结构的时候,发现了之前学习的知识遗忘很多,在发现对C/C++中关键字typedef的理解还是没有到位后,我翻阅了学C++

  • JS中的两种数据类型及实现引用类型的深拷贝的方法

    一.前言 我们知道,在JS中数据类型按照访问方式和存储方式的不同可分为基本类型和引用类型. 基本类型 基本类型有String.Boolean.Number,Undefined.Null,这些基本类型都是按值传递的,也称为值类型. 引用类型 引用类型有对象.数组.函数,它们都是按引用访问的. 二.存储方式区别 基本类型和引用类型由于两者在内存中存储的方式不同,造成两者访问的方式也不同.其中,基本类型存储在内存的栈中,是按值访问:引用类型存储在内存的堆中,是按引用访问.可如下图所示: 当有 var

随机推荐