本章包含:
类成员包括:字段、常量、方法、属性、构造函数、析构函数、运算符、索引、事件
格式:
[特性] [修饰符] 核心声明
修饰符
特性
特性以后再讲,现在先讲解修饰符,有: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,与其他静态成员一样:
调用时间:
与实例构造函数不同:
class Class1{ static Class1(){ // ... } }
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 修饰,一旦值被设定就不能改变。(和 const 非常像吧?)
const字段只能在字段的声明语句中初始化,而 readony字段可以在下列任意位置设置它的值。
const字段的值必须可在编译时决定,而readonly字段的值可以在运行时决定,这种自由性允许你在不同的环境或不同的构造函数中设置不同的值!
ccnst的行为总是静态的,而对于readonly字段以下两点是正值的:
this 一般在类中使用,是对当前实例的引用;只能被用在类成员的代码块中:
因为静态成员不是实例的一部分,所以不能在任何静态函数成员的代码中使用 this 关键字;
class A{ int Var1 = 10; public int FuncSum(int Var1){ return Var1 > this.Var1 ? Var1 : this.Var1; } }
理解它很重要的,但它实际上很少在代码中使用。
没有索引的简单类:
有时候,能用索引访问它们将会很方便,好像该实例是字段的数组一样,这正是索引器能做的事儿;
为 Employee写一个索引器,看起来像这样:
是一组 get 和 set 访问器,与属性类似。下图展示了一个类的索引器的表现形式:
索引器和属性在很多方面是相似的:
可以认为索引器是为类的多个数据成员提供get和set访问的属性。通过提供索引器,可以在许多可能的数据成员中进行选择。索引本身可以是任何类型,而不仅仅是数据类型。
关于索引器,还有一些注意事项:
声明索引器很类似于声明属性:
索引器被赋值时,Set调用;
Set 接收两个数据:
emp[0] = "Doe";
string s =emp[0];
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 之前时,它表示分部类型;
完