C#基础之类型和成员基础以及常量、字段、属性
首先吐糟一下今天杭州的天气,真是太热了!虽然没有妹子跟我约会,但宅在方寸大的窝里,也是烦躁不已!
接上一篇《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[] = 'X';
chars[] = 'Y';
chars[] = '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 > && value <= ) 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 = ;
p1.Name = "Heku";
p1.Age = ; //使用对象初始化器的语法
Person p2 = new Person() { Id = , Name = "Heku", Age = };
}
}
使用对象初始化器的语法时,实际上编译器为我们生成的代码和上面是一致的,但是下面的代码明显更加简洁。如果本来就是要调用类型的无参构造器,C#还允许我们省略大括号之前的小括号:
Person p2 = new Person { Id = , Name = "Heku", Age = };
如果一个属性的类型实现了IEnumerable或IEnumerable<T>接口,那么这个属性就被认为是一个集合,我们同样类似的语法来初始化一个集合。比如我们在上例中的Person类中加入一个新属性Skills
public List<string> Skills { get; set; }
然后可以用下面的语法来初始化
//使用简化的对象初始化器语法+简化集合初始化器语法
Person p3 = new Person { Id = , Name = "heku", Age = , 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 = , 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[];
} //有参属性
public string this[int index]
{
get
{
return array[index];
}
set
{
array[index] = value;
}
} void Main()
{
StringArray array = new StringArray();
array[] = "Hello";
array[] = "World"; string ss = array[] + array[];
}
}
上面的例子中,和定义无参属性不同的是,这里并没有属性名称,而是用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[ - index];
else return array[index];
}
set
{
if (isStartFromEnd) array[ - 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#
C#基础之类型和成员基础以及常量、字段、属性的更多相关文章
- CLR via C#深解笔记三 - 基元类型、引用类型和值类型 | 类型和成员基础 | 常量和字段
编程语言的基元类型 某些数据类型如此常用,以至于许多编译器允许代码以简化的语法来操纵它们. System.Int32 a = new System.Int32(); // a = 0 a = 1 ...
- [CLR via C#]6. 类型和成员基础
原文:[CLR via C#]6. 类型和成员基础 6.1 类型的各种成员 在一个类型中,可以定义0个或多个以下种类的成员: 1)常量 常量就是指出数据值恒定不变的符号.这些符号通常用于使代码更 ...
- [Clr via C#读书笔记]Cp6类型和成员基础
Cp6类型和成员基础 成员 常量:字段(静态字段和实例字段):实例构造器:类型构造器(用于静态字段的构造):方法(静态方法和实例方法):操作符重载(本质是一个方法):转换操作符:属性(本质还是方法): ...
- 014 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 08 “字符型”字面值
014 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 08 "字符型"字面值 字符型 字面值如何表示? 两个关键:单引号(必须是英文单引号). ...
- C#基础--类/接口/成员修饰符,多态、重载、重写,静态和非静态
C#基础--类/接口/成员修饰符,多态.重载.重写,静态和非静态 类/接口/成员修饰符 C#修饰符---接口: 接口默认访问符是internal接口的成员默认访问修饰符是public C#修饰符--类 ...
- 深入理解Magento – 第四章 – 模型和ORM基础
深入理解Magento 作者:Alan Storm 翻译:Hailong Zhang 第四章 – 模型和ORM基础 对于任何一个MVC架构,模型(Model)层的实现都是占据了很大一部分.对于Mage ...
- 020 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 14 变量与常量 知识总结
020 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 14 变量与常量 知识总结 本文知识点:变量与常量 知识总结 Java中的标识符 Java中的关键字 目前常 ...
- 019 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 13 数据类型转换的代码示例
019 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 13 数据类型转换的代码示例 本文知识点:Java中的数据类型转换案例 学习视频有误,导致没法写文,文章内容 ...
- 018 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 12 数据类型转换的基本概念
018 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 12 数据类型转换的基本概念 本文知识点:Java中的数据类型转换 类型转换 类型转换分类 2类,分别是: ...
随机推荐
- INT 21H 指令说明及使用方法
很多初学汇编语言的同学可能会对INT 21H这条指令感到困惑,不知道是什么意思,下面就以一段简单的程序为大家讲解: 例如:需要键盘输入,并且回显. AH的值需要查表取得,表在下面 指令: M ...
- vs2015解决fopen、fscanf 要求替换为fopen_s、fscanf_s的办法
在工程项目设置一下就行:项目属性 -- C/C++-- 预处理器 -- 预处理器定义,添加:_CRT_SECURE_NO_WARNINGS
- Java中是构造器创建对象吗?
首先,这里说明” Java中是构造器创建对象 “这句话是完全错误的. Java中构造器的作用主要是为了初始化变量的值...其实在执行构造器之前,Java对象所需要的内存空间,已经产生了... 一般可以 ...
- Python模块-requests(二)
会话对象 会话对象能够跨请求保持某些参数. 它也会在同一个 Session 实例发出的所有请求之间保持 cookie, 期间使用 urllib3 的 connection pooling 功能. 所以 ...
- Mac For Mongodb安装启动、停止及启动授权
1.到Mongodb官网下载相应的安装包 地址:https://www.mongodb.com/download-center?jmp=nav#community 2.Mac Mongodb安装过程 ...
- 共有11款Python 中文分词库开源软件
件过滤: 排序: 收录时间 | 浏览数 Python 中文分词库 Yaha "哑哈"中文分词,更快或更准确,由你来定义.通过简单定制,让分词模块更适用于你的需求. "Ya ...
- 【java并发编程艺术学习】(五)第二章 java并发机制的底层实现原理 学习记录(三) 原子操作的实现原理学习
章节介绍 主要包括 术语定义.处理器如何实现原子操作.Java如何实现原子操作: 原子(atomic)本意是 不能再进一步分割的最小粒子,“原子操作” 意为 不可被中断的一个或一系列操作. 术语定义 ...
- ViewController lifecyle(IOS学习)
斯坦福的ios教学视频笔记一张,如下
- JAVA基础知识总结4(面向对象特征之一:封装)
封 装:是指隐藏对象的属性和实现细节,仅对外提供公共访问方式. 好处:将变化隔离:便于使用:提高重用性:安全性. 封装原则:将不需要对外提供的内容都隐藏起来,把属性都隐藏,提供公共方法对其访问. th ...
- PullToRefresh------ListView的使用
第一步 :写出布局文件的设置 <com.handmark.pulltorefresh.library.PullToRefreshListView android:id="@+id/pu ...