Fork me on GitHub

The C# Programming Language(Third Edition) Part II

类是一种数据结构,可以包括:数据成员(常量和字段),函数成员(方法、属性、事件、索引,操作符、实例构造函数、析构函数和静态构造函数),以及嵌套类。

抽象类:abstract修饰符可以用来指明一个类是不完整的,并且它只能被用作基础类。抽象类和非抽象类的区别,

   抽象类不能被直接实例化,直接在抽象类上使用new操作符会产生编译期错误。尽管编译期类型时抽象的类也可以拥有字段和值,不过这样的字段和值要么是null,要么包含了从抽象类型继承而来的非抽象类的实例的引用。

   抽象类可以包含抽象成员。

   抽象类不可以是密封的。

非抽象类继承一个抽象类时,这个非抽象类必须包含所有继承抽象成员的实际实现。

abstract class A

{

      public abstract void F();

}

abstract class B:A

{

      public void G() {}

}

class C:B

{

      public override void F(){ ... }

}

 

密封类

sealed修饰符可以用来禁止一个类被继承。密封类不可以是抽象类。

 

静态类

static修饰符用来将一个类声明为静态类。静态类不可以被实例化,不可以被当做类型来使用,并且只能包含静态成员。只有静态类才可以包含展开函数。

常量和嵌套类型都会被归纳为静态成员。

 

partial修饰符

partial修饰符可以用来指明这个类声明是一个局部的类型声明。

 

类型参数

类型参数是一个简单的标识符,它作为被提供的类型实参的占位符来创建构造类型。

 

类型参数限制

泛型类型和方法声明可以选择通过包含类型形参限制子句来制定类类型形参的限制。

每个类型形参限制子句由标记where,加上类型形参的名字,加上一个冒号和那个类型形参限制的列表所组成。

 

局部方法

局部方法可以在类型声明的一个部分里定义,并且在另一个部分里实现。这个实现是可选的,如果没有实现这个局部方法,那么局部方法的声明和合所有对它的调用都会在组合时被移除。

局部方法只能在局部类或局部结构里声明。它不能声明在非局部类型或接口里。

局部方法不能定义访问修饰符,它们都是隐式私有的。它们的返回类型必须为void,并且它们的形参不可以带有out修饰符。

partial class Customer

{

string name;

public string Name{

      get { return name; }

      set {

               OnNameChanging(value);

               name = value;

               OnNameChanged();

      }

}

partial void OnNameChanging(string newName);

partial void OnNameChanged();

}

partial class Customer

{

      partial void OnNameChanging(string newName)

      {

            Console.WriteLine("changing" + name + "to" + newName);

      }

      partial void OnNameChanged()

      {

            Console.WriteLine("Changed to"+name);

      }

}

 

类成员

常量,代表了和类相关联的常数值。

字段,也就是类的变量。

方法,实现了类可以进行的计算和操作。

属性,定义了命名特性以及读写这些特性的操作。

事件,定义了类可以生成的通知。

索引,让类的实例可以像数组一样被索引。

操作符,定义了可以作用于类实例之上的表达式操作符。

实例构造函数,实现了初始化类所需的操作。

析构函数,实现了再彻底丢弃类实例之前要进行的操作。

静态构造函数,实例了初始化类本身所需的操作。

类型,代表了类的本地类型。

 

构造类型的成员

构造类型的非继承成员可以通过将成员声明里的每一个类型形参替换成构造类型里相应的类型实参来获得。替换过程是基于类型声明的羽翼含义,而非简单的文本替换。

class Gen<T,U>

{

      public T[,]  a;

      public void G(int i,T t,Gen<U,T> gt) {...}

      public U Prop{ get {...} set{...} }

      public int H(double d){...}

}

构造类型Gen<int[],IComparable<string>>具有以下成员:

public int[,][] a;

public void G(int i,int[] t,Gen<IComparable<string>,int[]> gt) {...}

public IComparable<string> Prop{ get{...} set{...} }

public int H(double d) {...}

在泛型类型声明Gen里成员a的类型时“T的二维数组”,所以上面给出的构造类型里成员a的类型就是“一维int数组的二维数组”,或int[,][]。

 

 继承

一个类会从它的直接基类类型里继承成员。意思是一个类会隐式地包含它的直接基类类型的所有成员,除了基类的实例构造函数、析构函数和静态构造函数以外。

 

 new修饰符

类成员声明允许声明和继承成员相同名字或签名的成员。这时,我们就说继承如果类成员隐藏了基类成员。可以用new修饰符来告知编译器这个隐藏是有意图的。

 

嵌套类型

非嵌套类型可以具有public或者internal声明可访问性,默认是internal。嵌套类型可访问性:

声明在类里的嵌套类型可以具有5种声明可访问性的任意一个(public,protected,internal.protected,internal,private).

声明在结构里的嵌套类型可以具有3种形式的声明可访问性(public,internal,private).

嵌套类也可以通过new修饰符来明确隐藏意图。

 

this访问

嵌套类型里的this不可以用来指向包含它的类型里的实例成员。当嵌套类型需要访问包含它的类型里的实例成员时,可以将包含类型实例的this作为嵌套类型的构造函数实参提供给这个访问。

class C

{

      int i = 123;

      public void F(){

             Nested n = new Nested(this);

             n.G();

      }

     

      public class Nested

      {

               C this_c;

               public Nested(C c){

                      this_c = c;

                }

                public void G(){

                      Console.WriteLine(this_c.i);

                }

       }

}

 

保留成员名

对于每一个属性、事件或索引的源成员声明,实现都必须根据成员声明的种类保留两份方法签名。

为属性保留的成员名

对于类型T的属性P来说,下面的签名都是保留的:

T get_P;

void set_P(T value);

例子

using System;

class A
{
      public int P{
        get { return 123; }
      }
}

class B:A
{
  new public int get_P(){
       return 456;
  }
  new public void set_P(int value){}
}

class test
{
static void Main()
{
   B b = new B();
   A a = b;
   Console.WriteLine(a.P);
   Console.WriteLine(b.P);
   Console.WriteLine(b.get_P());
}
}

(但是实际编译时,我这里产生:成员“B.set_P(int)”不会隐藏继承的成员。不需要关键字 new。的警告)

对于类型T的事件E来说,下面签名都是保留的:

void add_E(T handler);

void remove_E(T handler);

对于带有参数列表L的类型T的索引来说,下面的签名都是保留的:

T get_Item(L);

void set_Item(L,T value);

 

常量

常量是一个表示了常数值的类成员,即可以在编译期就计算出来的值。常量声明可以引入一个给定类型的一或多个常量。

因为new操作符不能用在常量表达式里,所以string以外的引用类型的常量唯一可能的值只有null。

当需要一个常量值的符号名,但那个值的类型不能出现在常量声明里,或者那个值不能在编译期被常量表达式计算出来的时候,就会采用readonly字段。

 

字段

字段是一个成员,表示了和一个对象或类关联的字段。

字段的类型必须至少和字段本身具有相同的可访问性。

当字段声明包含了static修饰符时,声明所引入的字段就是静态字段。当没有时,声明所引入的就是实例字段。

当一个字段声明包含了readonly修饰符时,声明所引入的字段就是只读字段。

静态只读字段作为常量使用,作为常量的替代方案。

当字段声明包含了volatile修饰符时,声明所引入的字段被称为易失字段。

当一个类初始化的时候,那个类的所有静态字段会先被初始化为默认值,然后再按照文本顺序依次执行静态字段初始化语句。

class Test

{

     static int a = b + 1;

     static int b = a + 1;

     static void Main(){

            Console.WriteLine("a ={0},b={1}",a,b);

     }

}

a = 1,b = 2

 

方法

方法是一个实现了可以作用于对象或者类的计算或操作的成员。

当一个实例方法声明包含了virtual修饰符时,这个方法就是一个虚拟方法。没有virtual修饰符时,这个方法就是非虚拟方法。

虚方法的实现可以被继承类取代。取代一个继承的虚拟方法实现的过程就叫做覆写那个方法。

在虚拟方法的调用里,调用发生的那个实例的运行时类型决定了实际要调用的是哪一个方法实现。在非虚拟的方法调用里,实例的编译期类型才是决定因素。

using System;

class A

{

      public void F() { Console.WriteLine("A.F"); }

      public virtual void G() { COnsole.WriteLine("A.G"); }

}

class B:A

{

      new public void F() { Console.WriteLine("B.F"); }

      public override void G() { Console.WriteLine("B.G"); }

}

class Test

{

      static void Main(){

            B b = new B();

            A a = b;

            a.F();      // A.F

            b.F();      // B.F

            a.G();      // B.G

            b.G();      // B.G

}

这里实例(就是B)的运行时类型(而不是实例(就是A)的编译期类型)决定了实际要调用的方法实现。

当实例方法声明里包含了override修饰符时,这个方法就是一个覆写方法。虚拟方法声明引入了一个新方法,而覆写方法声明则是通过为那个方法提供一份新的实现来专有化一个现存的继承虚拟方法。覆写基础方法是virtual、abstract或override方法。

覆写声明可以通过基础访问来访问覆写基础方法。

class A

{

      int x;

      public virtual void PrintFields(){

          Console.WriteLine("x={0}",x);

      }

}

class B:A

{

      int y;

      public override void PrintFields(){

             base.PrintFields();

             Console.WriteLine("Y =  {0}",y);

      }

}

基础访问禁止了虚拟调用机制,并且将base方法直接当成一个非虚拟方法。

class A

{

      public virtual void F() {}

}

class B:A

{

      new private void F() {}     //hides A.F within body of b

}

class C:B

{

      public override void F() {}      //okay,overrides A.F

}

B里的F方法隐藏了从A继承的虚拟方法F。因此在B里的新F具有private访问权限,所以它的作用域只包含了B的类主体,而不会扩展到C。

 

当实例方法声明里包含了sealed修饰符时,这个方法就是一个密封方法。如果一个实例方法声明包含了sealed修饰符,那么它必须还包括override修饰符。使用sealed修饰符可以防止继承进一步覆写这个方法。

 

当实例方法声明包含了abstract修饰符时,这个方法就是一个抽象方法。

抽象方法声明可以覆写虚拟方法。这种行为让一个抽象类可以强制它的继承类重新实现这个方法,并且让方法原来的实现无效。

 

当方法声明包含了extern修饰符时,这个方法就是一个外部方法。extern修饰符通常是和DllImport特性一起使用,这样就可以让外部方法由DLL来实现了。

 

当方法声明包含partial修饰符时,这个方法就是一个局部方法。

 

当方法的第一个形参包含了this修饰符时,这个方法就是一个扩展方法。扩展方法只能在非泛型、非嵌套的静态类里声明。

 

属性

属性是一个可以访问对象或类的特征成员。和公共字段不同,属性在对象的内部状态和它的公共接口之间提供了一层抽象。

class Label
{
 private int x, y;
 private string caption;
 public Label(int x, int y, string caption) {
  this.x = x;
  this.y = y;
  this.caption = caption;
 }
 public int X {
  get { return x; }
 }
 public int Y {
  get { return y; }
 }
 public Point Location {
  get { return new Point(x, y); }
 }
 public string Caption {
  get { return caption; }
 }
}
Label类使用了两个int字段x和y来保存它的位置。如果将来的版本的Lable里,选择将其位置更方便地作为Point保存在内部,

class Label
{
 private Point location;
 private string caption;
 public Label(int x, int y, string caption) {
  this.location = new Point(x, y);
  this.caption = caption;
 }
 public int X {
  get { return location.x; }
 }
 public int Y {
  get { return location.y; }
 }
 public Point Location {
  get { return location; }
 }
 public string Caption {
  get { return caption; }
 }
}

}

 

当属性被指定义为一个自动实现的属性时,那个属性会自动拥有一个隐藏的字段,并且会为读写这个字段实现访问器。

 

事件

事件是一个可以让对象或类发出通知得成员。

事件声明的类型必须是委托类型,而且必须有相同的可访问性。

在包含了时间声明的类或结构的程序文本里,有些事件可以被当做字段来使用。

 

索引

索引时一个能让成员想数组一样被访问的成员。实现了一个可以访问bit数组里单个bit的索引器。

using System;
class BitArray
{
 int[] bits;
 int length;
 public BitArray(int length) {
  if (length < 0) throw new ArgumentException();
  bits = new int[((length - 1) >> 5) + 1];
  this.length = length;
 }
 public int Length {
  get { return length; }
 }
 public bool this[int index] {
  get {
   if (index < 0 || index >= length) {
    throw new IndexOutOfRangeException();
   }
   return (bits[index >> 5] & 1 << index) != 0;
  }
  set {
   if (index < 0 || index >= length) {
    throw new IndexOutOfRangeException();
   }
   if (value) {
    bits[index >> 5] |= 1 << index;
   }
   else {
    bits[index >> 5] &= ~(1 << index);
   }
  }
 }
}

 

操作符

操作符是一个定义了可以应用于类实例之上的表达式操作符含义的成员。

装换操作符声明引入了一个自定义转换来补充预定义的隐式和显示转换。含有implicit关键字的转换操作符声明引入的是一个自定义的隐式转换。含有explicit关键字的转换操作符声明的是一个自定义的显示装换。

 

实例构造函数

实力构造函数是一个实现了药初始化一个类实例所必须进行的操作的成员。

字段初始化语句会被转换成赋值语句,而这些赋值语句又会在调用基类实例构造函数之前执行。

using System;
class A
{
 public A() {
  PrintFields();
 }
 public virtual void PrintFields() {}
}
class B: A
{
 int x = 1;
 int y;
 public B() {
  y = -1;
 }
 public override void PrintFields() {
  Console.WriteLine("x = {0}, y = {1}", x, y);
 }
}
当使用new B()来创建B的实例时,输出:

x = 1, y = 0

y的值为0,是因为对y的赋值直到基类构造函数返回以后才会执行。

如果一个类没有包含实例构造函数声明,那么就会为它自动提供一个默认的实例构造函数。默认构造函数会调用直接基类的无参构造函数。如果没有,就会编译错误。

当一个类T只声明了私有实例构造函数,那么在类T的程序文本之外是不可能继承T或是直接创建T的实例的。(单例模式)

 静态构造函数是一个实现了初始化一个封闭类类型所需操作的成员。

using System;
class Test
{
 static void Main() {
  A.F();
  B.F();
 }
}
class A
{
 static A() {
  Console.WriteLine("Init A");
 }
 public static void F() {
  Console.WriteLine("A.F");
 }
}
class B
{
 static B() {
  Console.WriteLine("Init B");
 }
 public static void F() {
  Console.WriteLine("B.F");
 }
}
会产生下面的输出:

Init A
A.F
Init B
B.F
这里,A的静态构造函数是由A.F的调用触发的,而B的静态构造函数是由对B.F的调用触发的。

using System;
class A
{
 public static int X;
 static A() {
  X = B.Y + 1;
 }
}
class B
{
 public static int Y = A.X + 1;
 static B() {}
 static void Main() {
  Console.WriteLine("X = {0}, Y = {1}", A.X, B.Y);
 }
}
会产生下面的输出:
X = 1, Y = 2

 

迭代器

用迭代器块实现的函数成员被称为迭代器。

迭代器引入额外的开销,务必谨慎使用。

 

posted @ 2010-02-28 10:34  idoku  阅读(233)  评论(0编辑  收藏  举报