类
1 字段和方法
1.1 字段 Field
字段是 Class 或 Struct 的成员,它是一个变量。如
public Class Book
{
string name;
public float price = 56.0 F;
}
-
readonly 修饰符
readonly
修饰符防止字段在构造之后被改变。readonly
字段只能在声明的时候被赋值,或在构造函数里被赋值。
-
字段初始化
- 字段可以可选初始化。
- 未初始化的字段有一个默认值。
- 字段的初始化在构造函数之前运行。
-
可以同时声明多个字段
public Class StartPoint { static readonly int x = 0, // ※ , 不是 ; y = 0; }
🌳 字段支持以下的修饰符:
- 静态修饰符:
static
- 访问权限修饰符:
public internal private protected
- 继承修饰符:
new
- 不安全代码修饰符:
unsafe
- 只读修饰符:
readonly
- 线程访问修饰符:
volatile
1.2 方法 Method
-
类型内方法的签名板必须唯一。
方法的签名:方法名和参数类型(含顺序,但与参数名称和返回类型无关) -
表达式体方法(Expression-bodied)
- 以下仅由一个表达式构成的方法
int Foo(int x) {return x * 2;}
- 可以使用
=>
(方法的逻辑)代替花括号和return
关键字
int Foo(int x) => x * 2;
- 也可以用 void 返回类型
void Foo(int x) => Console.WriteLine(x);
- 以下仅由一个表达式构成的方法
-
方法的重载 OverLoad
类型里的方法可以进行重载(允许多个方法共用一个名称),只要这些方法的签名不同void Foo(int x) {...} void Foo(double x) {...} void Foo(int x, float y) {...} void Foo(float x, int y) {...}
但是,下面两对方法则不能同时出现在一个类型中,因为方法的返回值类型和
params
修饰符不是方法签名的一部分。void Foo(int x) {...} float Foo(int x) {...} // Compile-time error void Goo(int[] x) {...} void Goo(params int[] x) {...} // Compile-time error
-
局部(本地)方法 Local
csharp7.0 允许在一个方法中定义另一个方法void WriteCubes() { Console.WriteLine(Cube(3)); // 27 Console.WriteLine(Cube(4)); // 64 Console.WriteLine(Cube(5)); // 125 int Cube(int value) => value * value * value; }
🍂 局部方法不能使用
static
修饰,如果父方法是静态的,那么局部方法也是隐式静态的。
2 构造函数和解构器
2.1 构造函数 Constructor
- 执行类或结构体的初始化代码。
- 和方法定义类似,区别仅在于构造函数名和返回值只能和类名一致(返回值省略不写)。
- 支持以下修饰符:
- 访问权限修饰符:
public internal private protected
- 非托管代码修饰符:
unsafe extern
- csharp7.0 支持仅包含一个语句的构造函数也可以写成 expression-bodied 形式。
namespace CSharp_Files
{
class Program
{
static void Main(string[] args)
{
Panda p = new Panda("Petey"); // Call constructor
}
}
public class Panda
{
string name; // Define field
public Panda(string n) // Define constructor
{
name = n; // Initialization code (set up field)
}
// public Panda(string n) => name = n;
}
}
- 构造函数重载
-
类和结构体可以重载构造函数。
-
构造函数可以使用
this
调用重载构造函数,当构造函数调用另一个构造函数时候时,被调用的构造函数先执行。namespace CSharp_Files { class Program { static void Main(string[] args) { Wine w = new Wine(999.0M, 2001); // 输出结果 1 // 2 } } public class Wine { public decimal price; public int year; public Wine(decimal price) { this.price = price; Console.WriteLine(1); } public Wine(decimal price, int year): this(price) // 构造函数调用构造函数 { this.year = year; Console.WriteLine(2); } } }
-
还可以向另一个构造函数传递表达式,但表达式内不能使用
this
引用,例如,不能调用实例方法(这是强制性的,因为对象当前还没有通过构造器初始化完毕,因此任何方法的调用都会失败)。但表达式可以调用 static 方法。public class Wine { public decimal price; public int year; public DateTime date; public Wine(decimal price) { this.price = price; } public Wine(decimal price, int year): this(price) { this.year = year; } public Wine(decimal price, DateTime date): this(price, date.Year) // 传递表达式 date.Year { this.date = date; } public Wine(decimal price, DateTime date): this(price, this.GetYear()) //Compile-time error 关键字 "this" 在当前上下文中不可用 { this.date = date; } public int GetYear() { return 2001; } }
- 隐式无参数构造函数
- csharp 编译器会自动为没有显式定义构造函数的类生成无参数共有构造函数。
- 如果显式定义了至少一个构造函数,系统就不会自动生成无参数的构造函数。
- 构造函数可以使用可选参数、命名参数。
- 构造函数和字段的初始化顺序
- 字段的初始化发生在构造函数执行之前。
- 字段的初始化按声明的先后顺序进行。
2.2 解构器 Deconstructor
csharp7 引入了解构器(Deconstructor)模式。一个解构器(解构方法)就像构造函数反过程:
- 构造函数使用若干参数,并将它们赋值给字段。
- 解构器则相反将字段反向赋值给若干变量。
-
解构方法的名字必须为 Deconstruct,并且拥有一个或多个
out
参数。public class Rectangle { public readonly float width, height; public Rectangle(float width, float height) // Constructor { this.width = width; this.height = height; } public void Deconstruct(out float width, out float height) //Deconstructor { width = this.width; height = this.height; } }
-
若要调用解析器,需使用特殊的语法:
var rect = new Rectangle(3, 4); (float width, float height) = rect; Console.WriteLine(width + " " + height); // 3 4
第二行是解构调用。它创建了两个局部变量并调用
Deconstruct
方法,等价于:float width, height; rect.Deconstruct(out width, out height);
或者
rect.Deconstruct(out var width, out var height);
解构调用允许隐式类型推断,因此可以简写为:
(var width, var height) = rect;
或者
var(width, height) = rect;
如果解构中的变量已经定义过了,那么可以忽略类型声明:
float width, height; (width, height) = rect;
上述操作也称为解构赋值。
-
可以通过重载
Deconstruct
方法向调用者提供一系列解构方案。 -
Deconstruct
方法可以是扩展方法。public static class Rect { public static void Deconstruct(this Rectangle rect, out float width, out float height) { width = rect.width; height = rect.height; } }
调用时:
var rect = new Rectangle(3, 4); Rect.Deconstruct(rect, out var width, out var height); Console.WriteLine(width + " " + height); // 3 4
3 对象初始化器
为了简化对象的初始化,可以在调用构造器之后,直接通过对象初始化器设置对象的任何可访问字或属性(csharp3.0 引入)。
public class Bunny
{
public string name;
public bool likesCarrots;
public bool likesHumans;
public Bunny() {} // Constructor1
public Bunny(string n) // // Constructor1
{
name = n;
}
}
使用对象初始化器对 Bunny
对象进行实例化:
Bunny b1 = new Bunny // 使用无参数构造函数,省略了()
{
name = "Bo",
likesCarrots = true,
likesHumans = false
};
Bunny b2 = new Bunny("Bo")
{
likesCarrots = true,
likesHumans = false
};
构造 b1
和 b2
的代码等价于(编译器生成的代码):
Bunny temp1 = new Bunny(); // temp1 is a compiler-generated name
temp1.name = "Bo";
temp1.likesCarrots = true;
temp1.likesHumans = false;
Bunny b1 = temp1;
Bunny temp2 = new Bunny("Bo");
temp2.likesCarrots = true;
temp2.likesHumans = false;
Bunny b2 = temp2;
使用临时变量是为了确保早初始化过程中如果抛出异常,则不会得到一个部分初始化的对象。
使用对象初始化器 VS 使用可选参数
如果不使用对象初始化器,还可以令 Bunny
的构造函数接受可选参数:
public Bunny(string name, bool likesCarrots = false, bool likesHumans = false)
{
this.name = name;
this.likesCarrots = likesCarrots;
this.likesHumans = likesHumans;
}
可以使用如下的语句构造 Bunny
对象:
Bunny b3 = new Bunny(name: "Bo", likesCarrots: true);
-
优点:可以将类的字段或属性设置为只读,如果在对象的生命周期内不需要字段值或属性值,则将其设置为只读是非常有用的。
-
缺点:每个可选参数的值都被嵌入到 calling site,csharp 会将构造函数翻译为
Bunny b3 = new Bunny("Bo", true, false);
如果另一个程序集(.exe 或 .dll)实例化
Bunny
,则当Bunny
类再添加一个可选参数(如likesCats
)的时候就会出错。除非引用该类的程序集也重新编译,否则它还将继续调用三个参数的构造函数(现在已经不存在了)而造成运行时错。还有一种难以发现的错误是,如果修改了某个可选参数的默认值,则另一个程序集的调用者在重新编译之前,还会继续使用旧的可选值。
4 this 引用
this
引用指代实例本身this
引用可用来区分字段、属性和局部变量this
引用仅在类或结构体的非静态成员中有效
5 属性 Property
从外表看,属性(Property)和字段(Field)很类似,但属性内部像方法一样含有逻辑,例如,从以下代码不能判断出 CurrentPrice
是字段还是属性:
Stock msft = new Stock();
msft.CurrentPrice = 30;
msft.CurrentPrice -= 3;
Console.WriteLine(msft.CurrentPrice);
5.1 属性声明
属性和字段的声明很类似,但属性比字段多出了get/set
代码块:
public class Stock
{
private decimal currentPrice; // The private "backing" field
public decimal CurrentPrice // The public property
{
get {return currentPrice;}
set {currentPrice = value;}
}
}
get
和 set
是属性的访问器。读属性时会运行 get
访问器,返回属性类型的值;给属性赋值时运行 set
访问器,它含有一个名为 value
的隐含参数,其类型和属性的类型相同,它的值一般来说会赋值给一个私有字段(currentPrice
)
属性支持以下的修饰符:
- 静态修饰符:
static
- 访问权限修饰符:
public internal private protected
- 继承修饰符:
new virtual abstract override sealed
- 非托管代码修饰符:
unsafe extern
5.2 属性 VS 字段
尽管访问属性和字段的方式是相同的,但不同之处在于,属性在获取和设置值的时候给实现者提供了完全的控制能力,这种控制能力使实现者可以选择任意的内部表示形式。
public class Stock
{
private decimal currentPrice; // The private "backing" field
public decimal CurrentPrice // The public property
{
get
{
return currentPrice * 10;
}
set
{
if (value >10 && value <= 20)
currentPrice = value;
}
}
}
- 字段(成员变量)
a. 字段主要是为类的内部做数据交互使用,字段一般是private
。
b. 字段可读可写。
c. 当字段需要为外部提供数据的时候,请将字段封装为属性,而不是使用公有字段(public
修饰符),这是面向对象思想所提倡的。 - 属性(方法)
a. 属性一般是向外提供数据,主要用来描述对象的静态特征,所以,属性一般是public
。
b. 属性具备get
和set
方法,可以在方法里加入逻辑处理数据,灵活拓展使用。
5.3 只读属性和计算属性
- 如果只定义了
get
访问器,属性就是只读的 - 如果只定义了
set
访问器,属性就是只写的,一般很少使用只写属性 - 通常属性会用一个专门的后台字段(backing field)来存储其所代表的数据,但属性也可以从其他数据计算得来
private decimal currentPrice, sharesOwned; // The private "backing" field
public decimal Worth
{
get {return currentPrice * sharesOwned;}
}
5.4 表达式体(Expression-bodied)属性
-
从 csharp 6 开始,只读属性可以简写为表达式体属性,使用 => 替换了花括号、
get
访问器和return
关键字。public decimal Worth => currentPrice * sharesOwned;
-
csharp 7 进一步允许在
set
访问器上使用表达式体:public class Stock { public decimal currentPrice; public decimal CurrentPrice { get => currentPrice; set => currentPrice = value; } }
public class Stock { public decimal currentPrice, sharesOwned; public decimal Worth { get => currentPrice * sharesOwned; set => sharesOwned = value / currentPrice; } }
5.5 自动属性
属性最常见的实现方式是使用 get/set
访问器读写私有字段,因而编译器会将自动属性(csharp 3.0)声明自动转换为这种实现方式:
public class Stock
{
...
public decimal CurrentPrice {get; set;}
}
- 编译器会自动生成一个后台私有字段(backing private field),该字段的名称由编译器生成且无法引用。
- 如果希望属性对其他类型暴露为只读属性,则可以将
set
访问器标记为private
或protected
。
5.6 属性初始化器
-
csharp 6 开始支持自动属性的初始化器:
public decimal CurrentPrice {get; set;} = 123;
上述方法将
CurrentPrice
的值初始化为 123。 -
只读的属性也可使用属性初始化器(也可以在构造函数中被赋值):
public decimal Maximum {get;} = 999;
5.7 get 和 set 的可访问性
get
和 set
访问器可以有不同的访问级别:
典型用法:public set
,internal/private set
public class Foo
{
private decimal x;
public decimal X
{
get {return x;}
private set {x = Math.Round(value, 2);}
}
}
5.8 CLR 属性的实现
csharp 属性访问器在内部会被编译为名为 get_XXX
和 set_XXX
的方法:
public decimal get_CurrentPrice {...}
public decimal set_CurrentPrice(decimal value) {...}
🍂 简单的非 virtual
属性访问器会被 JIT(即使)编译器内联处理,消除了属性和字段访问键的性能差距。内联是一种优化技术,它用方法的函数体代替方法调用(即直接使用方法的代码块)。
6 索引器 Indexer
索引器为访问类或结构体中封装的列表或字典型数组元素提供了自然的访问接口。
string s = "hello";
Console.WriteLine(s[0]); // h
🍂 使用索引器的语法和使用数组的语法类似,不同之处在于索引参数可以是任意类型。
6.1 索引器的实现
-
编写索引器首先定义一个名为
this
的属性,并将参数定义放在一对方括号中:public class Sentence { string[] words = "The quick brown fox".Split(); public string this [int wordNum] // indexer { get {return words[wordNum];} set {words[wordNum] = value;} } }
-
以下展示了索引器的使用方式
Sentence s = new Sentence(); Console.WriteLine(s[3]); // fox s[3] = "kangaroo"; Console.WriteLine(s[3]); // kangaroo
-
一个类型可以定义多个参数类型不同的索引器,而一个索引器也可以包含多个参数:
public string this [int arg1, string arg2] { get { ... } set { ... } }
如果省略了
set
访问器,则索引器就是只读的,并且可以使用 csharp 6 的表达式体语法来简化定义:public string this [int wordNum] => words [wordNum];
6.2 CLR 索引器的实现
索引器在内部会编译为名为 get_Item
和 set_Item
的方法:
public string get_Item (int wordNum) {...}
public void set_Item (int wordNum, string value) {...}
7 常量 Constant
-
常量是值永远不会改变的静态字段。
-
常量会在编译时静态赋值,编译器会在常量使用点上直接替换该值(类似于 C++ 的宏)。
-
常量可以是内置的数据类型、
bool
、char
、string
或者枚举(enum
)类型。 -
常量用关键字
const
声明,并且必须用值初始化。public class Test { public const string Message = "Hello World"; }
-
常量与静态只读(
static readonly
)字段-
常量在使用时比起静态只读字段有着更多的限制
- 能够使用的类型有限。
- 初始化字段的语句含义也不同。
public static double Circumference (double radius) { return 2 * System.Math.PI * radius; }
将编译为:
public static double Circumference (double radius) { return 6.2831853071795862 * radius; }
这样做是合理的,因为
PI
是常量,它的值永远不变。相反,static readonly
字段可以在每一个应用程序中有不同的值。 -
静态只读字段的好处还在于当提供给其他程序集时,可以在后续版本中更新其数值。假设程序集 X 提供了一个如下的常量:
public const decimal ProgramVersion = 2.3;
如果程序集 Y 引用了程序集 X 并使用了这个常量,那么值 2.3 将在编译时固定在程序集 Y 中。这意味着如果 X 后来重新编译将其值更改为 2.4,那么 Y 仍使用旧值 2.3 直至 Y 重新编译。而静态只读字段则不会存在这个问题。
🍂 从另一个角度看,未来可能发生变化的任何值从定义上都不是常量,因为不应当表示为常量。
-
8 静态构造函数 Static Constructor
-
静态构造函数,每个类型执行一次
非静态构造函数,每个实例执行一次
一个类型只能定义一个静态构造函数- 名称必须与类型同名。
- 没有参数。
class Test { static Test() { Console.WriteLine ("Type Initialized"); } }
-
运行时(runtime)在类型使用之前自动调用静态构造函数:
- 实例化类型。
- 访问类型的静态成员。
namespace CSharp_Files { class Program { static void Main(string[] args) { Foo f = new Foo(1); // This is the static constructor // 1 } } public class Foo { static Foo() { Console.WriteLine("This is the static constructor"); } public Foo(int x) { Console.WriteLine(x); } } }
-
静态构造函数仅支持两个修饰符:
unsafe
和extern
。 -
如果静态构造函数抛出了未处理的异常,则类型在整个应用程序的生命周期内都是不可用的。
-
静态构造函数和字段初始化顺序
- 静态字段初始化器在调用静态构造函数前运行
- 如果类型没有静态构造函数,字段会在类型被使用之前或者在运行时中更早的时间进行初始化
- 静态字段初始化器按照字段声明的先后顺序进行
class Foo { public static int X = Y; // X = 0 public static int Y = 3; // Y = 3 }
class Foo { public static int Y = 3; // Y = 3 public static int X = Y; // X = 3 }
class Program { static void Main() { Console.WriteLine (Foo.X); } // 3,静态字段在类型使用之前被初始化 // 先打印 0 后打印 3 } class Foo { public static Foo Instance = new Foo(); public static int X = 3; Foo() { Console.WriteLine (X); } // 0, Instance 和 X 两个字段,先初始化 Instance,执行 Foo,此时 X 尚未被初始化,故输出 0 }
9 静态类
- 类可以标记为
static
,静态类。 - 静态类的成员也必须全是静态的。
- 静态类不能派生子类。
例如:System.Console
和System.Math
。
10 析构函数(终结器) Finalizer
析构函数是类专有的一种方法,只能在类中使用。该方法在垃圾回收器(GC)回收未引用对象占用的内存前调用。析构函数的语法是类型的名称前加 ~
前缀。
class Class1
{
~Class1()
{
...
}
}
其实是 csharp 语言重写 Object
类的 Finalize
方法,编译为:
protected override void Finalize()
{
...
base.Finalize();
}
csharp 7 开始支持:
~Class1() => Console.WriteLine ("Finalizing");
11 分部类型和方法 Partial Types and Methods
11.1 分部类型
-
分部类型允许一个类型分开定义在多个地方(多个文件中),典型应用:一个类一部分自动生成,另一部分需要手动写代码:
// PaymentFormGen.cs - auto-generated partial class PaymentForm { ... } // PaymentForm.cs - hand-authored partial class PaymentForm { ... }
-
每个部分必须包含
partial
声明,以下写法不合法:partial class PaymentForm {} class PaymentForm {}
-
分部类型的各个组成部分不能包含冲突的成员,例如具有相同参数的构造函数。
-
分部类型完全由编译器处理,因此各部分在编译时必须可用,并且必须编译在同一个程序集。
-
可以在多个分部类声明中指定基类,但必须是同一个基类。
-
每一个分部类型组成部分可以独立指定实现的接口。
-
编译器无法保证各分部类的字段的初始化顺序。
11.2 分部方法
-
分部类型可以包含分部方法。这些方法能够令自动生成的分部类型为手动编写的代码提供自定义钩子(hook):
partial class PaymentForm // In auto-generated file { ... partial void ValidatePayment (decimal amount); } partial class PaymentForm // In hand-authored file { ... partial void ValidatePayment (decimal amount) { if (amount > 100) ... } }
-
分部方法由两部分组成:定义和实现
- 定义部分:一般由代码生成器生成。
- 实现部分:手动编写。 -
如果分部方法只有定义,没有实现,那么分部方法的定义会被编译器清除(调用它的代码也一样)。这样,自动生成的代码既可以自由提供钩子,也不用担心代码臃肿。
-
分部方法的返回值类型必须是
void
,且默认是private
的。
11.3 使用范围
- 处理大型项目时,使一个类分布在多个独立文件中可以让多位程序员同时对该类进行处理。
- 使用自动生成的源时,无需重新创建文件便可将代码添加到类中。如 Visual Studio 在创建 Windows 窗体、Web 窗体时,自动加上
partial
关键字。
12 nameof 运算符(csharp 6)
nameof
运算符返回任意符号的字符串名称(类型、成员、变量等),用于重构
int count = 123;
string name = nameof (count); // name is "count"