C#中的不可變數(shù)據(jù)類型介紹(不可變對象、不可變集合)
不可變對象
不可變(immutable): 即對象一旦被創(chuàng)建初始化后,它們的值就不能被改變,之后的每次改變都會(huì)產(chǎn)生一個(gè)新對象。
var str="mushroomsir";
str.Substring(0, 6)
c#中的string是不可變的,Substring(0, 6)返回的是一個(gè)新字符串值,而原字符串在共享域中是不變的。另外一個(gè)StringBuilder是可變的,這也是推薦使用StringBuilder的原因。
var age=18;
當(dāng)存儲(chǔ)值18的內(nèi)存分配給age變量時(shí),它的內(nèi)存值也是不可以被修改的。
age=2;
此時(shí)會(huì)在棧中開辟新值2賦值給age變量,而不能改變18這個(gè)內(nèi)存里的值,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 = "北大";
我們實(shí)例化MutableContact賦值給mutable,隨后我們可以修改MutableContact對象內(nèi)部字段值,它已經(jīng)不是初始后的值,可稱為可變(mutable)對象。
可變對象在多線程并發(fā)中共享,是存在一些問題的。多線程下A線程賦值到 Name = "大毛" 這一步,其他的線程有可能讀取到的數(shù)據(jù)就是:
mutable.Name == "大毛";
mutable.Address == "清華";
很明顯這樣數(shù)據(jù)完整性就不能保障,也有稱數(shù)據(jù)撕裂。我們把可變對象更改為不可變對象如下:
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);
}
}
使用時(shí)只能通過Contact2的構(gòu)造函數(shù)來初始化Name和Address字段。Contact2此時(shí)即為不可變對象,因?yàn)閷ο蟊旧硎莻€(gè)不可變整體。通過使用不可變對象可以不用擔(dān)心數(shù)據(jù)完整性,也能保證數(shù)據(jù)安全性,不會(huì)被其他線程修改。
自定義不可變集合
我們?nèi)ッ杜e可變集合時(shí),出于線程安全的考慮我們往往需要進(jìn)行加鎖處理,防止該集合在其他線程被修改,而使用不可變集合則能避免這個(gè)問題。我們平常使用的數(shù)據(jù)結(jié)構(gòu)都是采用可變模式來實(shí)現(xiàn)的,那怎么實(shí)現(xiàn)一個(gè)不可變數(shù)據(jù)結(jié)構(gòu)呢!以棧來示例,具體代碼如下:
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.入棧時(shí)會(huì)實(shí)例化一個(gè)新棧對象
2.將新值通過構(gòu)造函數(shù)傳入,并存放在新對象Head位置,舊棧對象放在在Tail位置引用
3.出棧時(shí)返回當(dāng)前棧對象的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都是一個(gè)新對象,舊對象不可修改,這樣在枚舉集合就不需要擔(dān)心其他線程修改了。
Net提供的不可變集合
不可變隊(duì)列,不可變列表等數(shù)據(jù)結(jié)構(gòu)如果都自己實(shí)現(xiàn)工作量確實(shí)有點(diǎn)大。幸好的是Net在4.5版本已經(jīng)提供了不可變集合的基礎(chǔ)類庫。 使用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不可變列表集合有一點(diǎn)要注意的是,當(dāng)我們Push值時(shí)要重新賦值給原變量才正確,因?yàn)閜ush后會(huì)生成一個(gè)新對象,原a1只是舊值:
ImmutableStack<int> a1 = ImmutableStack<int>.Empty;
a1.Push(10); //不正確,a1仍是空值值,push會(huì)生成新的棧。
a1 = a1.Push(10); //需要將新棧重新賦值給a1
NET提供的常用數(shù)據(jù)結(jié)構(gòu)
1.ImmutableStack
2.ImmutableQueue
3.ImmutableList
4.ImmutableHashSet
5.ImmutableSortedSet
6.ImmutableDictionary<K, V>
7.ImmutableSortedDictionary<K, V>
不可變集合和可變集合在算法復(fù)雜度上的不同:
不可變優(yōu)點(diǎn)
1.集合共享安全,從不被改變
2.訪問集合時(shí),不需要鎖集合(線程安全)
3.修改集合不擔(dān)心舊集合被改變
4.書寫更簡潔,函數(shù)式風(fēng)格。 var list = ImmutableList.Empty.Add(10).Add(20).Add(30);
5.保證數(shù)據(jù)完整性,安全性
不可變對象缺點(diǎn)
不可變本身的優(yōu)點(diǎn)即是缺點(diǎn),當(dāng)每次對象/集合操作都會(huì)返回個(gè)新值。而舊值依舊會(huì)保留一段時(shí)間,這會(huì)使內(nèi)存有極大開銷,也會(huì)給GC造成回收負(fù)擔(dān),性能也比可變集合差的多。
跟string和StringBuild一樣,Net提供的不可變集合也增加了批量操作的API,用來避免大量創(chuàng)建對象:
ImmutableList<string> immutable = ImmutableList<string>.Empty;
//轉(zhuǎn)換成可批量操作的集合
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);
}
另外一個(gè)缺點(diǎn)比較有趣,也有不少人忽略。 由于string的不可變特性,所以當(dāng)我們使用string在保存敏感信息時(shí),就需要特別注意。
比如密碼 var pwd="mushroomsir",此時(shí)密碼會(huì)以明文存儲(chǔ)在內(nèi)存中,也許你稍后會(huì)加密置空等,但這都是會(huì)生成新值的。而明文會(huì)長時(shí)間存儲(chǔ)在共享域內(nèi)存中,任何能拿到dump文件的人都可以看到明文,增加了密碼被竊取的風(fēng)險(xiǎn)。當(dāng)然這不是一個(gè)新問題,net2.0提供的有SecureString來進(jìn)行安全存儲(chǔ),使用時(shí)進(jìn)行恢復(fù)及清理。
IntPtr addr = Marshal.SecureStringToBSTR(secureString);
string temp = Marshal.PtrToStringBSTR(addr);
Marshal.ZeroFreeBSTR(addr);
WriteProcessMemory(...)
相關(guān)文章
C# FileStream實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳
這篇文章主要為大家詳細(xì)介紹了C# FileStream實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03C#將html table 導(dǎo)出成excel實(shí)例
C#將html table 導(dǎo)出成excel實(shí)例,需要的朋友可以參考一下2013-04-04C#實(shí)現(xiàn)HTTP訪問類HttpHelper的示例詳解
在項(xiàng)目開發(fā)過程中,我們經(jīng)常會(huì)訪問第三方接口,如我們需要接入的第三方接口是Web API,這時(shí)候我們就需要使用HttpHelper調(diào)用遠(yuǎn)程接口了。本文為大家介紹了C#實(shí)現(xiàn)HTTP訪問類HttpHelper的示例代碼,需要的可以參考一下2022-09-09c#動(dòng)態(tài)調(diào)用Webservice的兩種方法實(shí)例
這篇文章介紹了c#動(dòng)態(tài)調(diào)用Webservice的兩種方法實(shí)例,有需要的朋友可以參考一下2013-08-08Unity UGUI教程之實(shí)現(xiàn)滑頁效果
使用UGUI提供的ScrollRect和ScrollBar組件實(shí)現(xiàn)基本滑動(dòng)以及自己控制每次移動(dòng)一頁來達(dá)到滑頁的效果。具體實(shí)現(xiàn)思路請參考下本教程2016-04-04