C#类型基础----不可变类型

来源:互联网 发布:mac 拷贝隐藏文件 编辑:程序博客网 时间:2024/06/05 17:43

C#类型基础----不可变类型

 

前言

前面说过string类型是一种特殊的引用类型,成为不可变类型.本次就为大家说下什么是不可变类型.

 

正文

假如要设计一个存储收信人地址的类型(Type),叫做Address,它包含了这样几个属性:

Province    

City        

Zip         邮编

 

如果要对Zip格式进行控制(必须全为数字,且为6),那么可以在其中添加一个CheckZip()方法:

    public struct Address    {        private string province;         public string Province        {            get { return province; }            set { province = value; }        }          private string city;         public string City        {            get { return city; }            set { city = value; }        }          private string zip;         public string Zip        {            get { return zip; }            set {  CheckZip(value);zip = value; }        }                 private void CheckZip(string value)        {            string pattern = @"\d{6}";            if (!Regex.IsMatch(value, pattern))            {                throw new Exception("Zip is invalid! ");            }        }        public override string ToString()        {            return String.Format("Province: {0}, City: {1}, Zip: {2}", province, city, zip);        } } 

这里已经存在第一个问题:应该将Address定义为类(引用类型)还是结构(值类型)?

当定义一个类时,更多的是定义一系列相关的操作(或者叫行为,方法).类中也会包含字段和属性,但这些字段通常都是为类的方法所调用,而属性则常用于表示类的状态(比如StringBuilderLength),类的能力(比如StringBilderCapacity).而在定义一个结构时,通常仅仅用它来保存数据,而不提供方法,或者是仅提供对其自身操作或者转换的方法,而非对其他类型提供服务的方法.

 

因为Address不包含任何的方法,它仅仅是将Province,City,ZIp这样的三个数据组织起来成为一个独立的操作单元,所以最好将其声明为一个Struct而非Class.

 

因此,首先将Address声明为一个Struct而非Class

 

接下来使用一下刚刚创建的Address类型:

            Address a = new Address();            a.Province = "山东";            a.City = "淄博";            a.Zip = "250205";            Console.WriteLine(a.ToString()); 

看上去没问题,但是回想一下类型的定义,在给Zip属性赋值的时候可能会抛出异常,所以还是把它放在一个try-catch,同时向Zip赋一个错误的值,看一下会发生什么:

            Address a = new Address();            a.Province = "山东";            a.City = "淄博";            a.Zip = "250205";            try            {                a.City = "长沙";                a.Zip = "12345";                a.Province = "湖南";            }            catch (Exception)            {                                           }            Console.WriteLine(a.ToString()); 

通过输出,咱们就能发现部分问题,首要的问题是出现了数据不一致的现象,当为Zip赋值的时候,因为发生了一场,所以对Zip以及其后的Province的赋值都失败了,但是对City的赋值时成功的.

 

即使在赋值Zip时没有发生异常,也会出现问题:在多线程情况下,当当前线程执行到修改了City”湖南”,但还没有修改ZipProvince的时候(Zip仍为”250205”,Province仍为”山东”).如果此时其他线程访问类型实例a,那么也将会读取到不一致的数据.

 

现在已经知道了上面出现的为题,那么接下来就该引出咱们要输的两个概念了:常量性和原子性.

 

对象的原子性:对象的装填是一个整体,如果一个字段改变,其他字段也要同时做出相应改变.简单来说,就是要么不改变,要么全改.

对象的常量性:对象的状态一旦确定,就不能再次更改了.如果想要再次更改,需要重新构造一个对象.

 

接下来就是如何实现这两个概念了.对于原子性,实现的方法是添加一个构造函数,在这个构造函数中为对象的所有字段赋值.而为了实施常量性,不允许在为对象赋值以后还能对对象进行修改,所以讲属性中的set访问器删除,同时将字段声明为readonly:

    public struct Address    {        private readonly string province;         public string Province        {            get { return province; }                    }          private readonly string city;         public string City        {            get { return city; }                    }          private readonly string zip;         public string Zip        {            get { return zip; }                    }        public Address(string province, string city, string zip)        {            this.province = province;            this.city = city;            this.zip = zip;CheckZip(zip);        }                private void CheckZip(string value)        {            string pattern = @"\d{6}";            if (!Regex.IsMatch(value, pattern))            {                throw new Exception("Zip is invalid! ");            }        }        public override string ToString()        {            return String.Format("Province: {0}, City: {1}, Zip: {2}", province, city, zip);        }  } 

这样的话,当对Address对象进行创建时,将所有字段的赋值在构造函数中作为一个整体来进行;而当需要改变翻个字段的值时,也需要重新创建对象再赋值.示例如下:

            Address a = new Address("山东","淄博","250205");            try            {                a = new Address("湖南","长沙","12345");            }            catch (Exception)            {                                throw;            }            Console.WriteLine(a.ToString()); 


上面的方法解决了数据不一致的问题,但是还是漏掉了一点:当类型内部维护着一个引用类型字段时,比如数组,尽管将它声明为readonly,在类型外部还是可以对它进行修改.现在修改Address,为它添加一个数组phones,存储电话号码:

    public struct Address    {        private readonly string province;         public string Province        {            get { return province; }                    }          private readonly string city;         public string City        {            get { return city; }                    }          private readonly string zip;         public string Zip        {            get { return zip; }                    }        private readonly string[] phones;         public string[] Phones        {            get { return phones; }        }          public Address(string province, string city, string zip,string []phones)        {            this.province = province;            this.city = city;            this.zip = zip;                        this.phones = phones;            CheckZip(zip);        }                private void CheckZip(string value)        {            string pattern = @"\d{6}";            if (!Regex.IsMatch(value, pattern))            {                throw new Exception("Zip is invalid! ");            }        }        public override string ToString()        {            return String.Format("Province: {0}, City: {1}, Zip: {2}", province, city, zip);        }  } 

测试代码如下:

 

            string[] phones = {"18753377***","13953134***"};            Address a = new Address("山东","淄博","250205",phones);                        Console.WriteLine(a.Phones[0]);//输出187的电话            string[] b = a.Phones;            b[0] = "13869188***";//通过b修改了Address的内容            Console.WriteLine(a.Phones[0]);//输出138的电话 

可以看到,尽管将phones字段声明为了readonly,并且它也值提供了get属性访问器.仍然可以通过Address对象a外部的变量b,修改了a对象内部的内容.如何避免这种情况呢?

可以采取上面说过的深度复制的方式来解决,phonesget属性访问器中添加如下代码:

        public string[] Phones        {            get             {                string[] rtn = new string[phones.Length];                phones.CopyTo(rtn,0);                return rtn;             }        }  

get访问器中,创建一个新的数组,并将Address对象本身的数组内容进行了复制,然后返回给调用者.此时,再次运行刚才的代码,由于b指向了新创建的这个数组对象,而非Address对象a内部的数组对象,所以对b的修改不会影响到a.

 

但是,问题不会这么简单的就结束了,看下面这段代码:

            string[] phones = {"18753377***","13953134***"};            Address a = new Address("山东","淄博","250205",phones);                        Console.WriteLine(a.Phones[0]);//输出187的电话            phones[0] = "13869188***";//通过phones变量修改了Address对象北部的数据                        Console.WriteLine(a.Phones[0]);//输出138的电话 

在创建完Address对象后,依然可以通过之前的数组变量来修改对象内部的数据,收到前面的启发,咱们可以在构造函数中对外部传递进来的数组进行深度复制:

        public Address(string province, string city, string zip,string []phones)        {            this.province = province;            this.city = city;            this.zip = zip;                     this.phones = new string[phones.Length];            phones.CopyTo(this.phones, 0);            CheckZip(zip);        }


完美了!

 

小结

这一章关于C#基础类型的东西咱们就说这么多了,这几天,不对,这一个月吧,一直在看小说,可能把写博客,学习啊什么的忘了,嘻嘻,其实我也不想这样,一开始那本小说是5W,我连着看了一个月,好不容易看完了,结构TMMD作者又写了一本这本小书的续集!7W!真是够了!等楼主看完了,咱们在细细谈!对了小书的名字叫做<<铁器时代>>作者:骁骑校

 

 

 

 

0 0
原创粉丝点击