CLR类型设计之属性
在之前的随笔中,我们探讨了参数,字段,方法,我们在开始属性之前回顾一下,之前的探讨实际上串联起来就是OOP编程的思想,在接下来的文章中,我们还会讨论接口(就是行为),举个例子:我们如果要做一个学生档案,我们需要先抽象出来有那些对象实体,比如有一个学生类,里面有学生id,姓名,年龄,班级等字段。 但是这不能满足我们的需求,我们要做学生档案管理,需要知道学生的每科成绩,所以我们还需要一个成绩类,里面定义了学生的学生Id,科目,科目分数,下面是两个类的代码示例
public sealed class Student { //学员id public int StudentId; //姓名 public string Name; //年龄 public int Age; //班级名 public string classname; } public sealed class Score { //学员id public int StudentId; //科目 public string SubjectName; //成绩 public int Achievement; }
有了这两个类以后,我们就可以创建一个获取学员成绩的方法,方法的代码示例就不写了,OOP的思想最重要的在于尽可能的模块化可复用,当然为了实现这些,还有继承,多态等去实现目的,但是在继续实现过程中你可能会发现一些问题,当我们需要使用上面类型的时候,可以通过实例化直接使用,如下:
Student stu = new Student(); stu.Name = "苏云"; stu.Age = 22; stu.classname = "fantasy";
但是如果我输入一个Age为-15,程序也可以通过,字段值就会被改变为-15,年龄是不存在负数的,所以这个值是不应该通过的,这就是今天的主题,属性设置,面向对象一条很重要的原则就是数据封装,意味类型字段永远不应该公开,否则很容易因为不恰当使用而破坏对象的状态,如上文我们输入的负值,当然还有一些其他原因,比如线程安全,字段为逻辑字段,其值存在于内存中的字节,通过某个算法获取得到。但是这样做就会导致一个问题,外部方法想要访问时,由于内部不公开,所以外部无法访问
CLR中提供了属性机制,我们完全可以不用担心上面的问题,我们改写一下例子一的代码示例,在实例化学生的时候,如果Age<1,就会抛出异常,可以看到是那个参数报出的异常,值是多少。
public sealed class Student { private int studentId; private string name; private int age; private string classname; //学员id public int StudentId { get { return (studentId); }set { studentId = value; } } //姓名 public string Name{ get { return (name); }set { name = value; } } //年龄 public int Age { get { return (age); } set { if (value<1) throw new ArgumentOutOfRangeException("value", value.ToString(),"学生的年龄不能小于1岁"); age = value; } } //班级名 public string Classname { get { return (classname); }set { classname = value; } } }
属性机制使用起来很简单,每个属性都有名称和类型(类型不能为void),并且一个类中同一个字段名称只能出现一次,只需要get,set两个关键字,如果只有get那就是只读字段,只有set是只写字段。也可以在get上写计算方法获取到值,但是上述方法写起来是否觉得很麻烦?C#还支持自动属性实现,我们改写成绩类,示例代码如下,
public sealed class Score { //学员id public int StudentId { get; set; } //科目 public string SubjectName { get; set; } //成绩 public int Achievement { get; set; } }
在C#中声明get;set但是却未提供对应方法,C#会自动声明一个私有字段,这样就可以很快定义一个属性,和写字段是一样的,但需要注意的是,属性不能作为out或ref参数传给方法,而字段可以
对象和初始化器
在之前的代码中,我们初始化学生类需要分两步,第一实例化,第二赋值,但实际上我们可以使用更简单的语法,对象初始化器初始化一个对象,只需要像下面这样一句话就可以初始化一个对象并且赋值,他做的事情和例子2是相同的。在集合中也可以使用初始化器初始化集合。
重点: 如果类没有无参的构造函数就会出现编译时错误
Student stu1 = new Student() { Name="admain",Age=15,Classname="fantasy" };
我们提到集合也可以用初始化器的方法初始化,但是集合的初始化和对象并不一样,首先要求对象或字段继承了IEnummerable<T>接口,我们示例常见的Dictionary集合如何初始化
1 Dictionary<int, string> dic = new Dictionary<int, string> { 2 { 1,"张三"}, { 2,"李四"} 3 }; 4 //等价于 5 dic.Add(1, "张三"); 6 dic.Add(1, "李四");
有参属性:索引器
一个属性的get访问器方法不接收参数,则称为无参属性,用起来就和访问字段一样,除了这些与字段相似的属性,还有一种有参属性,C#里称其为索引器,下文中所有有参属性都用索引器替代,C#使用数组风格的语法来公开索引器,看下面的示例:
1 class MyListBox 2 { 3 public ArrayList data = new ArrayList(); 4 public object this[int idx] //this作索引器名称,idx是索引参数 5 { 6 get 7 { 8 if (idx > -1 && idx < data.Count) 9 { 10 return data[idx]; 11 } 12 else 13 { 14 return null; 15 } 16 } 17 set 18 { 19 if (idx > -1 && idx < data.Count) 20 { 21 data[idx] = value; 22 } 23 else if (idx <= data.Count) 24 { 25 data.Add(value); 26 } 27 else 28 { 29 throw new ArgumentOutOfRangeException("idx", idx, "超出数组索引范围"); 30 } 31 } 32 } 33 }
我们定义了一个类MyListBox,其中有一个ArrayList字段,在构造器中为其默认初始化了,在下面的代码中我们看到了如何声明一个索引器,我们返回的类型是object,索引器的返回类型一样不可以void,c#使用this[...]表达索引器,并且C#不支持静态索引器,尽管CLR支持静态有参属性,C#允许一个类型定义多个索引器,但是索引器参数集不能相同,其他一些语言中支持定义多个相同签名的索引器,因为其他一些语言中索引器可以自定命名,但是C#不允许这样做,因为C#中不允许开发人员指定索引器名称,C#为一个类型中的所有索引器都默认提供了一个叫做Item的名称,所以在C#中使用索引器只能通过不同签名来区分选择的索引器。
CLR并不区分有参属性和无参属性,对CLR来说,每个属性都只是类型中定义的一对方法,和一些元数据。下面的示例是如何调用索引器。使用起来也很简单吧
1 //初始化MyListBox 2 MyListBox ba = new MyListBox { 3 //集合初始化器初始化值 4 data = { "张三",20,30,40}, 5 }; 6 //调用添加方法为其添加值 7 ba.data.Add("5"); 8 ba.data.Add(6); 9 for (int i = 0; i < ba.data.Count; i++) 10 { 11 //使用索引器打印出指定值,具体实现请查看类中get方法 12 Console.WriteLine(ba.data[i]); 13 }
无参属性,初始化器,有参属性,有了这些你可以在你的方法中更好的使用字段,并且让你的数据封装更加安全,但是CLR作者本人却持有另外一种观点,作者觉得属性不如封装的方法。有兴趣的朋友可以自己翻阅CLR看看作者的观点。