目录
引言
静态构造函数
构造函数也可以声明为static。实例构造函数初始化类的每个新实例,而static构造函数初始化类级别的项。通常,静态构造函数初始化类的静态字段。
- 初始化类级别的项。
- 在引用任何静态成员之前。
- 在创建类的任何实例之前。
- 静态构造函数在以下方面与实例构造函数类似。
- 静态构造函数的名称必须和类名形同。
- 构造函数不能返回值。
- 静态构造函数在以下方面和实例构造函数不同。
- 静态构造函数声明中使用static关键字。
- 类只能有一个静态构造函数,而且不能带参数。
- 静态构造函数不能有访问修饰符。
关于静态构造应该了解的其他重要内容如下。
- 类既可以有静态构造函数也可以有实例构造函数。
- 如同静态方法,静态构造函数不能访问所在类的实例成员,因此也不能使用this访问器,我们马上会讲述这一内容。
- 不能从程序中显式调用静态构造函数,系统会自动调用它们:
- 在类的任何实例被创建之前;
- 在类的任意静态成员被引用之前。
静态构造函数示例
class RandomNumberClass
{
private static Random RandomKey;
static RandomNumberClass()
{
RandomKey = new Random();
}
public int GetRandomNumber()
{ return RandomKey.Next(); }
}
class Program
{
static void Main()
{
RandomNumberClass a = new RandomNumberClass();
RandomNumberClass b = new RandomNumberClass();
Console.WriteLine(a.GetRandomNumber());
Console.WriteLine(b.GetRandomNumber());
}
}
output
596049083
392840815
对象初始化语句
对象初始化语句扩展了创建语法,在表达式的尾部放置了一组成员初始化语句。利用对象初始化语句,可以在创建新的对象实例时,设置字段和属性的值。
例如,对于一个名为Point、有两个共有整型字段X和Y类,可以使用下面的表达式创建一个新对象:new Point {X = 5, Y = 6};
关于对象初始化语句要了解的重要内容如下。
- 创建对象的代码必须能够访问要初始化的字段和属性。例如,在之前的代码中X和Y必须是共有的。
- 初始化发生在构造方法执行之后,因此在构造方法设置的值可能会在之后对象初始化中重置为相同或不同的值。
示例:
public class Point
{
public int X=1;
public int Y=2;
}
class Program
{
static void Main()
{
Point pt1 = new Point();
Point pt2 = new Point {X=5,Y=6 };
Console.WriteLine($"pt1:{pt1.X},{pt1.Y}");
Console.WriteLine($"pt2:{pt2.X},{pt2.Y}");
}
}
pt1:1,2
pt2:5,6
readonly 修饰符
字段可以用readonly修饰符声明。其作用类似于将字段声明为const,一旦值被设定就不能改变。
- const字段只能在字段的声明语句中初始化,而readonly字段可以在下列任意位置设置它的值。
- 字段声明语句,类似于const。
- 类的任何构造函数。如果是static字段,初始化必须在静态构造函数中完成。
- const字段的值必须可在编译时决定,而readonly字段的值可以在运行时决定。这种自由性允许你在不同的环境或不同的构造函数中设置不同的值!
- const的行为总是静态的,而对于readonly字段以下两点是正确的。
- 它可以是实例字段,也可以是静态字段。
- 它在内存中有存储位置。
例如,下面的代码声明了一个名为Shape的类,它有两个readonly字段。
- 字段PI在它的声明中初始化。
- 字段NumberOfSide根据调用的构造函数被设置为3或4。
class Shape
{
readonly double Pi = 3.1416;
readonly int NumberOfSides;
public Shape(double side1,double side2)
{
NumberOfSides = 4;
}
public Shape(double side1,double side2,double side3)
{
NumberOfSides = 3;
}
}
this关键字
this关键字在类中使用,是对当前实例的引用。它只能被用在下列类成员的代码块中。
- 实例构造函数。
- 实例方法。
- 属性和索引器的实例访问器(索引器将在下一节阐述)。
很明显,因为静态成员不是实例的一部分,所以不能在任何静态函数成员的代码中使用this关键字。更适当的说,this用于下列目的:
- 用于区分类的成员和局部变量或参数;
- 作为调用方法的实参。
虽然理解this关键字的用途和功能很重要,但它实际上很少在代码中使用。请参阅本章后面介绍的索引器以及18章中介绍的扩展方法。
索引器
假设我们要定义一个类employee,它带有三个string型字段,那么可以使用字段的名称访问它们。
然而有的时候,如果能使用索引访问它们将会很方便,好像该实例就是字段的数组一样。索引器使用索引运算符,就像是数组。
索引器和属性
索引器和属性在很多方面是相似的。
- 和属性一样,索引器不用分配内存来存储。
- 索引器和属性都主要被用来访问其他数据成员,它们和与这些成员关联,并为它们提供获取和设置访问。
- 属性通常表示单个数据成员。
- 索引器通常表示多个数据成员。
可以认为索引器是为类的多个数据成员提供get和set访问的属性。通过提供索引器,可以在许多可能的数据成员中进行选择。索引本身可以是任何类型,而不仅仅是数值类型。
注意事项:
- 和属性一样,索引器可以只有一个访问器,也可以两个都有。
- 索引器总是实例成员,因此不能被声明为static。
- 和属性一样,实现get和set访问器的代码不一定要关联到某个字段或属性。这段代码可以做任何事情也可以什么都不做,只要get访问器返回某个指定类型的值即可
声明索引器
声明索引器的语法如下所示。请注意以下几点。
- 索引器没有名称。在名称的位置是关键字this。
- 参数列表在方括号中间。
- 参数列表中必须至少声明一个参数
ReturnType this [Type params1,...]
{
get
{
...
}
set
{
...
}
}
索引器的set访问器
当索引器被用于赋值时,set访问器被调用,并接受两项数据,如下:
- 一个名为value的隐式参数,其中持有要保存的数据;
- 一个或更多索引参数,表示数组应该保存到哪里。
在set访问器中的代码必须检查索引参数,以确定数据应该存往何处,然后保存它。
- 它的返回值是void。
- 它使用的参数列表和索引器声明中的相同。。
- 它有一个名为value的隐式参数,值参类型和索引器类型相同。
索引器的get访问器
当使用索引器获取值时,可以通过一个或多个索引参数调用get访问器。索引参数指示获取哪个值。
string s = emp[0];
get访问器方法体内的代码必须检查索引参数,确定它表示的是哪个字段,并返回该字段的值。
get访问器有如下语义。
- 它的参数列表和索引器声明中的相同。
- 它返回与索引器类型相同的值。
关于索引器的更多内容
和属性一样,不能显式调用get和set访问器。取而代之,当索引器用在表达式中取值时,将自动调用get访问器。当使用赋值语句对索引器赋值时,将自动调用set访问器。
在“调用”索引器时,要在方括号这种提供参数。
emp[0] = "Doe";
string NewName = emp[0];
为Employee实例声明索引器
下面的代码为先前示例中的类Employee声明了已给索引器。
- 索引器需要读写string类的值,所以string必须声明为索引器的类型。它必须声明为public,以便从类的外部访问。
- 3个字段被随意地索引为整数0~2,所以本例中方括号中间名为index的形参必须为int型。
- 在set访问器方法体内,代码确定索引指的是哪个字段,并把隐式变量value的值赋给它。在get访问器方法体内,代码确定索引指的是哪个字段,并返回该字段的值。
class Employee
{
public string LastName;
public string FirstName;
public string CityOfBrith;
public string this[int index]
{
set
{
switch (index)
{
case 0: this.LastName = value; break;
case 1: this.FirstName = value; break;
case 2: CityOfBrith = value; break;
default: throw new ArgumentOutOfRangeException("index");
}
}
}
}
另一个索引示例
下面的示例为类Class1的两个int字段设置索引。
class Class1
{
private int Temp0;
private int Temp1;
public int this[int index]
{
get
{
return (0 == index) ? Temp0 : Temp1;
}
set
{
if (0 == index)
Temp0 = value;
else
Temp1 = value;
}
}
}
class Example
{
static void Main()
{
Class1 a = new Class1();
Console.WriteLine("Values -- T0:{0},T1:{1}", a[0], a[1]);
a[0] = 15;
a[1] = 20;
Console.WriteLine($"Values -- T0:{a[0]},T1:{a[1]}");
}
}
Values – T0:0,T1:0
Values – T0:15,T1:20
索引器重载
只要索引器的参数列表不同,类就可以有任意多个索引器。索引器类型不同是不够的。这叫做索引器重载,因为所有的索引器都有相同的名称:this访问器引用。
例子源自GPT
class Dictionary
{
private List<string> keys = new List<string>();
private List<string> values = new List<string>();
public string this[int index]
{
get
{
return values[index];
}
set
{
if (index >= 0 && index < keys.Count)
{
values[index] = value;
}
}
}
public string this[string key]
{
get
{
int index = keys.IndexOf(key);
if (index >= 0)
{
return values[index];
}
else
{
return null;
}
}
set
{
int index = keys.IndexOf(key);
if (index >= 0)
{
values[index] = value;
}
else
{
keys.Add(key);
values.Add(value);
}
}
}
}
class Program
{
static void Main(string[] args)
{
Dictionary dictionary = new Dictionary();
dictionary["apple"] = "苹果";
dictionary["banana"] = "香蕉";
dictionary["orange"] = "橙子";
Console.WriteLine(dictionary[1]); // 输出:"香蕉"
Console.WriteLine(dictionary["orange"]); // 输出:"橙子"
}
}
请记住,类中重载的索引器必须有不同的参数列表
访问器的访问修饰符
默认情况下,成员的两个访问器的访问级别和成员自身相同。也就是说,如果一个属性的访问级别是public,那么它的两个访问器的访问级别也是如此。索引器也一样。
不过你可以为两个访问器分配不同的访问级别。例如,如下代码演示了一个常见而且重要的范式,那就是将set访问器声明为private,将get访问器声明为public。get之所以是public的,是因为属性的访问级别是public。
注意,在这段代码中,尽管可以从类的外部读取属性,但只能在类的内部设置它(本例中是在构造函数内设置)。这是一个非常重要的封装工具。
class Person
{
public string Name{get; private set;}
public Person(string name){Name = name;}
}
访问器的访问修饰符有几个限制。最重要的限制如下。
- 仅当成员(属性或索引器)既有get访问器也有set访问器时,其访问器才能有访问修饰符。
- 虽然两个访问器都必须出现,但它们中只能有一个有访问修饰符。
- 访问器的访问修饰符限制必须比成员的访问级别更严格。
分部类和分部类型
类的声明可以分割成几个分部类的声明。
- 每个分部类的声明都含有一些类成员的声明。
- 类的分部类声明可以在同一个文件中也可以在不同文件中。
每个分部类声明必须被标注为partial class,而不是单独的关键字class。分部类声明看起来和普通类声明相同,只是增加了类型修饰符partial。
类型修饰符partial不是关键字,所以在其他上下文中,可以在程序中把它用作标识符。但直接用在关键词class、struct或interface之前时,它表示分部类型。
组成类的所有分部类型声明必须在一起编译。使用分部类声明的类必须有相同的含义,就好像所有类成员都声明在一个单独的类声明体内。
来自GPT的示例:
// 文件:Person.cs
public partial class Person
{
private int age;
public void SayHello()
{
Console.WriteLine($"Hello, my name is {this.Name}, I'm {this.age} years old.");
}
}
// 文件:Person2.cs
public partial class Person
{
private string name;
public string Name
{
get { return this.name; }
set { this.name = value; }
}
public void SetAge(int age)
{
this.age = age;
}
}
// 文件:Program.cs
class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.Name = "Tom";
person.SetAge(20);
person.SayHello();
}
}
分部方法
分部方法是声明在分部类中不同部分的方法。分部方法的不同部分可以声明在分部类的不同部分中,也可以声明在同一个部分中。分部方法的两个部分如下。
- 定义分部方法声明
- 给出签名和返回类型。
- 声明的实现部分只是一个分号。
- 实现分部方法声明。
- 给出签名和返回类型。
- 以普通的语句块形式实现。
关于分部方法需要了解的重要内容如下。
- 定义声明和实现声明的签名和返回类型必须匹配。签名和返回类型有如下特征。
- 返回类型必须是void。
- 签名不能包括访问修饰符,这是分部方法隐式私有的。
- 参数列表不能包含out参数。
- 在定义声明和实现声明中都必须包含上下文关键字partial,并且直接放在关键字void之前。
- 可以有定义部分而没有实现部分。在这种情况 ,编译器把方法那个以及方法内部任何对方法的调用都移除。不能只有分布方法的实现部分而没有定义部分。
以下内容来自GPT:
在 C# 中,分部方法(partial methods)是一种特殊类型的方法,允许在一个类或结构的多个部分中定义,并且可以选择性地实现。分部方法可以用于将一个方法的实现拆分为两个或多个部分,其中一个部分由代码编写者提供,另一个部分由代码生成器自动生成。
以下是一个分部方法的示例:
// 文件:Calculator.cs
public partial class Calculator
{
partial void LogCalculation(int result); // 分部方法声明
public int Add(int a, int b)
{
int result = a + b;
LogCalculation(result); // 调用分部方法
return result;
}
}
// 文件:CalculatorLogger.cs
public partial class Calculator
{
partial void LogCalculation(int result) // 分部方法实现
{
Console.WriteLine($"Calculation result: {result}");
}
}
在这个示例中,我们通过 partial
关键字将 Calculator
类型的定义分为两个文件:Calculator.cs
和 CalculatorLogger.cs
。在 Calculator.cs
文件中定义了一个普通的方法 Add()
,它计算两个整数的和。在该方法内部,我们调用了一个分部方法 LogCalculation()
,并传递了计算结果作为参数。
而在 CalculatorLogger.cs
文件中,我们提供了 LogCalculation()
方法的实现。当 Add()
方法中调用 LogCalculation()
时,实际上是调用了 CalculatorLogger.cs
中的实现。在这个示例中,LogCalculation()
方法的实现是将计算结果打印到控制台。
需要注意的是,分部方法的实现是可选的,可以选择不提供实现。如果在所有部分中没有提供实现,编译器将完全移除对该分部方法的调用。这使得分部方法在某些情况下可以用于代码生成或条件性行为。
使用分部方法,我们可以将一个方法的定义和实现分隔到多个文件中,提高代码的组织性和可读性。它常用于代码生成工具、自动生成的类或者是在应用程序的不同部分中异步处理特定操作等情况下。
小结
卡卡卡卡,字一多就巨卡。