分享

探索c#之不可变数据类型

 深秋微凉3 2016-02-07



不可变对象


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

{

IStack Push(T value);

IStack Pop();

T Peek();

bool IsEmpty { get; }

}

public sealed class Stack : IStack

{

private sealed class EmptyStack : IStack

{

public bool IsEmpty { get { return true; } }

public T Peek() { throw new Exception('Empty stack'); }

public IStack Push(T value) { return new Stack(value, this); }

public IStack Pop() { throw new Exception('Empty stack'); }

public IEnumerator GetEnumerator() { yield break; }

IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }

}

private static readonly EmptyStack empty = new EmptyStack();

public static IStack Empty { get { return empty; } }

private readonly T head;

private readonly IStack tail;

private Stack(T head, IStack tail)

{

this.head = head;

this.tail = tail;

}

public bool IsEmpty { get { return false; } }

public T Peek() { return head; }

public IStack Pop() { return tail; }

public IStack Push(T value) { return new Stack(value, this); }

public IEnumerator GetEnumerator()

{

for (IStack stack = this; !stack.IsEmpty; stack = stack.Pop())

yield return stack.Peek();

}

IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); }

}


  • 入栈时会实例化一个新栈对象

  • 将新值通过构造函数传入,并存放在新对象Head位置,旧栈对象放在在Tail位置引用

  • 出栈时返回当前栈对象的Tail引用的栈对象


使用方法如下:


IStack s1 = Stack.Empty;

IStack s2 = s1.Push(10);

IStack s3 = s2.Push(20);

IStack s4 = s3.Push(30);

IStack v3 = s4.Pop();

foreach (var item in s4)

{

//dosomething

}


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


Net提供的不可变集合


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


Install-Package Microsoft.Bcl.Immutable


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


ImmutableStack a1 = ImmutableStack.Empty;

ImmutableStack a2 = a1.Push(10);

ImmutableStack a3 = a2.Push(20);

ImmutableStack a4 = a3.Push(30);

ImmutableStack iv3 = a4.Pop();


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


ImmutableStack a1 = ImmutableStack.Empty;

a1.Push(10); //不正确,a1仍是空值值,push会生成新的栈。

a1 = a1.Push(10); //需要将新栈重新赋值给a1


NET提供的常用数据结构


  • ImmutableStack

  • ImmutableQueue

  • ImmutableList

  • ImmutableHashSet

  • ImmutableSortedSet

  • ImmutableDictionary

  • ImmutableSortedDictionary


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




不可变优点


  • 集合共享安全,从不被改变

  • 访问集合时,不需要锁集合(线程安全)

  • 修改集合不担心旧集合被改变

  • 书写更简洁,函数式风格。 var list = ImmutableList.Empty.Add(10).Add(20).Add(30);

  • 保证数据完整性,安全性


不可变对象缺点


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


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


ImmutableList immutable = ImmutableList.Empty;

//转换成可批量操作的集合

var immutable2 = immutable.ToBuilder();

immutable2.Add('xx');

immutable2.Add('xxx');

//还原成不可变集合

immutable = immutable2.ToImmutable();


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




比较代码如下:


private static void List()

{

var list = new List();

var sp = Stopwatch.StartNew();


for (int i = 0; i < 1000="" *="" 10000;="">

{

var obj = new object();

list.Add(obj);

}

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

}

private static void BuilderImmutableList()

{

var list = ImmutableList.Empty;

var sp = Stopwatch.StartNew();

var blist= list.ToBuilder();

for (int i = 0; i < 1000="" *="" 10000;="">

{

var obj = new object();

blist.Add(obj);

}

list=blist.ToImmutable();


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

}

private static void ImmutableList()

{

var list = ImmutableList.Empty;

var sp = Stopwatch.StartNew();


for (int i = 0; i < 1000="" *="" 10000;="">

{

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

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多