.net 类和结构

3 对象和类型

本章的主要内容如下:

       类和结构的区别

       类成员

       按值和引用传送参数

       方法重载

       构造函数和静态构造函数

       只读字段

       部分类

       静态类

       Object类,其他类型都从该类派生而来

3.1  类和结构

类和结构实际上都是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法。类定义了每个类对象(称为实例)可以包含什么数据和功能。

类与结构的区别是它们在内存中的存储方式(类是存储在堆( heap 托管堆 )上的引用类型,而结构是存储在堆栈(stack)上的值类型)、访问方式和一些特征(如结构不支持继承)。语法上的主要区别是使用关键字struct代替class来声明结构。

3.2  类成员

类中的数据和函数称为类的成员。

3.2.1  数据成员

数据成员包含了类的数据—— 字段、常量和事件。数据成员可以是静态数据(与整个类相关)或实例数据(类的每个实例都有它自己的数据副本)

字段是与类相关的变量。

常量与类的关联方式同变量与类的关联方式一样。使用const关键字来声明常量。

事件是类的成员,在发生某些行为(例如改变类的字段或属性,或者进行了某种形式的用户交互操作)时,它可以让对象通知调用程序。

3.2.2  函数成员

函数成员提供了操作类中数据的某些功能,包括方法、属性、构造函数和终结器(finalizer)、运算符以及索引器。

方法是与某个类相关的函数,它们可以是实例方法,也可以是静态方法。

属性是可以在客户机上访问的函数组,其访问方式与访问类的公共字段类似。

构造函数是在实例化对象时自动调用的函数。它们必须与所属的类同名,且不能有返回类型。构造函数用于初始化字段的值。

终结器类似于构造函数,但是在CLR检测到不再需要某个对象时调用。它们的名称与类相同,但前面有一个~符号。

运算符执行的最简单的操作就是+和–。

索引器允许对象以数组或集合的方式进行索引。

1. 方法

(1) 方法的声明

C#中,定义方法的语法与C风格的语言相同,与C++Java中的语法也相同。与C++的主要语法区别是,在C#中,每个方法都单独声明为publicprivate,不能使用public:块把几个方法定义组合起来。另外,所有的C#方法都在类定义中声明和定义。在C#中,不能像在C++中那样把方法的实现代码分隔开来。

C#中,方法的定义包括方法的修饰符(例如方法的可访问性)、返回值的类型,然后是方法名、输入参数的列表(用圆括号括起来)和方法体(用花括号括起来)

[modifiers] return_type MethodName([parameters])

{

   // Method body

}

(2) 调用方法

C#中调用方法的语法与C++Java中的一样,C#Visual Basic的唯一区别是在C#中调用方法时,必须使用圆括号,这要比Visual Basic 6中有时需要括号,有时不需要括号的规则简单一些。

(3) 给方法传递参数

参数可以通过引用或值传递给方法。在变量通过引用传递给方法时,被调用的方法得到的就是这个变量,所以在方法内部对变量进行的任何改变在方法退出后仍旧发挥作用。而如果变量是通过值传送给方法的,被调用的方法得到的是变量的一个副本,也就是说,在方法退出后,对变量进行的修改会丢失。对于复杂的数据类型,按引用传递的效率更高,因为在按值传递时,必须复制大量的数据。

(4) ref参数

通过值传送变量是默认的,也可以迫使值参数通过引用传送给方法。为此,要使用ref关键字。如果把一个参数传递给方法,且这个方法的输入参数前带有ref关键字,则该方法对变量所作的任何改变都会影响原来对象的值:

 

static void SomeFunction(int[] ints, ref int i)

{

   ints[0] = 100;

   i = 100;       //the change to i will persist after SomeFunction() exits

}

在调用该方法时,还需要添加ref关键字:

SomeFunction(ints, ref i);

(5) out关键字

编译器使用out关键字来初始化。在方法的输入参数前面加上out关键字时,传递给该方法的变量可以不初始化。该变量通过引用传送,所以在从被调用的方法中返回时,方法对该变量进行的任何改变都会保留下来。在调用该方法时,还需要使用out关键字,与在定义该方法时一样:

 

static void SomeFunction(out int i)

{

   i = 100;

}

public static int Main()

{

   int i; // note how i is declared but not initialized

   SomeFunction(out i);

   Console.WriteLine(i);

   return 0;

}

out关键字是C#中的新增内容,在Visual BasicC++中没有对应的关键字,该关键字的引入使C#更安全,更不容易出错。如果在函数体中没有给out参数分配一个值,该方法就不能编译。

(6) 方法的重载

C#支持方法的重载——方法的几个有不同签名(方法名相同、但参数的个数和类型不同)的版本。

2. 属性

属性(property)不太常见,因为它们表示的概念是C#Visual Basic中提取的,而不是从C++/Java中提取的。属性的概念是:它是一个方法或一对方法,在客户机代码看来,它们是一个字段。

C#中定义属性,可以使用下面的语法:

private string foreName;

public string ForeName

{

   get

   {

      return foreName;

   }

   set

   {

      if (value.Length > 20)

         // code here to take error recovery action

         // (eg. throw an exception)

      else

         foreName = value;

   }

}

get访问器不带参数,且必须返回属性声明的类型。也不应为set访问器指定任何显式参数,但编译器假定它带一个参数,其类型也与属性相同,并表示为value

(1) 只读和只写属性

在属性定义中省略set访问器,就可以创建只读属性。同样,在属性定义中省略get访问器,就可以创建只写属性。(但是,这是不好的编程方式,因为这可能会使客户机代码的作者感到迷惑。一般情况下,如果要这么做,最好使用一个方法替代。)

(2) 属性的访问修饰符

C#允许给属性的getset 访问器设置不同的访问修饰符,所以属性可以有公共的get访问器和私有或受保护的set访问器。这有助于控制属性的设置方式或时间。(在get set 访问器中,必须有一个具备属性的访问级别。如果get访问器的访问级别是protected,就会产生一个编译错误,因为这会使两个访问器的访问级别都不是属性。)

public string Name

{

   get

   {

      return _name;

   }

   set

   {

      _name = value;

   }

}

(3) 自动实现的属性

如果属性的setget访问器中没有任何逻辑,就可以使用自动实现的属性。这种属性会自动实现基础成员变量。上例的代码如下:

public string ForeName {get; set;}

不需要声明private string foreName。编译器会自动创建它。

使用自动实现的属性,就不能在属性设置中进行属性的有效性验证。所以在上面的例子中,不能检查foreName是否少于20个字符。但必须有两个访问器。尝试把该属性设置为只读属性,就会出错:

public string ForeName {get; }

但是,每个访问器的访问级别可以不同。因此,下面的代码是合法的:

public string ForeName {get; private set;}

3. 构造函数

(1) 静态构造函数

C#的一个新特征是也可以给类编写无参数的静态构造函数。

编写静态构造函数的一个原因是,类有一些静态字段或属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性。

.NET运行库没有确保静态构造函数什么时候执行,所以不要把要求在某个特定时刻(例如,加载程序集时)执行的代码放在静态构造函数中。也不能预计不同类的静态构造函数按照什么顺序执行。但是,可以确保静态构造函数至多运行一次,即在代码引用类之前执行。在C#中,静态构造函数通常在第一次调用类的成员之前执行。

注意,静态构造函数没有访问修饰符,其他C#代码从来不调用它,但在加载类时,总是由.NET运行库调用它,所以像publicprivate这样的访问修饰符就没有意义了。同样,静态构造函数不能带任何参数,一个类也只能有一个静态构造函数。很显然,静态构造函数只能访问类的静态成员,不能访问实例成员。

注意,无参数的实例构造函数可以在类中与静态构造函数安全共存。尽管参数列表是相同的,但这并不矛盾,因为静态构造函数是在加载类时执行,而实例构造函数是在创建实例时执行,所以构造函数的执行不会有冲突。

 

(2) 从其他构造函数中调用构造函数

有时,在一个类中有几个构造函数,以容纳某些可选参数,这些构造函数都包含一些共同的代码。例如,下面的情况:

class Car

{

   private string description;

   private uint nWheels;

   public Car(string model, uint nWheels)

   {

      this.description = description;

      this.nWheels = nWheels;

   }

   public Car(string description)

   {

      this.description = description;

      this.nWheels = 4;

   }

// etc.

这两个构造函数初始化了相同的字段,显然,最好把所有的代码放在一个地方。C#有一个特殊的语法,称为构造函数初始化器,可以实现此目的:

class Car

{

   private string description;

   private uint nWheels;

   public Car(string description, uint nWheels)

   {

      this.description = description;

      this.nWheels = nWheels;

   }

   public Car(string description) : this(model, 4)

   {

   }

   // etc  

这里,this关键字仅调用参数最匹配的那个构造函数。注意,构造函数初始化器在构造函数之前执行。

3.2.3  只读字段

常量的概念就是一个包含不能修改的值的变量,常量是C#与大多数编程语言共有的。但是,常量不必满足所有的要求。有时可能需要一些变量,其值不应改变,但在运行之前其值是未知的。C#为这种情形提供了另一个类型的变量:只读字段。

readonly关键字比const灵活得多,允许把一个字段设置为常量,但可以执行一些运算,以确定它的初始值。其规则是可以在构造函数中给只读字段赋值,但不能在其他地方赋值。只读字段还可以是一个实例字段,而不是静态字段,类的每个实例可以有不同的值。与const字段不同,如果要把只读字段设置为静态,就必须显式声明。

只读字段不能在构造函数外部赋值:还要注意,在构造函数中不必给只读字段赋值,如果没有赋值,它的值就是其数据类型的默认值,或者在声明时给它初始化的值。这适用于静态和实例只读字段。

3.3  匿名类型

2章讨论了var关键字,用于表示隐式类型化的变量。varnew关键字一起使用时,可以创建匿名类型。

var captain = new {FirstName = "James", MiddleName = "T", LastName = "Kirk"};

3.4  结构

在许多方面,可以把C#中的结构看作是缩小的类。它们基本上与类相同,但更适合于把一些数据组合起来的场合。它们与类的区别在于:

       结构是值类型,不是引用类型。它们存储在堆栈中或存储为内联(inline)(如果它们是另一个保存在堆中的对象的一部分),其生存期的限制与简单的数据类型一样。

       结构不支持继承。

       结构的构造函数的工作方式有一些区别。尤其是编译器总是提供一个无参数的默认构造函数,这是不允许替换的。

       使用结构,可以指定字段如何在内存中布局(13章在介绍属性时将详细论述这个问题)

3.4.1  结构是值类型

虽然结构是值类型,但在语法上常常可以把它们当作类来处理。

   Dimensions point = new Dimensions();

   point.Length = 3;

   point.Width = 6;

注意,因为结构是值类型,所以new运算符与类和其他引用类型的工作方式不同。new运算符并不分配堆中的内存,而是调用相应的构造函数,根据传送给它的参数,初始化所有的字段。对于结构,可以编写下述代码:

   Dimensions point;

   point.Length = 3;

   point.Width = 6;

结构遵循其他数据类型都遵循的规则:在使用前所有的元素都必须进行初始化。结构是值类型,所以会影响性能,但根据使用结构的方式,这种影响可能是正面的,也可能是负面的。正面的影响是为结构分配内存时,速度非常快,因为它们将内联或者保存在堆栈中。在结构超出了作用域被删除时,速度也很快。另一方面,只要把结构作为参数来传递或者把一个结构赋给另一个结构(例如A=B,其中AB是结构),结构的所有内容就被复制,而对于类,则只复制引用。这样,就会有性能损失,根据结构的大小,性能损失也不同。注意,结构主要用于小的数据结构。但当把结构作为参数传递给方法时,就应把它作为ref参数传递,以避免性能损失——此时只传递了结构在内存中的地址,这样传递速度就与在类中的传递速度一样快了。另一方面,如果这样做,就必须注意被调用的方法可以改变结构的值。

3.4.2  结构和继承

结构不是为继承设计的。不能从一个结构中继承,唯一的例外是结构(C#中的其他类型一样)派生于类System.Object。注意,不能为结构提供其他基类:每个结构都派生于ValueType

3.4.3  结构的构造函数

为结构定义构造函数的方式与为类定义构造函数的方式相同,但不允许定义无参数的构造函数,不能提供字段的初始值。这看起来似乎没有意义,其原因隐藏在.NET运行库的执行方式中。

3.5  部分类

partial关键字允许把类、结构或接口放在多个文件中。一般情况下,一个类存储在单个文件中。但有时,多个开发人员需要访问同一个类,或者某种类型的代码生成器生成了一个类的某部分,所以把类放在多个文件中是有益的。

partial关键字的用法是:把partial放在classstructinterface关键字的前面。在下面的例子中,TheBigClass类位于两个不同的源文件BigClassPart1.csBigClassPart2.cs中:

//BigClassPart1.cs

partial class TheBigClass

{

  public void MethodOne()

  {

  }

}

 

//BigClassPart2.cs

partial class TheBigClass

{

  public void MethodTwo()

  {

  }

}

编译包含这两个源文件的项目时,会创建一个TheBigClass类,它有两个方法MethodOne() MethodTwo()

如果声明类时使用了下面的关键字,这些关键字将应用于同一个类的所有部分:

     public

     private

     protected

     internal

     abstract

     sealed

     new

     一般约束

在嵌套的类型中,只要partial关键字位于class关键字的前面,就可以嵌套部分类。在把部分类编译到类型中时,会合并属性、XML注释、接口、泛型类型的参数属性和成员。有如下两个源文件:

//BigClassPart1.cs

[CustomAttribute]

partial class TheBigClass : TheBigBaseClass, IBigClass

{

  public void MethodOne()

  {

  }

}

 

//BigClassPart2.cs

[AnotherAttribute]

partial class TheBigClass : IOtherBigClass

{

  public void MethodTwo()

  {

  }

}

编译后,源文件变成:

[CustomAttribute]

[AnotherAttribute]

partial class TheBigClass : TheBigBaseClass, IBigClass, IOtherBigClass

{

  public void MethodOne()

  {

  }

  public void MethodTwo()

  {

  }

}

3.8  扩展方法

有许多方法扩展类。如果有类的源代码,继承(如第4章所述)就是给对象添加功能的好方法。但如果没有源代码,该怎么办?此时可以使用扩展方法,它允许改变一个类,但不需要类的源代码。

扩展方法是静态方法,是类的一部分,但实际上没有放在类的源代码中。假定上例中的Money类需要一个方法AddToAmountdecimal amountToAdd)。但是,由于某种原因,程序集最初的源代码不能直接修改。此时就可以创建一个静态类,把方法AddToAmount添加为一个静态方法。代码如下:

namespace Chapter3.Extensions

{

public static class MoneyExtension

{

public static void AddToAmount(this Money money, decimal amountToAdd)

{

money.Amount += amountToAdd;

}

}

}

注意AddToAmount方法的参数。对于扩展方法,第一个参数是要扩展的类型,它放在this关键字的后面。这告诉编译器,这个方法是Money类型的一部分。在这个例子中,Money是要扩展的类型。在扩展方法中,可以访问所扩展类型的所有公共方法和属性。

在主程序中,AddToAmount方法看起来像是另一个方法。它没有显示第一个参数,也不能对它进行任何处理。要使用新方法,需要执行如下调用,这与其他方法相同:

cash1.AddToAmount(10M);

即使扩展方法是静态的,也要使用标准的实例方法语法。注意这里使用cash1实例变量来调用AddToAmount,而没有使用类型名。

如果扩展方法与类中的某个方法同名,扩展方法就从来不会被调用。类中已有的实例方法优先。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值