创建引用类型的实例时的步骤:
首先,为实例的数据字段分配内存;
接着,初始化对象的系统开销字段(类型对象指针和同步块索引);
最后,调用类型的实例构造器设置对象的初始状态。
ctor不能被继承,不能用virtual,new,override,sealed,abstract。
如果类中没有显示定义任何ctor,则默认定义一个无参ctor,这个ctor不执行任何语句,只是调用基类的无参ctor;当然,如果类中有ctor,则不存在这个默认的无参ctor。
如果类为abstract的,则默认的ctor是protected;否则这个默认ctor都是public的。
如果类为static的,则不会生成默认的ctor。
如果基类A中没有提供无参ctor,这里,我们只考虑A中只有一个有参ctor(如果连这个ctor也没有,那么A就是上面的那种情况了),这时,子类B必须显示调用基类的ctor,否则不能编译,如下所示:
public class A
{
public A(int t)
{
}
}
public class B : A
{
public B(int t) : base(t) //必须显示调用
{
}
}
{
public A(int t)
{
}
}
public class B : A
{
public B(int t) : base(t) //必须显示调用
{
}
}
最终,都会调用到System.Object的公有无参ctor。
不需要实例构造器的时候:反序列化;Object.MemberwiseClone()方法。
内联方法,其实就是在默认无参ctor中赋值。
2.实例构造器ctor(值类型)
1.C#中,struct不能包含显示的无参ctor——CLR允许struct有无参ctor
2.struct一定要显示调用其有参ctor,这样在new新对象的时候,才会执行ctor中的语句
3.CLR会保证所有实例字段(值类型和引用类型)都被初始化为0或者null (如果没有手动设置,就由CLR自动分配)。
4.在struct中,不能使用内联直接给字段赋值(初始化只能在显示ctor中做)
5.每个字段都要在ctor中初始化,不然编译器会报错
3.静态构造器cctor
cctor可以用于接口/引用类型/值类型,但是C#不支持接口。
cctor位于AppDomain中,在类第一次被访问时执行。
cctor不能超过一个(可以没有),而且永远没有参数。
cctor是私有的,但不能显示声明为私有。
在值类型中定义cctor是没有意义的——没有机会执行cctor。
cctor的调用过程:
编译时,JIT编译器将检查AppDomain是否执行了cctor(有记录的),以决定是否生成调用代码;
执行时,如果有多个线程,则需要一个互斥的线程同步锁,以确保只执行一次cctor。
避免在两个类的cctor中互相引用,CLR不能确保先执行哪一个cctor,可以编译,但是返回指不唯一。
如果cctor抛出异常,CLR会认为该类型不可用,与此类型相关的操作都会抛出System.TypeInitializationException异常。
cctor中只能访问静态字段,它的用途就是初始化这些静态字段。也可以使用内联初始化,与cctor效果一样。
cctor不应调用其基类的cctor,两者没有关系。
CLR不支持静态的Finalize()方法。
cctor的性能:
精确语义Precise:针对在cctor中初始化而言;
字段初始化前语义BeforeFieldInit:针对于内联初始化而言。
二者的区别在于是否会在MSIL中的cctor上附加一个BeforeFieldInit标记。
测试代码如下:
class Program
{
static void Main()
{
const Int32 iterations = 1000 * 1000 * 1000;
PrefTest1(iterations);
PrefTest2(iterations);
Console.ReadLine();
}
private static void PrefTest1(Int32 iterations)
{
Stopwatch sw = Stopwatch.StartNew();
for (Int32 i = 0; i < iterations; i++)
{
BeforeFieldInit.s_x = 1;
}
Console.WriteLine("PrefTest1: {0} BeforeFieldInit", sw.Elapsed);
sw = Stopwatch.StartNew();
for (Int32 j = 0; j < iterations; j++)
{
Precise.s_x = 1;
}
Console.WriteLine("PrefTest1: {0} Precise", sw.Elapsed);
}
private static void PrefTest2(Int32 iterations)
{
Stopwatch sw = Stopwatch.StartNew();
for (Int32 i = 0; i < iterations; i++)
{
BeforeFieldInit.s_x = 1;
}
Console.WriteLine("PrefTest2: {0} BeforeFieldInit", sw.Elapsed);
sw = Stopwatch.StartNew();
for (Int32 j = 0; j < iterations; j++)
{
Precise.s_x = 1;
}
Console.WriteLine("PrefTest2: {0} Precise", sw.Elapsed);
}
}
{
static void Main()
{
const Int32 iterations = 1000 * 1000 * 1000;
PrefTest1(iterations);
PrefTest2(iterations);
Console.ReadLine();
}
private static void PrefTest1(Int32 iterations)
{
Stopwatch sw = Stopwatch.StartNew();
for (Int32 i = 0; i < iterations; i++)
{
BeforeFieldInit.s_x = 1;
}
Console.WriteLine("PrefTest1: {0} BeforeFieldInit", sw.Elapsed);
sw = Stopwatch.StartNew();
for (Int32 j = 0; j < iterations; j++)
{
Precise.s_x = 1;
}
Console.WriteLine("PrefTest1: {0} Precise", sw.Elapsed);
}
private static void PrefTest2(Int32 iterations)
{
Stopwatch sw = Stopwatch.StartNew();
for (Int32 i = 0; i < iterations; i++)
{
BeforeFieldInit.s_x = 1;
}
Console.WriteLine("PrefTest2: {0} BeforeFieldInit", sw.Elapsed);
sw = Stopwatch.StartNew();
for (Int32 j = 0; j < iterations; j++)
{
Precise.s_x = 1;
}
Console.WriteLine("PrefTest2: {0} Precise", sw.Elapsed);
}
}
*Stopwatch类,提供一组方法和属性,可以准确地测量运行时间
4.操作符重载
CLR不知道操作符是什么;操作符是C#定义的,相应的生成编译器识别的语言。
操作符重载是public static的,有一元重载和二元操作两种方式。
要求重载方法的参数至少有一个参数与重载方法的类型一样。
运算符参数不能使用ref/out修饰符。
例子:对于运算符+,
在定义时,会在编译器中生成op_Addition()方法,在方法定义表中,这个方法属于specialname组——说明它是一个特殊方法
在调用时,编译器会在specialname组查找相应的op_Addition()方法,如果存在而且方法参数匹配,则执行;否则,编译错误。
对于操作符重载,建议同时定义一个友好的方法,如重载+,同时定义一个Add方法。
操作符语法详见http://www.cnblogs.com/Jax/archive/2007/09/13/891984.html
5.转换操作符方法
System.Decimal是一个很好的学习操作符重载/转换的Sample
为一个Rational定义转换符构造器和方法:
操作符转换也是public static的
public sealed class Rational
{
public Rational(Int32 num)
{
//由Int32构建一个Rational
}
public Rational(Single num)
{
//由Single构建一个Rational
}
public Int32 ToInt32()
{
//将Rational转换为Int32
}
public Single ToSingle()
{
//将Rational转换为Single
}
//将Int32隐式转为Rational,回调Rational(Int32)构造器
public static implicit operator Rational(Int32 num)
{
return new Rational(num);
}
//将Single隐式转为Rational,回调Rational(Single)构造器
public static implicit operator Rational(Single num)
{
return new Rational(num);
}
//将Rational显式转为Int32,回调ToInt32()
public static explicit operator Int32(Rational r)
{
return r.ToInt32();
}
//将Rational显式转为Single,回调ToSingle()
public static explicit operator Single(Rational r)
{
return r.ToSingle();
}
}
{
public Rational(Int32 num)
{
//由Int32构建一个Rational
}
public Rational(Single num)
{
//由Single构建一个Rational
}
public Int32 ToInt32()
{
//将Rational转换为Int32
}
public Single ToSingle()
{
//将Rational转换为Single
}
//将Int32隐式转为Rational,回调Rational(Int32)构造器
public static implicit operator Rational(Int32 num)
{
return new Rational(num);
}
//将Single隐式转为Rational,回调Rational(Single)构造器
public static implicit operator Rational(Single num)
{
return new Rational(num);
}
//将Rational显式转为Int32,回调ToInt32()
public static explicit operator Int32(Rational r)
{
return r.ToInt32();
}
//将Rational显式转为Single,回调ToSingle()
public static explicit operator Single(Rational r)
{
return r.ToSingle();
}
}
由上面代码可以看出,implicit/explicit是对两个构造器和两个ToXXX()方法的包装。
相应的IL代码:
public static Rational op_Implicit(Int32 num)
public static Rational op_Implicit(Single num)
public static Int32 op_Explicit(Rational r)
public static Single op_Explicit(Rational r)
第3个和第四个方法仅仅是返回类型不同,这样的语法只有在IL中允许。可以看到,操作符转换实际上是在利用IL的这一特性。public static Rational op_Implicit(Single num)
public static Int32 op_Explicit(Rational r)
public static Single op_Explicit(Rational r)
6.通过引用向方法传递参数
默认CLR的方法参数都是按值传递的。无论引用还是值类型参数,都是传递一个copy的副本——这样意味着方法可以修改对象,而不对方法外的对象有影响,仅在方法内部有影响。
CLR允许按照引用方式传递参数:out,ref,在声明和调用时都要加上ref/out关键字
二者在CLR中等效,在C#中有区别:
不能将未初始化的参数作为ref传递到方法
//正确使用ref
static void Main()
{
int x = 2;
GetRef(ref x);
}
private static void GetRef(ref int x)
{
x ++;
}
static void Main()
{
int x = 2;
GetRef(ref x);
}
private static void GetRef(ref int x)
{
x ++;
}
//错误使用ref
static void Main()
{
int x; //未初始化,即使方法中赋值也不行
GetRef(ref x);
}
private static void GetRef(ref int x)
{
x = 10;
}
static void Main()
{
int x; //未初始化,即使方法中赋值也不行
GetRef(ref x);
}
private static void GetRef(ref int x)
{
x = 10;
}
在out方法中必须给out参数初始化赋值,这时,无论调用前是否给out参数初始化赋值,out方法中都要赋值,否则会编译错误。示例如下:
//正确使用out
static void Main()
{
//以下两句话都是对的
int x;
int x = 10;
GetOut(out x);
}
private static void GetOut(out int x)
{
x = 10; //一定要初始化,即使调用前已经初始化了
}
static void Main()
{
//以下两句话都是对的
int x;
int x = 10;
GetOut(out x);
}
private static void GetOut(out int x)
{
x = 10; //一定要初始化,即使调用前已经初始化了
}
//错误使用out
static void Main()
{
int x = 10;
GetOut(out x);
}
private static void GetOut(out int x)
{
x ++; //一定要初始化,即使调用前已经初始化了
}
static void Main()
{
int x = 10;
GetOut(out x);
}
private static void GetOut(out int x)
{
x ++; //一定要初始化,即使调用前已经初始化了
}
方法可以基于ref/out可以重载,ref/out也算是方法签名的一部分。但是重载不能仅限于ref和out的差别,如下:
可以有
public sealed class Point
{
static void Add(Point p);
static void Add(ref Point p);
}
不能有{
static void Add(Point p);
static void Add(ref Point p);
}
public sealed class Point
{
static void Add(Point p);
static void Add(ref Point p);
static void Add(out Point p);
}
{
static void Add(Point p);
static void Add(ref Point p);
static void Add(out Point p);
}
用ref实现交换两个引用类型的方法:标准写法,使用泛型
public static void Swap<T>(ref T a, ref T b)
{
T t = b;
b = a;
a = t;
}
public static void SomeMethod()
{
String s1 = "Jax.Bao";
String s2 = "Fish.Xu";
Swap(ref s1, ref s2);
}
{
T t = b;
b = a;
a = t;
}
public static void SomeMethod()
{
String s1 = "Jax.Bao";
String s2 = "Fish.Xu";
Swap(ref s1, ref s2);
}
out/ref在值类型上使用和引用类型上使用行为相同。仅差在值类型实例分配到的是内存,引用类型分配到的是指针。
以后的FileStream可以写成以下模式:
static void Main()
{
FileStream fs = null;
ProcessFiles(ref fs);
for (; fs != null; ProcessFiles(ref fs))
{
fs.Read(.);
}
}
private static void ProcessFiles(ref FileStream fs)
{
if (fs != null)
{
fs.Close();
}
if (noMoreFileToProcess)
{
fs = null;
}
else
{
fs = new FileStream(.);
}
}
{
FileStream fs = null;
ProcessFiles(ref fs);
for (; fs != null; ProcessFiles(ref fs))
{
fs.Read(.);
}
}
private static void ProcessFiles(ref FileStream fs)
{
if (fs != null)
{
fs.Close();
}
if (noMoreFileToProcess)
{
fs = null;
}
else
{
fs = new FileStream(.);
}
}
7.向方法传递可变数量的参数 params关键字
只有最后一个参数可以是params的,而且必须是一个一维数组,可以传递null或0个元素的数组,甚至是不传值(忽略此参数,会默认生成0长的数组作为方法参数)。
调用的时候,不用创建数组对象,直接在方法中传入任意数量的参数。
static void Main()
{
Console.WriteLine(TestParams());
Console.WriteLine(TestParams("1", "2"));
}
public static int TestParams(params string[] p1)
{
return p1.Length;
}
{
Console.WriteLine(TestParams());
Console.WriteLine(TestParams("1", "2"));
}
public static int TestParams(params string[] p1)
{
return p1.Length;
}
8.声明方法的参数类型
原则1:方法参数尽可能指定为最弱的类型
选用IEnumberable<T>,而不是IList<T>或ICollection<T>
原则2:方法返回类型尽可能指定为最强的类型
返回FileStream,而不是Stream
原则3:如果希望在不影响调用代码的情况下,能对方法内部实现进行修改,这时候返回类型要使用最弱类型中的最强的一个。
使用IList<T> ,而不是List<T>
9.CLR不支持常量方法和常量参数