C#不可变类型深入解析
投稿:shichen2014
学过C#的人都知道string类型,但是string作为一种特殊的引用类型还有一个重要的特征就是恒定性,或者叫不可变性,即Immutable。作为不可变类型,最主要的特性表现是:一旦创建,只要修改,就会在托管堆上创建一个新的对象实例,而且和上一个对象实例是相邻的,在托管堆上分配到一块连续的内存空间。
那么为什么需要不可变类型呢?
在多线程情况下,一个线程,由于种种原因(比如异常)只修改了一个变量所代表类型的部分成员的值,这时候,另一个进程进来,也访问这个变量,第二个进程访问到的变量成员,一部分成员还是原来的值,另一部分成员的值是第一个线程修改的值,这样就出现了"数据不一致"。而不可变类型就是为了解决在多线程条件下的"数据不一致"的问题。
当然,字符串的不可变性或恒定性,不仅解决了"数据不一致"的问题,还为字符串的"驻留"提供了前提,这样才可以把不同的字符串以及托管堆上的内存地址以键值对的形式放到全局哈希表中。
一、亲眼目睹"数据不一致":
对Student的Score属性,在赋值的时候加上检测,检测是否是2位数整数。
public struct Student { private string name; private string score; public string Name { get { return name; } set { name = value; } } public string Score { get { return score; } set { CheckScore(value); score = value; } } //检测分数是否是2位数整数 private void CheckScore(string value) { string pattern = @"\d{2}"; if (!Regex.IsMatch(value, pattern)) { throw new Exception("不是有效分数!"); } } public override string ToString() { return String.Format("姓名:{0},分数:{1}", name, score); } }
在主程序中故意制造出一个异常,目的是只对一个变量所代表类型的某些成员赋值。
static void Main(string[] args) { Student student = new Student(); student.Name = "张三"; student.Score = "80"; Console.WriteLine(student.ToString()); try { student.Name = "李四"; student.Score = "8"; } catch (Exception) { throw; } Console.WriteLine(student.ToString()); Console.ReadKey(); }
打断点,运行,发现Student类型的student变量,在第二次赋值的时候,把student的Name属性值改了过来,而student的Score属性,由于发生了异常,没有修改过来。这就是"数据不一致"。
如下图所示:
二、动手设计不可变类型
1.不可变类型的2个特性:
①对象的原子性:要么不改,要改就把所有成员都改,从而创建新的对象。
②对象的常量性:对象一旦创建,就不能改变状态,即不能改变对象的属性,只能创建新的对象。
2.遵循以上不可变类型的2个特征
①在构造函数中对所有字段赋值。
②将属性中的set访问器删除。
class Program { static void Main(string[] args) { Student student = new Student("张三", "90"); student = new Student("李四","80"); Console.WriteLine(student.ToString()); Console.ReadKey(); } } public struct Student { private readonly string name; private readonly string score; public Student(string name, string score) { this.name = name; this.score = score; } public string Name { get { return name; } } public string Score { get { return score; } } public override string ToString() { return String.Format("姓名:{0},分数:{1}", name, score); } }
运行结果如下图所示:
由此可见,我们无法修改Student的其中某一个成员,只能通过构造函数创建一个新对象,满足"对象的原子性"。
而且也无法修改Student对象实例的某个属性值,符合"对象的常量性"。
3.如果有引用类型字段和属性,如何做到"不可变性"?
class Program { static void Main(string[] args) { string[] classes = {"语文", "数学"}; Student student = new Student("张三", "85", classes); Console.WriteLine("==修改之前=="); Console.WriteLine(student.ToString()); string[] tempArray = student.Classes; tempArray[0] = "英语"; Console.WriteLine("==修改之后=="); Console.WriteLine(student.ToString()); Console.ReadKey(); } } public struct Student { private readonly string name; private readonly string score; private readonly string[] classes; public Student(string name, string score, string[] classes) { this.name = name; this.score = score; this.classes = classes; } public string Name { get { return name; } } public string Score { get { return score; } } public string[] Classes { get { return classes; } } public override string ToString() { string temp = string.Empty; foreach (string item in classes) { temp += item + ","; } return String.Format("姓名:{0},总分:{1},参加的课程有:{2}", name, score,temp.Substring(0, temp.Length -1)); } }
结果如下图所示:
由此可见,还是可以对对象的属性间接修改赋值,不满足不可变类型的"常量性"特点。
4.通过在构造函数和属性的get访问器中复制的方式来满足不可变性
class Program { static void Main(string[] args) { string[] classes = {"语文", "数学"}; Student student = new Student("张三", "85", classes); Console.WriteLine("==修改之前=="); Console.WriteLine(student.ToString()); string[] tempArray = student.Classes; tempArray[0] = "英语"; Console.WriteLine("==修改之后=="); Console.WriteLine(student.ToString()); Console.ReadKey(); } } public struct Student { private readonly string name; private readonly string score; private readonly string[] classes; public Student(string name, string score, string[] classes) { this.name = name; this.score = score; this.classes = new string[classes.Length]; classes.CopyTo(this.classes, 0); CheckScore(score); } public string Name { get { return name; } } public string Score { get { return score; } } public string[] Classes { get { string[] result = new string[classes.Length]; classes.CopyTo(result,0); return result; } } //检测分数是否是2位数整数 private void CheckScore(string value) { string pattern = @"\d{2}"; if (!Regex.IsMatch(value, pattern)) { throw new Exception("不是有效分数!"); } } public override string ToString() { string temp = string.Empty; foreach (string item in classes) { temp += item + ","; } return String.Format("姓名:{0},总分:{1},参加的课程有:{2}", name, score,temp.Substring(0, temp.Length -1)); } }
运行结果如下图所示:
此外,如果让分数不满足条件,Student student = new Student("张三", "8", classes),就会报错: