目录
成员修饰符的顺序
类成员的声明语句由下列部分组成:核心声明、一组可选的修饰符和一组可选的特性。用于描述这个结构的语法如下。方括号表示方括号内的成分是可选的。
[特性] [修饰符] 核心声明
可选成分如下:
- 修饰符
- 如果有修饰符,必须放在核心声明之前。
- 如果有多个修饰符,可以任意顺序排列。
- 特性
- 如果有特性,必须放在修饰符和核心声明之前。
- 如果有多个特性,可以任意顺序排列。
静态字段
- 静态字段被类的所有实例共享,所有实例都访问同一内存位置。因此,如果该内存位置的值被一个实例改变了,这种改变对所有的实例都可见。
- 可以使用
static
修饰符将字段声明为静态。
从类外部访问静态成员
就像实例成员,静态成员也可以使用点运算符从类的外部访问。但因为没有实例,所以最常用的访问静态成员的方法使用类名,如下面的代码所示:D.Mem2 = 5
访问静态成员的另一种方法根本不需要使用前缀,只需在该成员所属的类中包含一个using static声明,如下所示:
using static System.Console;
using static System.Math;
...
WriteLine($"The square root of 16 is {Sqrt(16)}");
这等价于:
using System;
...
Console.WriteLine($"The square root of 16 is {Math.Sqrt(16)}");
静态字段示例
class D
{
int Mem1;
static int Mem2;
public void SetVars(int v1, int v2)
{
Mem1 = v1;
Mem2 = v2;
}
public void Display(string str)
{
Console.WriteLine($"{str}: Mem1={Mem1},Mem2={Mem2}");
}
}
class Program
{
static void Main()
{
D d1=new D(), d2=new D();
d1.SetVars(2, 4);
d1.Display("d1");
d2.SetVars(15, 17);
d2.Display("d2");
d1.Display("d1");
}
}
结果
d1: Mem1=2,Mem2=4
d2: Mem1=15,Mem2=17
d1: Mem1=2,Mem2=17
静态成员的生存期
静态成员的生命期与实例成员的不同。
- 之前我们也看到了,只有在实例创建之后才产生实例成员,在实例销毁之后实例成员也就不存在了。
- 但是即使类没有实例,也存在静态成员,并且可以访问。
即使不存在类实例,静态成员也存在。如果静态字段有初始化语句,那么会在使用该类的任何静态成员之前初始化该字段,但不一定在程序执行的开始就初始化。
静态函数成员
- 如同静态字段,静态函数成员独立于任何类实例。即使没有类的实例,仍然可以调用静态方法。
- 静态函数成员不能访问实例成员,但能访问其他静态成员。
其他静态类成员类型
可以为static
的类成员类型做了勾选标记
数据成员(存储数据) | 函数成员(执行代码) |
---|---|
√字段 | √方法 |
√类型 | √属性 |
常量 | √构造函数 |
运算符 | |
√索引器 | |
√事件 |
成员常量
成员常量类似前一章所述的局部变量,只是它们被声明在类声明中而不是方法内,如下面的示例:
class MyClass
{
const int IntVal =100; //定义int类型常量,值为100
}
const double PI = 3.1415 //错误:不能在类型声明之外声明
与局部常量类似,用于初始化成员常量的值在编译时必须是可计算的,而且通常是一个预定义简单类型或由它们组成的表达式。
与C和C++不同,在C#中没有全局变量。每个常量都必须声明在类型内。
常量与静态量
与真正的静态量不同,常量没有自己的存储位置,而是在编译时被编译器替换。这种方式类似于C和C++中的#define值。
虽然常量表现得很像静态值,但不能将常量声明为static。
属性
属性是代表类实例或类中数据项的成员。使用属性就像写入或读取一个字段,语法相同。
例如,下面的代码展示了名为MyClass的类的使用,它有一个共有字段和共有属性。从用法上无法区别它们。
MyClass mc = new MyClass();
mc.MyField = 5; //给字段赋值
mc.MyProperty = 10; //给属性赋值
Console.WriteLine($"{mc.MyField}{mc.MyProperty}"); //读取字段和属性
与字段相似,属性具有如下特征。
- 它是命名的类成员。
- 它有类型。
- 它可以被赋值和读取。
然而和字段不同,属性是一个函数成员。
- 它不一定为数据存储分配内存。
- 它执行代码。
属性是一组(两个)匹配的、命名的、称为访问器的方法。
- set访问器为属性赋值。
- get访问器从属性获取值。
属性声明和访问器
set和get访问器有预定义的语法和语义。可以把set访问器想象成一个方法,带有单一的参数的参数,它“设置”属性的值。get访问器没有参数并从属性返回一个值。
- set访问器总是:
- 拥有一个单独的、隐式的值参,名称为value,与属性的类型相同;
- 拥有一个返回类型void。
- get访问器总是:
- 没有参数;
- 拥有一个与属性结构类型相同的返回类型。
set访问器中的隐式参数value是一个普通的值参。和其他值参亦可以,可以用它发送数据到方法体或访问器块。在块的内部,可以像普通变量那样使用value,包括对它赋值。
访问器的其他要点如下。
- get访问器的所有执行路径必须包含一条return语句,它返回一个属性类型的值。
- 访问器set和get可以以任何顺序声明,并且,除了这两个访问器外,属性上不允许有其他方法。
属性示例
下面的代码展示了一个名为C1的类的声明示例,它含有一个名为MyValue的属性。
- 请注意,属性本身没有任何存储。取而代之,访问器决定如何处理发送进来的数据,以及应将什么数据发送出去。在这种情况下,属性使用一个名为TheRealValue的字段作为存储。
- set访问器接受它的输入参数value,并把它的值赋给字段TheRealValue。
- get访问器只返回字段TheRealValue的值。
(属性访问器常常使用字段作为存储)
class C1
{
private int theRealValue;
public int MyValue
{
set{ theRealValue = value;}
get{ return theRealValue;}
}
}
使用属性
就像之前看到的,写入和读取属性的方法与访问字段一样。访问器被隐式调用。
- 要写入一个属性,在赋值语句的左边使用属性的名称。
- 要读取一个属性,把属性的名称用在表达式中。
属性会根据是写入还是读取来隐式地调用适当的访问器。不能显式地调用访问器,因为这样做为产生编译错误。
属性和关联字段
属性常和字段关联。一种常见的方式是在类中将字段声明为private以封装该字段,并声明一个public属性来控制从类外部对该字段的访问。和属性关联的字段常被称为后备字段或后备存储。
例如,下面的代码使用公有属性MyValue来控制对私有字段TheRealValue的访问。
class C1
{
private int theRealValue = 10;
public int MyValue
{
set { theRealValue = value; }
get { return theRealValue; }
}
}
class Program
{
static void Main()
{
C1 c1 = new C1();
Console.WriteLine(c1.MyValue);
c1.MyValue = 20;
Console.WriteLine(c1.MyValue);
}
}
属性和它们的后备字段有几种命名约定。一种约定是两个名称使用相同的内容,但字段使用Camel大小写,属性使用Pascal大小写。虽然这违反了“仅使用大小写区分不同标识符是个坏习惯”这条一般规则,但它有个好处,即可以把两个标识符以一种有意义的方式联系在一起。
另一种约定是属性使用Pascal大小写,字段使用相同标识符的Camel大小写版本,并以下划线开始。
下面的代码展示了这两种约定。
private int firstField;
public int FirstField
{
set { firstField = value; }
get { return firstField; }
}
private int _secondField;
public int SecondField
{
get { return _secondField; }
set { _secondField = value; }
}
执行其他计算
属性访问器并不局限于对关联的后备字段传进传出数据。访问器get和set能执行任何计算,也可以不执行任何计算。唯一必需的行为是get访问器要返回一个属性类型的值。
例如,下面的示例展示了一个有效的(但可能没什么用处的)属性,它仅在get访问器被调用时返回值5。当set访问器被调用时,它什么也不做。隐式参数value的值被忽略了。
public int Useless
{
set{ }
get{return 5;}
}
下面的代码展示了一个更现实和有用的属性,其中set访问器在设置关联字段之前实现过滤。set访问器把字段TheRealValue的值设置成输入值,如果输入值大于100,就将TheRealValue设置为100。
int theRealValue = 10; //字段
int MyValue //属性
{
set { theRealValue = value > 100 ?100 : value; } //条件运算符
get { return theRealValue; }
}
C# 7.0为属性的getter/setter引入了另一种语法,这种语法使用表达函数体。虽然14章会详细讨论表达函数体(或者叫lambda表达式),但是为了完整性,这里演示了这种新的语法。这种语法只有在访问函数体由一个表达式组成的时候才能使用。
int MyValue
{
set => value > 100 ? 100 : value;
get => theRealValue;
}
在上面的代码示例中,从等号到语句结尾部分的语法叫做条件运算符,第9章会详细阐述。
只读和只写属性
要想不定义属性的某个访问器,可以忽略该访问器的声明。
- 只有get访问器的属性称为只读属性。只读属性能够安全地将一个数据项从类或类的实例中传出,而不必让调用者修改属性值。
- 只有set访问器的属性称为只写属性。只写属性很少见,因为它们几乎没有实际用途。如果想在赋值的时候触发一个副作用,应该使用方法而不是属性。
- 两个访问器中至少有一个必须定义,否则编译器会产生一条错误消息。
属性和共有字段
按照推荐的编码实践,属性比共有字段更好,理由如下。
- 属性是函数成员而不是数据成员,允许你处理输入和输出,而共有字段不行。
- 属性可以只读或者只写,而字段不行。
- 编译后的变量和编译后的属性语义不同。
如果要发布一个由其他代码引用的程序集,那么第三点将会带来一些影响。例如,有的时候开发人员可能想用共有字段代替属性,因为如果以后需要为字段的数据增加逻辑的话,可以再把字段改为属性。这没错,但是如果那样修改的话,所有访问这个字段的其他程序集都需要重新编译,因为字段的属性和编译后的语义不一样。另外如果实现的是属性,那么只需要修改属性的实现,而无需编译访问它的其他程序集。
计算只读属性示例
迄今为止,在大多示例中,属性都和一个后备字段关联,并且get和set访问器引用该字段。然而,属性并非必须和字段关联。在下面的示例中,get访问器计算出返回值。
class RightTriangle
{
public double A = 3;
public double B = 4;
public double Hypotenuse
{
get { return Math.Sqrt((A*A)+(B*B)); }
}
}
class Program
{
static void Main()
{
RightTriangle c = new RightTriangle();
Console.WriteLine($"Hypotenuse:{c.Hypotenuse}");
}
}
自动实现属性
因为属性经常被关联到后备字段,C#提供了自动实现属性(Auto-implemented Property),允许只声明属性而不声明后备字段。编译器会为你创建隐藏的后备字段,并且自动挂接到get和set访问器上。
自动实现属性的要点如下。
- 不声明后备字段——编译器根据属性的类型分配存储。
- 不能提供访问器的方法体——它们必须被简单地声明为分号。get担当简单的内存读,set担当简单的写。但是因为无法访问自动属性的方法体,所以在使用自动属性时调试代码通常会更加困难。
从C#6.0开始,可以使用只读属性了。此外还可以将自动属性初始化作为其声明的一部分。
除了方便之外,利用自动实现属性还能在想声明一个共有字段的地方轻松插入一个属性。
静态属性
属性也可以声明为static。静态属性的访问器和所有静态成员一样,具有以下特点。
- 不能访问类的实例成员,但能被实例成员访问。
- 不管类是否有实例,它们都是存在的。
- 在类的内部,可以仅使用名称来引用静态属性。
- 在类的外部,正如本章前面描述的,可以通过类名或者使用using static结构来应用静态头三行,即使没有类的实例,也能访问属性。Main的最后一行调用一个实例方法,它从类的内部访问属性。
using System;
using static ConsoleTestApp.Trivial;
namespace ConsoleTestApp
{
class Trivial
{
public static int MyValue { get; set; }
public void PrintValue()
{
Console.WriteLine($"Value from inside:{MyValue}");
}
}
class Program
{
static void Main()
{
Console.WriteLine($"Init Value: {Trivial.MyValue}");
Trivial.MyValue = 10;
Console.WriteLine($"Init Value: {Trivial.MyValue}");
MyValue = 20;
Console.WriteLine($"New Value : {MyValue}");
Trivial trivial = new Trivial();
trivial.PrintValue();
}
}
}
output:
Init Value: 0
Init Value: 10
New Value : 20
Value from inside:20
实例构造函数
实例构造函数是一个特殊的方法,它在创建类的每个新实例时执行。
- 构造函数用于初始化类实例的状态。
- 如果希望能从类的外部创建类的实例,需要将构造函数声明为public。
除了下面几点,构造函数看起来很像类声明中的其他方法。
- 构造函数的名称和类名相同。
- 构造函数不能有返回值。
带参数的构造函数
构造函数在下列方面和其他方法类似。
- 构造函数可以带参数。参数的语法和其他方法完全相同。
- 构造函数可以被重载。
在使用创建对象表达式创建新实例时,要使用new运算符,后面跟着类的某个构造函数。new运算符使用该构造函数创建类的实例。
例如,在下面的代码中,Class1有3个构造函数:一个不带参数,一个带int参数,一个带string参数。Main使用各个构造函数分别创建实例。
class Class1
{
int Id;
string Name;
public Class1() { Id = 28; Name = "Nemo"; }
public Class1(int val) { Id = val; Name = "Nemo"; }
public Class1(String name) { Name = name; }
public void SoundOff()
{
Console.WriteLine($"Name {Name}, Id {Id}");
}
}
class Program
{
static void Main()
{
Class1 a = new Class1(),
b = new Class1(7),
c = new Class1("Bill");
a.SoundOff();
b.SoundOff();
c.SoundOff();
}
}
output
Name Nemo, Id 28
Name Nemo, Id 7
Name Bill, Id 0
默认构造函数
如果在类的声明中没有显式地提供实力构造函数,那么编译器会提供一个隐式的默认构造函数,它有以下特征。
- 没有参数。
- 方法体为空。
如果你为类声明了任何构造函数,那么编译器将不会为该类定义默认构造函数。
例如,下面代码中的Class2声明了两个构造函数。
- 因为已经至少有一个显式定义的构造函数,所以编译器不会创建额外的构造函数。
- 在Main中,试图使用不带参数的构造函数创建新的实例。因为没有无参数的构造函数,所以编译器会产生一条错误消息。
class Class2
{
public Class2(int Value) { }
public Class2(string Value) { }
}
class Program
{
static void Main()
{
Class2 a = new Class2(); //错误!“Class2”不包含采用 0 个参数的构造函数
}
}
可以像对其他成员那样,对实例构造函数设置访问修饰符。可以将构造函数声明为public,这样在类的外部也能创建类的实例。也可以创建private构造函数,这样在类外部就不能调用该构造函数,但在类内部可以,下一章将讨论这一点。
小结
字数一多就开始卡,再开一个了,之后再把这些合并一下得了。