不可變物件
不可變(immutable): 即物件一旦被建立初始化後,它們的值就不能被改變,之後的每次改變都會產生一個新物件。
1 2 |
var str="mushroomsir"; str.Substring(0, 6) |
c#中的string是不可變的,Substring(0, 6)返回的是一個新字串值,而原字串在共享域中是不變的。另外一個StringBuilder是可變的,這也是推薦使用StringBuilder的原因。
1 |
var age=18; |
當儲存值18的記憶體分配給age變數時,它的記憶體值也是不可以被修改的。
1 |
age=2; |
此時會在棧中開闢新值2賦值給age變數,而不能改變18這個記憶體裡的值,int在c#中也是不可變的。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 = “大毛” 這一步,其他的執行緒有可能讀取到的資料就是:
1 2 |
mutable.Name == "大毛"; mutable.Address == "清華"; |
很明顯這樣資料完整性就不能保障,也有稱資料撕裂。我們把可變物件更改為不可變物件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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此時即為不可變物件,因為物件本身是個不可變整體。通過使用不可變物件可以不用擔心資料完整性,也能保證資料安全性,不會被其他執行緒修改。
自定義不可變集合
我們去列舉可變集合時,出於執行緒安全的考慮我們往往需要進行加鎖處理,防止該集合在其他執行緒被修改,而使用不可變集合則能避免這個問題。我們平常使用的資料結構都是採用可變模式來實現的,那怎麼實現一個不可變資料結構呢!以棧來示例,具體程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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(); } } |
- 入棧時會例項化一個新棧物件
- 將新值通過建構函式傳入,並存放在新物件Head位置,舊棧物件放在在Tail位置引用
- 出棧時返回當前棧物件的Tail引用的棧物件
使用方法如下:
1 2 3 4 5 6 7 8 9 |
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安裝:
1 |
Install-Package Microsoft.Bcl.Immutable |
使用如下,和上面我們自定義的幾乎一樣:
1 2 3 4 5 |
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只是舊值:
1 2 3 |
ImmutableStack<int> a1 = ImmutableStack<int>.Empty; a1.Push(10); //不正確,a1仍是空值值,push會生成新的棧。 a1 = a1.Push(10); //需要將新棧重新賦值給a1 |
NET提供的常用資料結構
- ImmutableStack
- ImmutableQueue
- ImmutableList
- ImmutableHashSet
- ImmutableSortedSet
- ImmutableDictionary<K, V>
- ImmutableSortedDictionary<K, V>
不可變集合和可變集合在演算法複雜度上的不同:
不可變優點
- 集合共享安全,從不被改變
- 訪問集合時,不需要鎖集合(執行緒安全)
- 修改集合不擔心舊集合被改變
- 書寫更簡潔,函式式風格。 var list = ImmutableList.Empty.Add(10).Add(20).Add(30);
- 保證資料完整性,安全性
不可變物件缺點
不可變本身的優點即是缺點,當每次物件/集合操作都會返回個新值。而舊值依舊會保留一段時間,這會使記憶體有極大開銷,也會給GC造成回收負擔,效能也比可變集合差的多。
跟string和StringBuild一樣,Net提供的不可變集合也增加了批量操作的API,用來避免大量建立物件:
1 2 3 4 5 6 7 |
ImmutableList<string> immutable = ImmutableList<string>.Empty; //轉換成可批量操作的集合 var immutable2 = immutable.ToBuilder(); immutable2.Add("xx"); immutable2.Add("xxx"); //還原成不可變集合 immutable = immutable2.ToImmutable(); |
我們來對比下可變集合、不可變Builder集合、不可變集合的效能,新增新物件1000W次:
比較程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
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來進行安全儲存,使用時進行恢復及清理。
1 2 3 4 |
IntPtr addr = Marshal.SecureStringToBSTR(secureString); string temp = Marshal.PtrToStringBSTR(addr); Marshal.ZeroFreeBSTR(addr); WriteProcessMemory(...) |