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>
不可变集合和可变集合在算法复杂度上的不同:
不可变优点
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(...)


猜你喜欢
- 本文实例讲述了C#实现抓取和分析网页类。分享给大家供大家参考。具体分析如下:这里介绍了抓取和分析网页的类。其主要功能有:1、提取网页的纯文本
- 1.使用API设置主题如下所示,在Activity中使用setThemesetTheme(R.style.MyTheme1);2.调用API
- 本文实例为大家分享了Unity实现弹球打砖块游戏的具体代码,供大家参考,具体内容如下创作界面记录摄像机所需脚本1射线shexianusing
- 本文实例讲述了Java简单实现约瑟夫环算法。分享给大家供大家参考,具体如下:1.算法背景:罗马人攻占了乔塔帕特,41人藏在一个山洞中躲过了这
- 关联篇:深入Android的消息机制源码详解-Handler,MessageQueue与Looper关系关联篇:Handler内存泄漏及其
- 本文实例讲述了C#实现基于IE内核的简单浏览器。分享给大家供大家参考。具体如下:Form1.cs如下:using System;using
- 本文实例为大家分享了shader实现基于世界坐标的贴图置换效果。效果如下:设置面板如下:可在面板上设置切换方向,与切换对象,及其切换速度。s
- 1、分布式锁简介分布式锁是控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,
- 由于工作和之前学习的原因,我需要同时使用Java和C#两门语言。这几年,我也一直在使用J2EE平台和.NET平台做开发。说起C#和Java这
- 最近安装了idea,觉得比eclipse好用很多,今天不知道为啥yml文件就不识别了,上面显示一个问号,我查了半天,解决办法就是安装一个插件
- springboot 2.0 mybatis mapper-locations扫描多个路径mapper-locations扫描多个路径,中间
- 在文章中,我们将对输入到机器学习模型中的数据集进行预处理。这里我们将对一个硬币数据集进行预处理,以便以后在监督学习模型中进行训练。在机器学习
- 搭建个SSM框架居然花费了我好长时间!特此记录!需要准备的环境:idea 2017.1jdk1.8Maven 3.3.9请提前将idea与M
- 背景众所周知,所有被打开的系统资源,比如流、文件或者Socket连接等,都需要被开发者手动关闭,否则随着程序的不断运行,资源泄露将会累积成重
- 本文实例讲述了Java线程组与未处理异常。分享给大家供大家参考,具体如下:一 点睛从JDK1.5开始,Java加强了线程的异常处理,如果线程
- 实现比较两个List之间的差异,包括获取两List的差集,交集,并集(不去重&去重)的API解法和优化解法的解决方案。求差集/**
- 本文实例讲述了C#使用linq查询大数据集的方法。分享给大家供大家参考。具体如下:using System;using System.Col
- 罗马数字转整数罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。
- 最近自己写了个小爬虫,里面用到了多线程技术,忽然发现对此技术竟然有些陌生了,于是乎开始疯狂的去问度娘,在此记录下来,以便自己和各位小伙伴们学
- 线程中断机制提供了一种方法,用于将线程从阻塞等待中唤醒,尝试打断目标线程的现有处理流程,使之响应新的命令。Java 留给开发者这一自由,我们