创建常量、原子性的值类型

概述

本文是《Effective C#》一书第七节的读书笔记。通过这篇文章,我主要想向大家说明一个我们平时可能不太会注意到的问题:创建具有常量性和原子性的值类型。

从类型设计谈起

从Class到Struct

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

Province   省
City       市
Zip        邮编

要求对Zip的格式进行控制(必须全为数字,且为6位),大家该如何设计呢?我想很多人会写成这样吧:

public class Address {
    private string province;
    private string city;
    private string zip;

public string Province {
       get { return province; }
       set { province = value; }
    }

public string City {
       get { return city; }
       set { city = value; }
    }

public string Zip {
       get { return zip; }
       set {
           CheckZip(value);  // 验证格式
           zip = value;
       }
    }

// 检测是不是正确的 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);
    }
}

这里已经存在第一个问题:当我们声明一个类时,更多的是定义一系列相关的操作(或者叫行为、方法),当然类中也会包含字段和属性,但这些字段通常都是为类的方法所使用,而属性则常用于表示类的状态(比如StringBuilder的Length),类的能力(比如StringBuilder的 Capacity),方法进行的状态或者阶段。而定义一个结构时,我们通常仅仅是用它来保存数据,而不提供方法,或者是仅提供对其自身进行操作或者转换的方法,而非对其它类型提供服务的方法。

Address 不包含任何的方法,它仅仅是将Provice、City、Zip这样的三个数据组织起来成为一个独立的个体,所以最好将其声明为一个Struct而非是一个Class。(这里也有例外的情况:如果Address包含二十个或者更多的字段,则考虑将其声明为Class,因为Class在参数传递时是传引用,而Struct是传值。在数据较小的情况下,传值的效率更高一些;而在数据较大的时候,传引用占据更小的内存空间。)

所以我们首先可以将Address声明为一个Struct而非Class。

数据不一致的问题

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

Address a = new Address();
a.Province = "陕西";
a.City = "西安";
a.Zip = "710068";
Console.WriteLine(a.ToString()); // Province: 陕西, City: 西安, Zip: 710068

看上去是没有问题的,但是回想下类型的定义,在给Zip属性赋值时是有可能抛出异常的,所以我们还是把它放在一个Try Catch语句中,同时我们给Zip赋一个错误的值,看会发生什么:

try {
    a.City = "青岛";
    a.Zip = "12345";      // 这里触发异常
    a.Province = "山东";
} catch {
}
Console.WriteLine(a.ToString());//Province: 陕西, City: 青岛, Zip: 710068

结果是出现了数据不一致的问题,当为Zip赋值的时候,因为引发了异常,所以对Zip以及其后的Province的赋值都失败了,但是对City的赋值是成功的。结果就是出现了Provice是陕西,City却是青岛这种情况。

即是在赋值Zip时没有引发异常,也会出现问题:在多线程情况下,当当前线程执行到修改了 City为“青岛”,但还没有修改 Zip 和 Province的时候(Zip仍为 “710068”、Province仍为“陕西”)。如果此时其他线程访问类型实例a,那么也将会读取到不一致的数据。

常量性和原子性

我们现在已经知道了上面存在的问题,那么接下来该如何改进呢?我们先来看看作者对常量性和原子性给的定义:

  • 对象的原子性:对象的状态是一个整体,如果一个字段改变,其他字段也要同时做出相应改变。简单来说,就是要么不改,要么全改。
  • 对象的常量性:对象的状态一旦确定,就不能再次更改了。如果想再次更改,需要重新构造一个对象。

我们已经知道了对象的原子性和常量性这两个概念,那么接下来该如何去实施呢?对于原子性,我们实施的办法是添加一个构造函数,在这个构造函数中为对象的所有字段赋值。而为了实施常量性,我们不允许在为对象赋值以后还能对对象状态进行修改,所以我们将属性中的set访问器删除掉,同时将字段声明为readonly:

public struct Address {
    private readonly string province;
    private readonly string city;
    private readonly string zip;

public Address(string province, string city, string zip) {
       this.city = city;           
       this.province = province;
       this.zip = zip;
    CheckZip(zip);     // 验证格式
    }

public string Province {
       get { return province; }
    }

public string City {
       get { return city; }
    }

public string Zip {
       get { return zip; }
    }
    // 其余略 ...
}

这样,我们对Address对象的创建,将所有字段的赋值都在构造函数中作为一个整体来进行;而当我们需要改变单个字段的值时,也需要重新创建对象再赋值。我们看下下面的测试:

Address a = new Address("陕西", "西安", "710068");

try {
    a = new Address("青岛", "山东", "22233");// 发生异常,对a重新赋值失败,但状态保持一致
} catch {
}

Console.WriteLine(a.ToString()); // 输出:Province: 陕西, City: 西安, Zip: 710068

避免外部类型对类型内部的访问

上面的方法解决了数据不一致的问题,但是还漏掉了一点:当类型内部维护着一个引用类型字段,比如说数组。尽管我们将它声明为了readonly,类型外部还是可以对它进行访问(如果你不清楚值类型和引用类型的区别,请参考 C#类型基础)。现在我们修改Address 类,添加一个数组phones,存储电话号码:

private readonly string[] phones;

public Address(string province, string city, string zip, string[] phones) {  
    // 略...
    this.phones = phones;
}

public string[] Phones {
    get { return phones; }
}

我们接下来做个测试:

string[] phones = { "029-88401100", "029-88500321" };
Address a = new Address("陕西", "西安", "710068", phones);

Console.WriteLine(a.Phones[0]);     // 输出: 029-88401100

string[] b = a.Phones;
b[0] = "029-XXXXXXXX";       // 通过b修改了 Address的内容

Console.WriteLine(a.Phones[0]); // 输出: 029-XXXXXXXX

可以看到,尽管 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。再次运行刚才的代码,我们可以得到 029-88401100 的输出。

但是问题还没有结束,我们再看下面这段代码:

string[] phones = { "029-88401100", "029-88500321" };
Address a = new Address("陕西", "西安", "710068", phones);

Console.WriteLine(a.Phones[0]);     // 输出: 029-88401100

phones[0] = "029-XXXXXXXX";         // 通过phones变量修改了Address对象内部的数据
Console.WriteLine(a.Phones[0]); // 输出: 029-XXXXXXXX

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

public Address(string province, string city, string zip, string[] phones) {       
    // 前面略...
    this.phones = new string[phones.Length];
    phones.CopyTo(this.phones, 0);
    CheckZip(zip);    // 验证格式
}

这样,我们再次运行上面的代码,对于phones的修改便不会再影响到Address对象本身。

总结

这篇文章向大家讲述了类型设计时需要注意的三个问题:1、当创建类型的目的是为了存储一组相关的数据,且数据量不是很大的时候,将它声明为Struct比Class会获得更高的效率;2、将类型声明为具有原子性和常量性,可以避免可能出现的数据不一致问题;3、通过在构造函数和Get访问器中,对对象的字段进行深度复制,可以避免在类型的外部修改类型内部数据的问题。

感谢阅读,希望这篇文章能带给你帮助!

转载于:https://www.cnblogs.com/JimmyZhang/archive/2008/05/30/1210376.html

[记]创建常量、原子性的值类型相关推荐

  1. Swift 值类型和引用类型的内存管理

    1.内存分配 1.1 值类型的内存分配 在 Swift 中定长的值类型都是保存在栈上的,操作时不会涉及堆上的内存.变长的值类型(字符串.集合类型是可变长度的值类型)会分配堆内存. 这相当于一个 &qu ...

  2. Windows Phone 开发起步之旅之二 C#中的值类型和引用类型

    今天和大家分享下本人也说不清楚的一个C#基础知识,我说不清楚,所以我才想把它总结一下,以帮助我自己理解这个知识上的盲点,顺便也和同我一样不是很清楚的人一起学习下.  一说起来C#中的数据类型有哪些,大 ...

  3. C#值类型-引用类型

    转换-值类型-引用类型-预定义分类表 转换 C#里,兼容的实例间可以进行相互转换 转换总是从一个值转换成一个新的值 隐式转换:隐式转换是自动发生的 显式转换:显式转换是手动操作的 长整型转换成整型的时 ...

  4. 变量/值类型/引用类型/常量/枚举

    变量 声明语法 datatype identifier; 如:int i; //声明一个int类型的变量,但是在没有初始化之前编译器不允许使用该变量 同时声明多个 int a,b;//同时声明两个in ...

  5. JDBC连接mysql、创建表、操作数据、PreparedStatement防注入、sql语句返回值类型知识汇总

    JDBC连接过程: import java.sql.*;/*** Description:* Created by CWG on 2020/10/29 21:05*/ public class Con ...

  6. 理解C#值类型与引用类型(收藏)

    从概念上看,值类型直接存储其值,而引用类型存储对其值的引用.这两种类型存储在内存的不同地方.在C#中,我们必须在设计类型的时候就决定类型实例的行为.这种决定非常重要,用<CLR via C#&g ...

  7. 理解C#值类型与引用类型

    这篇文章是我几个月前写的,今天进行了比较大的修订,重新发了出来,希望和大家共同探讨,并在此感谢Anytao 的讨论和帮助. 从概念上看,值类型直接存储其值,而引用类型存储对其值的引用.这两种类型存储在 ...

  8. C#基础——值类型和引用类型

    1.值类型,引用类型,拆,装箱,常用的引用类型,值类型. 栈:一种先进后出(后进先出)的存储数据的结构体 堆:一块连续的,自由的存储空间. 值类型:变量直接保存其数据. 引用类型:变量保存其数据的引用 ...

  9. 【深入理解CLR 六】基元类型、引用类型和值类型

    最近工作的事情比较忙,导致CLR很久没有更新了,恰巧周五听了涛涛的关于GC和内存管理的技术分享,想了下自己对CLR的学习得跟上,另外之前武哥推荐了一本书叫<码农翻身>,是一个IBM架构师写 ...

  10. 值类型 与引用的 copy

    结构体和枚举是值类型 值类型被赋予给一个变量,常数或者本身被传递给一个函数的时候,实际上操作的是其的拷贝. 在之前的章节中,我们已经大量使用了值类型.实际上,在 Swift 中,所有的基本类型:整数( ...

最新文章

  1. Linux下autoreconfig命令安装.
  2. AJAX(二)jquery ajax
  3. 会员系统中需要验证用户的邮箱是否真实存在
  4. SAP CRM interactive report的各种输入字段
  5. LeetCode 709. 转换成小写字母
  6. PTA9、计算利率 (10 分)
  7. Python 迁移学习实用指南 | iBooker·ApacheCN
  8. java.io.IOException: Could not find my address
  9. 一、云计算-云平台-国产-华为-FusionSphere+HCIE Cloud相关知识点+笔试题库
  10. 设置iPhone来电铃声(图文教程)
  11. 图像风格迁移cvpr2020_浅谈风格迁移(二)任意风格迁移
  12. 推荐收藏系列:一文理解JVM虚拟机(内存、垃圾回收、性能优化)解决面试中遇到问题(图解版)
  13. 从win10(1909)中彻底卸载智能云输入法
  14. java数据结构之数组
  15. EDIUS设置3D转场的方法
  16. 轮廓的最大面积内接矩形/内接圆计算
  17. 斜体,字体,标题,列表,a链接,描点
  18. React实战模版(AntDesignPro框架)
  19. word修订显示修订人_美丽的滑出导航修订
  20. 4G手机CE认证介绍

热门文章

  1. 华为搜索引擎面世,百度搜索有点危险了!
  2. 碉堡了,独家首发Java核心知识点总结,超全!
  3. 菜鸟数据中台技术演进之路
  4. 写代码千万别用User这个单词!
  5. SSH框架微服务改进实战
  6. 十年,从网管到首席架构师,我的成长感悟
  7. 5个相见恨晚的Linux命令
  8. three 天空球_three.js添加场景背景和天空盒(skybox)代码示例
  9. python 服务器_使用 Python 开发 EMQ X MQTT 服务器插件
  10. shell初学之PHP