8.1实例构造器和类(引用类型)
构造器(constructor)是允许将类型的实例初始化为良好状态的一种特殊方法。
编译后,构造器方法在“方法定义元数据表”中始终叫.ctor。
创建一个引用类型的实例,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象实例的初始状态。
实例构造器永远不能被继承,派生类会自动调用基类的构造函数,也就是说类只有类自己定义的实例构造器。不能将以下修饰符应用于实例构造器:virtual, new, override, sealed和abstract。(实例构造器永远不能被继承,因为如果带参数的构造函数写了很多个,那用哪一个呢?)
如果你定义的类没有显示定义任何构造器,C#编译器将自动隐式生成一个默认(无参)构造器,同时将字段初始化为它们的默认值。看如下代码:
public class A { }
//可以理解为它已经存在一个如下的构造函数
public class A { public A() { } }
派生类构造函数自动调用基类的不带参数的构造函数,看以下代码:
public class B : A { public B() { } }
//相当于
public class B : A { public B() : base() { } }
基类中带参数的构造函数必须显式调用,如下:
public class A { public A() { } public A(string str) { } } public class B : A { public B() : base("aaa") { } }
base关键字用于从派生类中访问基类的成员:https://msdn.microsoft.com/zh-cn/library/hfw7t1ce.aspx
v 调用基类上已被其他方法重写的方法。
v 指定创建派生类实例时应调用基类的构造方法。
什么是抽象类:
抽象类提供多个派生类共享基类的公共定义,它既可以提供抽象方法,也可以提供非抽象方法。
抽象类不能实例化,必须通过继承由派生类实现其抽象方法,因此对抽象类不能使用new关键字,也不能被密封。
如果派生类没有实现所有的抽象方法,则该派生类也必须声明为抽象类。另外,实现抽象方法由overriding方法来实现。
抽象类具有以下特性:
v 抽象方法是隐式的虚方法。
v 抽象类不能实例化。
v 不能用 sealed修饰符修饰抽象类,因为这两个修饰符的含义是相反的。
采用 sealed 修饰符的类无法继承,而 abstract 修饰符要求对类进行继承。
v 从抽象类派生的非抽象类必须包括继承的所有抽象方法和抽象访问器的实际实现。
v 抽象类可以包含抽象方法和抽象访问器。
v 只容许在抽象类中使用抽象方法声明。
v 因为抽象方法声明不提供实际的实现,所以没有方法体。
方法声明只是以一个分号结束,并且在签名后没有大括号“{}”。
v 在派生类中,通过包括使用override修饰符的属性声明,可以重写抽象的继承属性。
abstract class ShapesClass
{
abstract public int Area();
}
class Square : ShapesClass
{
int side = 0;
public Square(int n)
{
side = n;
}
// Area method is required to avoid
// a compile-time error.
public override int Area()
{
return side * side;
}
static void Main()
{
Square sq = new Square(12);
Console.WriteLine("Area of the square = {0}", sq.Area());
}
}
抽象方法和虚方法最重要的区别:
v 抽象方法不能实例化,要子类必须强制性的覆盖它的方法 。
而虚方法则是提供了选择,可以覆盖可以不覆盖,继承基类中的虚方法。
虚拟方法必须有一个实现部分,并为派生类提供了覆盖该方法的选项。
相反,抽象方法没有提供实现部分,强制派生类覆盖方法(否则 派生类不能成为具体类)。
v abstract方法只能在抽象类中声明,虚方法则不是。
v abstract方法必须在派生类中重写,而virtual则不必。
v abstract方法不能声明方法实体,虚方法则可以。
如果类的修饰符(modifier/modify declarations)为abstract,那么编译器生成的默认构造器的可访问性就为protected。
一个类型可以定义多个实例构造器。为了使代码可验证,类的实例构造器在访问从基类继承的任何字段前,必须先调用基类的构造器。
如果派生类的构造器没有显式调用基类的构造器,那么C#编译器会自动生成对默认的基类构造器的调用。
在极少数的情况下,可以在不调用实例构造器的前提下创建一个类型的实例。一个典型的例子是Object的MemberwiseClone方法。
该MemberwiseClone方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据复制到新对象中。
C#语言提供了一个简单的语法,允许在构造引用类型的一个实例时,对类型中定义的字段进行初始化。换句话说,允许以内联(inline)方法初始化实例字段。
8.2实例构造器和结构(值类型)
值类型构造器的工作方式与引用类型的构造器截然不同。
CLR总是允许创建值类型的实例,并且没有办法阻止值类型的实例化。所有,值类型其实并不需要定义构造器,C#编译器根本不会为值类型生成默认的无参构造器。
8.3类型构造器
类型构造器也称为静态构造器、类构造器或者类型初始化器。
类型构造器的作用是设置类型的初始状态。实例构造器的作用是设置类型的实例的初始状态。
默认情况下,类型没有定义类型构造器。类型构造器永远没有参数。
如下,C#为引用类型和值类型定义一个类型构造器:
internal sealed class SomeRefType { static SomeRefType() { } } internal struct SomeValType { static SomeValType() { } }
类型构造器的特点是:无参,static标记,而且可访问性都是private,但是不能显示指定为private。
定义类型构造器类似于定义无参实例构造器,区别在于必须将它们标记为static。但C#会自动把类型构造器标记为private。事实上如果在源代码中显示将类型构造器标记为private,C#编译器会显示以下错误消息“静态构造函数中不允许出现访问修饰符”。必须是私有的原因是,为了阻止任何由开发人员写的代码调用它,对它的调用总是由CLR完成的。
类型构造器的调用比较麻烦。当JIT编译器编译一个方法时,它会检查代码里面是否引入了其他类型。如果引入了其他类型的类型构造器,则JIT编译器会检测是否已经在AppDomain里面执行过。如果没有执行,则发起对类型构造器的调用,否则不调用。
多个线程同时调用某个类型的静态构造器时,如何确保构造器仅仅被执行一次:
在编译之后,线程会开始执行并最终获取调用构造函数的代码。实际上有可能是多个线程执行同一个方法,CLR想要确保一个类型构造器在一个AppDomain里面只执行一次。当一个类型构造器被调用时,调用的线程会获取一个互斥的线程同步锁,这时如果有其他的线程在调用,则会阻塞。第一个线程会执行静态构造器中的代码。当第一个线程执行完后离开,其他的线程被唤醒并发现构造器的代码执行过了,所以不会继续去执行了,从构造器方法返回。CLR通过这种方式来确保构造器仅仅被执行一次。
由于CLR会确保类型构造器在每一个AppDomain里面只会执行一次,是线程安全的。所以如果要初始化任何单例对象(singleton object),放在类型构造器里面是再合适不过了。
类型构造器里面的代码只能访问类型的静态字段,它的常规用途是初始化这些字段。C#提供了简单的语法来初始化类型的静态字段:
internal sealed class SomeType { private static Int32 s_x = 5; }
上面的代码生成时,编译器自动回SomeType创建一个类型构造器如下:
internal sealed class SomeType { private static Int32 s_x; static SomeType() { s_x = 5; } }
但是,C#不允许值类型使用内联字段初始化语法来实例化字段,所以下面这种方式就是错的:
internal sealed struct SomeType { private Int32 s_x = 5; //这样会报错,需要加static关键字 }
如果显式的定义了类型构造器,如下:
internal sealed class SomeType { private static Int32 s_x = 5; static SomeType() { s_x = 10; } }
最终s_x的结果是10。这里,C#编译器首先会生成一个类型构造器方法,这个构造器首先初始化s_x为5,然后初始化为10。换句话说,在类型构造器里面的显示定义的代码会在 使用内联字段初始化语法来实例化静态字段之后执行。
只有当AppDomain卸载时,类型才会卸载。
类型构造器的性能(不懂)
8.4操作符重载方法
有的编程语言允许一个类型定义操作符应该如何操作类型的实例。比如System.String重载了相等(==)和不等(!=)操作符。CLR对操作符重载一无所知,它甚至不知道什么是操作符。是编程语言定义了每个操作符的含义,以及当这些操作符出现时,应该生成什么样的代码。
例如在C#中,向基元数字应用+符合,编译器会生成将两个数加到一起的代码。将+操作符应用于String对象,C#编译器会生成将两个字符串连接到一起的代码。
编译 源代码时,编译器会生成一个标识操作符行为的方法。CLR规范要求操作符重载方法必须是public和static方法。
以下C#代码中展示了一个类中定义的操作符重载方法:
namespace HelloCSharp { class OperatorTest { public int Value { get; set; } public static void Main() { OperatorTest o1 = new OperatorTest(); o1.Value = 11; OperatorTest o2 = new OperatorTest(); o2.Value = 22; OperatorTest o3 = o1 + o2; Console.WriteLine(o3.Value); Console.ReadKey(); } public static OperatorTest operator +(OperatorTest o1, OperatorTest o2) { OperatorTest o = new OperatorTest(); o.Value = o1.Value + o2.Value; return o; } } }
C# 允许用户定义的类型通过使用 operator 关键字定义静态成员函数来重载运算符。注意必须用public修饰,必须是类的静态的方法。同时,重载相等运算符(==)时,还必须重载不相等运算(!=)。< 和 > 运算符以及 <= 和 >= 运算符也必须成对重载。
8.5转换操作符方法
以后重看
8.6扩展方法
由于StringBuilder是可变的(mutable),所以它是处理字符串方法的首选。现在假定你想亲自定义一些缺失的方法,以方便操作一个StringBuilder。列如,StringBuilder中没有自定义的IndexOf方法,你也许想自己定义一个IndexOf方法。
C#扩展方法所做的事情是它允许你定义一个静态方法,并用实例方法的语法来调用它。为了将Indexof方法转变成扩展方法,只需在第一个参数前添加this关键字:
public static class StringBuilderExtensions { public static Int32 IndexOf(this StringBuilder sb, Char value) { for (Int32 index = 0; index < sb.Length; index++) { if (sb[index] == value) return index; } return -1; } }
当C#编译器看到以下代码:
public class TestProgram { public static void Main() { StringBuilder sb = new StringBuilder("Hello. My name is Chris."); Int32 index = sb.IndexOf('!'); //Int32 index1 = StringBuilderExtensions.IndexOf(sb, '!'); } }
StringBuilderExtensions.IndexOf(sb, '!')影响了我们对代码行为的理解,StringBuilderExtensions的使用显得“小题大做”,造成程序员无法专注于当前要执行的操作:IndexOf。
编译器首先检查StringBuilder类或者它的任何基类是否提供了获取单个Char参数、名为IndexOf的一个实例方法。如果存在这样的一个实例方法,编译器会生成IL代码来调用它。如果没有发现匹配的实例方法,则继续检查是否存在任何静态类定义了一个名为IndexOf的静态方法。静态方法中的第一个参数的类型是和当前用于调用方法的那个表达式的类型匹配的一个类型,并且这个类型必须使用this关键字来标识。在本例中,表达式是sb,它是StringBuilder类型。编译器会查找一个静态IndexOf方法,它有两个参数:一个StringBuilder(用this关键字进行标记),以及一个Char。编译器发现了这个IndexOf方法,并生成IL代码来调用这个静态方法。
String(引用类型)的不变性(immutable):
v String最为显著的一个特点就是它具有恒定不变性:一旦创建了一个String对象,在managed heap 上为他分配了一块连续的内存空间,我们将不能以任何方式对这个String进行修改使之变长、变短、改变格式(不能修改String对象的值)。所有对这个String进行各项操作(比如调用ToUpper获得大写格式的String)而返回的String,实际上是另一个重新创建的String,其本身并不会产生任何变化。每次使用 String 类中的方法之一或进行运算时(如赋值、拼接等)时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。
v StringBuilder此类表示值为可变字符序列的类似字符串的对象。之所以说值是可变的,是因为在通过追加、移除、替换或插入字符而创建它后可以对它进行修改。大多数修改此类的实例的方法都返回对同一实例的引用。实例的 int Capacity 属性,它表示内存中为存储字符串而物理分配的字符串总数。该数字为当前实例的容量。容量可通过 Capacity 属性或 EnsureCapacity 方法来增加或减少,但它不能小于 Length 属性的值。
注: .NET Framework中可变集合类如ArrayList 的Capacity 属性也类似这种自动分配机制。
8.6.1规则和原则
v C#只支持扩展方法,不支持扩展属性、扩展事件、扩展操作符等。
v 扩展方法(第一个参数前面有this的方法)必须在非泛型的静态类中声明。类名没有限制。扩展方法至少要有一个参数,而且只有第一个参数能用this关键字标记。
v C#编译器查找这些静态类中定义的扩展方法时,要求这些静态类本身必须具有文件作用域。换言之,此静态类不能嵌套在另一个类中。
v 扩展方法有潜在的版本控制问题。如果Microsoft未来为StringBuilder添加了IndexOf实例方法,那么在重新编译我们的代码时,编译器会重新绑定Microsoft的IndexOf的实例方法,而不是我们的静态IndexOf方法。
8.6.2用扩展方法扩展各种类型
8.6.3 ExtensionAttribute类
8.7分部方法
分部方法partial method在分部类型的一个部分中定义它的签名,并在该类型的另外一个部分中定义它的实现。
//工具生成的代码,存储在某个源代码文件中 internal sealed partial class Base { private String m_name; //分布方法的声明 partial void OnNameChanging(String value); public String Name { get { return m_name; } set { OnNameChanging(value.ToUpper()); m_name = value; } } } //开发人员生成的代码,存储在另一个源代码文件中 internal sealed partial class Base { //分部方法的实现 partial void OnNameChanging(String value) { //Calling the base class OnNameChanging method: //base.OnNameChanging(value); if (String.IsNullOrEmpty(value)) { throw new ArgumentNullException(value); } } }
分部方法规则和原则:
v 它们只能在分部类中声明。
v 分部方法的返回类型始终是void,任何参数都不能用out修饰符标记(out和ref的区别就是传入的参数是否已经初始化了)。
原因:如果不是返回null,同时没有提供实现,那么调用一个未实现的方法,返回什么才合理呢?为了避免对返回值进行任何无端的猜测,c#的设计者决定只允许方法返回void。
v 分部方法的声明和实现必须具有完全一致的方法签名。
v 如果没有对应的实现部分,便不会在代码中创建一个委托来引用这个分部方法。
v 分部方法总是被视为隐式的private方法。但是C#编译器禁止你在分部方法声明之前添加访问修饰符关键字。
v 工具生成的代码,分布方法的声明要用partial关键字标记,无主体,没有方法实现。
v 开发者自己的代码中,分布方法的声明也要用partial关键字标记,有主体,有方法实现。
分部方法允许一个方法而不需要实现。如果没有实现分部方法,编译器会自动移除方法签名,不会生成任何代表分部方法的元数据。编译器也不会生成任何调用分部方法的IL指令。而且编译器也不会生成对本该传给分部方法的实参进行求值的IL的指令。在这个例子中,编译器不会生成调用ToUpper方法的指令。结果就是更少的元数据/IL,运行时的性能也得到大幅提升。