1.创建引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。
2.和其他方法不同,实例构造器永远不能被继承。
也就是说,类只有自己定义的实例构造器。因为不能被继承,所以实例构造器不能使用以下修饰符:virtual,new,override,sealed和abstract
。
3.如果类的修饰符为abstract
,那么编译器生成的默认构造器的可访问性就为protected
。 如果类的修饰符为static(sealed,abstract)
,那么编译器根本不会在类的定义中生成默认构造器。 如果类没有显示定义任何构造函数,C#
编译器将定义一个默认无参构造器。
在它的实现里,只是简单调用了基类的无参构造器。
public class SomeType{
}
// 等价于
public class SomeType{
public SomeType() : base() {}
}
4.为了使代码“可验证(verifiable)”,类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器,即使派生类的构造器没有显示调用基类构造器,C#
编译器也会自动生成对默认的基类构造器的调用。最终,System.Object
的公共无参构造器会得到调用,尽管它什么都不做。
极少数时候可以在不调用实例构造器的前提下创建类型的实例,比如:Object.MemberwiseClone,System.Rutime.Serialization.FormatterServices
中的GetUninitializedObject
和GetSafeUninitializedObject
。
5.C#
编译器提供了一个简化的语法,允许以“内联”(即嵌入)方法初始化实例字段。但是幕后,它会将这种语法转换成构造器方法中的代码来执行初始化。所以当有多个构造器时,我们应当注意代码膨胀。
internal sealed class SomeType{
private int m_x = 5;
private string m_s = "Hi there";
private double m_d = 3.14159;
private byte m_b;
public SomeType(){}
public SomeType(int x){}
public SomeType(string s){ m_d = 10; }
}
在上面三个构造器中,编译器为每个构造器方法生成初始化m_x,m_s,m_d
的代码,这是多余的。
在这些代码初始化后,编译器会插入对基类构造器的调用,再然后才回插入自己构造器的代码。
例如,对于public SomeType(string s)
的构造器,编译器生成的代码首先初始化m_x,m_s,m_d
,再调用基类(object)
的构造器,在执行自己的代码,这里是覆盖m_d
的值。
可以用优化为以下代码
internal sealed class SomeType{
private int m_x = 5;
private string m_s = "Hi there";
private double m_d = 3.14159;
private byte m_b;
public SomeType(){
m_x = 5;
m_s = "Hi there";
m_d = 3.14159;
m_b = 0xff;
}
public SomeType(int x) : this() {}
public SomeType(string s) : this() { m_d = 10; }
}
6.对于值类型结构体(struct
),编译器不会为其生成默认的构造器,并且即使定义了构造器,也要显式调用才会执行。
internal struct Point{
public int m_x, m_y;
public Point(int x, int y){
m_x = x;
m_y = y;
}
}
internal sealed class Rectangle{
public Point m_topLeft, m_bottomRight;
public Rectangle(){
// 需要显式调用
m_topLeft = new Point(1, 2);
m_bottomRight = new Right(100, 200);
}
}
对于下面的代码,m_x,m_y
字段将被初始化为0,因为没有显式调用Point构造器
internal struct Point{
public int m_x, m_y;
public Point(){
m_x = 5;
m_y = 6;
}
}
internal sealed class Rectangle{
public Point m_topLeft, m_bottomRight;
public Rectangle(){}
}
当然你会发现上面的代码编译不能通过,编译器会提示错误“结构不能包含显式的无参数构造器”。
值得注意的是,虽然C#不允许值类型带有无参构造器,但是CLR允许
,所以你可以用IL来定义无参构造器。
由于C#不允许值类型定义无参构造器,所以以下类型也会编译失败:
internal struct SomeValType{
// 结构中不能有实例字段初始值设定项
private int m_x = 5;
}
当然,在访问值类型的任何字段之前,需要对全部字段进行赋值,否则编译器报错:在控制返回到调用方之前,字段“SomeValType.m_y
”必须完全赋值。
internal struct SomeValType{
private int m_x, m_y;
// C#允许定义有参构造器
public SomeValType(int x){
m_x = x;
// m_y没有初始化
}
}
可以用下方案
public SomeValType(int x){
this = new SomeValType(); // 这里字段都会被初始化为0/null
m_x = x; // 用x覆盖m_x的0
}
this代表值类型本身的一个实例,用new创建的值类型的一个实例可以赋值给this。在new的过程中,会将所有字段设置为0。而在引用类型中this被认为是只读的,所以不能被赋值。
7.类型构造器,也称静态构造器(即静态构造函数)C#
总是会把它设置为private
的,因为对它的调用是由CLR
负责的。类型构造器是线程安全的,CLR
保证它有且被执行一次。
internal sealed class SomeType{
private static int s_x = 5;
static SomeType(){
s_x = 10;
}
}
在上述列子中,C#编译器只生成一个类型构造器方法。它首先将s_x初始化为5,再把它修改为10。也就是说,当C#编译器为类型构造器生成IL代码时,它首先生成的是初始化静态字段所需的代码,然后才会添加你的类型构造器方法中显式包含的代码。
8.转换操作符是将对象从一张类型转换成另一种类型的方法,并且CLR
规范要求转换操作符重载方法必须是public
和static
方法。
public sealed class Rational
{
public Rational(int num) { }
private float toFloat() => float.MaxValue;
/// <summary>
/// 由一个int隐式构造并返回Rational
/// </summary>
/// <param name="num"></param>
public static implicit operator Rational(int num)
{
return new Rational(num);
}
/// <summary>
/// 由一个Rational显式返回float
/// </summary>
/// <param name="num"></param>
public static explicit operator float(Rational num)
{
return num.toFloat();
}
}
在C#
中,implicit
关键字告诉编译器为了生成代码来调用方法,不需要在源代码中进行显示转型。相反,explicit
关键字告诉编译器只有在发现了显式转型时,才调用方法。
Rational r = 5;
float f = (float)r;
9.扩展方法可以定义在接口、枚举和委托上。
任何表达式,只要它最终的类型实现了IEnumerable<T>
接口,就能调用下面的扩展方法。
public static void ShowItems<T>(this IEnumerable<T> collection){
foreach (var item in collection)
Console.WriteLine(item);
}
// 下面展示如何调用
Action<object> action = o => Console.WriteLine(o.GetType()); // 抛出NullReferenceException
action.InvokeAndCatch<NullReferenceException>(null); // 不会抛出错误,因为异常被捕获了
并且C#编译器允许创建委托来引用一个对象上的扩展方法
public static void Main(){
Action a = "Jeff".ShowItems;
}
在C#中,一旦用this关键字标记了某个静态方法的第一个参数,编译器就会在在内部向该方法应用一个定制特性[Extension]
。该特性会在最终生成文件的元数据中持久性地存储下来。不仅如此,任何静态类只要包含了至少一个扩展方法,它的元数据中也会应用这个特性,当然任何程序集中只要包含了一个符合上述特点的静态类,它的元数据中也会应用这个特性。这样一来,编译器就能快速扫描引用的程序集,判断哪些包含了扩展方法。
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
public sealed class ExtensionAttribute : Attribute
{
}
}