字段是 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
类型内方法的签名板必须唯一。
方法的签名:方法名和参数类型(含顺序,但与参数名称和返回类型无关)
表达式体方法(Expression-bodied)
int Foo(int x) {return x * 2;}
=>
(方法的逻辑)代替花括号和 return
关键字int Foo(int x) => x * 2;
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
修饰,如果父方法是静态的,那么局部方法也是隐式静态的。
public internal private protected
unsafe extern
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;
}
}
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
为了简化对象的初始化,可以在调用构造器之后,直接通过对象初始化器设置对象的任何可访问字或属性(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
)的时候就会出错。除非引用该类的程序集也重新编译,否则它还将继续调用三个参数的构造函数(现在已经不存在了)而造成运行时错。还有一种难以发现的错误是,如果修改了某个可选参数的默认值,则另一个程序集的调用者在重新编译之前,还会继续使用旧的可选值。
this
引用指代实例本身this
引用可用来区分字段、属性和局部变量this
引用仅在类或结构体的非静态成员中有效从外表看,属性(Property)和字段(Field)很类似,但属性内部像方法一样含有逻辑,例如,从以下代码不能判断出 CurrentPrice
是字段还是属性:
Stock msft = new Stock();
msft.CurrentPrice = 30;
msft.CurrentPrice -= 3;
Console.WriteLine(msft.CurrentPrice);
属性和字段的声明很类似,但属性比字段多出了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
尽管访问属性和字段的方式是相同的,但不同之处在于,属性在获取和设置值的时候给实现者提供了完全的控制能力,这种控制能力使实现者可以选择任意的内部表示形式。
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;
}
}
}
private
。public
修饰符),这是面向对象思想所提倡的。public
。get
和 set
方法,可以在方法里加入逻辑处理数据,灵活拓展使用。get
访问器,属性就是只读的set
访问器,属性就是只写的,一般很少使用只写属性private decimal currentPrice, sharesOwned; // The private "backing" field
public decimal Worth
{
get {return currentPrice * sharesOwned;}
}
从 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;
}
}
属性最常见的实现方式是使用 get/set
访问器读写私有字段,因而编译器会将自动属性(csharp 3.0)声明自动转换为这种实现方式:
public class Stock
{
...
public decimal CurrentPrice {get; set;}
}
set
访问器标记为 private
或 protected
。csharp 6 开始支持自动属性的初始化器:
public decimal CurrentPrice {get; set;} = 123;
上述方法将 CurrentPrice
的值初始化为 123。
只读的属性也可使用属性初始化器(也可以在构造函数中被赋值):
public decimal Maximum {get;} = 999;
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);}
}
}
csharp 属性访问器在内部会被编译为名为 get_XXX
和 set_XXX
的方法:
public decimal get_CurrentPrice {...}
public decimal set_CurrentPrice(decimal value) {...}
简单的非 virtual
属性访问器会被 JIT(即使)编译器内联处理,消除了属性和字段访问键的性能差距。内联是一种优化技术,它用方法的函数体代替方法调用(即直接使用方法的代码块)。
索引器为访问类或结构体中封装的列表或字典型数组元素提供了自然的访问接口。
string s = "hello";
Console.WriteLine(s[0]); // h
使用索引器的语法和使用数组的语法类似,不同之处在于索引参数可以是任意类型。
编写索引器首先定义一个名为 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];
索引器在内部会编译为名为 get_Item
和 set_Item
的方法:
public string get_Item (int wordNum) {...}
public void set_Item (int wordNum, string value) {...}
常量是值永远不会改变的静态字段。
常量会在编译时静态赋值,编译器会在常量使用点上直接替换该值(类似于 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 重新编译。而静态只读字段则不会存在这个问题。
从另一个角度看,未来可能发生变化的任何值从定义上都不是常量,因为不应当表示为常量。
静态构造函数,每个类型执行一次
非静态构造函数,每个实例执行一次
一个类型只能定义一个静态构造函数
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
}
static
,静态类。System.Console
和 System.Math
。析构函数是类专有的一种方法,只能在类中使用。该方法在垃圾回收器(GC)回收未引用对象占用的内存前调用。析构函数的语法是类型的名称前加 ~
前缀。
class Class1
{
~Class1()
{
...
}
}
其实是 csharp 语言重写 Object
类的 Finalize
方法,编译为:
protected override void Finalize()
{
...
base.Finalize();
}
csharp 7 开始支持:
~Class1() => Console.WriteLine ("Finalizing");
分部类型允许一个类型分开定义在多个地方(多个文件中),典型应用:一个类一部分自动生成,另一部分需要手动写代码:
// PaymentFormGen.cs - auto-generated
partial class PaymentForm { ... }
// PaymentForm.cs - hand-authored
partial class PaymentForm { ... }
每个部分必须包含 partial
声明,以下写法不合法:
partial class PaymentForm {}
class PaymentForm {}
分部类型的各个组成部分不能包含冲突的成员,例如具有相同参数的构造函数。
分部类型完全由编译器处理,因此各部分在编译时必须可用,并且必须编译在同一个程序集。
可以在多个分部类声明中指定基类,但必须是同一个基类。
每一个分部类型组成部分可以独立指定实现的接口。
编译器无法保证各分部类的字段的初始化顺序。
分部类型可以包含分部方法。这些方法能够令自动生成的分部类型为手动编写的代码提供自定义钩子(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
的。
partial
关键字。nameof
运算符返回任意符号的字符串名称(类型、成员、变量等),用于重构
int count = 123;
string name = nameof (count); // name is "count"