您的位置:首页 > 编程语言 > C#

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

2016-01-05 20:53 405 查看

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定义为类(引用类型)还是结构(值类型)?

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

 

因为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为”湖南”,但还没有修改Zip和Province的时候(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对象内部的内容.如何避免这种情况呢?

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

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页!真是够了!等楼主看完了,咱们在细细谈!对了小书的名字叫做<<铁器时代>>作者:骁骑校

 

 

 

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C# .net