首先吐糟一下今天杭州的天气,真是太热了!虽然没有妹子跟我约会,但宅在方寸大的窝里,也是烦躁不已!
接上一篇《C#基础之基本类型》
类型和成员基础
在C#中,一个类型内部可以定义多种成员:常量、字段、实例构造器、类型构造器(静态构造器)、方法、操作符重载、转换操作符、属性、事件、类型。
类型的可见性有public和internal(默认)两种,前者定义的类型对所有程序集中的所有类型都可见,后者定义的类型只对同一程序集内部的所有类型可见:
public class PublicClass { } //所有处可见 internal class ExplicitlyInternalClass { } //程序集内可见 class ImplicitlyInternalClass { } //程序集内可见(C#编译器默认设置为internal)
成员的可访问性(按限制从大到小排列):
- Private只能由定义成员的类型或嵌套类型中方法访问
- Protected只能由定义成员的类型或嵌套类型或派生类型中方法访问
- Internal 只能由同程序集类型中方法访问
- Protected Internal 只能由定义成员的类型或嵌套类型或派生类型或同程序集类型中方法访问(注意这里是或的关系)
- Public 可由任何程序集中任何类型中方法访问
在C#中,如果没有显式声明成员的可访问性,编译器通常默认选择Private(限制最大的那个),CLR要求接口类型的所有成员都是Public访问性,C#编译器知道这一点,因此禁止显式指定接口成员的可访问性。同时C#还要求在继承过程中派生类重写成员时,不能更改成员的可访问性(CLR并没有作这个要求,CLR允许重写成员时放宽限制)。
静态类
永远不需要实例化的类,静态类中只能有静态成员。在C#中用static这个关键词定义一个静态类,但只能应用于class,不能应用于struct,因为CLR总是允许值类型实例化。
C#编译器对静态类作了如下限制:
- 静态类必须直接从System.Object派生
- 静态类不能实现任何接口(因为只有使用类的一个实例才能调用类的接口方法)
- 静态类只能定义静态成员(字段、方法、属性、事件)
- 静态类不能作为字段、方法参数或局部变量使用
- 静态类在编译后,会生成一个被标记为abstract和sealed的类,同时编译器不会生成实例构造器(.ctor方法)
分部类、结构和接口
C#编译器提供一个partial关键字,以允许将一个类、结构或接口定义在多个文件里。
在编译时,编译器自动将类、结构或接口的各部分合并起来。这仅是C#编译器提供的一个功能,CLR对此一无所知。
常量
常量就是代表一恒定数据值的符号,比如我们将圆周率3.12415926定义成名为PI的常量,使代码更容易阅读。而且常量是在编译时就代入运算的(常量就是一个符号,编译时编译器就会将该符号替换成实际值),不会造成任何性能上的损失。但这一点也可能会造成一个版本问题,即假如未来修改了常量所代表的值,那么用到此常量的地方都要重新编译(我个人认为这也是常量名称的由来,我们应该将恒定不变的值定义为常量,以免后期改动时产生版本问题)。下面的示例也验证了这一点,Test1和Test2方法内部的常量运算在编译后,就已经运算完成。
从上面示例,我们还能看出一点:常量key和value编译后是静态成员,这是因为常量通常与类型关联而不是与实例关联,从逻辑上说,常量始终是静态成员。但对于在方法内部定义的常量,由于作用域的限制,不可能有方法之外的地方引用到这个常量,所以在编译后,常量被优化了。
字段
字段是一种数据成员,在OOP的设计中,字段通常是用来封装一个类型的内部状态,而方法表示的是对这些状态的一些操作。
在C#中字段可用的修饰符有
- Static 声明的字段与类型关联,而不是与对象关联(默认情况下字段与对象关联)
- Readonly 声明的字段只能在构造器里写入值(可以通过反射修改)
- Volatile 声明的字段为易失字段(用于多线程环境)
这里要注意的是将一个字段标记为readonly时,不变的是引用,而不是引用的值。示例:
class ReadonlyField { //chars 保存的是一个数组的引用 public readonly char[] chars = new char[] { 'A', 'B', 'C' }; void Main() { //以下改变数组内存,可以改成功 chars[0] = 'X'; chars[1] = 'Y'; chars[2] = 'Z'; //以下更改chars引用,无法通过编译 chars = new char[] { 'X', 'Y', 'Z' }; } }
属性
CLR支持两种属性:无参属性和有参属性(C#中称为索引器)。
面向对象设计和编程的重要原则之一就是数据封装,这意味着字段(封装对象的内部状态)永远不应该公开。因此,CLR提供属性机制来访问字段内容(VS中输入propfull加两次Tab会为我们自动生成字段和属性的代码片断)。
下面的示例中,Person对象内部有一个表示年龄的字段,如果直接公开这个字段,则不能保存外部不会将age设置为0或1000,这显然是没有意义的(也破坏了数据封装性),所以通过属性,可以在操作字段时,加一些额外逻辑,以保证数据的有效性。
class Person { //Person对象的内部状态 private int age; //用属性来安全地访问字段 public int Age { get { return age; } set { if (value > 0 && value <= 150) age = value; else { } //抛出异常 } } }
编译上述代码后,实际上编译器会将属性内的get和set访问器各生成一个方法,方法名称是get_和set_加上属性名,所以说属性的本质是方法。
如果只是为了封装一个字段而创建一个属性,C#还为我们提供了一种更简单的语法,称为自动实现的属性(AIP)。下面是一个示例(在VS中输入prop加两次TAB会为我们生成AIP片断):
这里要注意一点,由于AIP的支持字段是编译器自动生成的,而且编译器每次编译都可能更改这个名称。所以在任何要序列化和反序列化的类型中,都不要使用AIP功能。
对象和集合初始化器
在实现编程中,我们经常构造一个对象,然后设置对象的一些公共属性或字段。为此C#为我们提供了一种简化的语法来完成这些操作。如下示例:
class Person { //AIP public string Name { get; set; } public int Id { get; set; } public int Age { get; set; } void Main() { //没有使用对象初始化器的语法 Person p1 = new Person(); p1.Id = 1; p1.Name = "Heku"; p1.Age = 24; //使用对象初始化器的语法 Person p2 = new Person() { Id = 1, Name = "Heku", Age = 24 }; } }
使用对象初始化器的语法时,实际上编译器为我们生成的代码和上面是一致的,但是下面的代码明显更加简洁。如果本来就是要调用类型的无参构造器,C#还允许我们省略大括号之前的小括号:
Person p2 = new Person { Id = 1, Name = "Heku", Age = 24 };
如果一个属性的类型实现了IEnumerable或IEnumerable<T>接口,那么这个属性就被认为是一个集合,我们同样类似的语法来初始化一个集合。比如我们在上例中的Person类中加入一个新属性Skills
public List<string> Skills { get; set; }
然后可以用下面的语法来初始化
//使用简化的对象初始化器语法+简化集合初始化器语法 Person p3 = new Person { Id = 1, Name = "heku", Age = 24, Skills = new List<string> { "C#", "jQuery" } };
这里我们用new List<string> { "C#", "jQuery" }一句来初始化了一个集合(实现上new List<string>完全可以省略,编译器会根据属性的类型来自动推断集合类型),并添加了两项纪录。编译器会我们生成的代码看起来是这样的:
p3.Skills = new List<string>(); p3.Skills.Add("C#"); p3.Skills.Add("jQuery");
匿名类型
有时候,我们需要封装一组数据,只有属性或字段,没有方法,并且只用于当前程序,不在项目间重用。
如果按传统做法,我们需要手工定义一个类来封装这组数据。匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。如下示例:
从上面示例可以看到,我们只写了一句
var obj = new { Id = 1, Name = "Heku", Addr = "China" };
编译器会为我们做大量的工作,首先为我们定义一个类型(类型名称是编译器编译时才生成的,编程时还不知道,所以叫匿名类型),类型内部包含了三个属性Id、Name、Addr以及对应的支持字段,同时也生成了三个get访问器方法,没有生成set访问器方法(说明匿名类型的属性是只读的)。
另外在Main方法中,由于我们编程过程中,并不知道类型名称,所以必须用var关键字来让编译器自行推荐类型(虽然也可以用object或dynamic,但这完全没有意义)。通过查看Main编译后的IL代码我们还可以发现,匿名类型的初始化是通过调用匿名类型的有参构造器来完成的,这点与之前也不相同(因为匿名类型属性是只读的,不能通过调用无参初始化器初始化后再设置属性值,编译器也根本没有生成匿名类型的无参构造器)。
有参属性
前面讲到的属性都没有参数,实现上还有一种可以带参数的属性,称之为有参属性(C#中叫索引器)。
class StringArray { private string[] array; public StringArray() { array = new string[10]; } //有参属性 public string this[int index] { get { return array[index]; } set { array[index] = value; } } void Main() { StringArray array = new StringArray(); array[0] = "Hello"; array[1] = "World"; string ss = array[0] + array[1]; } }
上面的例子中,和定义无参属性不同的是,这里并没有属性名称,而是用this[参数]的语法来定义一个有参属性(索引器),这是C#的要求。和无参属性不同,有参属性还支持重载:
//有参属性 public string this[int index] { get { return array[index]; } set { array[index] = value; } } //有参属性重载 public string this[int index, bool isStartFromEnd] { get { if (isStartFromEnd) return array[10 - index]; else return array[index]; } set { if (isStartFromEnd) array[10 - index] = value; else array[index] = value; } }
属性本质是方法,有参属性也一样(对CLR来说甚至并不分有参还是无参,对它来说都是方法的调用),那么有参属性的编译后生成的IL是什么样子呢?事实上C#对所有的有参属性生成的IL方法都默认命名为get_Item和set_Item。当然这是可以通过在索引器上应用System.runtime.CompliserServices.IndexerNameAttribute定制Attribute来改变这一默认行为。
属性访问器的可访问性
属性的get和set访问器是可以定义不同的访问性的,如果get和set访问器的可访问性不一致,C#要求必须为属性本身指定限制最小的那一个。
protected string Name { get { return name; } private set { name = value; } }
注意:如果同时设置get和set的访问性,会提示“不能为属性的两个访问器同时指定可访问性修改符”,因为对属性或索引器使用访问修饰符受以下条件的制约:
- 不能对接口或显式接口成员实现使用访问器修饰符
- 仅当属性或索引器同时具有 set 和 get 访问器时,才能使用访问器修饰符。这种情况下,只允许对其中一个访问器使用修饰符
- 如果属性或索引器具有 override 修饰符,则访问器修饰符必须与重写的访问器的访问性(如果有的话)匹配
- 访问器的可访问性级别必须比属性或索引器本身的可访问性级别具有更严格的限制
结尾的话
一边搬书一边敲代码一边写博文,不知不觉又坐了将近一天(汗~我要出去活动一下了)。
能力水平有限,博文主要还是出于自我总结目的,如果有文中有误,还请大家指出,感谢!
参考
1、CLR Via C#