静态成员的生命期
● 只有在实例创建之后才产生实例成员,在实例销毁后实例成员就不存在了。
● 但静态成员,即使类没有实例,也存在静态成员,并且可以访问。(在C++中不可以这样)
namespace Ch05Ex03
{
class D
{
static public int Mem2;
}
class Program
{
static void Main(string[] args)
{
D.Mem2 = 5; //通过类名直接为公有静态数据成员赋值, 但是如果该成员是私有,就不可以这样赋值
WriteLine(D.Mem2);
ReadKey();
}
}
}
输出结果为:
5
注意: 静态成员即使没有类的实例也存在。如果该静态字段有初始化语句,那么会在使用该类的任何静态成员之前初始化该字段, 但没必要在程序执行的开始就初始化。
静态成员函数
● 如同静态字段,静态成员函数独立于任何类实例。 即使没有类的实例,仍然可以调用静态方法。(C++中不可以这样)
● 注意:类成员函数(无论是static成员函数或非static成员函数)都可以直接访问static数据成员,static 成员函数仅能访问static 数据成员,不能访问非static的数据成员,也不能访问非static 的成员函数。
namespace Ch05Ex03
{
class D
{
static public int Mem2;
public int aa = 10; //在类定义中初始化一个非静态的公有成员
static public void PrintVal() //静态成员函数访问静态成员数据
{
WriteLine(Mem2);
//WriteLine(aa); 静态成员不可以访问非静态数据成员
}
public void getVal()
{
WriteLine(aa); // 输出aa的值
WriteLine(Mem2); //非静态函数可以访问静态的数据成员
}
}
class Program
{
static void Main(string[] args)
{
D.Mem2 = 5;
D.PrintVal();
D dd = new D(); // 创建该类的对象
dd.getVal();
ReadKey();
}
}
}
输出结果为:
5
10
5
在C++中可以声明常量为静态的常量成员数据, 而在C#中却不可以。
与C++不同,在C#中没有全局常量。 每个常量都必须声明在类型内。
常量与静态量
● 注意: 常量成员即使没有类的实例也可以使用。 与真正的静态量不同, 常量没有自己的存储位置, 而是在编译时被编译器替换。 跟C++中的 #define 类似。
看一个C++的程序:
class D
{
public:
const double PI = 3.14;
D(decltype(PI) aa):PI(aa)
{
}
};
int main()
{
D dd(22.3);
cout << dd.PI << endl;
system("pause");
return 0;
}
看一个C#程序:
namespace Ch05Ex03
{
class D
{
public const double PI = 3.14;
}
class Program
{
static void Main(string[] args)
{
WriteLine(D.PI); //访问静态数据成员
ReadKey();
}
}
}
在C#中常量成字段现得像一个静态量, 但不能将常量字段声明成 static:
static const double PI = 3.14; //错误
但是在C++中可以。
属性
● 属性指的是一组两个匹配的、称为访问器的方法:
set 访问器为属性赋值
get 访问器为属性获取值
属性和字段不同的在于: 属性属于一个成员函数:
它不为数据存储分配内存
它执行代码
不过它也有跟字段类似的地方,比如:
它是命名的类成员。
它有类型
它可以被赋值和读取。
它类似于这样:
int MyValue
{
set { }
get
{
return 0;
}
}
注意: 访问器不能被直接调用。
● set访问器总是:
拥有一个单独的、隐式的值参,名称为value, 与属性的类型相同。
set访问器返回类型为void。
● get访问器总是:
没有参数。
拥有一个与属性类型相同的返回类型。
访问器的重点如下:
get 访问器的所有执行路径都必须包含一条return 语句,返回一个属性类型的值。
访问器 set 和 get 可以任何顺序声明, 并且只能有这两个方法,不能有其他的。
要想不定义属性的某个访问器,可以忽略该访问器的声明。
两个访问器中至少有一个必须定义, 否则编译器会产生错误信息。
使用属性
● 写入和读取属性的访问器时,会被隐式调用:
要写入一个属性,在赋值语句的左边使用属性的名称。
要读取一个属性,把属性的名称用在表达式中。
注意: 不能显式地调用访问器,会出现编译错误。
namespace Ch05Ex03
{
class D
{
private double name = 3.14; // 为字段分配内存
public double MyValue // 属性不分配内存
{
set
{
name = value; //返回类型为void
}
get
{
return name; // 返回类型是属性的类型,属性的类型也要跟字段的数据类型一致
}
}
}
class Program
{
static void Main(string[] args)
{
D myD = new D();
myD.MyValue = 5.666; //给访问器中的字段赋值
WriteLine(myD.MyValue); //输出赋值后的字段的值
ReadKey();
}
}
}
● 我们经常将类中的字段声明private以封装该字段, 然后声明一个public的属性来控制从类的外部对该字段的访问。
和属性关联的字段称为后备字段或后备存储。
● 属性和后备字段需要注意的问题有:
一种约定是两个名称使用相同的内容, 但字段使用Camel 大小写, 属性使用 Pascal 大小写。
另一种是字段使用Camel 大小写,并以下划线开始,属性使用 Pascal 大小写。
private double nameField = 3.14; //第一种约定
public double NameFild
{
set
{
nameField = value;
}
get
{
return nameField;
}
}
private double _nameField = 3.14; // 第二种约定
public double NameFild
{
set
{
_nameField = value;
}
get
{
return _nameField;
}
}
● 属性访问器不仅仅只可以对关联的字段传入传出数据, 还可以执行任何计算,或者不执行任何计算。但是get访问器必须返回一个属性类型的值。
namespace Ch05Ex03
{
class D
{
private double _nameField = 3.14;
public double NameFild
{
set
{
_nameField = value > 100 ? 100 : value; //执行计算
}
get
{
return _nameField;
}
}
}
class Program
{
static void Main(string[] args)
{
D myD = new D();
myD.NameFild = 200; //给访问器中的字段赋值
WriteLine(myD.NameFild); //输出赋值后的字段的值
ReadKey();
}
}
}
自动实现属性
● 自动实现属性: 允许只声明属性而不声明后备字段。 编译器会为你创建隐藏的后备字段, 并且自动挂接到get 和 set 访问器上。
自动实现属性的要点如下:
不声明后备字段—— 编译器根据属性的类型分配存储。
不能提供访问器的方法体—— 它们必须被简单地声明为分号。 get 相当于简单的内存读, set 相当于简单的内存写。
除非通过访问器,否则不能访问后备字段。 因为不能用其他的方法访问它, 所以实现只读和只写属性没有意义, 因此必须同时提供读写访问器。
namespace Ch05Ex03
{
class D
{
public double NameFild //分配内存
{
set;get;
}
}
class Program
{
static void Main(string[] args)
{
D myD = new D();
myD.NameFild = 200; //给访问器中的字段赋值
WriteLine(myD.NameFild); //输出赋值后的字段的值
ReadKey();
}
}
}
静态属性
● 属性也可以声明为 static, 静态属性的访问器和所有静态成员一样,具有以下特点:
不能访问类的实例成员——可以访问类的静态实例成员, 但是它们可以在非静态方法中被访问。
不管类是否有实例,它们都是存在。
当从类的外部访问时,必需使用类名引用,而不是实例名。
静态构造函数
● 我们在使用类中的静态成员时,先要初始化这些成员。 在声明静态成员时、可以同时提供一个初始值。 但有时候我们需要执行更复杂的初始化操作, 这应该怎么办呢?
● 我们可以把构造函数声明成static的, 一般来说, 静态的构造函数初始化类的静态字段。
● 我们在使用类中的静态成员时,先要初始化这些成员。 在声明静态成员时、可以同时提供一个初始值。 但有时候我们需要执行更复杂的初始化操作, 这应该怎么办呢?
● 我们可以把构造函数声明成static的, 一般来说, 静态的构造函数初始化类的静态字段。
关于静态构造函数应注意的有:
● 那么一个类只能有一个静态构造函数,而且不能带参数、没有返回值。 也不能有访问修饰符(比如private)。 而且声明时使用static 关键字作为前辍。
● 那么一个类既可以有静态构造函数也可以有实例构造函数。
● 跟静态成员函数类似, 静态构造函数不能访问所在类的实例成员, 所以也不能使用this访问器。
● 不能在程序中显式调用静态构造函数,系统会自动调用它们,在:
只要一个类有静态构造函数,当我们创建第一个类实例时,都会调用静态构造函数、
只要一个类有静态构造函数,类的任何静态成员被引用之前。
在这两种情况下,会首先调用静态构造函数, 之后实例化类或访问静态成员。应注意的是: 其实我们创建了多个个类实例, 其静态构造函数都只调用一次。
下面看一个程序代码:
namespace Ch05Ex03
{
class RandomNum
{
static int RandomKey;
int aa;
int bb;
static RandomNum()
{
RandomKey = 15;
WriteLine("静态构造函数被调用!");
}
public RandomNum(int _aa,int _bb)
{
aa = _aa;
bb = _bb;
WriteLine("\n实例构造函数被调用!");
}
public void Show()
{
WriteLine($"输出RandomKey的静态成员的值:{RandomKey}");
WriteLine($"输出aa的静态成员的值:{aa}");
WriteLine($"输出bb的静态成员的值:{bb}");
WriteLine();
}
}
class Program
{
static void Main(string[] args)
{
RandomNum a = new RandomNum(2,3);
RandomNum b = new RandomNum(5,6);
a.Show();
b.Show();
ReadKey();
}
}
}
输出是结果为:
静态构造函数被调用!
实例构造函数被调用!
实例构造函数被调用!
输出RandomKey的静态成员的值:15
输出aa的值:2
输出bb的的值:3
输出RandomKey的静态成员的值:15
输出aa的值:5
输出bb的的值:6
在该程序中, 我们首先创建了该类的两个实例,分别为它填了两个实参, 我可以看到输出结果,不管创建多少个实例, 静态构造函数只调用只一次, 其次就是调用创建该实例的对应的实例构造函数, 上面的程序调用的是带两个形参的实例构造函数。 调用顺序为你创建实例的顺序。
那么每一个实例都有一个静态成员的值,它被所有类实例所共享。