本章包含:
- 类成员
- 成员修饰符的顺序
- 实例类成员静态字段
- 从类的外部访问静态成员
- 静态函数成员
- 其他静态类成员类型
- 成员常量
- 常量和静态量
- 属性
- 实例构造函数
- 静态构造函数对象初始化语句
- 析构函数
- readonly 修饰符
- this关键字
- 访问器的访问修饰符
- 分部类和分部类型
- 分部方法
类成员
类成员包括:字段、常量、方法、属性、构造函数、析构函数、运算符、索引、事件
修饰符顺序
格式:
[特性] [修饰符] 核心声明
修饰符
- 如果有修饰符,必须放在核心声明前
- 多个修饰符可以任意顺序排列;
特性
- 如果有特性,必须放在修饰符和核心声明前;
- 多个特性可以任意顺序排列;
特性以后再讲,现在先讲解修饰符,有:public private protected static async 等;
// 多个修饰符因为可以任意排序,所以下面两种写法等价:
public static int MaxVal;
static public int MaxVal;
每个实例成员都是类的一个副本,改变其中一个实例成员不会影响另一个实例成员;
class D{
public int Mem1;
}
D d1 = new D();
D d2 = new D();
d1.Mem1 = 10; d2.Mem2 = 20;
静态字段
静态字段被类的所有实例共享,所有实例都访问同一内存位置。因此,如果该内存位置的值被一个实例改变了,这种改变对所有的实例都可见。
class D{
int Mem1; // 实例字段
static int Mem2; // 静态字段
}
D d1 = new D();
D d2 = new D();
通过类访问:
public class D
{
static public int AA = 66;
}
class Program
{
static void Main()
{
Console.WriteLine($"{D.AA}");
}
}
甚至不需要使用类来点出来,而是通过using:
using System;
using System.IO;
using static ConsoleApp2.D;
using static System.Console;
using static System.Math;
namespace ConsoleApp2
{
public class D
{
static public int AA = 66;
}
class Program
{
static void Main()
{
WriteLine($"{AA}, {Sqrt(16)}");
}
}
}
静态成员的生命周期
静态成员的生命期与实例成员的不同。
即使类没有实例,也存在静态成员,并且可以访问。
上图中,没有类实例的静态成员仍然可以被赋值并读取,因为静态字段与类有关,而与实例无关
即使不存在类实例,静态成员也存在。
如果静态字段有初始化语句,那么会在使用该类的任何静态成员之前初始化该字段,但不一定在程序执行的开始就初始化。
静态函数 成员
与静态字段一样,独立于任何类的实例,即使没有类的实例,仍然可以调用静态方法;
但是:静态方法的方法体中,不能访问实例成员!但能访问其他静态成员;
class X{
static public int A;
static public void PrintValA(){
// 静态函数中只能访问静态成员
Console.WriteLine(A);
}
}
X.A = 10;
X.PrintValA();
可以声明的静态类成员
可以的数据成员(存储数据的):字段、类型
可以的函数成员(执行代码的):方法、属性、构造函数、运算符、事件
不能的:常量、索引器
成员常量
常量是只读的;
说白了,常量就是一个其值永远不会改变的静态字段。
常量的值会在编译时自动推算,编译器会在遇到常量时,将其逐个替换为该常量的值。
与局部常量一样,只是局部常量初始化在方法体中,而成员常量初始化在类中:
class MyClass{
const int IntVal = 100;
}
const double PI = 3.1416; // 错误,不能在类型声明之外声明
与C/C++ 不同,C#中没有全局常量!
静态只读变量
在某一个应用程序中初始化之后,不能对其进行修改。但是在不同的应用程序中可以有不同的值。
常量 和 静态只读变量 有什么区别?
常量在所有的程序中都是同一个值,而静态只读变量在不同的程序中可以有不同的值。
常量 与 静态量
成员常量 表现得更像是 静态值(即:静态的常/变量),成员常量对类的每个实例都是“可见的”;而且即使没有类的实例化也可以使用。
与真正的静态量不同,常量没有自己的存储位置,而是在编译时被编译器替换。类似C和C++的 #fefine 值。
虽然常量成员表现得像静态值,但不能将常量声明为static,如:
static const double PI = 3.14; // 错误,不能将常量声明为 static
属性
属性 和 字段 很像,用法上他们都几乎没区别;
但是,与字段不同,属性:
- 是一个函数成员
- 不一定为数据分配内存
- 它执行代码
属性本身没有任何存储,取而代之,访问器决定如何处理发送进来的数据,以及应该将什么数据发送出去。在这种情况下,属性使用一个名为 TheRealValue 的字作为存储。
class C1 {
private int theRealValue; // 字段:分配内存
public int MyValue{ // 属性:未分配内存
set { theRealValue = value; }
get { return theRealValue; }
}
}
属性的几种命名规则:
private int firstField; // Camel驼峰大小写
public int FirstField{ // Pascal 大小写
get { return firstField;}
set { firstField = value; }
}
private int _firstField; // 下划线 和 Camel驼峰大小写
public int FirstField{
get { return _firstField;}
set { _firstField = value; }
}
C#7.0 中为属性的 getter 和 setter 引入了另一种语法,这语法使用表达式函数体(Lambda):
int MyValue{
set => value >100 ? 100 : value;
get => theRealValue;
}
可以省略 set 或 get 中其中一个,如果只有get,则属性为只读,反之为只写;但是不能两个都省略;
属性和公有字段
属性比公有字段更好,理由:
- 属性是函数成员而不是数据成员,允许你处理输入和输出,而公有字段不行;
- 属性可以只读或只写,而字段不行
- 编译后的变量和编译后的属性语义不同
自动属性
public int MyValue { get; set;}
public int MyValue2 {get;}
静态属性
属性也能声明为 static,与其他静态成员一样:
- 不能访问类的实例成员,但能被实例成员访问;
- 不管类是否有实例,它们都是存在的;
- 在类内部,可以仅使用名称来引用静态属性;
- 在类外部,可通过类名或者使用 using static 结构来引用静态属性;
构造函数
静态构造函数
调用时间:
- 在引用任何静态成员之前
- 在创建类的任何实例之前
与实例构造函数不同:
- 静态构造函数使用 static 关键字
- 类只能有一个静态构造函数,且不能带参数
- 静态构造函数不能有访问修饰符
class Class1{
static Class1(){
// ...
}
}
- 类既可以有静态构造函数,也可以有实例构造函数
- 静态构造函数中,不能访问所在类的实例成员,因此也不能使用 this 访问器;
- 不能从程序中显式地调用静态构造函数,系统会自动调用它们:
- 在类的任何实例被创建之前自动调用;
- 在类的任何静态成员被引用之前自动调用;
class A{
private static Random RandomKey;
static A(){
RandomKey = new Random();
Console.WriteLine("静态构造函数");
}
public A(){
Console.WriteLine("实例构造函数");
}
public int GetRandomNumber(){
return RandomKey.Next();
}
}
A a = new A();
A b = new A();
a.GetRandomNumber();
b.GetRandomNumber();
// 输出:
静态构造函数
实例构造函数
324130517
实例构造函数
1771231000
对象初始化语句
public class Point{
public int X = 1;
public int Y = 2;
}
Point pt1 = new Point();
Point pt12 = new Point { X = 5, Y = 10 };
析构函数
析构函数(destuctor)执行在类的实例被销毁之前需要的清理或释放非托管资源的行为。
非托管资源是指通过 Win32 API 获得的文件句柄,或非托管内存块。
使用 .NET 资源是无法得到它们的,因此如果坚持使用 .NET类,就不需要为类编写析构函数,
因此,后面再介绍析构函数;
readonly 修饰符
字段可用 readonly 修饰,一旦值被设定就不能改变。(和 const 非常像吧?)
const字段只能在字段的声明语句中初始化,而 readony字段可以在下列任意位置设置它的值。
- 字段声明语何,类似于 const
- 类的任何构造的数。如果是static字段,初始化必须在静态构造函数的完成
const字段的值必须可在编译时决定,而readonly字段的值可以在运行时决定,这种*性允许你在不同的环境或不同的构造函数中设置不同的值!
ccnst的行为总是静态的,而对于readonly字段以下两点是正值的:
- 它可以是实例字段,也可以是静态字段
- 它在内存中有存储值
this 关键字
this 一般在类中使用,是对当前实例的引用;只能被用在类成员的代码块中:
- 实例构造函数
- 实例方法
- 属性和索引器的实例访问器
因为静态成员不是实例的一部分,所以不能在任何静态函数成员的代码中使用 this 关键字;
class A{
int Var1 = 10;
public int FuncSum(int Var1){
return Var1 > this.Var1 ? Var1 : this.Var1;
}
}
理解它很重要的,但它实际上很少在代码中使用。
索引器
没有索引的简单类:
有时候,能用索引访问它们将会很方便,好像该实例是字段的数组一样,这正是索引器能做的事儿;
为 Employee写一个索引器,看起来像这样:
什么是索引器?
是一组 get 和 set 访问器,与属性类似。下图展示了一个类的索引器的表现形式:
索引器和属性在很多方面是相似的:
- 和属性一样,索引器不用分配内存来存储。
- 索引器和属性都主要被用来访向 其他 数据成员,它们与这些成员关联,并为它们提供获取和设置访问。
- 属性通常表示 单个 数据成员
- 索引器通常表示 多个 数据成员
可以认为索引器是为类的多个数据成员提供get和set访问的属性。通过提供索引器,可以在许多可能的数据成员中进行选择。索引本身可以是任何类型,而不仅仅是数据类型。
关于索引器,还有一些注意事项:
- 和属性一样,索引器可以只有一个访问器,也可以两个都有!
- 索引器总是实例成员,因此不能被声明为static
- 和属性一样,实现get和set访问器的代码不一定要关联到某个字段或属性。这段代码可以做任何事情也可以什么都不做,只要get访问器返回某个指定类型的值即可。
声明索引器
- 索引器没有名称,在名称的位置是关键字 this
- 参数列表在方括号中间
- 参数列表中必须至少声明一个参数。
声明索引器很类似于声明属性:
索引器的 Set 访问器
索引器被赋值时,Set调用;
Set 接收两个数据:
- value的隐式参数,为要保存的数据
- 一个或多个索引参数,表示数据应该要存在哪里
emp[0] = "Doe";
索引器的 Get 访问器
string s =emp[0];
为 Employee 示例声明索引器
public string this[int index]
{
set
{
switch (index)
{
case 0: LastNmae = value; break;
case 1: FirstName = value; break;
case 2: CityBirth = value; break;
default: throw new ArgumentOutOfRangeException("index");
}
}
get
{
switch (index)
{
case 0: return LastNmae;
case 1: return FirstName;
case 2: return CityBirth;
default: throw new ArgumentOutOfRangeException("index");
}
}
}
另一个索引器示例:
class B
{
int Temp0;
int Temp1;
public int this [int index]
{
get
{
return (0 == index) ? Temp0 : Temp1;
}
set
{
if( 0 == index)
{
Temp0 = value;
}
else
{
Temp1 = value;
}
}
}
}
B b = new B();
b[0] = 15;
b[1] = 20;
索引器重载
访问器的访问修饰符
目前,属性 和 索引器 成员 都带有 get 和 set 访问器;
默认情况下,访问器的访问级别和 成员自身相同;
你可以为不同访问器设置不同的访问级别;
// get 为public,因为成员自身就是public修饰,而 set设置了private
public string Name { get; private set;}
只有成员同时有 get 和 set 时,才能有访问修饰符:
public string Name {private set;} // 错误
虽然两个访问器要同时出现,但是他们只有一个有访问修饰符:
public string Name {private get; protected set;} // 错误
访问器的访问修饰符的限制必须比成员的访问级别更加严格,即:访问器的访问级别必须比成员的访问级别低;
public string Name { get; public set; } // 错误
分部类 和 分部类型
类的声明可以分割成几个分部类的声明。
- 每个分部类的声明都含有一些类成员的声明
- 每个分部类可以在同一个文件中,也可以在不同文件中。
分部类必须被标注为 partial class ,而不是单独的 class 关键字;
分部类和普通类很一样,只是分部类用 partial 修饰了:
partial class A1{
public int t1 {get; set;}
public int t2 {get; set;}
}
partial class A1{
public int t3 {get; set;}
public int t4 {get; set;}
}
注意:partial 不是关键字,所以在其他上下文中,可以在程序中把它用作标识符。但是用在 class、struct 或 interface 之前时,它表示分部类型;
完